众所周知,Java程序不用像C++程序在程序中自行处理内存的回收释放。这是因为Java在JVM虚拟机上增加了垃圾回收(GC)机制,用以在合适的时间触发垃圾回收,将不需要的内存空间回收释放,避免无限制的内存增长导致的OOM。作为一个合格的Java程序员,有必要了解Java GC相关知识。掌握GC知识一方面可以帮助我们快速排查因JVM导致的线上问题,另一方面也可以帮助我们在Java应用发布之前合理地对JVM进行调优,提高应用的执行效率、可靠性和健壮性。
本文章的前半部分主要讲解一些前置知识。关于JVM中的集中收集器在第三部分。
所有的垃圾收集算法都面临同一个问题,那就是找出应用程序不可到达的内存块,将其释放,这里面讲的不可达主要是指应用程序已经没有内存块的引用了, 在Java中,某个对象对应用程序是可到达的是指:这个对象被根(根主要是指类的静态变量,或者活跃在所有线程栈的对象的引用)引用或者对象被另一个可到达的对象引用。
引用计数算法是垃圾回收器中的早期策略,在这种方法中,堆中的每个对象实例都有一个引用计数器,当一个对象被创建时,且该对象实例分配给一个变量,该变量计数设置为1 ,当任何其他变量赋值为这个对象的引用时,计数加1 ,(a=b ,则b引用的对象实例计数器+1)但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1,任何引用计数器为0 的对象实例可以当做垃圾收集。 当一个对象的实例被垃圾收集是,它引用的任何对象实例的引用计数器减1。
但是这种方法对于存在循环引用的情况并不能很好的解决,所以JVM并没有使用这种方法
在主流的商用语言中,都是使用可达性分析算法来判断对象是否存活的。这个算法是通过一系列的GC Roots
对象为起点,从这些对象开始朝下分析引用链,当某个对象通过这些引用链不可达时,就说明这个对象需要回收了。
java中可作为GC Root的对象有
1.虚拟机栈中引用的对象(本地变量表)
2.方法区中静态属性引用的对象
3. 方法区中常量引用的对象
4.本地方法栈中引用的对象(Native对象)
在JDK1.2之前,对引用的定义是:如果reference类型的值代表的是另外一块内存的起始,就称这块内存代表着一个引用。这种定义下,对象只有引用或者没有引用两种状态,对于描述一些“食之无味,弃之可惜”的对象就显得无能为力。所以Java对应用的概念进行了扩充,将引用分为强软弱虚四种:
引用 | 说明 |
---|---|
强引用 | 只要引用存在,就永远不会回收对象 |
软引用 | 用来描述一些还有用但是非必须的对象。对于软引用关联的对象,虚拟机发出内存溢出异常之前, 将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够内存,才会抛出内存溢出异常。 在JDK1.2之后,提供了SoftReference来实现软引用 |
弱引用 | 弱引用也是用来描述非必要对象的,但是强度比软引用更弱一些。 被弱引用关联的对象只能生存到下一次垃圾收集之前。 当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。 在JDK1.2之后,提供了WeakReference来实现弱引用 |
虚引用 | 也称为幽灵引用,它是最弱的一种引用关系。 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例。 为一个对象设置一个虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知。 在JDK1.2之后,提供了PhantomReference类来实现虚引用。 |
即使在可达性分析算法中不可达的对象,也不是非死不可的。真正宣告一个对象的死亡,需要经历两个标记过程。
在经过可达性分析后,如果对象没有与GC Root相连的引用链,那它将会被第一次标记并进行一次筛选。筛选的条件是此对象是否有必要执行finalize
方法。如果该对象没有覆盖finalize
方法或finalize方法已经被虚拟机调用过,则JVM将这两种情况视为没有必要执行。
如果对象被判定为有必要执行finalize方法,那么这个对象将会被放入到一个名为F-Queue
队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里的“执行”是指JVM会触发这个方法,但不会保证它运行结束。
稍后GC将会对F-Queue中的对象进行第二次小规模的标记。如果对象重新与引用链建立了关系,那么在第二次回收时该对象将会被移除“即将回收”的集合。
注意:finalize方法并不是C++中的析构函数,而是Java在刚诞生时为了让C/C++程序员更容易接受它的一个妥协。如果要做“关闭外部资源”的工作,使用try-finally
可能更适合,所以尽量不要使用finalize方法。
标记-清除(Mark-Sweep)算法分为两个阶段:
*
复制算法的思想是将内存分成大小相等的两块区域,每次使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块区域上,然后对该块进行内存回收。示例图如下所示:
这个算法实现简单,并且也相对高效,但是代价就是需要将牺牲一半的内存空间用于进行复制。有研究表明,新生代中的对象98%存活期很短,所以并不需要以1:1的比例来划分整个新生代,通常的做法是将新生代内存空间划分成一块较大的Eden区和两块较小的Survivor区,两块Survivor区域的大小保持一致。每次使用Eden和其中一块Survivor区,当回收的时候,将还存活的对象复制到另外一块Survivor空间上,最后清除Eden区和一开始使用的Survivor区。假如用于复制的Survivor区放不下存活的对象,那么会将对象存到老年代。
HotSpot虚拟机默认Eden和Survivor的大小比例是8:1:1,也就是说新生代中牺牲掉10%的空间而不是一半的空间。
标记-整理(Mark-Compact)算法有效预防了标记-清除算法中可能产生过多内存碎片的问题。在标记需要回收的对象以后,它会将所有存活的对象空间挪到一起,然后再执行清理。示例图如下所示:
标记-整理通常会在标记-清除算法里面作为备选方案,为了防止标记-清除后产生大量内存碎片而无法为大对象分配足够内存的情况,如后面所讲的Serial Old收集器(基于标记-整理算法实现)将会作为CMS收集器(基于标记-清除算法实现)的备选方案。
*
我们一般讨论的垃圾回收主要针对Java堆内存中的新生代和老年代,也正因为新生代和老年代结构上的不同,所以产生了分代回收算法,即新生代的垃圾回收和老年代的垃圾回收采用的是不同的回收算法。针对新生代,主要采用复制算法,而针对老年代,通常采用标记-清除算法或者标记-整理算法来进行回收。
为什么要分不同的 GC 类型,主要是:
1、对象有不同的生命周期,经研究,98%的对象都是临时对象;
2、根据各代的特点应用不同的 GC 算法,提高 GC 效率。
在JVM五种内存模型中,有三个是不需要进行垃圾回收的:程序计数器、JVM栈、本地方法栈。因为它们的生命周期是和线程同步的,随着线程的销毁,它们占用的内存会自动释放,所以只有方法区和堆需要进行GC。
我们知道代码是在线程里执行的, GC的代码也是在线程里执行, 如果执行GC的时候其他线程也同时执行的话, heap的状态将是难以追踪的.。所以需要在一个引用状态不会变化的时间点进行GC,关于安全点和安全区域的内容,可以参考如下文章:
关于OopMap、SafePoint(安全点)以及安全区域
接下来将讨论JDK1.7u14之后由hotspot虚拟机提供的七种收集器(在这个版本中正式提供了商用的G1收集器,之前G1仍处于实验状态)。各个收集器的关系可以参考下图:
下面通过表格展现了各个收集器的原理及优缺点
-
单线程
=
多线程
c
copying 复制算法
mc
mark-compact 标记-整理算法
ms
mark-sweep 标记-清除算法
浮动垃圾:在并发清除过程中,程序还在运行,可能产生新的垃圾,但是本次 GC 确不可能清除掉这些新产生的垃圾了,所以这些新产生垃圾就叫浮动垃圾,也就是说在一次 CMS 的 GC 后,用户获取不到一个完全干净的内存空间,还是或多或少存在浮动垃圾的。
参数 | 含义 |
---|---|
-XX:PretenureSizeThreshold | 大于这个大小的对象直接进入老年代 |
-XX:MaxTenuringThreshold | GC年龄,经过这么多次GC后的对象将进入老年代 |
-XX:PrintGCDetails | 打印GC日志 |
UseSerialGC | |
UseParNewGC | |
UseConcMarkSweepGC | |
UseParallelGC | |
SurvivorRatio | 新生代Eden与Survivor的比值。默认为8,即Eden:Survivor=8:1 |
UseAdaptiveSizePolicy | 动态调整堆中各个区域的大小以及进入老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败 |
ParallelGCThreads | |
GCTimeRatio | GC占总时间的比率,默认99%,即允许1%的GC时间。仅在使用ParallelScavenge收集器时生效 |
MaxGCPauseMillis | 设置最大GC停顿时间。 仅在使用ParallelScavenge收集器时生效 |
CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认为68%。 |
UseCMSCompactAtFullCollection | 设置CMS在进行完一次垃圾收集后是否要进行一次内存碎片整理。仅在使用CMS收集器时生效 |
CMSFullGCsBeforeCompaction | 设置CMS在经过若干次GC后再进行碎片整理。仅在使用CMS收集器时生效 |
《深入理解Java虚拟机 - JVM高级特性与最佳实践》(周志明著)
Java GC 介绍
Java Garbage Collection Basics
云栖社区: Java GC
JVM调优:选择合适的GC collector
(要是CSDN的版面能再宽一点就好了Ծ‸ Ծ )