JVM(二) 对象存活判断和垃圾回收算法
对象的创建
概述
下面简要介绍创建对象的几个重要步骤 :
- 检查能否在常量池定位到一个类的符号引用,并检查这个符号代表的类是否已被加载,解析和初始化过。如果没有则执行类加载的操作。(即是说对象的引用放在方法区里的)
- 堆中分配内存,分配有两种方式
- 指针碰撞(Bump the Pointer)--中间分条线一边已分配,一边未分配
- 空闲列表(free list)--已分配的空间在一个列表中进行记录
选择哪种分配方式java 堆是否规整决定,而java是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用 Serial,ParNew 等带 Compact 过程的收集器时,系统采用的分配算法是指针碰撞,而使用 CMS 这种基于 Mark-Sweep 算法的收集器时,通常采用空闲列表
指针指向问题,分配过程中,为了避免并发情况发生使用了下面两种方式 :
1.CAS并且失败重试2.每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存就在哪个线程的TLAB上分配,只有TLAB用完并分配新的 TLAB 时,才需要同步锁定。
- 设置对象信息
对象的布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域: 对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。
对象头
对象头包括两部分信息
- Mark Word : 自身运行时需要的信息,包括 HashCode ,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID,偏向时间戳等
- 类型指针 :
对象直线它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
对齐填充
没什么含义,只是起到占位符的作用,对齐填充。
对象的访问定位
目前主流的访问有使用句柄和直接指针两种。两种的优势对比 :
- 句柄访问
- 直接指针
reference 中储存的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。(下面的两张图可以看到句柄方式向实例数据是由另外一个指针指向的!!)
速度更快(由下图可以看到,直接指针只需要一次指针定位)
句柄访问
java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例与类型数据各自的具体地址信息。
直接指针
上图。
垃圾收集器与内存分配策略
概述
垃圾收集实际就三个问题 :
- 哪些内存需要回收
- 什么时候回收
- 怎么回收
引用计数算法
效率高,可是存在对象之间相互循环引用的问题。
1 public class ReferenceGC { 2 public Object instance = null; 3 public void test(){ 4 ReferenceGC ob1 = new ReferenceGC(); 5 ReferenceGC ob2 = new ReferenceGC(); 6 ob1.instance = ob2; 7 ob2.instance = ob1; 8 9 ob1 = null; 10 ob2 = null; 11 //假设在这行发生GC ,两个对象是否能被回收? 12 System.gc(); 13 14 } 15 } 16 17 18
可达性分析算法(Reachability Analysis)
通过一系列的称为 “GC Roots” 的对象作为起点,从这些起点开始向下搜索,搜索所走过的路径称之为引用链(Reference Chain)当一个对象到 GC Roots 没有任何引用链相连(用图论的话,就是GC Roots到该对象不可达),则这个对象不可用。
可以成为 GC Roots 的有 :
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 本地方法栈中 JNI (即一般说的Native方法) 引用的对象
引用还可分为:
- 强引用
- 软引用
- 虚引用
- 弱引用
生存还是死亡
对象被被回收至少经过两次标记,过程如下 :
而判断有没必要执行finalize 方法有两方面 :
- 对象是否覆盖finalize()方法
- finalize()方法已经被虚拟机调用过
下面有个Demo 可以简述一下这个过程。
1 public class FinalizeEscapeGC { 2 3 public static FinalizeEscapeGC SAVE_HOOK = null; 4 5 public void isAlive() { 6 System.out.println("yes,I am still alive :)"); 7 } 8 9 @Override 10 protected void finalize() throws Throwable { 11 super.finalize(); 12 System.out.println("finalize method executed!"); 13 FinalizeEscapeGC.SAVE_HOOK = this; 14 } 15 16 public static void main(String[] args) throws Throwable { 17 SAVE_HOOK = new FinalizeEscapeGC(); 18 19 //对象第一次成功拯救自己 20 SAVE_HOOK = null; 21 System.gc(); 22 //因为finalize方法优先级很低,所以暂停0.5s等待 23 Thread.sleep(500); 24 if (SAVE_HOOK != null) { 25 SAVE_HOOK.isAlive(); 26 } else { 27 System.out.println("no,I am dead :("); 28 } 29 30 //第二次拯救失败 31 SAVE_HOOK = null; 32 System.gc(); 33 //因为finalize方法优先级很低,所以暂停0.5s等待 34 Thread.sleep(500); 35 if (SAVE_HOOK != null) { 36 SAVE_HOOK.isAlive(); 37 } else { 38 System.out.println("no,I am dead :("); 39 } 40 }}
输出的结果 :
finalize method executed! yes,I am still alive :) no,I am dead :(
另外,在完全一样的两端代码片段里,第二次的执行结果确实逃脱失败了。这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法就不会被再次执行。
最后,在JVM中并不鼓励使用finalize()对象来拯救对象。因此它的运行代码非常高昂而且不确定性大。finalize()方法能做的工作,使用try-finally或者其他方式都可以做的更好更及时。
回收方法区
永久代的垃圾收集主要回收两部分内容 : 废弃常量和无用的类。例如一个字符串“abc”在常量池中,却没有被引用。无用类的回收必须满足以下三个条件 :
- 该类所有的实例都已经被回收,也就是JAVA 堆中不存在该类的任何实例
- 加载该类的ClassLoader 已经被回收
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
垃圾收集算法
标记-清除算法(Mark-Sweep)
下图就可以知道标记清除算法的过程,这个算法存在两个不足:
- 效率不高
- 产生大量不连续的内存碎片
复制算法(Copying)
分两部分内存,不需要清除的挑出来,复制放在没使用的内存上,然后清理掉需要清理的。现在的商业虚拟机都采用这种收集算法来手机新生代。HotSpot 内存中就有分 Eden 和 Survivor (存活者的意思)区域的比例 = 8 : 1 , 那么我们可以猜想到要是 survivor 的区域不够放了怎么办?不够就先新生代借,这个叫 “分配担保(Handle Promotion)”.
标记-整理算法(Mark-Compact)
结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象,即是说标记-整理使用到的只是一块内存空间,而复制算法是两块。如图:
分代收集算法(Generational Collection)
分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为
- 老生代(Tenured/Old Generation)
- 新生代(Young Generation)。
老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
目前大部分JVM的GC对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。
而老生代因为每次只回收少量对象,因而采用Mark-Compact算法。
对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老生代。当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。如果To Space无法足够存储某个对象,则将这个对象存储到老生代。在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。当对象在Survivor区躲过一次GC后,其年龄就会+1。默认情况下年龄到达15的对象会被移到老生代中。