爱锋贝

 找回密码
 立即注册

只需一步,快速开始

扫一扫,极速登录

开启左侧

【面试必问系列】JVM垃圾回收算法、收集器看这一篇就够了

[复制链接]
发表于 2023-4-4 06:33:56 | 显示全部楼层 |阅读模式

一键注册,加入手机圈

您需要 登录 才可以下载或查看,没有帐号?立即注册   

x
一、如何确定是垃圾

1、引用计数法

对象如果没有与之关联的引用,计数器为0的对象,就是可回收的对象。(目前python就使用)
优点:判定效率高,实现简单。
缺点:不完全准确,无法回收循环引用的对象,容易内存泄漏。
2、可达性分析(根可达)

通过一系列GC Roots的对象作为起始点,从这些根节点开始向下搜,搜索所有走过的路叫做引用连,当一个对象到GC Roots没有任何的引用链相连时,则说明此对象不可用。
优点:解决相互循环引用问题。
注:不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程,可以通过finalize()自救。
3、GC Roots对象

GC Roots对象包括:
虚拟机栈栈帧中本地变量表引用的对象;
方法区中类静态属性引用;
方法区中常量引用的对象;
本地方法栈中JNI引用的对象;
所有被同步锁持有的对象等;
jvm中跨代引用的对象等;
.......还有其他的几种,常用的前四种。
二、垃圾回收算法

java是自动回收内存的,C、c++等都要手工命令回收。
1、复制算法

按内存容量将内存划分为大小相等的两块,每次使用一块,这一块满后,将尚存活的复制到另一块上去,把它使用的内存清空掉。
优点:实现简单,不易产生碎片;
缺点:可用内存被压缩为原来的一半,且存活对象多的话,此算法效率大大降低。
2、标记清除算法(Mark-Sweep)

标记要回收的对象,回收被标记的对象占用的空间。
缺点:内存碎片化严重;
3、标记整理算法(Mark-Compact)

标记要回收的对象,标记后不是清楚标记对象,而是将存活的对象移向内存的一端,然后清楚边界外的对象。
优点:没有碎片化;
4、分代收集算法

目前大部分JVM采用的【新生代、老年代、永久代】
根据对象存活的不同生命周期,将内存划分为不同的域,一般情况下,垃圾回收主要回收堆空间(因为几乎大部分对象都在堆空间,特例逃逸分析:栈上分配),所以将堆划分为新生代(1/3)、老年代(2/3)。
老年代:大对象直接放在老年代;长期存活的对象进入老年代;
每次只有少量对象需要被回收,存活率高,比较稳定。所以老年代选择标记整理算法或者标记清除算法。老年代的垃圾回收叫Major GC;
新生代:存放新生对象,对象朝生夕死,大量对象被回收,少量存活所以复制成本低,所以新生代选择复制算法。新生代的垃圾回收叫Minor GC(复制-清空-互换)。
永久代:方法区的永生代,用来存储class类、常量、方法描述等,对永生代的回收主要是废弃的常量和无用的类。垃圾较少,收益一般较小,所以垃圾回收主要回收堆空间。1.8之后叫做元空间。
Java内存模型

【面试必问系列】JVM垃圾回收算法、收集器看这一篇就够了-1.jpg

  • 其中新生代又划分为一个eden区,两个survivor区,默认比例为8:1:1。
  • 新生对象分配在eden区。如果eden区的垃圾经过一次GC幸存,就复制到survivor区,从eden-->survivor 对象年龄+1,survivor-->eden 对象年龄+1,如果最后存活对象的年龄达到15将被移至老年代。
  • 动态年龄:按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值。eg:survivor区已经有一半年龄为4的对象了,占用了一半survivor区内存了,也将这些对象移至老年代。
注:Major GC前一般会先进行Minor GC,Minor GC频繁被触发,Major GC不会,无法找到足够大的连续空间分配给新创建的较大对象时,也会提前触发Major GC;Major GC速度一般比Minor GC慢10倍以上;
Full GC :是清理整个堆空间。

触发条件
①手动条用System.gc()
②老年代空间不足;
③方法区空间不足;
④经过Minor GC后,进行移动或分配的对象大小大于老年代可用空间。
小结:分代收集算法就是根据不同的区域对象生命周期特点选择不同的回收算法;
垃圾回收主要回收堆内存;
survivor区晋升年龄阈值有两种情况,年龄15或者达到survivor区的50%。
三、GC 性能衡量指标

1、吞吐量:

这里的衡量吞吐量是指应用程序所花费的时间和系统总运行时间的比值。我们可以按照这个公式来计算 GC 的吞吐量:系统总运行时间 = 应用程序耗时+GC 耗时。如果系统运行了 100 分钟,GC 耗时 1 分钟,则系统吞吐量为 99%。GC 的吞吐量一般不能低于 95%。
2、停顿时间:

指垃圾回收器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时间可能会比较长;而使用并发回收器,由于垃圾收集器和应用程序交替
运行,程序的停顿时间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低。
3、垃圾回收频率:

通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以我们需要适当地增大堆内存空间,保证正常的垃圾回收频率即可。
四、垃圾收集器

