第三章:垃圾收集器与内存分配策略
3.3 垃圾回收算法
两大类:引用计数式垃圾收集、追踪式垃圾收集
分代收集理论
-
弱分代假说:绝大多数对象都是朝生夕灭的
-
强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
-
跨代引用假说:跨代引用相对于同代引用来说仅占极少数
由上述两种假说推出。举个例子:如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这种跨代引用也随即被消除了。
故在新生代上建立全局的数据结构——记忆集(Remembered Set),这个结构吧老年代划分成若干小块,表示出老年代的哪一块内存会存在跨代引用。此后发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
部分专业术语
- 部分收集(Partial GC)
- 新生代收集(Minor GC/Young GC)
- 老年代收集(Major GC/Old GC):目前只有CMS收集器会有单独收集老年代的行为
- 混合收集(Mixed GC):目前只有G1
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
标记-清除算法
- 效率不稳定
- 内存空间碎片化
标记-复制算法
新生代中的对象有98%熬不过第一轮收集。因此不需要按照1:1的比例来划分新生代的内存空间
- 将新生代划分为Eden和两块较小的Survivor,每次分配只使用Eden和其中一块Survivor,存活的对象复制到另一块Survivor上
- HotSpot默认Eden和Survivor的大小比例是8:1
- 分配担保:当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(大多数是老年代)进行分配担保
标记-整理算法
对象移动操作必须全程暂停用户应用程序才能进行——Stop The World
Q:标记复制需要暂停吗?
-
关注吞吐量的Parallel Scavenge收集器基于标记-整理
-
关注延迟的CMS收集器基于标记-清除
暂时容忍空间碎片,碎片化程度影响到对象分配时,再用标记-整理算法收集一次
3.4 HotSpot的算法细节实现
根节点枚举
- 可达性分析算法耗时
- 查找引用链过程:耗时最长,已经可以并发
- 根节点枚举:一定要Stop The World
如何理解?
准确式垃圾收集:虚拟机有办法得知哪些地方存折对象引用:使用OopMap的数据结构实现
一旦类加载动作完成的时候,HotSpot就会把对象内什么便宜量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置时引用。这样收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等GC Roots开始查找
Q:P82代码没有看懂
安全点
- 抢先式中断
- 主动式中断√
- 轮询标志,若中断标志为真,则自己在最近的安全电商主动中断挂起
- 汇编test %eax和一个地址。暂停时,设置该地址不可读,则会产生一个自陷异常信号,然后在预先注册的异常处理器中挂起
安全区域
指能够确保在某一段代码片段中,引用关系不会发生变化
可以把安全区域看作被拓展拉伸了的安全点
记忆集与卡表
记忆集有三种记录精度
- 字长精度
- 对象精度
- 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针
卡表是卡精度的实现方式
按照AD的理解,只有新生代才有卡表。
// 在下面的写屏障中我们可以知道,在赋值之后,能够修改该对象对应的卡表。
写屏障
赋值前:写前屏障;赋值后:写后屏障
目前只有G1用到了写前屏障,其他的只用到了写后屏障
伪共享
现代中央处理器的缓存系统是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。
解决方案:先判断卡表是否已被修改,如果没有才修改
使用-XX:+UseCondCardMark开启卡表更新判断
并发的可达性分析
(黑色灰色白色代表含义见P87)
-
产生“对象消失”的条件
- 赋值器插入了一条或多条从黑色对想到白色对象的新引用
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
-
增量更新:破坏第一个条件
原始快照:破坏第二个条件
-
CMS基于增量更新,G1和Shenandoah使用原始快照
经典垃圾收集器
JDK1.7和JDK1.8默认使用Parallel Scavenge和Parallel Old
JDK1.9默认使用G1
-
Serial
-
标记-复制
-
单线程,Stop The World
-
-
ParNew收集器
- 标记-复制
-
Parallel Scavenge
-
标记-复制
-
关注吞吐量
-
-
Serial Old
-
Parallel Old
-
CMS
-
G1
实战:内存分配与回收策略
-
对象超过一定阈值可能会直接分配在老年代
-
分配担保:回收之后的对象S放不下,全部放到老年代,然后把新的对象放到Eden
-
年龄:第一次移动到S,设置年龄为一岁,15岁到达老年代
-
动态对象年龄判定
如果Survivor空间中相同年龄所有对象大小的综合大于Survivo空间的一般,年龄大于或等于该对象的对象就可以直接进入老年代,无需等到-XX:MaxTenuringThreshold中要求的年龄。
-
空间分配担保
发生Minor GC之前:
1
2
3
4
5
6
7
8
9
10
11
12
13
if(/*老年代最大可用连续空间 > 新生代所有对象总空间*/) {
// 本次Minor GC安全
} else {
if (/*-XX:HandlePromotionFailure参数允许担保失败*/) {
if (/*老年代最大可用的连续空间 > 历次晋升到老年代对象的平均大小*/) {
// 尝试一次Minor GC
} else {
// Full GC
}
} else {
// Full GC
}
}
其他
OpenJDK
ParallelScavengeHeap.java中有
1
2
3
4
5
private static synchronized void initialize(TypeDataBase db) {
Type type = db.lookupType("ParallelScavengeHeap");
youngGenField = type.getAddressField("_young_gen");
oldGenField = type.getAddressField("_old_gen");
}
分配地址、内存应该是要用到这个Field的
进入getAddressField(),是一个接口,其实现类为BasicType
1
2
3
4
5
6
7
8
public AddressField getAddressField(String fieldName) {
// This type can not be inferred (for now), so provide a wrapper
Field field = getField(fieldName);
if (field == null) {
return null;
}
return new BasicAddressFieldWrapper(field);
}
进入getField(String),最终定位到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Field getField(String fieldName, boolean searchSuperclassFields,
boolean throwExceptionIfNotFound) {
Field field = null;
if (nameToFieldMap != null) {
field = (Field) nameToFieldMap.get(fieldName);
if (field != null) {
return field;
}
}
if (searchSuperclassFields) {
if (superclass != null) {
field = superclass.getField(fieldName, searchSuperclassFields, false);
}
}
if (field == null && throwExceptionIfNotFound) {
throw new RuntimeException("field \"" + fieldName + "\" not found in type " + name);
}
return field;
}
其中有一步field = (Field) nameToFieldMap.get(fieldName);
nameToFieldMap是一个HashMap,使用addField()对该Map进行添加
1
2
3
4
5
6
7
8
9
10
11
/** This method should only be used by the builder of the
TypeDataBase. Throws a RuntimeException if a field with this
name was already present in this class. */
public void addField(Field field) {
if (nameToFieldMap.get(field.getName()) != null) {
throw new RuntimeException("field of name \"" + field.getName() + "\" already present in type " + this);
}
nameToFieldMap.put(field.getName(), field);
fieldList.add(field);
}
这个方法被HotSpotTypeDataBase.java的createField方法调用,而createField方法又在CommandProcessor.java中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
new Command("field", "field [ type [ name fieldtype isStatic offset address ] ]", true) {
public void doit(Tokens t) {
if (t.countTokens() != 1 && t.countTokens() != 0 && t.countTokens() != 6) {
usage();
return;
}
if (t.countTokens() == 1) {
Type type = agent.getTypeDataBase().lookupType(t.nextToken());
dumpFields(type);
} else if (t.countTokens() == 0) {
Iterator i = agent.getTypeDataBase().getTypes();
while (i.hasNext()) {
dumpFields((Type)i.next());
}
} else {
// 略...
// Create field by type
HotSpotTypeDataBase db = (HotSpotTypeDataBase)agent.getTypeDataBase();
db.createField(containingType,
fieldName, fieldType,
isStatic,
offset,
staticAddress);
}
}
},
这是一个Command类的对象,放在一个数组中,然后应该是根据匹配情况来执行
不知道field [ type [ name fieldtype isStatic offset address ] ]
代表什么,说不定可以手动设置地址
接下来干什么
- 查找有没有源码解读的书
- Q:为什么OpenJDK中既有Java又有C++?二者是互补、依赖还是相互独立?
- Debugger实现原理