当前位置: 首页 > article >正文

JVM机制

文章目录

  • JVM 简介
  • JVM内存划分
    • 堆(线程共享)
    • Java虚拟机栈(线程私有)
    • 本地方法栈(线程私有)
    • 程序计数器(线程私有)
    • 方法区(线程共享)
  • JVM类加载机制
    • 类加载过程
    • 双亲委派模型
  • JVM垃圾回收机制
    • 找到谁是垃圾
      • 引用计数算法(不是JVM采取的方案,而是 Python/PHP 的方案)
      • 可达性分析算法(JVM采用)
    • 释放对应的内存的算法
      • 标记-清除算法(不实用)
      • 复制算法
      • 标记-整理算法
      • 分代算法

JVM 简介

JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。JVM 是 Java 运行的基础,也是实现一次编译到处执行的关键。

虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。常见的虚拟机:JVM、VMwave、Virtual Box

JVM 和其他两个虚拟机的区别:

  1. VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器
  2. JVM则是通过软件模拟Java字节码的指令集,JVM中主要保留了PC寄存器,其他的寄存器都进行了裁剪

PS: 本文以下部分,默认都是使用 HotSpot,也就是 Oracle Java 默认的虚拟机为前提来进行介绍的

JVM内存划分

注意它和 Java 内存模型(Java Memory Model,简称 JMM)完全不同,属于完全不同的两个概念,它由以下 5 大部分组成:

堆(线程共享)

堆是整个JVM内存区域中最大的区域, 放的就是程序中创建的所有对象

常见的 JVM 参数设置 -Xms10m 是最小启动内存,-Xmx10m 是最大运行内存 ,这都是针对堆的(ms 是 memory start 简称,mx 是 memory max 的简称)

Java虚拟机栈(线程私有)

Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建⼀个栈帧(Stack Frame), 用于存储局部变量表、操作栈、动态链接、方法出口等信息。

本地方法栈(线程私有)

本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM (Java实现的方法)使用的,而本地方法栈是给本地方法(C++实现的方法)使用的。

程序计数器(线程私有)

程序计数器是内存区域中最小的区域,保存当前要执行的下一条指令(JVM字节码,不是cpu指令)的地址

如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是⼀个Native方法,这个计数器值为空。

程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM情况的区域!

方法区(线程共享)

方法区用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

在《Java虚拟机规范中》把此区域称之为“方法区”,而在 HotSpot 虚拟机的实现中,在 JDK 7 时此区域叫做永久代(PermGen),JDK 8 中叫做元空间(Metaspace),也叫元数据区

JVM类加载机制

类加载过程

  1. 加载

在硬盘上找到对应的.class文件,读取文件内容加载到内存中

  1. 验证

这一阶段的目的是确保Class文件中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全

  1. 准备

准备阶段是为类中定义的静态变量分配内存(在元数据区中)并设置初始值为0。被final修饰的static字段不会设置,因为final在编译的时候就分配了

  1. 解析

针对字符串常量来初始化, 把.class文件的常量放到"元数据区"

  1. 初始化

针对类对象进行初始化(不是针对对象的初始化,和构造方法无关), 执行静态代码块

执行完这五步后类对象就创建完成了,后续代码就可以使用这个类对象创建实例,或者使用里面的静态成员了

双亲委派模型

描述了JVM加载.class文件过程中,找文件的过程

"类加载器"负责 类加载 工作。自 JDK 1.2 以来,Java 一直保持着三层类加载器、双亲委派的类加载架构器

  • Bootstrap ClassLoader(爷爷) : 负责加载 标准库 的类, 标准库是Java官方给出的"规范文档"上面要求提供的类
  • Extension ClassLoader(父亲) : 负责加载 JVM扩展库 的类,各个JVM厂商在实现JVM的时候会根据需要,在标准库的基础上做出一些扩展。扩展库是JVM自带的, 安装了JVM就有的(现在很少使用)
  • Application ClassLoader(儿子) : 负责加载 第三方库 和 自己写的类

此处的"父子关系"不是通过 类 的继承表示(不是 父类 子类), 而是 通过一个"parent" 字段指向自己的"父亲"

什么是双亲委派模型?

如果一个类加载器收到了类加载的请求,它不会自己先去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该委托到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才自己尝试去完成加载

在这里插入图片描述

工作过程:
例如,给定一个自己写的 类 。全限定类名(包名+类名): java111.Test

此时加载过程如下:

  1. 从Application ClassLoader 开始。Application ClassLoader 并不会立即搜索第三方库的相关目录而是把任务交给自己的父亲(Extension ClassLoader)来处理
  2. 工作就到了 Extension ClassLoader。Extension ClassLoader 也不会立即搜索扩展库的目录,也是把任务交给自己的父亲(Bootstrap ClassLoader)来处理
  3. 工作就到了 Bootstrap ClassLoader。Bootstrap ClassLoader 也想交给自己的父亲来处理,但是它的parent指向null, 只能自己处理。Bootstrap ClassLoader 就在标准库的目录中搜索 java111.Test
  4. 如果这个类在标准库找到了,找文件的过程就结束了;如果没找到,任务还是继续交给儿子来处理
  5. 工作回到了Extension ClassLoader。此时就搜索扩展库对应的目录。如果找到就结束,没找到就还给儿子处理
  6. 工作回到了Application ClassLoader。此时就搜索第三方库/用户自己写的目录了,找到了结束,没找到,任务还是继续交给儿子来处理, 此时没有儿子了,就会抛出ClassNotFoundException异常

