JVM-GC 垃圾回收机制

什么是垃圾回收机制(GC)

因为内存空间有限,创建的每个对象和变量都会占据内存,GC做的就是对象清除将内存释放出来。

GC 作用位置

堆是Java虚拟机进行垃圾回收的主要场所,其次要场所是方法区。

JVM 如何判断一个对象是否应该被回收?

判断一个对象是否应该被回收,主要是看其是否还有引用。判断对象是否存在引用关系的方法包括引用计数法以及可达性分析。

引用计数法:是一种比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只需要收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。

可达性分析:可达性分析的基本思路就是通过一系列可以做为root的对象作为起始点,从这些节点开始向下搜索(找出内存中的引用链)。那么链中的对象表示可达,即不能作为被垃圾回收的。引用链之外的对象即可作为垃圾回收。简单来说就是通过判断对象的引用链是否可达来决定对象是否被回收。

// 面试题
public class ReferenceCountGC{
	public Object instance = null;
	public static void testGC(){
		ReferenceCountGC gc1 = new ReferenceCountGC();
		ReferenceCountGC gc2 = new ReferenceCountGC();
		gc1.instance = gc2;
		gc2.instance = gc1;
		gc1 = null;
		gc2 = null;
		System.gc();
	}
	public static void main(String[] args){
		testGC();  // gc1和gc2 会被回收吗?不会!原因就是互相存在引用。
	}
}

以下对象会被认为是root对象(GC root):

  • 栈内存中引用的对象
public class Rumenz{
    public static  void main(String[] args) {
         Rumenz a = new Rumenz();
         a = null;
    }

}

a是栈帧中的本地变量,a就是GC Root,由于a=null、a与new Rumenz()对象断开了链接,所以对象会被回收。

注意作用域问题:

public class Rumenz{
    public static  void main(String[] args) {
         {
             Rumenz a = new Rumenz();
         }
         System.gc();
    }

}

a是栈帧中的本地变量,a就是GC Root,由于作用域问题,作用域外无其他局部变量的读写,虽然作用域内不存在引用关系,a 是不会被回收的。

public class Rumenz{
    public static  void main(String[] args) {
         {
             Rumenz a = new Rumenz();
         }
         int i = 10;
         System.gc();
    }

}

a是栈帧中的本地变量,a、i就是GC Root,由于作用域问题,作用域外无存在局部变量的读写;作用域结束a 被回收。

  • 方法区中静态引用和常量引用指向的对象
public class Rumenz{
    public static Rumenz r;
    public static void main(String[] args){
       Rumenz a=new Rumenz();
       a.r=new Rumenz();
       a=null;
    }
}

栈帧中的本地变量a=null,由于a断开了与GC Root对象(a对象)的联系,所以a对象会被回收。由于给Rumenz的成员变量r赋值了变量的引用,并且r成员变量是静态的,所以r就是一个GC Root对象,所以r指向的对象不会被回收。

public class Rumenz{
    public static final Rumenz r=new Rumenz();
    public static void main(String[] args){
       Rumenz a=new Rumenz();
       a=null;
    }
  
}

栈帧中的本地变量a=null,由于a断开了与GC Root对象(a对象)的联系,所以a对象会被回收。由于给Rumenz的成员常量r赋值了引用,所以r就是一个GC Root对象,所以r指向的对象不会被回收。

  • 被启动类(bootstrap加载器)加载的类和创建的对象

  • Native方法中JNI引用的对象。

什么时候进行垃圾回收

  1. 会在cpu 空闲的时候自动回收

  2. 堆内存存满后

  3. 主动调用System.gc()后进行回收

如何回收(收集算法)

  1. 标记清算法,效率与碎片问题,适用于对象存活率较高的场景
  • 从引用根节点开始标记所有被引用的对象,

  • 遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,并且会产生内存碎片

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-380vKqEq-1653482893186)(imgclip.png "imgclip.png")]

缺点:执行效率不稳定,会因为对象数量增长,效率变低。标记清除后会有大量的不连续的内存碎片,空间碎片太多就会导致无法分配较大对象,无法找到足够大的连续内存,而发生gc。

  1. 复制算法,性能问题,适用于对象存活率较低的场景
  • 复制算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。复制算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j75z6IHL-1653482893189)(imgclip_1.png "imgclip_1.png")]

