垃圾回收器基础:可达性分析、方法区的回收、垃圾回收算法和GC策略

导读:本篇文章讲解 垃圾回收器基础:可达性分析、方法区的回收、垃圾回收算法和GC策略,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

目标:先了解垃圾回收器的基础知识,对GC有一个基础的认识,以便之后理解不同垃圾回收器的特点,即后续根据环境对JVM的设置和调整。

内容概述:先了解GC时回收对象是如何判断的,然后是方法区进行那些垃圾回收。因为不同的对象有不同的回收策略我们接着看一些垃圾算法,以及垃圾回收策略。

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

1.1. 引用计数法

给对象添加一个计数器,当对象增加一个引用时计数器加 1,引用失效时减 1。计数为 0 时,对象可被回收。
当两个对象出现循环引用时,计数器永远不为 0,导致无法对它们进行回收。

正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
 

1.2. 可达性分析

以GC Roots的根对象作为起点,根据对象的引用向下搜索,能搜索到的对象即为不可回收对象,搜索过程所走过的路径称为:引用链。
 
如果某个对象向上找不到任何GC Roots对象,则对象不可达,对象被回收。

如下图:
在这里插入图片描述

GC Roots 指的是局部变量,或者类的静态变量,具体如下:

方法区的:常量引用的变量、类中static变量引用的对象
栈(本地变量表)中引用的(正在被使用)对象
本地方法栈中引用的对象(native修饰的)

 

1.3 引用类型与垃圾回收

有 GC Roots 的对象,如果是软引用或者弱引用,也有可能被回收

Java中有强引用、软引用、弱引用和虚引用:

强引用,被强引用的对象不会被回收。
软引用,内存不足时会回收。
弱引用,弱引用就像没有被引用,GC时直接回收
虚引用,很少用到需引用,GC时直接回收

看一个例子:

 //强引用
 String S = "强 引用 ";
 //软引用
 SoftReference<String> stringSoftReference = new SoftReference<>(new String("软 引用 仔"));
 System.out.println(stringSoftReference.get());
 System.gc(); //通知JVM进行内存回收
 System.out.println(stringSoftReference.get());
 //弱引用
 WeakReference<String> str = new WeakReference<>("弱reference ");
 System.out.println(str.get());
 System.gc();  
 System.out.println(str.get());
 //虚引用
 ReferenceQueue<Object> queue = new ReferenceQueue<>();
 PhantomReference<String> strs = new PhantomReference<>("虚引用 ", queue);
 System.out.println(strs.get());
 System.out.println(S);
 

软 引用 仔
软 引用 仔
弱reference 
null
null
强 引用 

 

1.4 通过finialize() 进行自救

一个对象没有被 GC Roots 引用,是否立即就会被回收呢?

也不一定立即被回收,该类可以重写finialize()方法,在finialize()中让一个 GC Roots 重新引用这个对象,就能自救

2. 方法区的回收

主要是对常量池的回收和对类的卸载。

在大量使用反射、动态代理、CGLib 、动态生成 JSP 以及 OSGi 场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出

类的卸载条件:

堆中不含有该类的任何实例
该类的classloader被回收
该类对应的class对象没有被任何地方所引用,即没有通过反射访问该类方法。

不过满足也不一定被卸载,可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。

 

3. 垃圾回收算法

算法 解释
标记-清除 在这里插入图片描述

将存活的对象标记,然后清除未标记的对象。

缺点:标记、清除效率不高,且会产生不连续的内存碎片,导致无法给大对象分配内存

标记-整理
在这里插入图片描述

所有存活对象都向一端移动,然后直接清理掉端边界外的对象。实现了内存连续

复制 在这里插入图片描述

将内存划分为两块,每次只使用其中一块,内存用完将存活的对象复制到另外上,然后进行清理。

HotSpot虚拟机中Eden和S区比例是:8:1:1,内存的利用率达到90%,如果每次回收有多于10%的存活对象,此时要借用老年代来存储放不下的对象。

分代收集
根据对象存活周期将内存划分为几块,不同区域采用不同算法。

一般堆分为新生代和老年代:新生代使用复制算法;老年代使用标记-清除或标记-整理算法。

 

4. 7种垃圾回收器

在这里插入图片描述
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。大体可以按照多线程与否、串行并行分类

单线程与多线程收集器:即使用单个线程收集,还是多个线程收集。
串行与并行收集器:

  • 串行:收集器与程序交替运行,这代表GC时用户程序是停止的(即STW(stop the world));
  • 并行:收集器和程序同时执行。除了CMS 和 G1之外,其他都是串行的。

 

4.1. Serial + Serial Old

Serial:
是单线程、串行的垃圾回收器(出现STW)、复制算法。
 
Client 模式下的默认新生代收集器
缺点:会造成STW,在用户不知情的情况下把用户的正常的执行的工作线程的全部暂停
优点:对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率

Serial Old:
Serial收集器的老年代版本,单线程、串行,使用标记-整理算法,主要在client模式下使用。
 
用在server模式下的两大用途:

  • JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
  • 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。

在这里插入图片描述

 

4.2. ParNew + Serial Old

ParNew:
Serial多线程版本、串行收集(会发生STW)、复制算法。
Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了Serial 收集器,只有它能与 CMS 收集器配合工作。
 
默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。

在这里插入图片描述

