java垃圾回收机制

本文介绍了Java内存管理机制及垃圾回收(GC)的过程。详细解释了GC如何通过可达性算法发现废弃对象,并通过标记清理、复制、标记整理等方式回收内存资源。探讨了分代回收策略、Stop-the-world现象、FullGC触发条件以及不同垃圾回收器的特点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

java的内存资源管理是交给jvm来管理的,java的程序员不需要关系对象申请内存资源及资源的释放问题。jvm的GC过程解决的问题是:1、发现不再使用的对象。2、释放这些对象占用的内存资源。

不同的jvm可能有不同的GC方法,我们以JDK自带的Hotspot虚拟机为基础,探索java的GC机制。

为何需要GC?

根据经验,java程序在运行时创建的绝大部分对象都是存活时间很短。那些不再使用的对象如果不从内存中清理出去,将会撑爆jvm内存,造成程序崩溃,后果难以承受。因此需要有方法将不再使用的对象所占用的资源清理出来,保证java程序顺畅运行。相较于C程序员需要自己写函数释放内存资源,java将这些工作交给jvm来做,只需要在程序启动时设置一些参数既可以完成java内存分配及回收工作。

如何发现对象已经废弃?

1.引用计数法

当一个对象不再被任何引用指向时即表示废弃,这种方法的缺点是无法发现环的存在。

环即是对象A中引入了对象B,对象B中又引入了对象A,形成一个环状引用链。如果使用此方法就会发现,A和B对象都无法被回收,因为回收A前提是回收B(消除B对A的引用),但是回收B前提又是回收A(消除A对B的引用),这样导致引用计数法始终认为A和B有对象指向它们。实际情况可能是A和B对象均已经不再使用了,这样的结果就是实际废弃的对象占用的资源无法被GC回收。

2.可达性算法

这种方法的思路是设立一系列名为GC Roots的对象,学习对象作为起点,向下开始搜索,搜素所经过的链路被称为引用链,当一个对象到任何GC Roots都没有引用链存在时,则认为此对象是无用的。这种方式能够解决循环引用问题。

垃圾如何回收?

总的回收算法有三种:

1.标记清理

标记依然存活的对象,将未被标记的对象直接从内存清除。会产生内存碎片。

2.复制

准备两块内存,GC时将一块内存中存活的对象拷贝到另一块内存,然后将之前的那块内存整块清理掉。但是会造成内存浪费。

3.标记整理

标记依然存活的对象,将其向内存一端移动,修改引用到这些对象的地址指针,最后将未被标记的对象清理掉。这个是折中方案,即不会造成内存碎片,也不会出现内存浪费

分代回收策略

分代是根据对象的存活时间长短,将内存分为年轻代和老年代,对于不同年代使用上面三种算法中适合的算法。

1.年轻代

所有对象在创建时均放在年轻代,由于绝大部分对象的存活时间很短,因此大部分对象均会在年轻代GC时被回收。年轻代GC被称为Minor GC。

年轻代由三个部分构成:eden,survivor1,survivor2。

所有的新建对象均放在eden区,触发GC时,会将伊甸园还存活的对象拷贝到survivor1区中,并清空伊甸园空间。如果survivor1区满了,此时会将伊甸园和survivor1中的存活对象拷贝到survivor2区中,并清空伊甸园和survivor1的内存空间,此时当一个存活的对象大小大于survivor2剩余空间时,或者没有连续的空间来存储这个对象时,这个对象被直接晋升至老年代。

2.老年代

老年代存放的是存活时间长的对象,一般经过多次Minor GC后依然存活的对象会被转入老年代。一般的老年代内存空间满了后会触发Major GC。一般Major GC与Full GC是等价的,但对于CMS回收器及G1回收器有不同:

  1. 对于CMS回收器,老生代GC只正对整个老生代堆内存。
  2. 对于G1回收器,老生代GC会针对整个年轻代及部分老年代内存。

3.持久代

jvm发现有些对象在创建开始就一直存在,几乎不会被回收,为提升老年代GC效率,将这些对象单独化出一块内存存放,这个空间就被称为持久代。持久代一般存类的定义、结构、字段、方法及常量等信息。jdk8后删除了持久代,取而代之的是元空间。持久代一般不会响应GC,其大小可以设置,当持久代内存满了后会触发Full GC。

