一、什么是垃圾 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/3
- 存活对象少
- Copying算法
- MinorGC/YGC :年轻代空间耗尽时触发(-Xmn)
新生代分为 伊甸园区 eden 和 幸存者1区 servivor 1 和 幸存者2区 servivior 2
比例 8 :1 :1
而新生代和老年代比例 1 :2
4.2 老年代 old / tenured(终身)
- 堆内存占比 2/3
- 存活对象多
- Mark Compact算法或MarkSweep算法;
- MajorGC/FullGC:在老年代无法继续分配空间时触发,新生代和老年代同时进行回收(-Xms -Xmn)
注意:永久代Permanant 是方法区MethodArea
新生代和老年代在堆,永久代在方法区
存class信息、代码的编译信息等
方法区的实现
JDK1.8之前 | JDK1.8之后 |
---|---|
永久代 | 元数据区 |
必须指定大小 | 元数据区可以设置大小,也可以不设置,无上限(受限于物理内存) |
字符串常量在永久代 | 字符串常量在堆 |
五、对象分配
5.1 哪些对象会 栈上分配?
- 线程私有小对象
- 无逃逸 (这个对象就某一块代码里集中使用,其余地方不用)
- 支持标量替换 (用几个普通的类型就能代替这个对象)
- 无需调整
-XX:-DoEscapeAnalysis 去掉逃逸分析
-XX:-DoEscapeAnalysis 去掉标量替换
-XX:-UseTLAB 去掉使用线程本地分配
5.2 哪些对象会 线程本地分配?
线程本地分配 TLAB ( Thread Local Allocation Buffer )
占用eden,默认1%
每个线程在伊甸园区取1%作为自己的,每次分配内存时,首先往自己的线程本地分配,
多线程时候不用竞争eden就可以申请空间,提高效率
- 小对象
- 无需调整
六、对象何时进入老年代?
- 大对象 直接进入老年代
- 幸存者区达到最大年龄的对象 ( 默认15.因为在markword中年龄占4个bit,即最大为1111 )
超过 XX:MaxTenuringThreshold指定次数 YGC
Parallel Scavenge 15
CMS 6
G1 15- 动态年龄
幸存者1区 或 幸存者2区 的对象超过当前区总内存的50%时
把年龄最大的放入老年代(也就是说不一定到15岁)- 分配担保
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 Table用BitMap来实现
7.4.4 新老年代空间比例
5%—60%
一般不用手工指定
也不要手工指定
因为这是 G1 预测停顿时间 的基准
G1 可以自己动态调大小
7.4.5 GC何时触发
YGC
伊甸园区空间不足
多线程并行执行
FGC
Old空间不足
System.gc( )
7.4.5 如果G1产生FGC,我应该如何做
G1的调优目标就是不用让它FGC
产生FGC一定是空间不足
- 扩内存
- 提高CPU性能
- 降低MinedGC触发的阈值,让MixedGC提早发生(默认45%)
MixedGC相当于CMS
- 初始标记 STW
- 并发标记
- 最终标记 STW(重新标记)
- 筛选回收 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过程 ( 高响应,低停顿 )
- 初始标记阶段 initial mark
--------- STW 标记存活的对象,只标记一些根对象 (根可达法)
--------- 所以速度 很快
------------- 本阶段仅标记 GC roots 直接连接的对象
- 并发标记阶段 concurrent mark
--------- 边 标记 边 运行 工作线程- 重新标记阶段 remark (大多数的垃圾在并发标记时期已经标记完了)
--------- STW 过程,工作线程停止,标记新产生的垃圾
--------- 因为新产生的垃圾并不多,所以也很快- 并发清理阶段 concurrent sweep
--------- 边清理标记的垃圾,边运行工作线程,此时产生的浮动垃圾由CMS下次清理
7.8.2 CMS的两大问题
- 浮动垃圾
解决方案:降低触发CMS的阈值,保持老年代有足够的空间
Concurrent Model Failure
PromotionFaoled
如果是有很多碎片了,实在放不下对象了
CMS就会把老奶奶 Old Sorial 请出来清理
会SWT,停止工作线程,Old Sorial要清理很久很久很久
降低CMS触发的阈值
- 内存碎片化
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会删除灰色到白色的引用,此时白色对象就找不到了
因为灰色->白色
在并发标记时,引用 可能 产生 变化,白色对象有可能被错误回收
- 白:未标记的对象
- 灰:自身被标记,成员变量未被标记
本来已经标记成垃圾,
在应用执行过程中,
又有引用指向它
- 黑:自身和成员变量均已标记完成
解决方案
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
一组可被回收的分区的集合
记录哪些分区需要回收