目前主流的7个:
新生代收集器:Serial、ParNew、Parallel Scavenge;
老年代收集器:Serial Old、Parallel Old、CMS;
整堆收集器:G1;
下面一个个介绍下这些收集器特点及作用:
1、Serial(单线程,复制算法,新生代)

Serial在收集垃圾时,必须暂停其他所有工作线程,直至垃圾回收结束,对于单个cpu来说,没有线程交互的开销,效率高。
因此Serial是java虚拟机运行在Client模式下默认的新生代垃圾收集器。
2、ParNew(Serial的并行的多线程版,复制算法,新生代)

ParNew除了使用多线程进行垃圾回收以外,其他行为和Serial一样,也要暂停所有工作线程。
ParNew默认开启和cpu数目相同的线程,可以通过参数-XX:ParallelGCThreads限制线程数量,是很多虚拟机在Server模式下新生代默认的垃圾收集器。
3、Parallel Scavenge(并行的多线程版,复制算法,新生代)

重点关注程序可达到的一个可控制的吞吐量,有自适应调节策略,提升用户的
体验。是1.8默认的新生代收集器。
吞吐量=cpu运行用户代码时间/cpu总消耗时间
自适应调节策略:
Parallel Scavenge收集器能够配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成。只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio参数(更关注吞吐量)给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。
1)java -XX:+PrintFlagsFinal 可以看到1.8默认的是 UseParallelGC
ParallelGC 默认的是 Parallel Scavenge(新生代)+ Parallel Old(老年代)
2)自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
4、Serial Old(单线程,标记整理算法,老年代)

主要运行在Client模式下,java虚拟机默认的老年代垃圾收集器,
在Server模式下两种用途:
①jdk1.5之前与新生代的Parallel Scavenge搭配使用;
②作为老年代中CMS收集器的后备垃圾收集方案。
5、Parallel Old(并行的多线程,标记整理算法,老年代)

若同样考虑老年代的吞吐量,可以考虑搭配新生代的Parallel Scavenge使用。
6、CMS(Concurent Mark Sweep)(并发的多线程,标记清除算法、老年代)

主要目的是获取最短垃圾回收停顿时间,可以为交互较高的程序提高用户体验。是目前老年代中唯一一个标记清除算法而不是标记整理算法的垃圾收集器。
缺点:cpu敏感、浮动垃圾、内存碎片
CMS分为四个阶段:
1)初始标记(暂停):只是标记一下GC Roots能直接关联的对象,速度快,暂停所有工作线程;
2)并发标记(并发):进行GC Roots跟踪,和用户线程一起工作;
1)重新标记(暂停):修正并发标记期间,因程序运行导致的标记变动那部分对象的标记,暂停所有工作线程;
1)并发清除(并发):清除GC Roots不可达对象,和用户线程一起工作。
所以总体上看,CMS收集器的内存回收和用户线程是一起并发执行的。eg:web程序,B/S服务。
7、G1(Garbage first)

G1最突出改进:
1)基于标记整理算法,没有碎片;
2)可以非常精准的控制停顿时间,在不牺牲吞吐量的前提下,实现低停顿的回收,让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾回收上的时间不得超过N毫秒。
G1是避免全区域的垃圾收集,而是将java堆划分为多个大小固定的独立区域,并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。
8、垃圾收集器的搭配

上述7种垃圾收集器的搭配使用,有连接的可以搭配。

【面试必问系列】JVM垃圾回收算法、收集器看这一篇就够了-2.jpg
注意几个概念:单线程、并行、并发是不一样的。
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程依旧处于等待状态; 如ParNew、Parallel Scavenge、Parallel Old;
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),如CMS、G1;
CMS、G1的并发标记 :采用了三色标记法:分别是白色、灰色和黑色。三色标记最大的好处是可以异步执行,从而可以以中断时间极少的代价或者完全没有中断来进行整个 GC。
并发标记容易产品漏标问题,CMS从根从新扫描,G1快照方式对比差异,具体此处不多延伸。
五、安全点与安全区域

1、安全点

不是在任何时候都可以随便GC的,当系统要进行垃圾回收时,业务线程不是立马停下来的,可想而知立马停下可能会有问题,为了准确安全地回收内存,JVM是在Safe Point点时才进行回收,
就是业务线程去按照某种策略轮询检查这个变量一旦发现是安全点(Safe Point)就主动挂起,那样当JVM达到Safe Point就可以安全准确的GC了。
安全点主要在以下位置设置:
1) 循环的末尾
2)方法返回前
3)调用方法的call之后
4) 抛出异常的位置
2、安全区域

若用户线程sleep了等,不能主动检测变量走向安全点,显然JVM也不可能等待程序唤醒,这时候就需要安全区域了。
安全区域是指一段代码片中,引用关系不会发生变化,在这个区域任何地方GC都是安全的,安全区域可以看做是安全点的一个扩展。线程执行到安全区域的代码时,首先标识自己进入了安全区域,这样GC时就不用管进入安全区域的线层了,线程要离开安全区域时就检查JVM是否完成了GC Roots枚举,如果完成就继续执行,如果没有完成就等待直到收到可以安全离开的信号。

-----------------------------
精选高品质二手iPhone,上爱锋贝APP
您需要登录后才可以回帖 登录 | 立即注册   

本版积分规则

快速回复 返回顶部 返回列表