前言
- 本博文主要讲 invoke 指令、常用GC垃圾清除算法、堆内存逻辑分区、栈上分配、。
- Java虚拟机基本结构
一、GC(Garbage Collector)Tuning 垃圾回收器
1、什么是垃圾
垃圾: 没有引用指向的任何对象,都叫做垃圾。
2、java与C++的区别
- java
- GC处理垃圾
- 开发效率高,执行效率低
- C++
- 手工处理垃圾
- 忘记回收
- 内存泄漏
- 回收多次
- 非法访问
- 开发效率低,执行效率高
3、how to find a garbage?
标题:如何找到垃圾?
- 两个方法的资料 https://cloud.tencent.com/developer/article/1656844
- 一般有两种方法
reference count
引用计数法root searching
根可达算法
a、reference count 引用计数法(java不用)
- 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器就减1。任何时刻计数器为0的对象就是不再被使用的。
无法解决的一种问题:循环引用
。引用计数法其实是很难解决对象之间相互循环引用的问题,所以,Java虚拟机里面没有选用引用计数算法来管理内存。- Python 用的就是引用计数,但是怎么解决循环引用的,自行探索
- 循环引用:如下图,三个对象互相引用,各自的计数器为1,但是没有其他对象引用这个循环引用,所以这是个垃圾,所以引用计数法无法解决这个问题。
b、Root Searching 根可达算法(java用)
- 在主流商用程序语言的主流实现中,都是称通过可达性分析来判定对象是否存活的
- 算法的基本思路就是通过
一系列称为GC Roots的对象作为起始点
,从这些节点开始向下搜索,搜索走过的路径被称为引用链
,当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用的。(说明根是一系列的对象) - 一系列的 GC roots 的对象,如下所示:
JVM stack
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
static references in method area
- 方法区中类静态属性引用的对象。
run-time constant pool
- 方法区中常量引用的对象。
native method stack
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
堆应该也是一个 GC roots 的对象
,但是我不知道为什么网上其他的文章几乎都没有写 堆的,大量的对象都在堆上,怎么可能不应该写呢。这里记录一下(20230225)
4、引用的两次标记过程
https://cloud.tencent.com/developer/article/1656844
5、强引用、软引用、弱引用和虚引用
- https://cloud.tencent.com/developer/article/1656844
重要:引用的两次标记过程 - 四种引用的实现
- 强引用:Object o = new Object
- 软引用:SoftReference
- 弱引用:WeakReference
- 虚引用:PhantomReference
6、总结
简单的对上面做一个总结,在JVM中判断一个对象是都需要回收有两种算法:引用计数法和可达性算法。引用计数法是通过判断引用的计数器的值是否为0来确认回收与否。这种算法听起来很简单,但是存在一个缺陷,就可以可能存在循环引用的情况。
还有一种就是可达性算法,可达性算法是通过判断引用能够被 GC Roots 访问到来确认回收与否。能被称为GC Roots对象也是有条件的主要有四种:虚拟机栈中引用的对象、方法中类静态属性引用的对象、方法中常量引用的对象和本地方法栈(native方法)中JNI引用的对象。
引用分为四种类型:强引用、软引用、弱引用和虚引用。
二、GC Algorithms(GC常用垃圾清除算法)
- 标记清除(mark sweep)
- 位置不连续
- 缺点:产生碎片、效率偏低(两遍扫描)、不适合Eden区(因为该区存活对象不多)。
- 拷贝算法 (copying)
- 没有碎片,浪费空间
- 缺点:移动复制对象,需要调整对象引用。
- 标记压缩(mark compact)
- 没有碎片,
- 缺点:效率偏低(两遍扫描,指针需要调整)
1、mark sweep
- 优点
- 算法相对简单,存活对象比较多的情况下效率较高
- 所以不适合Eden(伊甸园区),因为伊甸园区的存活对象不多。
- 缺点
- 两遍扫描,效率偏低,容易产生碎片
2、copying
- 优点
- 适用于存活对象较少的情况,只扫描一次,效率提高,没有碎片
- 适合Eden区。
- 缺点
- 空间浪费
- 移动复制对象,需要调整对象引用 (所以使用句柄定位法中的变量是不用变的,只需要变句柄池中改变指针即可;具柄池和直接指针 在 JVM知识体系学习四第六章中:https://developer.aliyun.com/article/1626527)。
3、mark compact标记-压缩
- 优点
- 不会产生碎片,方便对象分配
- 不会产生内存减半
- 缺点
- 扫描两次
- 需要移动对象,效率偏低
三、堆内存逻辑分区
1、部分垃圾回收器使用的模型
- 除Epsilon、ZGC、 Shenandoah之外的GC都是使用逻辑分代模型
- G1是逻辑分代,物理不分代
- 除此之外不仅逻辑分代,而且物理分代
2、java heap 模型
上图中解释如下:
new 区
,也叫young 区,也叫新生代old 区
,也就是老年代,也叫 tenured 区,(新生代:老年代=1:3)新生代
= Eden + 2个suvivor区 (也称 from 、to)(比例也是8:1:1)- 图中可以看出
- 新生代 采用复制GC算法。
- 老年代 采用 标记清楚 or 标记压缩 GC算法。
新生代 + 老年代(这两个是在heap中) + 永久代(1.7)Perm Generation(永久代实现的方法区) / 元数据区(1.8) Metaspace 取代 永久代 实现 方法区 。资料:(https://www.cnblogs.com/xiaofuge/p/14244755.html)
可以说 永久代或者元空间 等同于方法区,不能说方法区等同于永久代。
方法区是JVM的规范,而永久代是jdk1.8以前Hotspot对于方法区的实现。在jdk1.7以前,字符串常量池就保存在里面。1.7以后提出了去永久代的概念,第一步做的就是将字符串常量池移到了堆中。
jdk1.8以后,移除永久代,在本地内存上开辟了一块空间,称为元空间,里面存放运行时常量池、6个基本数据包装类常量池,class文件在jvm里的运行时数据结构,各种元数据等等,将静态变量移到了堆中。- 永久代 和 元数据区:存的是Class数据
- `永久代必须指定大小限制 ,元数据可以设置,也可以不设置,无上限(受限于物理内存)`
字符串常量
, 1.7 在 永久代,1.8 在 堆中- MethodArea逻辑概念 - 永久代、元数据区
新生代 = Eden + 2个suvivor区 (进行YGC:Young GC)
- YGC回收之后,大多数的对象会被回收,活着的进入s0
- 再次YGC,活着的对象eden + s0 -> s1
- 再次YGC,eden + s1 -> s0
- 年龄足够(15岁) -> 老年代 (15 CMS 6)
- s区装不下 -> 老年代
老年代 (进行FGC:Full GC)
- 顽固分子
- 老年代满了FGC
GC Tuning (Generation)
- 尽量减少FGC
- MinorGC = YGC
- MajorGC = FGC
3、一个对象从出生到消亡
- 一个对象产生之后,首先尝试栈上进行分配。
- 栈上分配如果分配不下,就进行Eden区
- Eden区经过一次垃圾回收之后, 进入 S1区(survive区)。
- S1区再经过一次垃圾回收机制之后,就进入S2区。
- 在S2和S1区之间来回经历,然后经过很老之后(年龄)就进入了老年代。
- S1-S2 之间的复制年龄超过限制时,进入old区通过参数:
-XX:MaxTenuringThreshold
配置。
4、专业名词:YGC/FGC
从图中可以看出:
MainorGC/YoungerGC 即 YGC
:年轻代空间耗尽时触发。MajorGC 即 FullGC/FGC
:在老年代无法继续分配空间时触发,新生代老年代同时进行回收。
四、栈上分配和TLAB(不少对象放的位置)
- 面试中可能会问:对象都会分配到 java heap 上嘛?
- 答案肯定是否定的。那除了分配到 heap 上,还会分配到哪里呢?
- 答:大多数对象会分配到 java heap 上,但是还有一些对象比较例外,如果都放到 java heap 上,会引起效率低下;所以还会放到
stack 和 TLAB(Thread Local Allocation Buffer,即线程本地分配缓存区)上
。 - 为什么会效率低下呢?有些对象是线程私有的,在方法内部产生使用,并没有去到外部,这种对象就随着方法或者线程结束而消失;所以这种对象就没有必要放在java heap 中,放在 stack 或者TLAB上就好。
- 详细资料一:Java常见面试题—栈分配与TLAB
- 详细资料二:Java对象栈上分配
- 答:大多数对象会分配到 java heap 上,但是还有一些对象比较例外,如果都放到 java heap 上,会引起效率低下;所以还会放到
- 继续问:什么样的内容会分配到栈上呢?什么样的内容会继续往Eden区分配?
1、栈上分配
题外话:(有对象放在了
java stack
上,也就回答了 在判定对象是否存活的·根可达算法
上的GC root
上,为啥会有个根在java stack
上了,因为有对象放在线程私有的java stack
上 )。在JVM中,堆是线程共享的,因此堆上的对象对于各个线程都是共享和可见的,只要持有对象的引用,就可以访问堆中存储的对象数据。虚拟机的垃圾收集系统可以回收堆中不再使用的对象,但对于垃圾收集器来说,无论筛选可回收对象,还是回收和整理内存都需要耗费时间。
如果确定一个对象的作用域不会逃逸出方法之外,那可以将这个对象分配在栈上,这样,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,无须通过垃圾收集器回收,可以减小垃圾收集器的负载。
JVM允许将线程私有的对象打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统性能。
栈上分配的技术基础:
- 一是 逃逸分析:逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。关于逃逸分析的问题 请看资料:Java中的逃逸分析
- 二是 标量替换:允许将对象打散分配在栈上,比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配。
只能在server模式下才能启用逃逸分析,
- 参数
-XX:DoEscapeAnalysis
启用逃逸分析, - 参数
-XX:+EliminateAllocations
开启标量替换(默认打开)。 - Java SE 6u23版本之后,HotSpot中默认就开启了逃逸分析,可以通过选项
-XX:+PrintEscapeAnalysis
查看逃逸分析的筛选结果。
- 参数
2、TLAB(Thread Local Allocation Buffer)
TLAB的全称是Thread Local Allocation Buffer,即
线程本地分配缓存区
,这是一个线程专用的内存分配区域。由于对象一般会分配在堆上,而堆是全局共享的。因此在同一时间,可能会有多个线程在堆上申请空间。因此,每次对象分配都必须要进行同步(虚拟机采用CAS配上失败重试的方式保证更新操作的原子性),而在竞争激烈的场合分配的效率又会进一步下降。JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。
TLAB本身占用eEden区空间,在开启TLAB的情况下,虚拟机会为每个Java线程分配一块TLAB空间。参数-XX:+UseTLAB开启TLAB,默认是开启的。TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%
,当然可以通过选项-XX:TLABWasteTargetPercent
设置TLAB空间所占用Eden空间的百分比大小。由于TLAB空间一般不会很大,因此大对象无法在TLAB上进行分配,总是会直接分配在堆上。TLAB空间由于比较小,因此很容易装满。
- 比如,一个100K的空间,已经使用了80KB,当需要再分配一个30KB的对象时,肯定就无能为力了。这时虚拟机会有两种选择,
- 第一,废弃当前TLAB,这样就会浪费20KB空间;
- 第二,将这30KB的对象直接分配在堆上,保留当前的TLAB,这样可以希望将来有小于20KB的对象分配请求可以直接使用这块空间。
- 实际上虚拟机内部会维护一个叫作refill_waste的值,当请求对象大于
refill_waste
时,会选择在堆中分配,若小于该值,则会废弃当前TLAB,新建TLAB来分配对象。这个阈值可以使用TLABRefillWasteFraction
来调整,它表示TLAB中允许产生这种浪费的比例。默认值为64,即表示使用约为1/64的TLAB空间作为refill_waste。默认情况下,TLAB和refill_waste都会在运行时不断调整的,使系统的运行状态达到最优。如果想要禁用自动调整TLAB的大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB,并使用-XX:TLABSize手工指定一个TLAB的大小。
- 比如,一个100K的空间,已经使用了80KB,当需要再分配一个30KB的对象时,肯定就无能为力了。这时虚拟机会有两种选择,
-XX:+PrintTLAB
可以跟踪TLAB的使用情况。一般不建议手工修改TLAB相关参数,推荐使用虚拟机默认行为。
3、程序测试(栈上分配和TLAB)
a、代码
package com.mashibing.jvm.c5_gc;
//-XX:-DoEscapeAnalysis(去掉逃逸分析) -XX:-EliminateAllocations(去掉标量替换) -XX:-UseTLAB(去掉TLAB) -Xlog:c5_gc*
// 逃逸分析 标量替换 线程专有对象分配
public class TestTLAB {
//User u;
class User {
int id;
String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
}
void alloc(int i) {
new User(i, "name " + i);
}
public static void main(String[] args) {
TestTLAB t = new TestTLAB();
long start = System.currentTimeMillis();
for(int i=0; i<1000_0000; i++) t.alloc(i);
long end = System.currentTimeMillis();
System.out.println(end - start);
//for(;;);
}
}
b、结果分析
- 虚拟机默认开启 栈上分配(逃逸分析、标量替换)、TLAB 的,在方法里创建了1000W个对象所需时间如下:
- 然后关掉 栈上分配、TLAB:
-XX:-DoEscapeAnalysis
:(去掉逃逸分析)-XX:-EliminateAllocations
:(去掉标量替换)-XX:-UseTLAB
:(去掉TLAB)- 结果如下:时间会变长。
4、总结
栈上分配
- 线程私有小对象:放在栈针的局部变量表中
- 无逃逸
- 支持 逃逸分析、标量替换
- 无需调整:虚拟机默认设置好了
线程本地分配TLAB (Thread Local Allocation Buffer)
- 占用eden,默认1%
- 多线程的时候不用竞争eden就可以申请空间,提高效率
- 小对象
- 无需调整:虚拟机默认设置好了
老年代
- 大对象
5、对象内存分配的两种方法
这部分内容,按说应该放在对象部分,但是作为补充就放在这里了
为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
a、指针碰撞
- 指针碰撞(Serial、ParNew等带Compact过程的收集器)
- 假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。
b、空闲列表
- 空闲列表(CMS这种基于Mark-Sweep算法的收集器)
- 如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。
五、对象何时进入老年代
1、各种GC算法
- 超过 XX:MaxTenuringThreshold 指定次数(YGC)进入老年代
- `Parallel Scavenge 15(最大就是15,在讲JMM的时候,对象头的GC age 就是4位,最大就是15,不能调整了)
CMS
6G1
15
- 动态年龄
- s1 - > s2超过50%
- 把年龄最大的放入O
2、整体流程图
- 略过栈上分配。(简单)
- 对象足够大的时候,直接进入 old 区,通过FGC回收
- 如果不大,进入TLAB区,最终进入Eden区
- 进行判断,是否清除,如果清除则直接回收
- 否则进入S1区,判断年龄是否到位了,如果够了直接进入old区,否则进入S2区,然后判断是否应该清除 进入S1区循环。
六、有关老年代、新生代两个问题
1、java 自带命令查找
-
开头 是标准参数-X
开头 是非标准参数-XX
开头 是非标不稳定参数(非标就是非标准版本)
2、动态年龄:(不重要)
https://www.jianshu.com/p/989d3b06a49d
3、分配担保:(不重要)
- YGC期间
survivor
区空间不够了 空间担保直接进入老年代 - 参考:https://cloud.tencent.com/developer/article/1082730
七、常见的垃圾回收器(背)
1、概述
(右上角那个 Epsilon
是debug用的,不用管。)
上图解释
- 左边的六个,都是逻辑分代,物理也分代;上面三个是 新生代的垃圾回收器,下面三个是老年代的垃圾回收器。
- 右边的G1 是 逻辑分代,物理不分代。
- ZFC、Shenandoah 逻辑、物理都不分代。
所有的垃圾回收器 都是 STW(stop the Word)。
Tips
:通过名字就可以看出来,并行
的都带有Parallel关键字,ParNew的Par也是Parallel缩写。1.8默认的垃圾回收:PS(ParallelScavenge) + ParallelOld。
- 不光是逻辑上分新生代、老年代;物理上也分新生代和老年代。
- jvm 1.8 默认的垃圾回收器
资料:
2、三种常见组合
从上图连线中可以看出:
Serial(单线程)+Serial Old
Parallel Scavenge(多线程) + Parallel Old
ParNew + CMS
- 其他虚线的都是不常见组合
3、垃圾回收器详细介绍(听的有点费劲)
垃圾回收作用在堆上的分代模型上(新生代YGC、老年代FGC)。
- 1、
串行回收
:一个回收线程到屋子(项目)里去回收,回收时项目需要STW。 - 2、
并行回收
:多个回收线程到屋子(项目)里去回收,回收时项目也需要STW - CMS(Concurrent Mark Sweep):
并发回收
,是一边运行一边回收,但是问题较多,目前没有一个JDK版本默认使用CMS。也正是它的出现,才诞生了G1、ZGC。- 注意:并发回收 和 并行回收不是一回事儿。jVM串行、并行、并发垃圾回收器
并行回收
是多个垃圾线程去干活,但是会有STW。同一时刻,要么用户线程要么垃圾回收线程 且 垃圾回收是多线程
。- 3、
并发回收
是多个垃圾线程和工作线程同时干活,没有STW。同一时刻,用户线程 和 垃圾回收线程 可能同时执行。 - 两个缺点:碎片化导致FGC和浮动垃圾。所以使用CMS+PN 要避免FGC,但是避免不了,无办法。
- 1、
JDK诞生 Serial;随着内存变大,Serial满足不了效率了,为了追随 提高效率,诞生了PS。
- 为了配合CMS,诞生了PN,CMS是1.4版本后期引入,CMS是里程碑式的GC,它
开启了并发回收的过程
,但是CMS毛病较多,因此目前没有任何一个JDK版本默认是CMS并发垃圾回收(CMS)是因为无法忍受STW。 G1是1.7引入的,9稳定使用
- 为了配合CMS,诞生了PN,CMS是1.4版本后期引入,CMS是里程碑式的GC,它
- 垃圾回收器回收垃圾时,会全部扫描整个内存,当内存太大的时候,STW时间也会更久,这是项目所不能允许的,所以才会诞生CMS。
a、Serial GC(年轻)
Serial
,年轻代,串行回收- a stop-the-world (STW,使用这个属于), copying collector which uses a single GC thread。
- 单CPU效率最高,虚拟机是Client模式的默认垃圾回收器
safe point
:安全点。- 用的极少。
- 如下图形象展示:
b、PS(Parallel Scavenge)(年轻)
PS(Parallel Scavenge)
,年轻代,并行回收,JDK1.8 默认使用。- 虚拟机1.8默认的就是
PS+PO
组合垃圾回收器。
c、ParNew(Parallel New)(年轻)
ParNew
,年轻代,配合CMS的并行回收
- 对 PS 的增强,
- 和CMS组合使用
d、Serial Old(老年)
Serial Old
,老年代,很老的回收器,串行。- 之前内存小,一个串行的可以解决
e、Parallel Old(老年)
PS(Parallel Old
,老年代,并行回收
f、CMS(Concurrent Mark Sweep)(老年)(******)
- 前后古人的新算法,老年代。
开启了并发回收的过程
,正是它的出现,才诞生了G1、ZGC
i、CMS概述
CMS(Concurrent(并发) Mark Sweep)
,1.4版本后期引入, 老年代 并发的, 垃圾回收和应用程序同时运行,降低STW的时间(200ms)。- 一边运行一边垃圾回收。
ii、四个阶段
CMS问题比较多,
所以现在没有一个版本默认是CMS,只能手工指定
。从线程角度来看,有四个阶段 (有的地方写6个阶段)如图所示
initial mark
,初始标记:先标记根上的(不多),这是STW的,时间非常短。concurrent mark
,并发标记
(最耗时阶段):和 并发线程 同时标记,无STW。但是在找的过程(并发过程),有可能发现有的垃圾此时被引用了,此时就不是垃圾了,此时进入第三阶段:remark阶段。remark
,重新标记:STW,标记产生新的垃圾,所以需要stop,但是新产生垃圾不多,所以时间也很快。concurrent sweep
:并发清理
:最后清理。产生的问题就是 ,同时也会产生新的垃圾,这种就是 浮动垃圾(下面线程角度中也有讲) 。只能等下一次运行才能清掉。当看到第2步和第3步的时候,
在JVM里看到Concurrent,说明垃圾回收线程和工作线程在一块儿工作,是这个意思
。
线程角度:
在下图中,可以看出,在第二次STW时,工作进程也在进行,肯定也会产生垃圾,这里的垃圾就叫 浮动垃圾。这种浮动垃圾 只能等下一次运行才能清掉。CMS既然是采用
Mark-Sweep(标记-清除)
算法,就一定会有碎片化的问题,碎片到达一定程度,CMS的老年代分配对象分配不下的时候,使用SerialOld 进行老年代回收。想象一下:
- PS + PO -> 加内存 换垃圾回收器 -> PN + CMS + SerialOld(几个小时 - 几天的STW)
- 几十个G的内存,单线程回收 -> G1 + FGC 几十个G -> 上T内存的服务器 ZGC
- 算法:三色标记 + Incremental Update
iii、CMS的问题
Memory Fragmentation(内存碎片 )
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction 默认为0 指的是经过多少次FGC才进行压缩Floating Garbage(浮动垃圾)
Concurrent Mode Failure
产生:if the concurrent collector is unable to finish reclaiming the unreachable objects before the tenured generation fills up, or if an allocation cannot be satisfiedwith the available free space blocks in the tenured generation, then theapplication is paused and the collection is completed with all the applicationthreads stopped
解决方案:降低触发CMS的阈值
PromotionFailed
解决方案类似,保持老年代有足够的空间
–XX:CMSInitiatingOccupancyFraction 92% 可以降低这个值,让CMS保持老年代足够的空间
iv、CMS缺点(也就是上面的问题)
- memory fragmentation
- -XX:CMSFullGCsBeforeCompaction
- floating garbage
- Concurrent Mode Failure –XX:CMSInitiatingOccupancyFraction 92%
- SerialOld
v、CMS日志分析
执行命令:
java -Xms20M -Xmx20M
-XX:+PrintGCDetails
-XX:+UseConcMarkSweepGC com.mashibing.jvm.gc.T15_FullGC_Problem01
GC (Allocation Failure) [ParNew: 6144K->640K(6144K), 0.0265885 secs] 6585K->2770K(19840K), 0.0268035 secs
;ParNew:年轻代收集器
6144->640:收集前后的对比
(6144):整个年轻代容量
6585 -> 2770:整个堆的情况
(19840):整个堆大小
g、G1(JDK9 )
i、基础概述和常见问题
G1阐述入门:Garbage First Garbage Collector Tuning
- The Garbage First Garbage Collector (G1 GC) is the lowpause, server-style generational garbage collector for Java HotSpot VM. The G1 GC uses concurrent(并发) and parallel(并行) phases to achieve its target pause time and to maintain good throughput(吞吐量). When G1 GC determines that a garbage collection is necessary, it collects the regions with the least live data first (garbage first G1的来源).
- G1的吞吐量和PS相比,要降低了10%-15%,但是STW停顿时间一般是200ms。
- 如果想让程序200ms都有响应用G1,
- 如果应用程序追求throughput(吞吐量)则用PS。
- G1是一种服务端应用使用的垃圾收集器,目标是用在多核、大内 存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同 时还能保持较高的吞吐量
G1(10ms)算法:三色标记 + SATB
在分代算法中:采用逻辑分代,物理不分代。
计算机设计架构中的两大思想:
- 分而治之
- 分层思想
基本概念(不是很重要)
card table
- 黄色小球是一个个的对象,红色小球为GCroot的根对象,红色小球有可能引用指向到了老年代,而老年代有可能也指向了年轻代里。做一个YGC,有可能需要去遍历老年代里所有的对象,看有没有指向。所以很费劲。
- 所以把内存分为一个个的card,则一个个对象都存在一个个的card中。
- 由于做YGC时,需要扫描整个OLD区,效率非常低,所以JVM设计了CardTable, 如果一个OLD区CardTable中有对象指向Y区,就将它设为Dirty,下次扫描时,只需要扫描Dirty Card
- 在结构上,Card Table用BitMap来实现
CSet = Collection Set
:G1根据内部的算法,有好多card,有哪些card需要被回收,这些被回收的card放到了一张表格里,这个表格就是
Collection Set
。一组可被回收的分区的集合。
- 在CSet中存活的数据会在GC过程中被移动到另一个可用分区,
- CSet中的分区可以来自Eden空间、survivor空间、或者老年代。
- CSet会占用不到整个堆空间的1%大小。
RSet: RememberedSet
,(ZGC 舍弃了,没有了)- 记录了
其他Region
中的对象到本Region
的引用, - RSet的价值在于:使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,
只需要扫描RSet即可
。
- 记录了
扩展:阿里的多租户JVM
- 每租户单空间
- session based GC
新老年代的比例
- 5%-60%,不需要调优啦
- 一般不用手工指定
- 也不要手工指定,因为这是G1预测停顿时间的基准。
- 5%-60%,不需要调优啦
GC何时触发(有三种GC)
- 产生YGC 的情况
- Eden空间不足
- 多线程并行执行
- 产生MixedGC 的情况
- 等价于 CMS ;YGC已经不行了,对象的产生特别多了,超过45%了。这时启动MixedGC。
- 产生FGC 的情况
- Old空间不足
- System.gc()
- 产生YGC 的情况
如果G1产生FGC,你应该做什么?
- 扩内存
- 提高CPU性能(回收的快,业务逻辑产生对象的速度固定,垃圾回收越快,内存空间越大)
- 降低
MixedGC
触发的阈值,让MixedGC
提早发生(默认是45%)
G1中的MixedGC
- 等价于 CMS ;YGC已经不行了,对象的产生特别多了,超过45%了。这时启动MixedGC。
-XX:InitiatingHeapOccupacyPercent
- 默认值45%,可以自定义。
- 当O超过这个值时,启动
MixedGC
MixedGC的过程:四步(和CMS的回收阶段大致一样)
初始标记,标记根 STW
并发标记
SATB算法:
Snapshot At The BeginningGC开始时,通过root tracing得到一个Snapshot维持并发GC的正确性如何做到并发GC的正确性:
三色标记算法:
白:对象没有标记,标记阶段结束后,会被回收
灰:对象标记了,但是他的Field还没有标记或标记完
黑:对象标记了,且他的Field也标记完成了最终标记 STW (重新标记)
筛选回收 STW (并行):
和CMS的区别之处就是这里有筛选过程:筛选最需要回收的,垃圾占得最多的Region;将Region复制到另一块区域里,复制同时进行压缩。碎片也就没有CMS那么多。并行筛选回收
G1的Full GC:
java 10以前是串行FullGC,之后是并行FullGC。
- G1调优目标之一:尽量减少FGC,
- 扩展回忆上面知识:如果G1产生FGC,你应该做什么?
ii、G1的内存物理模型
G1的内存物理模型:将内存分为一小块的
Region(区域)
,对应逻辑分代上的 新生区(Eden)、存活区(Survivor)、老年区(Old)、大对象区(Humongous)。一般都是2n 次方大小。每个 Region 有多大
- headpRegion.cpp
- 取值:1,2,4,8,16,32
- 手工指定:-XX:G1HeapRegionSize
Humongous Object
- 超过单个region的50%
- 超过单个region的50%
每个分区都可能是年轻代也可能是老年代,但是在同一时
刻只能属于某个代。年轻代、幸存区、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。在物理上不需要连续,则带来了额外的好处——有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾,这也就是G1名字的由来,即首先收集垃圾最多的分区。
新生代其实并不是适用于这种算法的,依然是在新生代满了的时候,对整个新生代进行回收——整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小。
G1还是一种带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩。每个分区的大小从1M到32M不等,但是都是2的冥次方。
iii、四大特点
- 并发收集
颜色指针(color pointer)
: JVM中,class pointer(类指针) 没有压缩,是64位,拿出三位,做标记,当指向不同引用时,指针产生变化,指向A,又指向了B,在产生变化的过程中,用这三个标记位给标记出来,这个指针变过了;当垃圾回收时,会扫描变化过的指针,这就是color pointer。(不是这里的知识点)三色标记
:把对象分为三个不同的颜色,每个不同的颜色标记着它到底有没有被标记过,标记了一半,还是完全没有被标记过。
- 压缩空闲空间不会延长GC的暂停时间;
- 更易预测的GC暂停时间(STW);
- 适用不需要实现很高的吞吐量的场景
h、ZGC (JDK11)
- ZGC(Z Garbage Collector)(OpenJDK11开始使用) (1ms) PK C++
- 算法:ColoredPointers + LoadBarrier
- 理解并应用JVM垃圾收集器-ZGC
i、Shenandoah(JDK)
Shenandoah
算法:ColoredPointers + WriteBarrier
j、Eplison(debug 使用)
Eplison,debug 使用。
k、PS 和 PN区别的延伸阅读
l、垃圾收集器跟内存大小的关系
- Serial 几十兆
- PS 上百兆 - 几个G
- CMS - 20G
- G1 - 上百G
- ZGC - 4T - 16T(JDK13)
八、常见垃圾回收器组合参数设定
-XX:+UseSerialGC
= Serial New (DefNew) + Serial Old- 小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器
-XX:+UseParNewGC
= ParNew + SerialOld- -XX:+UseConc(urrent)MarkSweepGC = ParNew + CMS + Serial Old
-XX:+UseParallelGC
= Parallel Scavenge + Parallel Old (1.8默认) 【PS + SerialOld】-XX:+UseParallelOldGC
= Parallel Scavenge + Parallel Old-XX:+UseG1GC
= G1- Linux中没找到默认GC的查看方法,而windows中会打印UseParallelGC
java +XX:+PrintCommandLineFlags -version
- 通过GC的日志来分辨
- Linux下1.8版本默认的垃圾回收器到底是什么?
- 1.8.0_181 默认(看不出来)Copy MarkCompact
- 1.8.0_222 默认 PS + PO