java垃圾回收

垃圾回收主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。

1.判断一个对象是否可被回收

引用计数算法

为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

根搜索算法

以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:

  • 虚拟机栈中局部变量表中引用的对象
  • 本地方法栈中 JNI 中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象

实际上,在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与 GC Roots 相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize()方法。当对象没有覆盖 finalize()方法,或 finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果该对象被判定为有必要执行 finalize()方法,那么这个对象将会被放置在一个名为 F-Queue 队列中,并在稍后由一条由虚拟机自动建立的、低优先级的 Finalizer 线程去执行 finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的 finalize()方法最多只会被系统自动调用一次),稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果要在 finalize()方法中成功拯救自己,只要在 finalize()方法中让该对象重引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。

2.垃圾收集算法

标记—清除算法

标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。
不足:标记和清除过程效率都不高;会产生大量不连续的内存碎片,导致无法给大对象分配内存。

标记—整理算法

该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点:不会产生内存碎片。
不足:需要移动大量对象,处理效率比较低。

复制

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
不足:只使用了内存的一半。

分代收集

当前商业虚拟机的垃圾收集 都采用分代收集,它根据对象的存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。

3.垃圾收集器

垃圾收集器是内存回收算法的具体实现,Java 虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别。Sun HotSpot 虚拟机包含了如下收集器:新生代收集器:Serial、ParNew、Parallel Scavenge;老年代收集器:Serial Old、Parallel Old、CMS;整堆收集器:G1;



两个收集器间有连线,表明它们可以搭配使用:
Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案。

并发垃圾收集和并行垃圾收集的区别
  • 并行(Parallel)
    指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
    如ParNew、Parallel Scavenge、Parallel Old;
  • 并发(Concurrent)
    指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);
    用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
    如CMS、G1(也有并行);
Serial收集器

Serial(串行,单线程)垃圾收集器是最基本、发展历史最悠久的收集器; JDK1.3.1前是HotSpot新生代收集的唯一选择;
它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
设置参数 -XX:+UseSerialGC来显式的使用串行垃圾收集器。

ParNew收集器

它是 Serial 收集器的多线程版本。
它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
-XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器;
-XX:+UseParNewGC:强制指定使用ParNew;
-XX:ParallelGCThreads:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

Parallel Scavenge收集器

与 ParNew 一样是多线程收集器。
其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
-XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,大于0的毫秒数;
MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;
因为可能导致垃圾收集发生得更频繁;
-XX:GCTimeRatio:设置垃圾收集时间占总时间的比率,0 GCTimeRatio相当于设置吞吐量大小;垃圾收集执行时间占应用程序执行时间的比例的计算方法是:1 / (1 + n);默认值是1%--1/(1+99),即n=99;
-XX:+UseAdptiveSizePolicy : 开启这个参数后,就不用手工指定一些细节参数,如:
新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

Serial Old收集器

Serial Old是 Serial收集器的老年代版本,也是给 Client 场景下的虚拟机使用。

Parallel Old收集器

Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本;JDK1.6中才开始提供。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
-XX:+UseParallelOldGC:指定使用Parallel Old收集器。

CMS收集器

并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器。
分为以下四个流程:

  • 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
  • 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
  • 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,采用多线程并行执行来提升效率,需要停顿。
  • 并发清除:回收所有的垃圾对象,不需要停顿。
    整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作; 所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行。
    -XX:+UseConcMarkSweepGC:指定使用CMS收集器。

CMS收集器3个明显的缺点

  • 对CPU资源非常敏感:并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。
  • 无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败:浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
  • 产生大量内存碎片:往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
G1收集器

G1(Garbage-First),是JDK7-u4才推出商用的收集器,它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。



通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

G1收集器运作过程
不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。

  • 初始标记(Initial Marking):仅标记一下GC Roots能直接关联到的对象;
    且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;需要停顿,但速度很快。
  • 并发标记(Concurrent Marking):进行GC Roots Tracing的过程;刚才产生的集合中标记出存活对象;耗时较长,但应用程序也在运行;并不能保证可以标记出所有的存活对象。
  • 最终标记(Final Marking):为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;上一阶段对象的变化记录在线程的Remembered Set Log;这里把Remembered Set Log合并到Remembered Set中;需要停顿,且停顿时间比初始标记稍长,但远比并发标记短;采用多线程并行执行来提升效率。
  • 筛选回收(Live Data Counting and Evacuation):首先排序各个Region的回收价值和成本;然后根据用户期望的GC停顿时间来制定回收计划;最后按计划回收一些价值高的Region中垃圾对象; 回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;
    可以并发进行,降低停顿时间,并增加吞吐量。

4.内存分配与回收策略

内存分配策略
  • 对象优先在 Eden 分配
    大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
  • 大对象直接进入老年代
    大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
    经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
    -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
  • 长期存活的对象进入老年代
    为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
    -XX:MaxTenuringThreshold 用来定义年龄的阈值。
  • 动态对象年龄判定
    虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
  • 空间分配担保
    在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
    如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
Full GC 的触发条件
  • 调用 System.gc()
  • 老年代空间不足
  • 空间分配担保失败
  • JDK 1.7 及以前版本的永久代空间不足
  • CMS收集时发生Concurrent Mode Failure错误

参考资料
周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版
深入理解 JVM(3)——7 种垃圾收集器
深入理解 JVM(2)——GC 算法与内存分配策略

你可能感兴趣的:(java垃圾回收)