深入理解java虚拟机之垃圾回收算法和垃圾回收器

Garbage Collection

上一篇文章我们介绍了jvm中的内存模型,其中PCR、虚拟机栈(VM stack)、本地方法栈(Native Stack)是线程独立的,线程结束之后就会自动消失。java堆和方法区是线程共享的,所以需要在一个方法结束之后进行垃圾回收。
首先,我们来学习一下如何判断对象已死:

  1. 引用计数算法(Reference Counting) :给类的对象添加一个计数器,出现一个新的引用,就加一,如果该引用失效,就减一。任何时刻计数器为0的对象就是不可能再被使用的。
    两个对象会出现循环引用问题,此时引用计数器永远不为 0,导致 GC 收集器无法回收。

    objA.instance = objB; objB.instance = objA;

  2. 根搜索算法(GC Roots Tracing) (被java C# Lisp等语言采用的算法)算法基本思路就是通过一系列的称为“GC Roots”的点作为起始进行向下搜索,当一个对象到GC Roots没有任何引用链(Reference Chain)相连,则证明此对象是不可用的。

在Java语言中,GC Roots包括:
  1.在VM栈(帧中的本地变量)中的引用
  2.方法区中的静态引用(类静态属性引用)和常量引用
  3.JNI(即一般说的Native方法)中的引用

方法区的回收:

JDK8中已经不叫永久代Perm,叫做元空间,在本地内存分配。原来方法区中的常量池已经放到了堆中,类元信息、静态方法和变量、常量、字段等放在元空间中。

对于方法区(永久代)来说,对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,HotSpot 虚拟机把它当成永久代来进行垃圾回收。这两点回收思想与Java堆中的对象回收很类似,都是搜索是否存在引用,常量的相对很简单,与对象类似的判定即可。而类的回收则比较苛刻,需要满足下面3个条件:

  1. 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
  2. 加载该类的ClassLoader已经被GC。
  3. 该类对应的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法。
      是否对类进行回收可使用-XX:+ClassUnloading参数进行控制,还可以使用-verbose:class或者-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载、卸载信息。
      在大量使用反射、动态代理、CGLib等bytecode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要JVM具备类卸载的支持以保证永久代不会溢出。

finalize()

当一个对象可被回收时,如果该对象有必要执行 finalize() 方法,那么就有可能可能通过在该方法中让对象重新被引用,从而实现自救。
finalize() 类似 C++ 的虚构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。

垃圾回收算法:

1.标记清除算法:标记+(分别)清除:效率不高;且易产生大量的不连续的内存碎片,导致后续使用中无法找到足够的连续内存。

深入理解java虚拟机之垃圾回收算法和垃圾回收器_第1张图片

2.标记整理算法Mark-Compact:标记之后,存活对象向一边移动,直接清理边界以外的内存。

深入理解java虚拟机之垃圾回收算法和垃圾回收器_第2张图片

3.复制算法:将内存分为两块,每次使用其中一块,另一块作为回收时的替换,只需要移动堆顶的指针,按照顺序分配内存。代价较高。

深入理解java虚拟机之垃圾回收算法和垃圾回收器_第3张图片
现在的商业虚拟机中都是用了这一种收集算法来回收新生代,IBM有专门研究表明新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的eden空间和2块较少的survivor空间,每次使用eden和其中一块survivor,当回收时将eden和survivor还存活的对象一次过拷贝到另外一块survivor空间上,然后清理掉eden和用过的survivor。**Sun Hotspot虚拟机默认eden和survivor的大小比例是8:1,**也就是每次只有10%的内存是“浪费”的。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有10%以内的对象存活,当survivor空间不够用时,需要依赖其他内存(譬如老年代)进行分配担保(Handle Promotion)。

4.分代收集算法

现在的商业虚拟机采用分代收集算法,它使用了前面介绍的几种收集算法,根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将 Java 堆分为新生代和老年代。

  1. 新生代使用:复制算法
  2. 老年代使用:标记 - 清理 或者 标记 - 整理 算法。

垃圾回收器

垃圾回收器是垃圾回收算法的具体实现,不同的虚拟机会提供不同的垃圾回收器,并且根据参数供用户提供根据自己的应用特点和要求组合各个年代所使用的收集器。
一般我们用基于Sun Hotspot虚拟机1.6版,其中提供了6中不同年代的收集器。
连线表示垃圾收集器可以配合使用,没有最好的收集器,只有最合适的收集器。
深入理解java虚拟机之垃圾回收算法和垃圾回收器_第4张图片

1.Serial收集器

单线程收集器,收集时会暂停所有工作线程(我们将这件事情称之为Stop The World,下称STW),使用复制收集算法,虚拟机运行在Client模式时的默认新生代收集器。
是单线程效率最高的收集器算法
深入理解java虚拟机之垃圾回收算法和垃圾回收器_第5张图片

2.ParNew收集器

ParNew收集器就是Serial的多线程版本,除了使用多条收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一摸一样。对应的这种收集器是虚拟机运行在Server模式的默认新生代收集器,在单CPU的环境中,ParNew收集器并不会比Serial收集器有更好的效果。
  是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
默认开始的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。
深入理解java虚拟机之垃圾回收算法和垃圾回收器_第6张图片

3.Parallel Scavenge收集器

Parallel Scavenge收集器(下称PS收集器)也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而
高吞吐量则可以高效率地利用 CPU
时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数(值为大于 0 且小于 100 的整数)。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。自适应调节策略也是它与 ParNew 收集器的一个重要区别。

4.Serial Old收集器

Serial Old是单线程收集器,使用标记-整理算法,是老年代的收集器,上面三种都是使用在新生代收集器。
深入理解java虚拟机之垃圾回收算法和垃圾回收器_第7张图片
Serial Old回收器也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:
a.在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
b.作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。

5.Parallel Old收集器

老年代版本吞吐量优先收集器,使用多线程和标记-整理算法,JVM 1.6提供,在此之前,新生代使用了PS收集器的话,老年代除Serial Old外别无选择,因为PS无法与CMS收集器配合工作。
深入理解java虚拟机之垃圾回收算法和垃圾回收器_第8张图片

6.CMS(Concurrent Mark Sweep)收集器

CMS是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率最高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,这一点对于实时或者高交互性应用(譬如证券交易)来说至关重要,这类应用对于长时间STW一般是不可容忍的。CMS收集器使用的是标记-清除算法,也就是说它在运行期间会产生空间碎片,所以虚拟机提供了参数开启CMS收集结束后再进行一次内存压缩。
  并发收集,低停顿

分为以下四个流程:
	初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
	并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
	重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
	并发清除:不需要停顿。

在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
深入理解java虚拟机之垃圾回收算法和垃圾回收器_第9张图片
具有以下缺点:
1.对CPU敏感:CMS 默认启动的回收线程数是 (CPU 数量 + 3) / 4,当 CPU 不足 4 个时,CMS 对用户程序的影响就可能变得很大,如果本来 CPU 负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了 50%,其实也让人无法接受。并且低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率变低。
2.无法处理浮动垃圾。由于并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留到下一次 GC 时再清理掉,这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此它不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。可以使用 -XX:CMSInitiatingOccupancyFraction 的值来改变触发收集器工作的内存占用百分比,JDK 1.5 默认设置下该值为 68,也就是当老年代使用了 68% 的空间之后会触发收集器工作。如果该值设置的太高,导致浮动垃圾无法保存,那么就会出现 Concurrent Mode Failure,此时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集。
3.标记 - 清除算法导致的空间碎片,给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前出发一次 Full GC。

**G1(Garbage-First)**收集器

是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot 开发团队赋予它的使命是(在比较长期的)未来可以替换掉 JDK 1.5 中发布的 CMS 收集器。
深入理解java虚拟机之垃圾回收算法和垃圾回收器_第10张图片
G1收集器具有以下特点:

  1. 并行与并发:能充分利用多 CPU 环境下的硬件优势,使用多个 CPU 来缩短停顿时间;
  2. 分代收集:分代概念依然得以保留,虽然它不需要其它收集器配合就能独立管理整个 GC ,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次 GC 的旧对象来获取更好的收集效果。
  3. 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
  4. 可预测的停顿:这是它相对 CMS可预测的停顿:这是它相对 CMS 的一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒,这几乎已经是实时 Java(RTSJjava 的Real Time Specification For Java)的垃圾收集器的特征了。
    G1收集器将整个 Java 堆划分为多个大小相等的独立区域(Region)。虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分 Region(不需要连续)的集合。
    之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。它跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是 Garbage-First 名称的来由)。
    这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了它在有限的时间内可以获取尽可能高的收集效率。
    Remembered Set
    Region中的某一个对象都是可以和java堆中的任意对象发生引用关系,那么我们做可达性分析判断对象是否存活的时候,就需要扫描整个java堆去保证准确性,效率极低。
    Remembered Set就是与每一个region对应的记录引用信息的区域。
    虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中,如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 之中。当进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏。
    如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
    1.初始标记
    2.并发标记
    3.最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
    4.筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
    深入理解java虚拟机之垃圾回收算法和垃圾回收器_第11张图片

部分转载自:https://blog.csdn.net/a1342772/article/details/77688148
关于内存回收策略:《深入理解java虚拟机之 内存分配和回收策略》

你可能感兴趣的:(Java)