JVM中具体是如何实现垃圾回收的机制?

2026-05-27 17:201阅读0评论SEO资源
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计3500个文字,预计阅读时间需要14分钟。

JVM中具体是如何实现垃圾回收的机制?

1. 如何判断对象可以被回收? - 引用计数法:定义:只要有一个对象引用,对象就不会被回收。引用计数+1,引用计数-1。

2. 引用计数法: - 定义:只有当一个对象被一个变量引用时,引用计数才+1;如果变量不再引用这个对象,引用计数才-1。

3. 定义: - 只有一个对象被变量引用,引用计数+1。 - 如果变量不再引用对象,引用计数-1。

一、如何判断对象可以可以被回收 1.1 引用计数法

 定义:只要一个对象被变量所引用,则该对象计数就+1,若被引用了两次,则它的引用计数就变为2,如果某一个变量不再引用它了,则它的引用计数就减一,当该对象的引用变为0的时候就表示没有变量引用它了,该对象就可以被当作垃圾回收了。

 弊端:当两个对象循环引用时候,但是又没有被别的变量引用,并且这两个对象不在有实用价值,这时,垃圾回收并不能够回收这两个对象,可能导致內存溢出。

1.2 可达性分析算法

 根对象:那些肯定不能被当成垃圾回收的对象

 定义:在垃圾回收之前,先对堆中的所有对象进行一次扫描,判断每一个对象是否被根对象直接或者间接的引用,如果是,则不能被回收,反之则可以被当成垃圾回收

 Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

 扫描堆中的对象,看是否能够沿着GC Root对象(一系列对象)为起点的引用链找到该对象,找不到,表示可以回收

 哪些对象可以作为GC Root ?

  虚拟机栈(栈帧中的本地变量表)中引用的对象。
  方法区中类静态属性引用的对象。.
  方法区中常量引用的对象。
  本地方法栈中JNI(即-般说的Native方法)引用的对象。

1.3 四种引用

 1.强引用
  只有所有GC Roots对象都不通过[强引用]引用该对象,该对象才能被垃圾回收
 2.软引用(SoftReference)
  仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
  可以配合引用队列来释放软引用自身
 3.弱引用(WeakReference)
  仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
  可以配合引用队列来释放弱引用自身
 4.虚引用(PhantomReference)
  必须配合引用队列使用,主要配合ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存
 5.终结器引用(FinalReference)
  无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收), 再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时
  才能回收被引用对象

二、垃圾回收算法 2.1 标记清除

  特点:速度较快、会造成內存碎片

2.2 标记整理

  特点:速度慢、没有內存碎片

2.3 复制

  特点:没有內存碎片、需要占用双倍内存空间

三、分代垃圾回收 3.1 分代垃圾回收机制

JVM中具体是如何实现垃圾回收的机制?

  对象首先分配在伊甸园区域
  新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄加1并且交换from to
  minor gc会引发stop the world, 暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  当对象寿命超过阈值时,会晋升至老年代,最大寿命是15 (4bit)
  当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc, stw的时间更长

3.2 相关VM参数

四、垃圾回收器 4.1串行 

  单线程
  堆内存较小,适合个人电脑

4.2吞吐量优先

  多线程
  堆内存较大,多核cpu
  让单位时间内,STW的时间最短0.2 0.2=0.4

4.3响应时间优先

  多线程
  堆内存较大,多核cpu
  尽可能让单次STW的时间最短0.1 0.10.1 0.10.1=0.5

4.4G1

适用场景
  同时注重吞吐量(Throughput) 和低延迟(Low latency) ,默认的暂停目标是200 ms
  超大堆内存,会将堆划分为多个大小相等的Region
  整体上是标记+整理算法,两个区域之间是复制算法
相关JVM参数
  -XX:UseG1GC
  -XX:G1HeapRegionSize=size
  -XX:MaxGCPauseMillis=time

G1垃圾回收阶段

1)Young Collection

2)Young Collection + CM

  在Young GC时会进行GC Root的初始标记
  老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
  -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

3)Mixed Collection 

会对E、S、O进行全面垃圾回收
  最终标记(Remark) 会STW
  拷贝存活(Evacuation) 会STW
  - XX:MaxGCPauseMillis=ms

FullGC

