JVM~垃圾收集器

本文围绕Java的GC机制展开,介绍了判断对象是否死亡的引用计数和可达性分析算法,以及标记 - 清除、复制等垃圾收集算法。还阐述了Serial、ParNew等多种垃圾收集器的特点和使用场景,最后讲解了对象在Eden分配、大对象进老年代等内存分配与回收策略。

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

      GC(Garbage Collection)机制,是Java与C++/C的主要区别之一,Java开发者,一般不需要单独处理内存的回收,GC会负责内存的释放。

      java运行时区域中程序计数器、虚拟机栈、本地方法栈3个区域随线程生命周期结束而结束,Java堆、方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

1.判断对象是否死亡

判断对象是否死亡通常有引用计数算法、可达性分析算法

引用计数算法

该算法通常是给对象中添加一个引用计数器,每当有一个地方引用它是,计数器就会+1,引用失效,计数器值就会-1,如果计数器值为0,对象就不会被再使用,就可以回收该对象了。但该算法很难解决循环引用的问题。

可达性分析算法

在主流的商用程序语言(Java、C#)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。基本思路就是通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

GC Roots对象包括以下几种:

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

引用

判定对象是否存活都与"引用"有关,Java对引用概念进行了扩充。

强引用(Strong Reference)

Object obj = new Object();  //new 创建的,只要强引用在就不回收。

软引用(Soft Reference)

SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。

String str=new String("abc");           // 强引用 SoftReference<String> softRef=new SoftReference<String>(str);  // 软引用

弱引用(Weak Reference)

WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。

String str=new String("abc");     WeakReference<String> abcWeakRef = new WeakReference<String>(str); str=null; //如果你想把这个对象变成强引用的话可以使用 String  abc = abcWeakRef.get();

虚引用(Phantom Reference)

PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

二.垃圾收集算法

1.标记-清除算法

首先标记出所有需要回收的对象,再标记完成后统一回收所有被标记的对象。效率不高,且容易产生内存碎片。

算法学习   https://chenhongliang.blog.csdn.net/article/details/102663989?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control&dist_request_id=1594324c-102a-441f-ac90-b7c5ac05cc26&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control

2.复制算法

为了解决效率问题,“复制”(Copying)的收集算法将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。

一般是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

算法学习 https://blog.csdn.net/MrLiii/article/details/113391263

3.标记-整理算法

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行整理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

4.分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法,一般把Java堆分为新生代老生代,这样就可以根据各个年代的特点采用最合适的收集算法。

算法学习 https://blog.csdn.net/hp910315/article/details/50985877

三.垃圾收集器

吞吐量:在java程序背景环境下,CPU执行用户程序代码的时间占CPU总执行时间的比值。

停顿时间:GC过程中,JVM的Stop The World

并行(Parallel)收集器:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;

       并发(Concurrent)收集器:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

Serial收集器

介绍:采用了复制算法、串行回收和“Stop-The-World"机制的方式执行内存回收。

    单线程收集器,只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。

优势:

1.简单而高效

2.用户的桌面应用场景中,可用内存一般不大,可以在较短时间内完成垃圾收集,只要不频繁发生,使用串行回收器是可以接受的。

3.在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器 

ParNew收集器

介绍:ParNew收集器其实就是Serial收集器的多线程版本,是一款并行垃圾收集器。除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。

特点: 高吞吐量,ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有ParNew能与CMS收集器配合工作(因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码)

相关参数

   由于单CPU存在多线程交互的开销,ParNew收集器在单CPU的环境中可能不会有比Serial收集器更好的效果,默认开启的收集线程数与CPU的数量相同,可以使用 -XX:ParallelGCThreads 来限制垃圾收集的线程数。

   -XX:+UseConcMarkSweepGC 新生代使用ParNew,老年代使用CMS

   -XX:+UseParNewGC 新生代使用ParNew,老年代默认使用Serial Old。

使用场景:

1.ParNew与CMS收集器配合工作

Parallel Scavenge收集器   [ˈpærəlel]  [ˈskævɪndʒ]

介绍:新生代收集器,使用复制算法的并行的多线程收集器。

特点:  重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU用于运行用户代码的时间/CPU总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别,它也经常称为“吞吐量优先”收集器。

相关参数

  -XX:MaxGCPauseMillis  控制最大垃圾收集停顿时间。参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。

  -XX:GCTimeRatio 直接设置吞吐量大小的,参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1/(1+19)),默认值为99,就是允许最大1%(即1/(1+99))的垃圾收集时间。

