JVM学习07——GC垃圾回收

发布于:2023-01-20 ⋅ 阅读:(558) ⋅ 点赞:(0)

一、什么是垃圾 garbage?

没有引用 指向的 对象 都是垃圾

1. 垃圾回收 Java VS C++

Java C++
GC处理垃圾 手工处理垃圾
开发效率高,执行效率低 开发效率低,执行效率高
缺陷1 :忘记回收会造成内存泄漏
缺陷2 :回收多次可能造成非法访问

二、如何找到垃圾?

2.1 Reference Count 引用计数(如Python)

一个对象被几个指针指向。就记录其指针的数量
当 引用==null 时,count - -
当 count == 0 时,这就是垃圾了

弊端:不能找到循环引用组成的垃圾
在这里插入图片描述

2.2 Root Searching 根可达算法

GC roots : 线程栈变量、 静态变量、常量池、JNI指针( java本地接口 )
以上 根 指向的地方、可以根据引用抵达的地方 不是垃圾,其余都是

在这里插入图片描述

三、GC常用的垃圾清除算法

3.1 Mark-Sweep 标记清除法 ( 标记的活 )

存活对象多时效率高,如老年代

标记还 有用的对象,清除无用的
存活对象比较多的情况下,效率较高
所以不适合伊甸园区

算法缺陷
	两遍扫描,效率偏低(第一遍标记、第二遍清除)
	容易产生碎片

3.2 Copying 拷贝法

存活对象少时效率高,适用于伊甸园区内存一分为2
找到有用的,把有用的对象拷贝到另一边空地,全找完后本区域全部清除

优点 弊端
适用于存活对象较少的情况(如伊甸园区),只扫描一次,效率高 移动复制对象,需要调整对象的引用(所以 java 即使一直指向同一个对象,它的引用的值也会变。)
没有碎片 空间浪费

3.3 Mark-Compact 标记压缩法

第一遍先标记出还有用的对象
第二遍把有用的对象挪到一起,清除剩下的垃圾。所以剩下的空间就很整齐了。

优点 弊端
不会产生碎片,方便对象分配 扫描两次
不会产生内存减半 需要移动对象,效率偏低

四、堆内存逻辑分区

分代算法
部分垃圾回收期使用的模型
除 Epsilon ZGC Shenandoah之外的GC都是使用逻辑分代模型
G1 是逻辑 分代,物理不分代
除此之外 都 逻辑分代 + 物理分代

在这里插入图片描述

4.1 新生代 new / young

  1. 堆内存占比 1/3
  2. 存活对象少
  3. Copying算法
  4. MinorGC/YGC :年轻代空间耗尽时触发(-Xmn)

新生代分为 伊甸园区 eden 和 幸存者1区 servivor 1 和 幸存者2区 servivior 2
比例 8 :1 :1

而新生代和老年代比例 1 :2

4.2 老年代 old / tenured(终身)

  1. 堆内存占比 2/3
  2. 存活对象多
  3. Mark Compact算法或MarkSweep算法;
  4. MajorGC/FullGC:在老年代无法继续分配空间时触发,新生代和老年代同时进行回收(-Xms -Xmn)

注意:永久代Permanant 是方法区MethodArea

新生代和老年代在堆,永久代在方法区
存class信息、代码的编译信息等

方法区的实现

JDK1.8之前 JDK1.8之后
永久代 元数据区
必须指定大小 元数据区可以设置大小,也可以不设置,无上限(受限于物理内存)
字符串常量在永久代 字符串常量在堆

五、对象分配

在这里插入图片描述

5.1 哪些对象会 栈上分配?

  1. 线程私有小对象
  2. 无逃逸 (这个对象就某一块代码里集中使用,其余地方不用)
  3. 支持标量替换 (用几个普通的类型就能代替这个对象)
  4. 无需调整

-XX:-DoEscapeAnalysis 去掉逃逸分析
-XX:-DoEscapeAnalysis 去掉标量替换
-XX:-UseTLAB 去掉使用线程本地分配

5.2 哪些对象会 线程本地分配?

线程本地分配 TLAB ( Thread Local Allocation Buffer )
占用eden,默认1%
每个线程在伊甸园区取1%作为自己的,每次分配内存时,首先往自己的线程本地分配,
多线程时候不用竞争eden就可以申请空间,提高效率

  1. 小对象
  2. 无需调整

