JVM性能优化实践-读书笔记-第6章
[
]
6.理解垃圾收集
6.1 标记和清除
标记和清除算法(mark and sweep)

- 循环遍历已分配链表,清空标记位
- 从GC根开始,寻找活跃对象
- 在到达的每一个对象上设置一个标记位
- 循环遍历已分配链表,对于每个标记位尚位设置的对象: a. 回收堆中内存,将其放回空闲链表 b. 从已分配链表中移除该对象
活跃对象按照深度优先来定位,生成的图叫活跃对象图(live object graph) ,也称为可达对象的传递闭包(transitive closure of reachable object)
垃圾收集术语
- 全部停顿 – 在进行垃圾收集时用户线程会停止
- 并发 – 垃圾可以在应用程序线程运行的时候运行
- 并行 – 多个线程执行垃圾收集
- 精确 – 有足够的类型信息
- 保守 – 缺乏精确模式的类型信息
- 移动 – 对象在内存中的位置可能变化
- 压缩 – 已分配的内在被组织成一个单一的连续的内存区
- 疏散 – 已经收集区域完全为空
6.2 Hotspot运行时
java的两种类型值
- 基本类型
- 对象引用
java是纯粹的按指引用(call by value)
Java是用偏移操作符来访问字段和调用对象引用的方法。
6.2.1 对象的运行时表示
Hotspot用oop的结构表示运行时java对象,即 普通对象指针(ordinary object pointer)
oop的继承结构:
oop(抽象基类)
|-instanceOop(实例对象)
|-methodOop(方法的表示)
|-arrayOop(数组的抽象基类)
|-symobolOop(内部符号/字符串类)
|-klassOop(klass头部,只在Java7以更早的版本里)
|-markOop
oop的每一个对象上都有两个机器字的头。
- 第一个是mark word,是一个指针,指向该实例的元数据;
- 第二个是kclass word,指向同级别的元数据;
Java7 及之前的版本里,kclass word 指向名为
PermGen的内存区,是堆的一部分,需要一个相应的对象头 在这种情况下,把元数据据为klassOopJava8 及之后的版本中,kclass 被保存在堆的主要内存之外,不需要有对象头。
表示运行时对象时一个指针指向类级别的元数据,一个指针指向实例级别的原数据是一种常见的设计形式
6.2.2 GC根和Arena
GC根—-内存的“锚点”,从内存区外指向一个内存区。
与之相对的是内部指针,从内存区内指向同一个内存区内的另一个区域。
垃圾收集器是以内存区域的方式工作的,这里的内存区统称为Arena
6.3 分配与生命周期
垃圾收集行为由两个驱动因素:
- 分配率—-新创建的对象在一个时间段内所使用的内存量,通常会用
Mb/s来计量; - 对象生命周期
弱分代假说
- JVM 和类似的软件的中,对象生命周期表现为双峰分布—-大部分对象生命周期很短,次一级的对象寿命长于预期
- 有极少数从老年代指向新生代的引用
HotSpot利用弱分代假说 1 的机制:
- 跟踪每个对象的年龄(即代数,熬过的垃圾收集次数)
- 除大对象外,在Eden(也叫Nursery)空间中创建新对象
- 维护一个单独的内存区域(Old Gerneration / Tenured Generation) 保存已经存活足够长而且很有可能继续存活下去的对象
应对弱分代假说2,hotspot 维护了一个叫卡表card table的结构,用以记录哪些老年代对象可能指向新生代对象
6.4 Hotspot 中的垃圾收集
- 新生代(Eden)内存分配以线程本地分配进行;
- 收集为结合Survivor空间进行半空间收集
6.4.1 纯程本地分配
线程本地缓冲区(thread-local allocation buffer,TLAB)
- 将eden分为若干个缓冲区
- 将缓冲区交给应用程序线程使用
- 每个线程不用考虑其他线程
6.4.2 半空间收集
半空间疏散式收集器(hemispheric evacuation collector)
- 使用两个空间
- 将空间作为实际寿命比较短的临时对象存储区
好处
- 防止短寿命对象把老年代弄乱
- 降低整堆收集的频率
空间的基本属性
- 当收集器在收集当前活跃的半空间时,对象会被以压缩方式移到另一半空间,完成收集的一半空间会被清空以等待利用
- 任何时间,总有一半空间是空的
Hotspot用这种方法,结合Eden空间为新生代提供收集器 Hotsopot新生代堆的半空间部分被称为Survivor空间
6.5 并行收集器
在JAVA8之前,默认的收集器是并行收集器,并且新生代和老年代的收集都是全部停顿(STW)
主要有如下:
- Parallel GC —- 用于新生代的简单垃圾收集
- ParNew —-与CMS配合使用的并行垃圾收集变种
- ParalleOld —- 用于老年代的并行收集器
6.5.1 新生代并行收集
场景
当一个线试图向Eden空间中分配对象,但是TLAB中已经没有足够空间
步骤
- 停止应用线程
- 查看新生代(
Eden+非空Suvivor)的非垃圾对象; - 用卡表查找老年代对新生代的引用;
- 将所有的幸存对象疏散到非空的
Survivor空间中,同时存活次数加一 - 将
Eden和疏散过的Survivor空间标记为可用空间 - 启动应用线程
缺点
public static void main(String[] args){
int[] anInt = new int[1];
anInt[0]=42;
Runnable r = ()->{
anInt[0]++;
System.out.println("Chanded: "+anInt[0]);
}
new Thread(r).start();
}
- TLAB 是单个线程私有的,只有在分配时才成立;
- 这个环境分配后就会被销毁;
6.5.2 老年代并行收集
ParallOld是一个连续内存压缩的收集器
缺点
- 老年代空间在堆中占大部分(大小默认为新生代的7倍)
- 随着堆大小不断增加,暂停时间变得糟糕
6.6 分配的作用
例
堆参数
| 区域 | 大小 |
|---|---|
| 总体 | 2G |
| 老年代 | 1.5G |
| 新生代 | 500Mb |
| Eden | 400Mb |
| Survivor1 | 50Mb |
| Survivor2 | 50Mb |
垃圾收集指标
- 内存分配率 100 Mb/s
- 新生代垃圾收集时间 2ms
- 整堆收集时间 100ms
- 对象生命周期 200ms
分析
Eden空间填满的时间:
400Mb / 100Mb/s = 4s
此时产生第一次清理,因为对象生命周期是200ms,故要保留的内存大小:
400Mb / ( 4s / 200ms) = 20Mb
因此
GC0 @ 4s 20Mb Eden -> Survivor1
即: GC0 发生在第4秒,将 20Mb 内存从 Eden 疏散到 Survivor1
过了4秒后,进行第二次收集;
GC0 @ 8s+2ms 20Mb Eden -> Survivor2
即: GC1 发生在第8秒,将 20Mb 内存从 Eden 疏散到 Survivor2 2ms为第一次收集的耗时
第三次:
GC2 @ 12s+4ms 20Mb Eden -> Survivor1
问题
- 因为对象存活时间是200ms,在这个场景里没有对象进入老年代
另一种场景
如果Eden中已经分配了200Mb,同时分配锋值到达,另外200Mb在200ms分配,虽然对象的存活时间都是200ms,而且最近分配的对象存活时间刚到100ms,但此时JVM只能让对象进行进入老年代。 但
GC0 @ 2.2s 100Mb Eden -> Tenured(100)
按此类推:
GC1 @ 2.602s 200Mb Eden -> Tenured(300)
GC2 @ 3.004s 200Mb Eden -> Tenured(500)
GC3 @ 7.006s 20Mb Eden -> Survivor1(20)
如果分配率过高,会将对象最终晋升到老年代,称为过早晋升(premature promotion),这是垃圾收集器的间接效应之一,也是调优的起点