(二)学习JVM —— 垃圾回收机制

(一)学习JVM ——运行时数据区域

(二)学习JVM —— 垃圾回收机制

(三)学习JVM —— 垃圾回收器

(四)学习JVM —— 内存分配与回收策略

对象何时可被回收?

在Java堆中存放着Java中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是确定这些对象哪些还存活着,哪些已经可回收。

可达性分析

Java采用可达性分析来判定对象是否是存活的,这个算法的基本思路就是通过一系列称为"GC Root"的对象作为起始点,从这些节点向下搜索,走过引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则证明这个对象已经可以回收。

Java中可作为GC Root的对象有四种:

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

引用的种类

引用的定义是:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。

Java堆引用概念进行了扩展,分为下述4种:

  • 强引用(Strong Reference):只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象;
  • 软引用(Soft Reference):将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会内存溢出;
  • 弱引用(Weak Reference):弱引用只能生存到下一次垃圾回收发生之前。
  • 虚引用(Phantom Reference):虚引用(幻影引用)唯一的目的就是能在这个对象被回收时收到一个系统通知。

回收前的细节

真正标记一个对象为可回收,至少要经历两次标记过程:

在经历可达性分析后,发现没有与GC Root关联,会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。对没有覆盖或者已经调用过finalize()方法的对象,JVM都不会去执行finalize()方法了。

如果对象有必要执行finalize()方法,那么该对象会被放置到F-Queue队列,并在稍后由一个自动建立的,低优先级的Finalizer线程去执行它。

方法区怎么回收?

JVM的共享内存分为堆和方法区,有很多人认为方法区(或永久代)是没有垃圾回收的,JVM规范也确实说过可以不对方法区进行回收,因为性价比较低,一般在堆中,尤其是新生代中,常规应用进行一次垃圾回收可以回收70%~95%的空间,而方法区则不然。

方法区如果进行垃圾收集,主要回收废弃常量和无用的类。

判定常量是否是废弃的常量,是指进入了常量池,但是系统中没有任何一个地方引用了这个字面量,就可以回收。

判定一个类是否是一个无用的类的条件比较苛刻,有3个条件:

  1. 该类所有的实例已经被回收,堆中不存在该类的任何实例;
  2. 加载该类的ClassLoader已经被回收;
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法;

是否对类进行回收,可以用-Xnoclassgc参数进行控制,还可以在Product版虚拟机中,用-verbose:class以及-XX:+TraceClassLoading查看类加载和卸载信息。在FastDebug版虚拟机中用-XX:+TraceClassUnLoading参数查看。

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证方法区不会溢出。

对象回收的算法

垃圾回收的常用算法有三种,分别是标记&清除算法、复制算法和标记&整理算法。

标记&清除算法

标记&清除(Mark&Sweep)分为两个阶段,标记和清除。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,该算法的主要弊端在于,清除之后会产生大量不连续的内存碎片,碎片太多会导致以后在运行过程中需要分配较大对象时,无法找到连续的内存空间,而不得不提前出发另一次垃圾收集。

复制算法

复制(Copying)算法,可以将内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完了,就将还存活着的对象复制到另一块,然后把已经使用过的内存一次清理掉。只是这种做法将内存缩小到了原来的一半,成本高了一点。

根据IBM公司专门研究表明,新生代中有98%的对象时朝升西落的,所以,并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一个Survivor空间。

当回收时,将Eden和Survivor中还存活的对象,一次性的复制到另一块Survivor中,然后清理掉Eden和刚才使用的那个Survivor空间。JVM默认采用8:1:1的方式分配Eden和两个Survivor。

当复制过程中发生Survivor不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion),将这些对象直接分配到老年代。

标记&整理算法

标记&整理(Mark&Compact),标记过程与标记清理一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 

分代回收算法

当前商业虚拟机的垃圾收集都采用分代收集(Generational Collection),根据对象存活周期的不同将内存划分为几块。一般是把Java堆分成新生代和老年代。

新生代对象存活率低,选用复制算法,将内存以8:1:1分配。

老年代对象存活率高,没有额外空间对它进行分配担保,所以采用标记&清理,或标记&整理算法。

HotSpot虚拟机回收算法的实现

前面从理论层面介绍了对象存活的判断方式和垃圾回收算法,而JVM在实现这些算法时,必须对算法的执行效率有严格的考量,才能保证JVM高效运行。

枚举根节点

JVM在进行可达性分析时,并不会真的从每一个GC Root进行引用链检查,现在很多应用仅仅是方法区就有数百兆,如果要逐一检查,必然会消耗很多时间。

另外,可达性分析还体现在GC停顿上,为了确保分析准备,不可以出现在分析过程中对象引用关系还在不断变化的情况,GC停顿还被Sun公司成为称为Stop The World。

HotSpot实现中,使用一组OopMap的数据结构来帮助快速检查引用链,在类加载完成时,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录栈河寄存器中那些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。

但是可能导致引用关系变化的指令非常多,如果为每一条都生成对应的OopMap,那将需要大量额外的空间,这样GC的成本会变得很高。下面要介绍的安全点则解决了这个问题。

安全点

程序执行时并非在所有位置都可以停下来GC,只有在达到安全点(Safepoint)时才能暂停。

安全点的选定不能太多,也不能太少,它的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准来选定的,长时间执行最明显的特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,具有这些功能的指令才会产生安全点。

另一个要考虑的问题是,如何在GC发生时,让所有线程都跑到最近的安全点停顿下来。

目前,基本上所有的虚拟机都采用主动式中断的思想,当GC需要中断线程的时候,轮询一个标志,发现中断标志为true时,就自己中断挂起,轮询标志的地方河安全点是重合的。

安全区域

程序不执行的时候,就是没有分配CPU时间时,线程无法响应JVM的中断请求,这时就需要安全区域(Safe Regin)来解决。

安全区域是指一段代码片段中,引用关系不会发生变化。在这个区域GC都是安全的。

线程进入安全区域时会进行标记,离开时要检查自己是否完成了根节点枚举,如果完成了就继续工作,否则要等到可以安全离开的信号为止。 

(一)学习JVM ——运行时数据区域

(二)学习JVM —— 垃圾回收机制

(三)学习JVM —— 垃圾回收器

(四)学习JVM —— 内存分配与回收策略

猜你喜欢

转载自my.oschina.net/u/2450666/blog/1612117