缺点:可用内存缩成了一半,浪费空间

  1. 标记整理算法,适用于对象存活率较高的场景
  • 从根节点开始标记所有被引用对象,结合“标记-清除”和“复制”两个算法的优点。

  • 遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aX8AMx3E-1653482893203)(imgclip_2.png "imgclip_2.png")]

  1. 分代收集算法

结合上述三种方法自动选取最佳执行方法。

所谓的分代就分为三代:

  • 新生代YG:新生代又将区域分为Eden(8),survivor0(1),survivor1(1)。新生代的目标就是尽可能快速地收集那些生命周期短的对象,一般情况下所有新生成的对象首先都是放在新生代中的eden。新生代空间不足、进行垃圾回收时,先将eden 区存活的对象复制到survivor0中,然后清空eden 区,当survivor0 区满了则将eden 区与survivor0 区存活的对象复制到survivor1 区中,然后清空eden 以及survivor0 区,此时survivor0 区是空的,然后存活的对象年龄加1、交换survivor0 与survivor1 的角色(即下一次垃圾回收扫描eden 区以及survivor1 区),以此往复。当最终某一个survivor区不足以储存eden区以及另外一个survivor区的存活对象时或者当对象寿命超过阈值时,这些对象就会被放置到老年代中。如果老年代也满了就会先触发一次MinorGC,如果之后空间仍不足,那么触发触发一次Full GC(新生代,老年代都进行垃圾回收)。注意:新生代发生的GC 称为MinorGC,并且发生频率非常高,不一定是Eden区满了才执行。MinorGC会引发stop the world(暂停服务),暂停其它用户的线程,等待垃圾回收结束,用户线程才恢复运行。

  • 老年代OG:老年代存放的都是一些生命周期比较长的对象,在新生代尽力过N(晋升阈值)次MinorGC后仍然存活的对象会被放置到老年代中。老年代的存活时间比较长因此FullGC 发生的频率比较低。同样会引发stop the world (暂停服务)时间更长。

  • 永久代PG(jdk8 元空间):永久代主要用于存放一些静态文件,比如java 类、方法等

所以:在新生代中每次垃圾回收都会有大批量对象被清除掉所以使用复制算法只需要付出少量复制成本就可以完成收集。老年代因为对象存活率较高、没有额外的空间对他进行分配所以就必须使用标记整理或标记清算的方法。

注意:

在jdk8的时候java废弃了永久代,但是并不意味着我们以上的结论失效,因为java提供了与永久代类似的叫做“元空间”的技术。

废弃永久代的原因:由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryErroy。元空间的本质和永久代类似。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。也就是不局限与jvm可以使用系统的内存。理论上取决于32位/64位系统可虚拟的内存大小。

垃圾收集器:收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0owsvSJn-1653482893207)(imgclip_4.png "imgclip_4.png")]

说明:如果两个收集器之间存在连线说明他们之间可以搭配使用。

1. Serial 收集器

串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。单线程,使用 复制算法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NNdL9aPZ-1653482893208)(imgclip_3.png "imgclip_3.png")]

2. Serial Old 收集器

收集器的老年代版本,单线程,使用 标记 —— 整理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IzTUpyMR-1653482893210)(imgclip_7.png "imgclip_7.png")]

3. ParNew 收集器

ParNew收集器 ParNew收集器其实就是Serial收集器的多线程版本。多线程,使用 复制算法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QzbVVt7P-1653482893211)(imgclip_5.png "imgclip_5.png")]

4. Parallel Scavenge 收集器

Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;多线程,使用 复制算法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BQAEQ31T-1653482893212)(imgclip_5.png "imgclip_5.png")]

5. Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记 —— 整理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GfjNlGoc-1653482893214)(imgclip_6.png "imgclip_6.png")]

6. CMS收集器

CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —— 清除 算法实现。

运行步骤:

  • 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象

  • 并发标记(CMS concurrent mark):进行 GC Roots Tracing

  • 重新标记(CMS remark):修正并发标记期间的变动部分

  • 并发清除(CMS concurrent sweep)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gYsz8JOE-1653482893215)(imgclip_8.png "imgclip_8.png")]

其中初始标记、重新标记这两个步骤仍然需要停止服务。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

优点:并发收集、低停顿。缺点:产生碎片。

7. G1 收集器

相比于CMS 收集器基于标记 —— 整理 算法实现