-XX:+UseAdaptiveSizePolicy  当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)

  -XX:+UseParallelGC 新生代使用Parallel Scavenge,老年代默认使用Serial Old。

  -XX:+UseParallelOldGC 新生代使用Parallel Scavenge,老年代使用Parallel Old。

使用场景:

高吞吐量程序,比如网关

Serial Old 收集器

介绍:老年代的单线程收集器,使用“标记-整理算法,除了与新生代的Serial收集器配合之外,Serial Old 收集器的另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

使用场景:

Serial Old在Server模式下主要有两个用途:

1.与新生代的Parallel Scavenge配合使用

2.作为老年代CMS收集器的后备垃圾收集方案

Parallel Old收集器

介绍:Parallel Old 收集器是Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理算法。

特点:  高吞吐量

使用场景:在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Scavenge 加 Parallel Old 收集器。

CMS收集器

介绍:用户线程和GC线程并发,以获取最短回收停顿时间为目标的收集器,采用“标记-清除”算法实现,使用多线程的算法去扫描堆,对发现未使用的对象进行回收。

收集步骤:

1.初始标记  标记 GC Roots 能直接关联到的对象

2.并发标记   进行 GC Roots Tracing

4.重新标记  修正并发标记期间的变动部分

5.并发清除

优点:响应时间优先,减少垃圾收集停顿时间

缺点:

1.对CPU资源敏感

2.对浮动垃圾处理不得当

3.空间碎片过多

相关参数:

 -XX:+UseConcMarkSweepGC 使用CMS

使用场景:

服务器、电信领域等

G1收集器 (Garbage First)

介绍:面向服务端的垃圾回收器,堆被划分成 许多个连续的区域(region)。采用G1算法进行回收,吸收了CMS收集器特点。

收集步骤:

1.初始标记

2.并发标记

3.最终标记

4.筛选回收

特点:

1.支持很大的堆,高吞吐量,并行与并发、分代收集、空间整合、可预测停顿

       2.支持多CPU和垃圾回收线程

       3.在主线程暂停的情况下,使用并行收集

       4.在主线程运行的情况下,使用并发收集

相关参数:

–XX:+UseG1GC 使用G1垃圾回收器

使用场景:

可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收,Java8以上

四.内存分配与回收策略

对象优先在Eden分配

对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲区,将线程优先在 (TLAB) 上分配。少数情况会直接分配在老年代中。

新生代 GC (Minor GC):发生在新生代的垃圾回收动作,频繁,速度快。

老年代 GC (Major GC / Full GC):发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。

大对象直接进入老年代

Minor GC的规则

大对象直接进入老年代  -XX:PretenureSizeThreshold 指定大于该数值的对象直接进入老年代,避免在新生代的Eden和两个Survivor区域来回复制,产生大量内存复制操作。

缺点:只对Serial和ParNew两个新生代收集器有用

Minor GC的规则

jdk6_24之前,Minor GC之前先去老年代判断剩余连续内存空间是否大于新生代对象总和,如果大于就进行一次Minor GC;如果小于的话,会去判断你是否打开了分配担保策略,如果打开了就去判断老年代剩余连续内存空间是否大于之前每次Minor GC晋升老年代对象的平均大小,如果大于的话就尝试进行一次Minor GC,如果小于,或者没有打开分配担保策略的话就直接Full GC。