六、对象何时进入老年代?

  1. 大对象 直接进入老年代
  2. 幸存者区达到最大年龄的对象 ( 默认15.因为在markword中年龄占4个bit,即最大为1111 )
    超过 XX:MaxTenuringThreshold指定次数 YGC
    Parallel Scavenge 15
    CMS 6
    G1 15
  3. 动态年龄
    幸存者1区 或 幸存者2区 的对象超过当前区总内存的50%时
    把年龄最大的放入老年代(也就是说不一定到15岁)
  4. 分配担保
    YGC(新生代垃圾回收)期间,survivor区空间不够了,通过空间担保直接进入老年代

七、常见的垃圾回收器

老年代内存不够用了,触发FGC,清理老年代和新生代,可以用以下垃圾回收器回收;
除了圈出来的是 物理不分代 + 逻辑分代
其余都是 物理分代 + 逻辑分代

在这里插入图片描述

JDK诞生,Serial追随,为了提高效率,诞生了Parallel Scavenge,
为了配合CMS,诞生了ParNew.
CMS是JDK 1.4 之后引入的
CMS是里程碑式的GC, 开启了并发回收
但是CMS毛病多,因此目前没有任何一个JDK版本默认是CMS,都是默认PS
CMS 收集器是以 获取 最短停顿时间 为 目标的收集器
并行收集

7.1 Serial + Serial Old (单线程)

STW 垃圾清理时工作暂停,单线程清理垃圾
目前还没有不会产生STW 的垃圾回收器
STW Stop The World

7.2 Parallel(平行) Scavenge(回收) + Parallel Old

当前JVM默认是这种 ( 并行的 )
PS + PO 和STW差不多,垃圾清理时工作暂停,不过是多线程清理垃圾。吞吐量优先

7.3 ParNew 多线程 + CMS

默认线程数为CPU核数,响应时间优先
是PS的变种,和CMS配合使用

7.4 G1(10ms)

JDK1.8出现,1.9完善
Garbage First Garbage Collectior G1 GC
目标是用于 多核、大内存 的机器上
通过并发 和 并行 实现 暂停时间短,同时还能保持较高的吞吐量

7.4.1 区域

Old
	老对象
Survivor
	幸存者
Eden
	伊甸园区
Humongous
	大对象 超过单个区域的 50 %

7.4.2 特点

1. 并发收集、并发标记、并发回收
2. 压缩空间不会延长 GC 的暂停时间
3. 更易预测的 GC暂停时间
5. 适用不需要实现很高的吞吐量的场景,需要很快响应时间的场景
5. 首先收集垃圾最多的分区
6.分而治之 

7.4.3 Card Table

由于做YGC,需要扫描整个old区,效率非常低,
所以JVM设计了CardTable
如果一个old区cardTable中有对象指向young区,
就将它设为 Dirty,下次扫描时,只需要扫描Dirty Card
从结构上,Card TableBitMap来实现

7.4.4 新老年代空间比例

5%60%
一般不用手工指定
也不要手工指定
因为这是 G1 预测停顿时间 的基准
G1 可以自己动态调大小

7.4.5 GC何时触发

YGC
伊甸园区空间不足
多线程并行执行

FGC
Old空间不足
System.gc( )

7.4.5 如果G1产生FGC,我应该如何做

G1的调优目标就是不用让它FGC

产生FGC一定是空间不足

  1. 扩内存
  2. 提高CPU性能
  3. 降低MinedGC触发的阈值,让MixedGC提早发生(默认45%)

MixedGC相当于CMS

  1. 初始标记 STW
  2. 并发标记
  3. 最终标记 STW(重新标记)
  4. 筛选回收 STW ( 并行 )

XX:InitiationHeapOccupacyPercent
默认45%
当O超过这个值,启动MixedGC

7.5 ZGC (1ms)

适合4T——16T内存 , JDK11后才有

7.6 Shenandoah

7.7 Epsilon

debug用的

7.8 CMS (垃圾回收和工作线程并发执行, 1.4+)

Concurrent Mark Sweep 并发标记清除
[多个线程同时回收是并行,这里是并发,可以垃圾回收和工作线程并发执行]

并发垃圾回收是因为无法忍受STW
以前内存不大,停下来清理也快。
现在内存很大,停下来清理要很久很久,甚至要停几天

CMS也是老年代垃圾回收器,老年代满了就触发 FGC

7.8.1 CMS过程 ( 高响应,低停顿 )

