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)你只要看这一篇就够了!

原文地址:https://www.cnblogs.com/haimishasha/p/11330254.html

时间: 2024-08-29 17:47:34

JVM系列之七:HotSpot 虚拟机的相关文章

JVM系列一:虚拟机内存区域

虚拟机栈 1.虚拟机栈维护一个线程中所有方法的栈帧,每个栈帧中保存着这个方法中用到的局部变量表,操作数栈,常量引用 2.可以用-Xss来设置每个线程中虚拟机栈的大小,在jdk1.4之前默认虚拟机栈大小是256K,在jdk1.5+默认虚拟机栈大小是1M java -Xss2M HackTheJava 3.该区域可能抛出的异常 当线程请求的栈深度超过最大限制后,或抛出StackOverflowError 当栈进行动态扩展无法申请到内存后,会抛出OutOfMemoryError 本地方法栈 本地方法栈

深入理解JVM:HotSpot虚拟机对象探秘

对象的创建 java是一门面向对象的语言.在Java程序执行过程中无时无刻有Java对象被创建出来.在语言层面上,创建对象(克隆.反序列化)一般是一个newkeyword而已,而在虚拟机中,对象的创建步骤例如以下: 1.当虚拟机遇到new指令时.首先将去检查这个指令參数能否在常量池中定位到一个类的引用符号,而且检查这个符号引用代表的类是否被载入.解析和初始化过.假设没有.那必须先执行相应的类载入过程. 2.在类载入检查通过以后.接下来虚拟机将为新生对象分配内存.对象所需的内存大小在类载入后便确定

JVM:Hotspot虚拟机中的对象

在HotSpot虚拟机中,对象在内存中存储的布局可以被分为3个区域:对象头(Header).实例数据(Instance data)和对齐填充(Padding).对象头包括两部分信息,第一部分存储自身的运行时数据,如哈希值.GC分代年龄.锁状态标志.线程持有的锁.偏向锁ID.偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中(未开启压缩指针)分别为32bit和64bit,官方称它为 Mark Word. 存储内容 标志位 状态 对象哈希码.对象分代年龄 01 未锁定 指向锁记录的指针 00

[转]JVM系列一:JVM内存组成及分配

原文地址:http://www.cnblogs.com/redcreen/archive/2011/05/04/2036387.html JVM系列一:JVM内存组成及分配 java内存组成介绍:堆(Heap)和非堆(Non-heap)内存 按照官方的说法:"Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配.堆是在 Java 虚拟机启动时创建的.""在JVM中堆之外的内存称为非堆内存(Non-heap memory)".可以看出JV

深入JVM系列(二)之GC机制、收集器与GC调优(转)

一.回顾JVM内存分配 需要了解更多内存模式与内存分配的,请看 深入JVM系列(一)之内存模型与内存分配 1.1.内存分配: 1.对象优先在EDEN分配2.大对象直接进入老年代 3.长期存活的对象将进入老年代 4.适龄对象也可能进入老年代:动态对象年龄判断 动态对象年龄判断: 虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,当Survivor空间的相同年龄的所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代

jvm系列(八):jvm知识点总览-高级Java工程师面试必备

在江湖中要练就绝世武功必须内外兼备,精妙的招式和深厚的内功,武功的基础是内功.对于武功低(就像江南七怪)的人,招式更重要,因为他们不能靠内功直接去伤人,只能靠招式,利刃上优势来取胜了,但是练到高手之后,内功就更主要了.一个内功低的人招式在奇妙也打不过一个内功高的人.比如,你剑法再厉害,一剑刺过来,别人一掌打断你的剑,你还怎么使剑法,你一掌打到一个武功高的人身上,那人没什么事,却把你震伤了,你还怎么打.同样两者也是相辅相成的,内功深厚之后,原来普通的一招一式威力也会倍增. 对于搞开发的我们其实也是

JVM系列文章(一):Java内存区域分析

作为一个程序员,仅仅知道怎么用是远远不够的.起码,你需要知道为什么可以这么用,即我们所谓底层的东西. 那到底什么是底层呢?我觉得这不能一概而论.以我现在的知识水平而言:对于Web开发者,TCP/IP.HTTP等等协议可能就是底层:对于C.C++程序员,内存.指针等等可能就是底层的东西.那对于Java开发者,你的Java代码运行所在的JVM可能就是你所需要去了解.理解的东西. 我会在接下来的一段时间,和读者您一起去学习JVM,所有内容均参考自<深入理解Java虚拟机:JVM高级特性与最佳实践>(

深入JVM系列(二)之GC机制、收集器与GC调优

一.回想JVM内存分配 须要了解很多其它内存模式与内存分配的,请看 深入JVM系列(一)之内存模型与内存分配 1.1.内存分配: 1.对象优先在EDEN分配 2.大对象直接进入老年代 3.长期存活的对象将进入老年代 4.适龄对象也可能进入老年代:动态对象年龄推断 动态对象年龄推断: 虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才干晋升到老年代,当Survivor空间的同样年龄的全部对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就能够直接进入

JVM系列第4讲:从源代码到机器码,发生了什么?

在上篇文章我们聊到,无论什么语言写的代码,其到最后都是通过机器码运行的,无一例外.那么对于 Java 语言来说,其从源代码到机器码,这中间到底发生了什么呢?这就是今天我们要聊的. 如下图所示,编译器可以分为:前端编译器.JIT 编译器和AOT编译器.下面我们逐个讲解. 前端编译器:源代码到字节码 之前我们说到:对于 Java 虚拟机来说,其实际输入的是字节码文件,而不是 Java 文件.那么对于 Java 语言而言,其实怎么将 Java 代码转化成字节码文件的呢?我们知道在 JDK 的安装目录里