stop-the-world!

无论使用哪种回收算法,jvm在执行GC线程时都会挂起其它所有线程,这就是stop-the-world(简称STW)。STW时间过长对于一些实时性要求高的应用是致命的。但是STW在GC过程中是不可避免的,jvm不断更新GC算法也只是尽可能减少STW时间,将其控制在可以接受的范围内。一般垃圾回收器在GC过程中都会记录日志,写日志如果阻塞将会动作长时间处于STW状态,例如日志的磁盘空间满了,或则操作系统在执行write时由于其他IO占用问题导致write长时间阻塞。

Full GC及触发情况

Full GC也叫Major GC,这个过程将会遍历整个堆内存。Full GC触发时将会造成程序出现比较长时间的暂停,有时由于Full GC暂停时间过长,而导致程序出错,可能会引起严重后果。因此,我们需要尽量避免Full GC,尽量缩短Full GC时间,减少Full GC频率。下面是有可能出现Full GC的情况:

  1. 代码里显示调用System.gc()
    这种方式很可能会触发Full GC,因此代码里基本上是禁止显示调用该方法进行GC。可以通过jvm参数-XX+disableExplicitGC来禁止直行显示GC。
  2. 老年代内存空间满了或连续的空间不足
  3. 持久代内存空间满了

回收器

  1. serial
    serial回收器是单线程运行,运行时总会暂停其它线程,适用于年轻代的内存回收。多用于jvm是client模式。
  2. parnew
    是serial的多线程版本,jvm是server模式下默认的年轻代收集器,通常与cms收集器配合使用。
  3. parallel scavenge
    多线程收集器,注重吞吐量的控制和GC自适应调节,适用于年轻代GC。
  4. serial old
    serial收集器的老年代版本,适用于老年代GC。通常在jvm是client模式下使用。jvm是server模式下通常作为cms的备用收集器。
  5. parallel old
    多线程收集器,parallel scavenge的老年代版本。
  6. cms
    高并发、低停顿的收集器。适用于老年代GC。GC过程大概包括4个阶段:
    1.初始标记。标记GC Roots能直接关联到的对象。这个阶段会暂停其它线程。
    2.并发标记。开始遍历GC Roots,进行标记。这个阶段是与应用线程并行进行的。
    3.重复标记。修正那些在并发标记过程中由于应用程序运行导致的标记记录变更的标记。这个过程会暂停其它线程。
    4.并发清除。清楚未被标记的对象。
    这种收集器对CPU要求比较高,需要cpu核数尽量大。如果cpu核数少,并发标记时将会影响应用程序的性能。
    且在并发标记时,应用程序运行产生的新的垃圾将不会在本次GC被回收,这些垃圾被叫做浮游垃圾。由于浮游垃圾占用内存资源,因此需要为应用程序预留足够的空间,jvm参数-XX:CMSInitiatingOccupancyFraction用来设置老年代触发GC的老年代内存使用比率,默认是68,表示老年代使用68%时触发GC。当浮游垃圾过多时,可能会出现浮游垃圾总量大于老年代剩余空间,这时会出现Concurrent mode failure问题,这个问题会导致Full GC。此时一般是使用CMS的备用收集器serial old进行垃圾回收,将会造成性能降低,停顿时间变长。
    由于CMS采用标记清除算法,因此会造成内存碎片,从而可能出现大的对象无法放入老生代,从而导致FullGC。这种情况可以使用jvm参数-XX:+UseCMSCompactAtFullCollection,让jvm在进行Full GC前先整理内存碎片。还可以使用参数-XX:CMSFullGCsBeforeCompaction,规定在进行几次Full GC后进行一次带整理的Full GC。
  7. G1
    G1将堆内存空间划分成一个个独立的空间,我们不必再单独为年轻代、老年代设置内存,只需要设置一个堆内存即可。G1是对一个一个独立空间分别进行GC,并且使用复制或标记整理的方式清理内存,不会出现内存碎片。对于庞大的对象,默认是放入老年代空间里,但比如一个大小超过一个独立空间50%的对象,G1专门划分一个叫humongous空间来存放,如果一个空间也无法存放就会使用几个连续的humongous空间存放。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值