■SerialGC
  新生代内存不足发生的垃圾收集- minor gc
  老年代内存不足发生的垃圾收集- full gc
■ParallelGC
  新生代内存不足发生的垃圾收集- minor gc
  老年代内存不足发生的垃圾收集- full gc
■CMS
  新生代内存不足发生的垃圾收集- minor gc
  老年代内存不足:并发失败以后,才叫Full GC,否则不会触发Full GC
■G1
  新生代内存不足发生的垃圾收集- minor gc
  老年代内存不足:当老年代內存跟堆內存占比达到45%以上,会触发并发标记的阶段,以及后续混合收集的阶段,如果垃圾回收的速度比新产生的垃圾的速度要快,来的及打扫,这是还不叫Full GC,还是并发垃圾回收的阶段(也会有暂停,但是时间很短), 当垃圾回收的速度跟不上垃圾产生的速度,并发收集就会失败,转化为Full GC(并发进行),stw时间也会更长。

Young Collection 跨代引用

  卡表与Remembered Set
  在引用变更时通过post-write barrier + dirty card queue
  concurrent refinement threads更新Remembered Set

Remark

JDK 8u20 字符串去重

  优点:节省大量内存
  缺点:略微多占用了cpu时间,新生代回收时间略微增加
  -XX: +UseStringDeduplication 默认开启

  将所有新分配的字符串放入一个队列
  当新生代回收时, G1并发检查是否有字符串重复
  如果它们值一样,让它们引用同-一个char[]
  注意,与String. intern()不- -样
  String. intern()关注的是字符串对象
  而字符串去重关注的是char[]
  在JVM内部,使用了不同的字符串表

JDK 8u40 并发标记类卸载

  所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它
  所加载的所有类
  -XX:+ClassUnloadingWi thConcurrentMark默认启用

JDK 8u60 回收巨型对象

  一个对象大于region的一半时,称之为巨型对象
  G1不会对巨型对象进行拷贝
  回收时被优先考虑
  G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

JDK 9 并发标记起始时间的调整

  并发标记必须在堆空间占满前完成,否则退化为FullGC
  JDK9之前需要使用-XX:InitiatingHeap0ccupancyPercent
  JDK 9可以动态调整
   -XX: Initiat ingHeapOccupancyPercent用来设置初始值
   进行数据采样并动态调整
   总会添加一个安全的空档空间

JDK 9 更高效的回收

  250+增强

  180+bug修复

五、垃圾回收调优 

  预备知识
   掌握GC相关的VM参数,会基本的空间调整
   掌握相关工具
   明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则

5.1 调优领域

  ■内存
  ■锁竞争
  ■cpu占用
  ■io

5.2 确定目标

  [低延迟]还是[高吞吐量],选择合适的回收器
  CMS,G1(JDK9,在更大的內存下工作的比CMS要好),ZGC (低延迟)
  ParallelGC (高吞吐量)

  Zing(stw 0停顿、可管理超大的內存)

互联网项目主要是针对 低延迟

5.3 最快的GC是不发生GC

查看FullGC前后的内存占用,考虑下面几个问题
  数据是不是太多?
resultSet = statement.executeQuery("select * from大表limit n")
  数据表示是否太臃肿?
   对象图
   对象大小16 Integer 24 int 4
  是否存在内存泄漏?
   static Map map =
     软
   弱
   第三方缓存实现

5.4 新生代调优

新生代的特点

  所有的new操作的内存分配非常廉价
   TLABlthread-local allocation buffer
  死亡对象的回收代价是零
  大部分对象用过即死
  Minor GC的时间远远低于Full GC

  新生代能容纳所有[并发量* (请求-响应)]的数据

  幸存区大到能保留[当前活跃对象+需要晋升对象]

  升阈值配置得当,让长时间存活对象尽快晋升
  -XX:MaxTenuringThreshold=threshold
  -XX:+PrintTenuringDistribution

  

5.5 老年代调优

以CMS为例

  ■CMS的老年代内存越大越好
  ■先尝试不做调优,如果没有Full GC那么已经...否则先尝试调优新生代
  ■观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~ 1/3
  ■-XX: CMSInitiatingOccupancyFraction=percent

5.6 调优案例