在这里插入图片描述

  1. 初始标记阶段 initial mark
    --------- STW 标记存活的对象,只标记一些根对象 (根可达法)
    --------- 所以速度 很快
    在这里插入图片描述
    ------------- 本阶段仅标记 GC roots 直接连接的对象
  1. 并发标记阶段 concurrent mark
    --------- 边 标记 边 运行 工作线程
  2. 重新标记阶段 remark (大多数的垃圾在并发标记时期已经标记完了)
    --------- STW 过程,工作线程停止,标记新产生的垃圾
    --------- 因为新产生的垃圾并不多,所以也很快
  3. 并发清理阶段 concurrent sweep
    --------- 边清理标记的垃圾,边运行工作线程,此时产生的浮动垃圾由CMS下次清理

7.8.2 CMS的两大问题

  1. 浮动垃圾

解决方案:降低触发CMS的阈值,保持老年代有足够的空间

Concurrent Model Failure
PromotionFaoled
如果是有很多碎片了,实在放不下对象了
CMS就会把老奶奶 Old Sorial 请出来清理
会SWT,停止工作线程,Old Sorial要清理很久很久很久
降低CMS触发的阈值

  1. 内存碎片化

CMS
Concurrent Mark Sweep
并发标记清除法
标记清除,会产生大量碎片

-XX:CNSInitiationOccupancyFraction 92%
降低触发CMS的阈值,保持老年代有足够的空间

7.9 session based GC

内存从小变大,产生了对应的算法
阿里的多租户JVM
每租户单空间
用于 web 应用

八、并发标记的算法 Concurrent Mark 阶段的算法

CMS ( 三色标记法 + incremental Update )
G1 ( 三色标记法 + SATB )

为什么G1用SATB

灰色->白色 引用消失时,如果没有黑色指向白色,引用会被push到堆栈
下次扫描时拿到这个引用,由于有RSet的存在,不需要扫描整个堆去查找指向白色的引用,只要看这个在栈中的白色的引用查看RSet看看被谁引用了,如果没人引用,则这个白色对象就是垃圾,可以回收了。
SATB配合RSet
如果是增量更新,得扫描有谁指向我,不然RSet直接查看谁指向我

ZGC ( Coloredpointers 颜色指针 + 写屏障 )
Shenandoah ( ColoredPointers + 读屏障 )

8.1 三色扫描算法:白 灰 黑

漏标( 本来是活着的对象,但是由于没有遍历到,被当成垃圾回收到了 )
在remark过程中,黑色指向白色
如果不对黑色重新扫描,则会漏标
会把白色对象当做没有新的引用指向而回收掉
因为
并发标记过程中,Mutator会删除灰色到白色的引用,此时白色对象就找不到了
因为灰色->白色

在这里插入图片描述

在并发标记时,引用 可能 产生 变化,白色对象有可能被错误回收

  1. 白:未标记的对象
  2. 灰:自身被标记,成员变量未被标记

本来已经标记成垃圾,
在应用执行过程中,
又有引用指向它

  1. 黑:自身和成员变量均已标记完成

解决方案
SATB 关注引用删除

配合Rset(G1用的)

shapshot at the begining
在起始的时候做一个快照
当B->D消失时,要把和这个 引用 (是灰色对象指向白色对象的引用) 推到 GC的堆栈,保证D还能被GC扫描到
关注引用的删除

incurmental Update (增量更新)

当黑指向白,黑变灰(CMS用的)

当一个白色对象被一个黑色对象引用
将黑色对象重新标记为灰色,让collector重新扫描
关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性

8.2 ColoredPointers 颜色指针

JVM 中一个指针占用8个字节,即64bit
其中三个bit作为标记位
如果这个指针一旦变化,不再指向原对象,
标记位对应做改变,标志这个指针变过了
当扫描时就会扫描那些变化过的指针

8.3 RSet与赋值效率

因为RSet需要记录谁指向自己
所以在每次给对象赋引用时,需要做额外的操作
指在RSet中做额外的记录( 在GC中被称为写屏障 )
No Silver Bullet
没有银弹
没有完美的解决方案

8.4 RSet

RSet = RememberedSet
记录其他区域中的对象到本区域的引用
目的,不需要扫描堆,通过扫描rset就能找到谁引用了本分区中的对象

8.5 CSet

Cset = Collection Set
一组可被回收的分区的集合
记录哪些分区需要回收

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到