JVM(Java Virtual Machine)即Java虚拟机,Java代码都是在JVM上运行的,所以了解JVM是成为Java高手的毕竟之路。
本系列内容将对JVM的知识进行介绍,是从头学习JVM知识的笔记。
本系列内容根据自己的学习和理解的基础上,并参考《深入理解Java虚拟机》一书介绍的知识所写。如果有写的不对的地方,请各位多多提点。
从头开始学习JVM(三)—— 对象结构和生存判定
Java对象
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、和 对齐填充(Padding)。
- 对象头
对象头主要有两部分组成,分别是由MarkWord和Klass Point(类型指针)。
- Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态等等。如果对象是数组对象,那么对象头占用3个字宽(Word),如果对象是非数组对象,那么对象头占用2个字宽。(1word = 2 Byte = 16 bit)。
- 其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据不一定要经过对象本身,这点将在下一节 [对象的访问地址] 中讲到。
在32位的虚拟机中MarkWord的结构:
在64位的虚拟机中MarkWord的结构:
- 实例数据
实例数据存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐。
- 对齐填充
对齐填充,因为虚拟机要求对象字节必须是8字节的整数倍,填充字符就是用于凑齐这个整数倍的。
对象的访问地址
建立对象是为了使用对象,我们的Java程序需要通过栈上的 reference
数据来操作堆上的具体对象。由于reference
类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中对象的具体位置,所以对象的寻址方式是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种。
使用句柄寻址
如果使用的是句柄方式,那么Java堆中将会划分一块内存作为句柄池,reference
中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
使用直接指针
如果使用的直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference
中存储的直接就是对象地址。
两种方式比较
这两种方式各有优势。
- 使用句柄的最大好处是
reference
中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference
自身不需要修改。 - 直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。
- 如果是对象频繁 GC 那么句柄方法好,如果是对象频繁访问则直接指针访问好。而HotSpot虚拟机使用的是直接指针方式。
对象是否存活
在堆中存放着Java里几乎所有的实例对象,如何判断这些对象是否“存活”呢?一般有两种方式,引用计数算法和可达性分析算法。JVM使用的是可达性分析算法。
引用计数算法
引用计数法(Reference Counting)是这么判断对象存活的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器只加1;当引用失效时就减1;任何时刻计数器为0的对象就是不可能再被使用的。
引用计数算法实现简单,判定效率也高,但是它很难解决对象之间互相循环引用的问题,所以JVM没有使用该方法。
可达性分析算法
可达性分析算法(Reachability Analysis)的基本思路是,通过一系列的成为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(用图论的话来说,就是从GC Roots到这个对象不可达),就证明这个对象是不可用的。
例如在下图中,对象Object5、6、7即为不可达对象,将判定为可回收对象。
在Java语言中,可作为 GC Roots 对象的有以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象。
存活与引用
以上提到的两种判定对象存活的算法,判定对象是否存活都与“引用”相关。在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),4中引用强度依次减弱。
- 强引用(Strong Reference)
强引用在程序代码中普遍存在,类似 Object obj = new Object();
的这类引用。强引用的对象是垃圾收集器永远不会被回收的。
- 软引用(Soft Reference)
提供了SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。若此次回收后还没有足够的内存,就会抛出OOM异常。
- 弱引用(Weak Reference)
提供了WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
- 虚引用(Phantom Reference)
提供了PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
finalize方法与对象的死亡
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时出于“缓刑”阶段,一个对象的真正死亡至少要经历两次标记过程:如果对象在进行中可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会出发这个方法,并不承诺或等待它运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象建立关联即可。如果逃脱不了就基本上真被回收了。
finalize() 方法只会被系统自动调用一次。它的运行代价高,不确定性大,无法保证各个对象的调用顺序,因此建议不要使用该方法,甚至是忘掉它的存在。