虚拟机server模式和client模式
 
它们的主要区别如下:

  1. JVM在client模式默认-Xms是1M,-Xmx是64M;JVM在Server模式默认-Xms是128M,-Xmx是1024M;

  2. Server启动慢,编译更完全,针对服务端应用优化,在服务器环境中最大化程序执行速度而设计;Client启动快速,内存占用少,编译快,针对桌面应用程序优化,为在客户端环境中减少启动时间而优化;

 
当不指定运行模式参数时,虚拟机启动检测主机是否为服务器,如果是,则以Server模式启动,否则以Client模式启动(J2SE5.0检测的根据是至少2个CPU和最低2GB内存)

 

4.3. Parallel Scavenge 与 Parallel Old -注重吞吐量的回收器

Parallel Scavenge
是多线程收集器,和ParNew一样使用的是都是复制算法进行新生代的垃圾回收。
此收集器目标是达到一个可控制的吞吐量。因此也称为吞吐量优先的收集器。

Parallel Old
是 Parallel Scavenge 的老年代版本,使用多线程和“标记 – 清除”算法进行垃圾回收。
在注重吞吐量和CPU资源敏感的情况下,可以优先使用两者的结合。如下图

在这里插入图片描述

 

4.3. CMS 收集器(并发的标记-清除收集器)

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 – 清除算法。

分为四个步骤:

初始标记:mark下GC Roots能直接关联的对象,需要停顿;
并发标记:进行GC Roots Tracing,整个回收耗时最长,不需要停顿;
重新标记:修正并发标记期间,因程序继续运作导致的一些变动标记;
并发清除:耗时较长,不需要停顿。

在这里插入图片描述
 
CMS为什么不使用标记整理算法?

因为当并发清除时,如果用Compact整理内存,原来的用户线程使用的内存将无法使用。所以要保证用户线程能继续执行,前提的它运行的资源不受影响。Mark Compact更适合“stop the world”。

 

缺点:

  1. 吞吐量低:因为有停顿时间,导致CPU利用率低,牺牲了吞吐量。
  2. 可能出现 Concurrent Mode Failure:由于并发清除阶段程序会继续运行,这期间可能会产生垃圾,称为浮动垃圾,这部分只能等到下一次GC时才能回收。因为浮动垃圾的存在,所以需要预留出一部分内存存储浮动垃圾,如果不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时就需要 Serial Old来替代CMS。
  3. 标记 -清除算法会出现内存不连续情况,导致老年代有空间剩余,但是无法找到足够的连续空间来分配对象,导致提前触发Full GC。

 

4.4. G1收集器

G1 可以直接对新生代和老年代一起回收。

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
在这里插入图片描述

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

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:

  1. 初始标记
  2. 并发标记
  3. 最终标记:修正在并发标记阶段因程序继续运作而产生的一些变动标记,并记录在 Remembered Set Logs 里,随后合到Remembered Set中。这阶段需要STW。
  4. 筛选回收:对各个region中回收价值和成本排序,根据用户所期望的GC停顿时间来指定回收计划。

特点:

从整体看是基于“标记 – 整理”算法,从局部(两个region)看是基于复制算法,这意味着运行期间不会产生内存空间碎片。
可预测的停顿:能让使用者指定一个时间片段让GC的时间不超过这个时间。

参考:the G1 Garbage Collector

 
 

5. 垃圾回收策略:Minor GC、Major GC、Full GC

JVM 在进行 GC 时,并非每次都对堆内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。

策略 讲解
部分回收:新生代收集 (Minor GC/Young GC)
只是新生代的垃圾收集
部分回收:老年代收集(Major GC/Old GC)
只有 CMS GC 会有单独收集老年代的行为。

很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收

混合收集(Mixed GC)
收集整个新生代以及部分老年代的垃圾收集。

目前只有 G1 GC 会有这种行为

整堆收集(Full GC)
收集整个 Java 堆和方法区的垃圾

 
 

6. 内存分配策略

策略 说明
对象优先在 Eden 分配 大多数情况下,对象在新生代 Eden 区分配
大对象直接进入老年代
1. 大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

2. 经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

3.
-XX:PretenureSizeThreshold:大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。

长期存活的对象进入老年代
1. 为对象定义年龄计数器,对象存活一次年龄加一。

2. -XX:MaxTenuringThreshold 用来定义年龄的阈值。

空间分配担保
1. 发生 Minor GC 之前,虚拟机先检查
老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

2. 如果不成立则查看HandlePromotionFailure值,如果允许则检查老年代连续可用空间是否大于历次晋升到老年代对象的平均大小,如果大于尝试一次Minor GC;如果小于或者不允许,则进行一次Full GC

 

7. Minor GC 和 Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。

Full GC 则相对复杂,有以下条件:

条件 说明
调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
老年代空间不足
1. 大对象直接进入老年代、长期存活的对象进入老年代等都可能因为空间不足导致发生Full GC,所以尽量不要创建过大的对象以及数组。

2. 可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。

3. 通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

空间分配担保失败
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC
JDK 1.7 及以前的永久代空间不足
1.JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

2. 当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。

3.如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

4. 可采用的方法为增大永久代空间或转为使用 CMS GC。

 
 
参考:
https://pdai.tech/md/java/jvm/java-jvm-gc.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/65374.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!