双亲委派模型主要是为了应对这个场景:比如自己代码里写的全限定类名和标准库冲突了,JVM会确保加载的类是标准库的(不加载自己写的类了),如果标准库缺失,整个Java进程没法正常工作了

类名可以重复,全限定类名不能重复

双亲委派模型的优点

  1. 避免重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了
  2. 安全性:使用双亲委派模型也可以保证 Java 的核心 API 不被篡改
  3. 确保自己添加的类加载器都能被执行到

类加载器并非是固定就只有3个,还可以手动添加更多的类加载器到中间

JVM垃圾回收机制

  • 程序计数器、虚拟机栈、本地方法栈:不需要额外回收。这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了
  • 元数据区: 一般也不需要,都是加载类,很少"卸载类"
  • 堆: GC的主力部分

Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经"死去"。

堆在内存中, GC回收的是"内存" , 而且一定是回收完整的对象,而不是回收半个对象

GC主要是两个步骤:

  1. 找到谁是垃圾(不用的对象)
  2. 释放对应的内存

找到谁是垃圾

如果某个对象没有引用指向它,就认为是不再使用了。介绍两种方式判定某个对象是否有引用指向:

引用计数算法(不是JVM采取的方案,而是 Python/PHP 的方案)

每个对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1。计数器为0的对象就是不再被使用的,即对象已"死"。

引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法,但两个缺陷:

  1. 消耗额外的存储空间。对象比较小,引用计数空间占比就大了,并且对象越多,空间浪费就越多
  2. 在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题。

循环引用示例:

class Test {
    Test t;
}


public class Main {
    public static void main(String[] args) {
        Test a = new Test();
        Test b = new Test();
        a.t = b;
        b.t = a;
        a = null;
        b = null;
    }
}

a实例里面有b的实例,b实例里面有a的实例,就算双方置为null, 双方的引用计数还是1

可达性分析算法(JVM采用)

虽然解决了空间和循环引用问题,但是花了更多的时间

把对象之间的引用关系,用"树形结构"管理起来。会周期性不停遍历这样的结构,把能够遍历到的对象标记为"可达",剩下的就是"不可达"

此算法的核心思想为 : 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的

在Java中,可作为GC Roots的对象包含下面几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  2. 方法区中类静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中 JNI(Native方法)引用的对象。

由于可达性分析需要消耗一定的时间,因此Java的垃圾回收,没法做到"实时性"。周期性进行扫描(JVM提供了一组专门负责GC的线程,不停的进行"扫描"工作)

除了最早我们使用"引用"来查找对象,现在我们还可以使用“引用”来判断死亡对象了。

释放对应的内存的算法

通过上面的算法我们可以将死亡对象标记出来了,标记出来之后我们就可以进行垃圾回收操作了,我们先看下垃圾回收机器使用的几种算法:

标记-清除算法(不实用)

"标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。后续的收集算法都是基于这种思路并对其不足加以改进而已。

"标记-清除"算法的不足主要有两个 :

  • 效率问题 : 标记和清除这两个过程的效率都不高
  • 空间问题 : 标记清除后会产生大量不连续的内存碎片,碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够大的连续内存而不得不提前触发另一次垃圾收集(申请的内存都是"连续的")

复制算法

"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。最大的问题是空间浪费太多了,如果要保留的空间比较大,回收的空间比较少,复制的开销也不小

标记-整理算法

标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存

能解决内存碎片和空间利用率问题,但是时间开销更大

分代算法

当前JVM实际的方案是综合上述方案的"分代回收"。分代算法是根据对象存活周期将堆划分为几块。通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收。

某个对象,经历了一轮GC之后,还不是垃圾,"年龄"就会+1

在这里插入图片描述

一般是把Java堆分为新生代和老年代。新创建的对象都会进入新生代,在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法(存活的对象较少,复制开销也很低,生存区(S0/S1)空间也不必很大)

HotSpot实现的复制算法流程如下:

  1. 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
  2. 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
  3. 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代

复制算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不使用复制算法。老年代中对象存活率高(GC扫描频率低)、没有额外空间对它进行分配担保,就采用"标记-清理"或者"标记-整理"算法。


http://www.kler.cn/a/373203.html

相关文章:

  • Vue生成名片二维码带logo并支持下载
  • Twitter网页版怎么登录?详细步骤与常见问题解答
  • 滚柱导轨出现异常损坏的原因
  • arcgis pro 3.3.1安装教程
  • ZooKeeper的应用场景:深入探讨分布式系统中的多样化应用
  • 音频重采样(libresample)
  • 视频美颜平台的搭建指南:基于直播美颜SDK的完整解决方案
  • 可视化应急指挥平台在应急通信中的优势
  • 视觉目标检测标注xml格式文件解析可视化 - python 实现
  • 【数据结构】五分钟自测主干知识(十二)
  • 两步GMM计算权重矩阵
  • HTML5新增属性
  • 蓝桥杯练习笔记(十九-质数筛)
  • Github 2024-10-27 php开源项目日报 Top10
  • 【verilog】模十计数器
  • 电商直播带货乱象频出,食品经销商如何规避高额损失?
  • Word 每次打开时都会弹出“要还原的文件”对话框
  • iframe视频宽度高度自适应( pc+移动都可以用,jq写法 )
  • Unity控制物体透明度的改变
  • Matplotlib 网格线
  • PostgreSQL 删除角色
  • 面向对象高级-static
  • 为什么选择 Spring data hadoop
  • 蓝牙BLE开发——红米手机无法搜索蓝牙设备?
  • 编程小白如何成为大神?大学新生的最佳入门攻略
  • QT 12.自定义信号、信号emit、信号参数注册_ev