但是jdk6_24之后,就变了,不关心分配担保策略了,如果老年代剩余的连续内存空间大于之前Minor GC晋升老年代对象的平均大小的话,就进行Minor GC,如果小于的话就直接进行Full GC。

长期存活的对象将进入老年代

虚拟机采用分代收集的思想来管理内存,内存回收时必须识别哪些对象放入新生代,哪些对象放入老年代。为了做到这点,虚拟机为每个对象定义了一个对象年龄计数器。

如果对象在Eden出生并经过一次Minor GC仍然存活,并且能被Survivor容纳,将被移动到Survivor区,并且对象年龄设置为1.对象每经过一次Minor GC后仍保持存活,年龄+1

当对象年龄到达一定程度(一般15岁),那么它会晋升到老年代。对象晋升的年龄限制 -XX:MaxTenuringThreshold设定

为了更好的适应不同程度的内存状况,虚拟机并不是永远地要求对象的年龄必须到达MaxTenuringThreshold才能晋升进入老年代,当Survivor中相同年龄所有对象大小总和大于Survivor空间一半,年龄大于该年龄的对象直接进入老年代。

空间分配担保

在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有空间总和,如果条件程离,那么Minor GC是安全的,如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行Minor GC 否则可能进行一次Full GC。新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间作为轮换备份,因此当出现大量对象在Minor GC仍然存活的情况(最极端情况为内存回收后新生代所有对象都存活),就需要老年代进行担保,把Survivor无法容纳的对象存入老年代。但老年代需要足够空间,所以需要进行判断,当不足时 进行Full GC腾出老年代空间。

动态对象年龄判定

虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的

一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。学习jvm的人,基本都阅读过上面这段话,这里讲的是动态年龄的判定。对于动态的判定的条件就是相同年龄所有对象大小的总和大于Survivor空间的一半,然后算出的年龄要和MaxTenuringThreshold的值进行比较,以此保证MaxTenuringThreshold设置太大(默认15),导致对象无法晋升。

场景

如果说非得相同年龄所有对象大小总和大于Survivor空间的一半才能晋升。我们看下面的场景

  1. MaxTenuringThreshold为15
  2. 年龄1的对象占用了33%
  3. 年龄2的对象占用33%
  4. 年龄3的对象占用34%。

开始推论

  1. 按照晋升的标准。首先年龄不满足MaxTenuringThreshold,不会晋升。
  2. 每个年龄的对象都不满足50%。,不会晋升。

得到假设结论

Survivor都占用了100%了,但是对象就不晋升。导致老年代明明有空间,但是对象就停留在年轻代。但这个结论似乎与jvm的表现不符合,只要老年代有空间,最后还会晋升的。

问题的解答

uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
	//survivor_capacity是survivor空间的大小
  size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
  size_t total = 0;
  uint age = 1;
  while (age < table_size) {
    total += sizes[age];//sizes数组是每个年龄段对象大小
    if (total > desired_survivor_size) break;
    age++;
  }
  uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
	...
}

我把晋升年龄计算的代码摘出。我们来看看动态年龄的计算。代码中有一个TargetSurvivorRatio的值。

-XX:TargetSurvivorRatio 目标存活率,默认为50%

  1. 通过这个比率来计算一个期望值,desired_survivor_size 。
  2. 然后用一个total计数器,累加每个年龄段对象大小的总和。
  3. 当total大于desired_survivor_size 停止。
  4. 然后用当前age和MaxTenuringThreshold 对比找出最小值作为结果

总体表征就是,年龄从小到大进行累加,当加入某个年龄段后,累加和超过survivor区域*TargetSurvivorRatio的时候,就从这个年龄段网上的年龄的对象进行晋升。

再次推演

还是上面的场景。 年龄1的占用了33%,年龄2的占用了33%,累加和超过默认的TargetSurvivorRatio(50%),年龄2和年龄3的对象都要晋升。

总结

动态对象年龄判断,主要是被TargetSurvivorRatio这个参数来控制。而且算的是年龄从小到大的累加和,而不是某个年龄段对象的大小。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值