案例1Full GC和Minor GC频繁

 分析:GC频繁说明空间紧张,究竟是哪一空间紧张呢,如果是新生代,当业务高峰期来了,大量的对象被创建,新生代的空间很快就满了,幸存区的空间紧张,那么它的最大晋升阈值就会降低,导致很多本来生存周期很短的对象,也会晋升到老年代去,这样情况就进一步恶化,这样就导致老年代的Full GC频繁发生。

 解决:通过检测工具去观察堆空间的大小,发现新生代的內存设置的太小了,先试着增大新生代的內存,新生代的內存充裕了之后,新生代的垃圾回收就变得不那么频繁了,同时增大了幸存区的空间,以及晋升阈值,这样就能够使得生命周期较短的对象尽可能的留在新生代里,这样就可以让老年代的FullGC也不那么频繁了。

案例2 请求高峰期发生Full GC,单次暂停时间特别长(CMS)

 分析:已确定垃圾回收器是CMS,先去查看GC日志,看看CMS哪个阶段耗时比较长,CMS在重新标记的时候要扫描整个堆内存,如果业务高峰期的时候,新生代的对象较多标记时间会变的很长。

 解决:在重新标记之前对新生代先做一次垃圾回收,减少新生代对象的数量,这样就可以减少在重新标记时所耗费的时间。 - XX:+CMSScavengeBeforeRemark

案例3 老年代充裕情况下发生Full GC(JDK1.7 CMS)

 分析:之前介绍过CMS可能由于空间不足,导致并发失败,或者是空间碎片比较多,会产生Full GC;若日志排查后,在GC日志里没有并发失败,碎片过多的错入提示,说明老年代的空间是充裕的,不是老年代空间不足产生的Full GC。从案例中可知项目所用的JDK为1.7,JDK1.8是元空间作为方法区的实现,JDK1.7及以前是永久代作为方法区的实现,JDK1.7以前的永久代空间不足也会导致Full GC,JDK1.8以后,元空间不再由JAVA控制,元空间的默认情况下他的內存空间是使用了操作系统的內存空间,空间的容量一般是比较充裕的。而JDK1.7以前永久代的空间如果设小了,就会导致触发整个堆的Full GC。

 解决:增大永久代的最大值和初始值。

本文共计3500个文字,预计阅读时间需要14分钟。

JVM中具体是如何实现垃圾回收的机制?

1. 如何判断对象可以被回收? - 引用计数法:定义:只要有一个对象引用,对象就不会被回收。引用计数+1,引用计数-1。

2. 引用计数法: - 定义:只有当一个对象被一个变量引用时,引用计数才+1;如果变量不再引用这个对象,引用计数才-1。

3. 定义: - 只有一个对象被变量引用,引用计数+1。 - 如果变量不再引用对象,引用计数-1。

一、如何判断对象可以可以被回收 1.1 引用计数法

 定义:只要一个对象被变量所引用,则该对象计数就+1,若被引用了两次,则它的引用计数就变为2,如果某一个变量不再引用它了,则它的引用计数就减一,当该对象的引用变为0的时候就表示没有变量引用它了,该对象就可以被当作垃圾回收了。

 弊端:当两个对象循环引用时候,但是又没有被别的变量引用,并且这两个对象不在有实用价值,这时,垃圾回收并不能够回收这两个对象,可能导致內存溢出。

1.2 可达性分析算法

 根对象:那些肯定不能被当成垃圾回收的对象

 定义:在垃圾回收之前,先对堆中的所有对象进行一次扫描,判断每一个对象是否被根对象直接或者间接的引用,如果是,则不能被回收,反之则可以被当成垃圾回收

 Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

 扫描堆中的对象,看是否能够沿着GC Root对象(一系列对象)为起点的引用链找到该对象,找不到,表示可以回收

 哪些对象可以作为GC Root ?

  虚拟机栈(栈帧中的本地变量表)中引用的对象。
  方法区中类静态属性引用的对象。.
  方法区中常量引用的对象。
  本地方法栈中JNI(即-般说的Native方法)引用的对象。

1.3 四种引用

 1.强引用
  只有所有GC Roots对象都不通过[强引用]引用该对象,该对象才能被垃圾回收
 2.软引用(SoftReference)
  仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
  可以配合引用队列来释放软引用自身
 3.弱引用(WeakReference)
  仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
  可以配合引用队列来释放弱引用自身
 4.虚引用(PhantomReference)
  必须配合引用队列使用,主要配合ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存
 5.终结器引用(FinalReference)
  无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收), 再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时
  才能回收被引用对象

