打赏

JVM系列之七:HotSpot 虚拟机

1. 对象的创建

1. 遇到 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,执行相应的类加载

2. 类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。在堆的空闲内存中划分一块区域(‘指针碰撞-内存规整’或‘空闲列表-内存交错’的分配方式)。

    A、假设Java堆是规整的,所有用过的内存放在一边,空闲的内存放在另外一边,中间放着一个指针作为分界点的指示器。那分配内存只是把指针向空闲空间那边挪动与对象大小相等的距离,这种分配称为“指针碰撞”
    B、假设Java堆不是规整的,用过的内存和空闲的内存相互交错,那就没办法进行“指针碰撞”。虚拟机通过维护一个列表,记录哪些内存块是可用的,在分配的时候找出一块足够大的空间分配给对象实例,并更新表上的记录。这种分配方式称为“空闲列表“。
    C、使用哪种分配方式由Java堆是否规整决定。Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
    D、分配对象保证线程安全的做法:虚拟机使用CAS失败重试的方式保证更新操作的原子性。(实际上还有另外一种方案:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,TLAB。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才进行同步锁定。虚拟机是否使用TLAB,由-XX:+/-UseTLAB参数决定)

3. 每个线程在堆中都会有私有的分配缓冲区(TLAB),这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全。

4. 内存空间分配完成后会初始化为 0(不包括对象头)

5. 接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。

6.  执行<init>方法,把对象按照程序员的意愿进行初始化。执行 init 方法后才算一份真正可用的对象创建完成。

2. 对象的内存布局

在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

1. 对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。

2. 实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。

3. 对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍

3. 对象的访问定位

一般来说,一个Java的引用访问涉及到3个内存区域:JVM栈,堆,方法区。

以最简单的本地变量引用:

Object objRef = new Object()为例: Object objRef 表示一个本地引用,存储在JVM栈的本地变量表中,表示一个reference类型数据;
new Object()作为实例对象数据存储在中;
堆中还记录了能够查询到此Object对象的类型数据(接口、方法、field、对象类型等)的地址,实际的数据则存储在方法区中;

 

在Java虚拟机规范中,只规定了指向对象的引用,对于通过reference类型引用访问具体对象的方式并未做规定,不过目前主流的实现方式主要有两种:

1. 通过句柄访问

  通过句柄访问的实现方式中,JVM堆中会划分单独一块内存区域作为句柄池,句柄池中存储了对象实例数据(在堆中)对象类型数据(在方法区中)指针。这种实现方法由于用句柄表示地址,因此十分稳定。 Java 堆中会分配一块内存作为句柄池。reference 存储的是句柄地址。详情见图。

 

 

2. 使用直接指针访问

  通过直接指针访问的方式中,reference中存储的就是对象在堆中的实际地址在堆中存储的对象信息中包含了在方法区中的相应类型数据。这种方法最大的优势是速度快,在HotSpot虚拟机中用的就是这种方式。 

 

比较:

  使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。

  直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销

  如果是对象频繁 GC 那么句柄方法好,

  如果是对象频繁访问则直接指针访问好。

 

4. HotSpot的GC算法实现

(1)HotSpot怎么快速找到GC Root?

HotSpot使用一组称为OopMap的数据结构。
类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来
JIT编译过程中,也会在栈和寄存器中哪些位置是引用
这样子,在GC扫描的时候,就可以直接知道哪些是可达对象了。

 

(2)安全点:

A、HotSpot只在特定的位置生成OopMap,这些位置称为安全点。
B、程序执行过程中并非所有地方都可以停下来开始GC,只有在到达安全点是才可以暂停。
C、安全点的选定基本上以“是否具有让程序长时间执行“的特征选定的。比如说方法调用、循环跳转、异常跳转等。具有这些功能的指令才会产生Safepoint

 

(3)中断方式:

A、抢占式中断在GC发生时,首先把所有线程中断,如果发现有线程不在安全点上,就恢复线程,让它跑到安全点上。
B、主动式中断GC需要中断线程时,不直接对线程操作,仅仅设置一个标志,各个线程执行时主动去轮询这个标志,当发现中断标记为真就自己中断挂起。轮询标记的地方和安全点是重合的。

 

 

(4)安全区域

一段代码片段中,对象的引用关系不会发生变化,在这个区域中任何地方开始GC都是安全的。
在线程进入安全区域时,它首先标志自己已经进入安全区域,在这段时间里,当JVM发起GC时,就不用管进入安全区域的线程了。
在线程将要离开安全区域时,它检查系统是否完成了GC过程,如果完成了,它就继续前行。否则,它就必须等待直到收到可以离开安全区域的信号。

 

(5)GC时为什么要停顿所有Java线程?

因为GC先进行可达性分析。
可达性分析是判断GC Root对象到其他对象是否可达,
假如分析过程中对象的引用关系在不断变化,分析结果的准确性就无法得到保证。

 

5. 举个栗子

public class Test{
    public static void main(String[] args){
        Student stu = new Student();
        stu.setName("John");
        System.out.println(stu);
    }
}

1. 通过java.exe运行Test.class,Test.class文件会被AppClassLoader加载器(因为ExtClassLoader和BootStrap加载器都不会加载它[双亲委派模型])加载到JVM中,元空间存储着类的信息(包括类的名称、方法信息、字段信息..)。
2. 然后JVM找到Test的主函数入口(main),为main函数创建栈帧,开始执行main函数
3. main函数的第一条命令是Student Student stu= new Student();就是让JVM创建一个Student对象,但是这时候方法区中没有Student类的信息,所以JVM马上加载Student类,把Student类的类型信息放到方法区中(元空间)
4. 加载完Student类之后,Java虚拟机做的第一件事情就是在堆区中为一个新的Student实例分配内存, 然后调用构造函数初始化Student实例这个Student实例持有着指向方法区的Student类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用
5. 当使用Student.setName("John");的时候,JVM根据Student引用找到Student对象,然后根据Student对象持有的引用定位到方法区中Student类的类型信息的方法表,获得setName()函数的字节码的地址
6. 为setName()函数创建栈帧,开始运行setName()函数

 

参考网址

  1. 学习JVM是如何从入门到放弃的?
  2. Java虚拟机(JVM)你只要看这一篇就够了!
posted @ 2019-08-14 13:22  海米傻傻  阅读(2697)  评论(0编辑  收藏  举报