运行步骤:

  • 初始标记(Initial Marking)

  • 并发标记(Concurrent Marking)

  • 最终标记(Final Marking)

  • 筛选回收(Live Data Counting and Evacuation)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UGe22agS-1653482893217)(imgclip_9.png "imgclip_9.png")]

优点:空间整合、可预测停顿。

收集器组合

组合新生代收集器老年代收集器说明
1SerialSerialOldSerial和Serial Old都是单线程进行GC,特点就是GC时暂停所有应用线程。
2SerialCMS + SerialOldCMS(Concurrent Mark Sweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用Serial Old策略进行GC。
3ParNewCMS使用 -XX:+UseParNewGC选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。如果指定了选项 -XX:+UseConcMarkSweepGC选项,则新生代默认使用ParNew GC策略。
4ParNewSerial Old使用 -XX:+UseParNewGC选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。
5Parallel ScavengeSerial OldParallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序
6Parallel ScavengeParallel OldParallel Old是Serial Old的并行版本
7G1GCG1GC-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC #开启; -XX:MaxGCPauseMillis=50 #暂停时间目标; -XX:GCPauseIntervalMillis=200 #暂停间隔目标; -XX:+G1YoungGenSize=512m #年轻代大小; -XX:SurvivorRatio=6 #幸存区比例

参数配置

参数说明
-XX:+UseSerialGC新生代和老年代都使用串行回收器,新生代使用复制算法,老年代使用标记-整理算法。
-XX:+UseParNewGC新生代进行并行回收,老年代仍旧使用串行回收。新生代使用复制算法,老年代使用标记-整理算法。
-XX:+UseParallelGC新生代使用Parallel收集器,老年代使用串行收集器。新生代使用复制算法,老年代使用标记-整理算法。
-XX:+UseParallelOldGC新生代和老年代都使用Parallel收集器。新生代使用复制算法,老年代使用标记-整理算法。
-XX:+UseConcMarkSweepGC新生代进行并行回收,老年代使用CMS收集器。新生代使用复制算法,老年代使用标记-清除算法
-XX:+UseG1GC[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FPLJX8H0-1653482893218)(imgclip_10.png "imgclip_10.png")]

拓展

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ODQAE7Z8-1653483106306)(imgclip.png "imgclip.png")]

比例:1:2:2

为什么要有Survivor区

为什么需要Survivor空间。我们看看如果没有 Survivor 空间的话,垃圾收集将会怎样进行:一遍新生代 gc 过后,不管三七二十一,活着的对象全部进入老年代,即便它在接下来的几次 gc 过程中极有可能被回收掉。这样的话老年代很快被填满, Full GC 的频率大大增加。我们知道,老年代一般都会被规划成比新生代大很多,对它进行垃圾收集会消耗比较长的时间;如果收集的频率又很快的话,那就更糟糕了。基于这种考虑,虚拟机引进了“幸存区”的概念:如果对象在某次新生代 gc 之后任然存活,让它暂时进入幸存区;以后每熬过一次 gc ,让对象的年龄+1,直到其年龄达到某个设定的值(比如15岁), JVM 认为它很有可能是个“老不死的”对象,再呆在幸存区没有必要(而且老是在两个幸存区之间反复地复制也需要消耗资源),才会把它转移到老年代。

Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

为什么有两个Survivor区

问题1:为什么 Survivor 分区不能是 1 个?

如果 Survivor 分区是 1 个的话,假设我们把两个区域分为 1:1,那么任何时候都有一半的内存空间是闲置的,显然空间利用率太低不是最佳的方案。

但如果设置内存空间的比例是 8:2 ,只是看起来似乎“很好”,假设新生代的内存为 100 MB( Survivor 大小为 20 MB ),现在有 70 MB 对象进行垃圾回收之后,剩余活跃的对象为 15 MB 进入 Survivor 区,这个时候新生代可用的内存空间只剩了 5 MB,这样很快又要进行垃圾回收操作,显然这种垃圾回收器最大的问题就在于,需要频繁进行垃圾回收。

问题1:为什么 Survivor 分区是 2 个?

刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中。上述机制最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。

那么,Survivor为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果Survivor区再细分下去,每一块的空间就会比较小,很容易导致Survivor区满

总结

根据上面的分析可以得知,当新生代的 Survivor 分区为 2 个的时候,不论是空间利用率还是程序运行的效率都是最优的,所以这也是为什么 Survivor 分区是 2 个的原因了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值