二、垃圾回收算法 2.1 标记清除

  特点:速度较快、会造成內存碎片

2.2 标记整理

  特点:速度慢、没有內存碎片

2.3 复制

  特点:没有內存碎片、需要占用双倍内存空间

三、分代垃圾回收 3.1 分代垃圾回收机制

JVM中具体是如何实现垃圾回收的机制?

  对象首先分配在伊甸园区域
  新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄加1并且交换from to
  minor gc会引发stop the world, 暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  当对象寿命超过阈值时,会晋升至老年代,最大寿命是15 (4bit)
  当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc, stw的时间更长

3.2 相关VM参数

四、垃圾回收器 4.1串行 

  单线程
  堆内存较小,适合个人电脑

4.2吞吐量优先

  多线程
  堆内存较大,多核cpu
  让单位时间内,STW的时间最短0.2 0.2=0.4

4.3响应时间优先

  多线程
  堆内存较大,多核cpu
  尽可能让单次STW的时间最短0.1 0.10.1 0.10.1=0.5

4.4G1

适用场景
  同时注重吞吐量(Throughput) 和低延迟(Low latency) ,默认的暂停目标是200 ms
  超大堆内存,会将堆划分为多个大小相等的Region
  整体上是标记+整理算法,两个区域之间是复制算法
相关JVM参数
  -XX:UseG1GC
  -XX:G1HeapRegionSize=size
  -XX:MaxGCPauseMillis=time

G1垃圾回收阶段

1)Young Collection

2)Young Collection + CM

  在Young GC时会进行GC Root的初始标记
  老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
  -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

3)Mixed Collection 

会对E、S、O进行全面垃圾回收
  最终标记(Remark) 会STW
  拷贝存活(Evacuation) 会STW
  - XX:MaxGCPauseMillis=ms

FullGC

■SerialGC
  新生代内存不足发生的垃圾收集- minor gc
  老年代内存不足发生的垃圾收集- full gc
■ParallelGC
  新生代内存不足发生的垃圾收集- minor gc
  老年代内存不足发生的垃圾收集- full gc
■CMS
  新生代内存不足发生的垃圾收集- minor gc
  老年代内存不足:并发失败以后,才叫Full GC,否则不会触发Full GC
■G1
  新生代内存不足发生的垃圾收集- minor gc
  老年代内存不足:当老年代內存跟堆內存占比达到45%以上,会触发并发标记的阶段,以及后续混合收集的阶段,如果垃圾回收的速度比新产生的垃圾的速度要快,来的及打扫,这是还不叫Full GC,还是并发垃圾回收的阶段(也会有暂停,但是时间很短), 当垃圾回收的速度跟不上垃圾产生的速度,并发收集就会失败,转化为Full GC(并发进行),stw时间也会更长。

Young Collection 跨代引用

  卡表与Remembered Set
  在引用变更时通过post-write barrier + dirty card queue
  concurrent refinement threads更新Remembered Set

Remark

JDK 8u20 字符串去重

  优点:节省大量内存
  缺点:略微多占用了cpu时间,新生代回收时间略微增加
  -XX: +UseStringDeduplication 默认开启

  将所有新分配的字符串放入一个队列
  当新生代回收时, G1并发检查是否有字符串重复
  如果它们值一样,让它们引用同-一个char[]
  注意,与String. intern()不- -样
  String. intern()关注的是字符串对象
  而字符串去重关注的是char[]
  在JVM内部,使用了不同的字符串表

JDK 8u40 并发标记类卸载

  所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它
  所加载的所有类
  -XX:+ClassUnloadingWi thConcurrentMark默认启用

JDK 8u60 回收巨型对象

  一个对象大于region的一半时,称之为巨型对象
  G1不会对巨型对象进行拷贝
  回收时被优先考虑
  G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

JDK 9 并发标记起始时间的调整

  并发标记必须在堆空间占满前完成,否则退化为FullGC
  JDK9之前需要使用-XX:InitiatingHeap0ccupancyPercent
  JDK 9可以动态调整
   -XX: Initiat ingHeapOccupancyPercent用来设置初始值
   进行数据采样并动态调整
   总会添加一个安全的空档空间

