1. 概述
JAVA的内存回收的原理是作为JAVA开发者必须要了解的,因为:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要去对JVM的自动GC做必要的监控和调节。
2. JVM是如何判断对象已死
- 引用计数法
此方法很简单,判断效率也高,在大多数情况下都是不错的算法,也有一些比较成功的案例。但是主流的JVM都没有采用此方法,很简单,他不能解决对象之间相互循环引用的问题。 - 可达性分析算法
在主流的商用程序语言的主流实现中,都是通过此算法来判断对象是否存活的。
基本思想:通过一系列的的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则判断此对象是不可用的。
可作为GC Roots的对象有:虚拟机栈(栈帧中的本地变量表)中引用的对象;方法区中的类静态属性引用的对象;方法区中的常量引用的对象;本地方法栈中JNI(即一般所说的Native方法)引用的对象。 - 在谈引用
如何描述这样的对象:当内存还足够的时候,则保留在内存中,当内存空间在进行垃圾回收后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的场景。
在JDK1.2之后,java对引用的概念进行了扩充,分为四类:
- 强引用:类似Object.obj = new Object();这类的引用,只要强引用还存在,则垃圾收集器永远也不会回收掉被引用的对象。
- 软引用:用来描述一些还有用,但是不是必须的对象。对于软引用的对象,垃圾回收器会在系统发生内存溢出异常之前将这些对象列为回收范围中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。jdk1.2以后,提供了SoftReference来实现软引用。
- 弱引用:同样用来描述非必须的对象的,但是她的强度比软引用还要弱一点,被弱引用关联的对象只能生存到下次垃圾收集发生之前。JDK1.2之后,提供WeakReference来实现。
- 虚引用:又称为幽灵引用或者幻影引用,它是最弱的一种引用关系。虚引用对对象的生存时间没有任何影响,只是在被回收的时候收到一个系统通知。JDK1.2之后,提供PhantomReference实现。
对象是如何死亡的
宣告一个对象死亡,需要经过两次标记过程:当一个对象在进行可达性的分析后,发现没有和任何GC Roots有相连接的引用链,此时被标记一次;第二步进行筛选是否有必要执行 finalize()方法,当对象没有覆盖finalize()方法,或者对象的finalize()方法已经被调用了一次,虚拟机将这两种情况都视为没有必要执行finalize()方法。
F-Queue队列和Finalize线程是后面的两步,Finalize是用来消费F-Queue中的待回收对象,而且是非阻塞的,在消费的时候,如果队列中的对象又重新和GC Roots的引用链有连接关系了,又会被移除队列,从而保存了自己。
一个对象的自我拯救的过程:/** * Created by jiaxiuya * <p> * 一个对象逃离GC的自我拯救的过程,但是只有一次机会,因为Finalize只会被执行一次 * * @Date 2016/5/14 17:45. * @Version nothing */ public class FinalizeEscapeGC { public static FinalizeEscapeGC finalizeEscapeGC = null; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize方法被调用"); FinalizeEscapeGC.finalizeEscapeGC = this; } public static void main(String[] args) { // 第一次拯救自己 finalizeEscapeGC = new FinalizeEscapeGC(); finalizeEscapeGC= null; System.gc(); try { // 由于gc方法的优先级很低,所以先睡眠0.5秒 Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if(finalizeEscapeGC != null){ System.out.println("第一次拯救成功"); }else{ System.out.printf("第一次拯救失败"); } // 第二次拯救失败 finalizeEscapeGC = null; System.gc(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if(finalizeEscapeGC != null){ System.out.println("第二次拯救成功"); }else{ System.out.printf("第二次拯救失败"); } } }
上面的自我拯救过程,虽然看起来比较美好,但是还是要忘记这个方法,因为这个方法的代价高昂,执行顺 序不保证等。
回收方法区
方法区(Hotspot虚拟机中的永久代),虽然称为永久代,但是也是有垃圾回收的。永久代的垃圾回收有两部分:废弃常量和无用的类。
判断一个常量是否是废弃的常量比较简单,比如对于一个“ABC”的字符串已经进入常量池中,只要判断当前系统中没有任何String对象叫做“ABC”,也没有其他的地方引用这个字面量。
判断一个类是否是无用的类就比较复杂:该类的所有实例已经被回收,也就是java堆中不存在该类的任何实例;加载该类的ClassLoader已经被回收;该类对应的java.lang.Class对象没有任何地方引用,并且无法在任何地方通过反射访问该类的方法。
虚拟机通过-Xnoclassgc参数来控制无用的类是否回收。
3. 垃圾收集算法
- 标记-清除算法
- 复制算法
- 标记-整理算法
- 分代收集算法
4. Hotspot的算法实现
- 枚举根节点
从可达性分析中从GC Roots节点找引用链这个操作为例,可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表),现在很多应用,方法区就有几百兆,如果要逐个检查里面的引用,那么必然消耗很多时间。
另外,可达性的分析关键在GC停顿上,因为在分析时,必须保证对象的引用之间不能变动。这点是导致GC进行时,必须停顿所有JAVA执行线程的其中一个重要的原因。
JVM通过一组称为OopMap的数据结构来达到直接得知哪些地方存放着对象引用。 - 安全点
在OomMap的协助下, Hotspot可以快速切准确完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本会非常高。
Hotspot虚拟机并没有为每条指令都生成OopMap,只是在特定的位置记录信息,这些位置称为安全点,即程序执行时并非在所有地方都能停下来开始GC,而是在到达安全点才能暂停。
安全点解决了如何进入GC的问题。 - 安全区域
安全点并不能完全解决如何进入GC的问题,因为存在空闲的时候,典型的例子是线程属于sleep阶段。此时就需要安全区域来解决此问题。安全区域是指在一段代码片段中,引用关系不会发生变化。在此区域的任意地方GC都是安全的,也可以看成是安全点的扩展。
5. 垃圾收集器
Serial收集器
Serial是最基本,发展最悠久的收集器。此收集器在执行垃圾收集的时候回暂停用户线程。虽然现在有很多新的收集器,Serial似乎并没有什么用,因为会导致用户线程暂停,但直到目前为止,它依然是虚拟机运行在Client模式下的默认新生代的收集器。ParNew收集器
ParNew收集器其实是Serial收集器的多线程版本。虽然和Serial收集器对比没有太多的创新之处,但是它却是许多运行在 Server模式下的虚拟机首选的垃圾收集器,其中有一个与性能无关的原因是:除了Serial收集器外,只有它能和CMS收集器配合工作。Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器,也是一个使用复制算法的收集器,又是并行的多线程收集器……它的关注点和其它收集器不一样,它的关注点是达到一个可控制的吞吐量(运行用户代码的时间/(运行用户代码的时间+垃圾收集时间))。-XX:MaxGCPauseMillis参数设置最大垃圾收集停顿时间,-XX:GCTimeRatio设置吞吐量大小。-XX:+UseAdaptiveSizePolicy设置虚拟机自动调节新生代大小(-Xmn)、Eden与 Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数。Serial Old收集器
是Serial收集器的老年代版本,单线程收集器,使用标记-整理算法。 它主要是给Client模式下使用的。Parallel Old收集器
是Parallel Scavenge收集器的老年代版本。使用多线程和标记-整理算法。主要是配合Parallel Scavenge收集器使用。CMS收集器
Concurrent Mark Sweep 是一种已获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现(初始标记,并发标记,重新标记,并发清除)。优点:并发收集,低停顿;但是CMS收集器存在3个明显的缺点,一:CMS收集器对CPU资源非常敏感,因为所有面向并发的程序设计对CPU资源都非常敏感。G1收集器
当今收集器技术最前沿的成果之一,G1收集器是面向服务端的收集器,它的使命是在未来替换掉JDK1.5中的CMS收集器。
特点,一:并行与并发;二:分代收集;三:空间整合,整体来看是基于CMS收集器的“标记-整理”算法,从局部来看(两个Region之间)来看是基于“复制”算法实现的,这两种算法都不会产生内存碎片;四:可预测的停顿
在G1收集器之前的其他收集器的收集范围都是整个新生代和老年代,而G1不是这样。使用G1收集器时,Java堆的内存布局就与其他收集器就有很大区别。他将Java堆规划为 多个大小相等的区域(Region),新生代和老年代的概念还存在,但是他们不再是物理隔离的,他们都是一部分Region(不需要连续)的集合。垃圾收集器参数总结
-XX:+< option > 启用选项
-XX:-< option > 不启用选项
-XX:< option > = < number >
-XX:< option > = < string >
参数名 | 参数描述 |
---|---|
-XX:+UseSerialGC | Jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 |
-XX:+UseParNewGC | 打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收 |
-XX:+UseConcMarkSweepGC | 使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。 |
-XX:+UseParallelGC | Jvm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行回收 |
-XX:+UseParallelOldGC | 使用Parallel Scavenge + Parallel Old的收集器组合进行回收 |
-XX:SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1 |
-XX:PretenureSizeThreshold | 直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
-XX:MaxTenuringThreshold | 晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代 |
-XX:UseAdaptiveSizePolicy | 动态调整java堆中各个区域的大小以及进入老年代的年龄 |
-XX:+HandlePromotionFailure | 是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留 |
-XX:ParallelGCThreads | 设置并行GC进行内存回收的线程数 |
-XX:GCTimeRatio | GC时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效 |
-XX:MaxGCPauseMillis | 设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效 |
-XX:CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值为68%,仅在CMS收集器时有效,-XX:CMSInitiatingOccupancyFraction=70 |
-XX:+UseCMSCompactAtFullCollection | 由于CMS收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效 |
-XX:+CMSFullGCBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与UseCMSCompactAtFullCollection参数一起使用 |
-XX:+UseFastAccessorMethods | 原始类型优化 |
-XX:+DisableExplicitGC | 是否关闭手动System.gc |
-XX:+CMSParallelRemarkEnabled | 降低标记停顿 |
-XX:LargePageSizeInBytes | 内存页的大小不可设置过大,会影响Perm的大小,-XX:LargePageSizeInBytes=128m |