JDK 9 更高效的回收

  250+增强

  180+bug修复

五、垃圾回收调优 

  预备知识
   掌握GC相关的VM参数,会基本的空间调整
   掌握相关工具
   明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则

5.1 调优领域

  ■内存
  ■锁竞争
  ■cpu占用
  ■io

5.2 确定目标

  [低延迟]还是[高吞吐量],选择合适的回收器
  CMS,G1(JDK9,在更大的內存下工作的比CMS要好),ZGC (低延迟)
  ParallelGC (高吞吐量)

  Zing(stw 0停顿、可管理超大的內存)

互联网项目主要是针对 低延迟

5.3 最快的GC是不发生GC

查看FullGC前后的内存占用,考虑下面几个问题
  数据是不是太多?
resultSet = statement.executeQuery("select * from大表limit n")
  数据表示是否太臃肿?
   对象图
   对象大小16 Integer 24 int 4
  是否存在内存泄漏?
   static Map map =
     软
   弱
   第三方缓存实现

5.4 新生代调优

新生代的特点

  所有的new操作的内存分配非常廉价
   TLABlthread-local allocation buffer
  死亡对象的回收代价是零
  大部分对象用过即死
  Minor GC的时间远远低于Full GC

  新生代能容纳所有[并发量* (请求-响应)]的数据

  幸存区大到能保留[当前活跃对象+需要晋升对象]

  升阈值配置得当,让长时间存活对象尽快晋升
  -XX:MaxTenuringThreshold=threshold
  -XX:+PrintTenuringDistribution

  

5.5 老年代调优

以CMS为例

  ■CMS的老年代内存越大越好
  ■先尝试不做调优,如果没有Full GC那么已经...否则先尝试调优新生代
  ■观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~ 1/3
  ■-XX: CMSInitiatingOccupancyFraction=percent

5.6 调优案例

案例1Full GC和Minor GC频繁

 分析:GC频繁说明空间紧张,究竟是哪一空间紧张呢,如果是新生代,当业务高峰期来了,大量的对象被创建,新生代的空间很快就满了,幸存区的空间紧张,那么它的最大晋升阈值就会降低,导致很多本来生存周期很短的对象,也会晋升到老年代去,这样情况就进一步恶化,这样就导致老年代的Full GC频繁发生。

 解决:通过检测工具去观察堆空间的大小,发现新生代的內存设置的太小了,先试着增大新生代的內存,新生代的內存充裕了之后,新生代的垃圾回收就变得不那么频繁了,同时增大了幸存区的空间,以及晋升阈值,这样就能够使得生命周期较短的对象尽可能的留在新生代里,这样就可以让老年代的FullGC也不那么频繁了。

案例2 请求高峰期发生Full GC,单次暂停时间特别长(CMS)

 分析:已确定垃圾回收器是CMS,先去查看GC日志,看看CMS哪个阶段耗时比较长,CMS在重新标记的时候要扫描整个堆内存,如果业务高峰期的时候,新生代的对象较多标记时间会变的很长。

 解决:在重新标记之前对新生代先做一次垃圾回收,减少新生代对象的数量,这样就可以减少在重新标记时所耗费的时间。 - XX:+CMSScavengeBeforeRemark

案例3 老年代充裕情况下发生Full GC(JDK1.7 CMS)

 分析:之前介绍过CMS可能由于空间不足,导致并发失败,或者是空间碎片比较多,会产生Full GC;若日志排查后,在GC日志里没有并发失败,碎片过多的错入提示,说明老年代的空间是充裕的,不是老年代空间不足产生的Full GC。从案例中可知项目所用的JDK为1.7,JDK1.8是元空间作为方法区的实现,JDK1.7及以前是永久代作为方法区的实现,JDK1.7以前的永久代空间不足也会导致Full GC,JDK1.8以后,元空间不再由JAVA控制,元空间的默认情况下他的內存空间是使用了操作系统的內存空间,空间的容量一般是比较充裕的。而JDK1.7以前永久代的空间如果设小了,就会导致触发整个堆的Full GC。

 解决:增大永久代的最大值和初始值。