1-Jvm 分配对象的基本原理
源码位于: ./hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp
CASE(_new): {
u2 index = Bytes::get_Java_u2(pc+1);
ConstantPool* constants = istate->method()->constants();
// 1. 断言确保是 klassOop 和 instanceKlassOop, 下面2个 assert
if (!constants->tag_at(index).is_unresolved_klass()) {
// Make sure klass is initialized and doesn't have a finalizer
Klass* entry = constants->slot_at(index).get_klass();
assert(entry->is_klass(), "Should be resolved klass");
Klass* k_entry = (Klass*) entry;
assert(k_entry->oop_is_instance(), "Should be InstanceKlass");
InstanceKlass* ik = (InstanceKlass*) k_entry;
// 2. 确保当前的类型经过了初始化阶段
if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) {
// 获取对象的长度
size_t obj_size = ik->size_helper();
oop result = NULL;
// If the TLAB isn't pre-zeroed then we'll have to do it
// 记录是否将所有的对象的字段初始化为零值
bool need_zero = !ZeroTLAB;
// 如果是要在 TLAB 中分配对象
if (UseTLAB) {
result = (oop) THREAD->tlab().allocate(obj_size);
}
if (result == NULL) {
// 表明需要去 堆中分配对象 .
need_zero = true;
// Try allocate in shared eden
// 开始准备进去循环模式
retry:
HeapWord* compare_to = *Universe::heap()->top_addr();
HeapWord* new_top = compare_to + obj_size;
if (new_top <= *Universe::heap()->end_addr()) {
// 通过 CAS 指令分配空间. 失败会调到 retry .
if (Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) {
goto retry;
}
result = (oop) compare_to;
}
}
// result 不为空表示分配成功
if (result != NULL) {
// Initialize object (if nonzero size and need) and then the header
// 表示要初始化零值
if (need_zero ) {
HeapWord* to_zero = (HeapWord*) result + sizeof(oopDesc) / oopSize;
obj_size -= sizeof(oopDesc) / oopSize;
if (obj_size > 0 ) {
memset(to_zero, 0, obj_size * HeapWordSize);
}
}
// 如果要使用偏向锁
if (UseBiasedLocking) {
result->set_mark(ik->prototype_header());
} else {
result->set_mark(markOopDesc::prototype());
}
result->set_klass_gap(0);
result->set_klass(k_entry);
// 将对象引用放入到栈中、继续执行下一条指令
SET_STACK_OBJECT(result, 0);
UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
}
}
}
// Slow case allocation
CALL_VM(InterpreterRuntime::_new(THREAD, METHOD->constants(), index),
handle_exception);
SET_STACK_OBJECT(THREAD->vm_result(), 0);
THREAD->set_vm_result(NULL);
UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
}-
要先初始化
KClass, 这个信息在常量池中, 一般是双亲委任 类加载机制 ; -
对象的长度通过
size_helper这个函数获取, 细节很多 ; -
对象的分配大致经过如下的三个路径:
Fast Allocate with TLAB: 在Thread Local Allocation Buffer中优先分配, 这个结构的作用是用来减少 并发冲突的 ;- 如果
TLAB失败则降级为 乐观锁分配内存,不断地通过CAS操作去尝试SWAP堆顶的指针, 成功的线程即可成功 分配内存 ; - 上面再失败则是通过 更复杂的机制了,
InterpreterRuntime::_new,会涉及到 通用对象的分配path,例如锁机制等等
-
内存分配了,就要给字段设置 0值并设置对象头信息, 其中包含了如下的元数据:
- 对象所属类的信息
- 如果找到类的元信息
- 对象的
Hashcode, 实际的计算会后置到真正调用Object::hashCode的时候计算 - 对象的
GC分代年龄 - …
-
零值分配后通过
invokespecial指令来触发init方法, 也就是对象的构造器
我们用 一个
demo来输出一个类的内存信息.
对象的内存布局分为三个区域:
- 对象头
- 实例数据
padding信息
public static class B {
Object o;
int e;
// @sun.misc.Contended("first")
int f;
// @sun.misc.Contended("first")
int g;
// @sun.misc.Contended("last")
// int i;
// @sun.misc.Contended("last")
// int k;
}
public static void main(String[] args) {
System.out.println(VM.current().details());
System.out.println(ClassLayout.parseClass(B.class).toPrintable());
B b1 = new B();
B b2 = new B();
System.err.println(VM.current().sizeOf(b1));
System.err.println(VM.current().addressOf(b1));
System.err.println(VM.current().sizeOf(b2));
System.err.println(VM.current().addressOf(b2) - VM.current().addressOf(b1));
}com.ysz.dm.fast.basic.jol.Jol_Dm_001$B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int B.e N/A
16 4 int B.f N/A
20 4 int B.g N/A
24 4 java.lang.Object B.o N/A
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
32 // 对象 A 的大小
31877408928
32 // 对象 B 的大小
32 // 对象 B 和 对象 A 的偏移
通过分析输出有如下结论:
- 对象头占据了
12Bytes, 非常的占内存,这从侧面说明了fastutil这种原生类型库的作用 ; Object占据了4Bytes, 这个是使用了 对象头压缩技术, 如果分配的JVM超过32G,4Bytes无法代表这么多内存,会变为8Bytes;- 字段的布局被重新排序了,
o分配了最下面. 一般重排序的规则是 大的在前面,然后按照类型 ; - 因为
padding占据了4Bytes,Hotspot要保证是8Bytes的整数倍 ;
2-三色标记法 和 CMS GC
前置
你需要知道 GC 的基本理论,分代,sweep, compact , 引用计数和可达性分析 ;
以下是个人理解.
三色是哪三色
- 白色: 收集器没有访问过的对象
- 灰色: 收集器访问的中间状态,假设一个对象有3个引用,只要这3个引用有一个没有被访问,就是灰色状态
- 黑色:收集器访问过 它和它的所有引用
三色标记法的理论,类似
bfs
- 所有对象都没有访问,因此都是 白色
- 根据规则定义出
GC Roots, 作为bfs的第一层 ; - 层序遍历,取出这一层, 对某个对象 :
- 标记为 灰色
- 然后取出他们的所有的引用作为子节点,标记为灰色
- 所有的子节点标记为灰色后,自己标记为 黑色, 代表全部扫描了
- 当没有 灰色节点的时候,只有 黑和白, 白色的就可以干掉了
三色标记法并发场景的2个问题
由于在 并发标记的时候,线上应用不会暂停, 所以有2大类问题
第一类:
- 浮动垃圾: 新生成的垃圾,这个不是特别重要,可以选择下一次再回收 ;
- 漏扫: 一个对象在并发的过程中从 不是垃圾 → 垃圾 ;
第二类: 一个对象从 垃圾 →. 不是垃圾, 这个 比较严重, 一定要处理, 下面举个场景:
- 有一个对象
A,一开始是白色的 ; - 线程1, 删除了这个引用, (删除引用) ;
- 线程2 并发的 有一个黑色对象 B, 引用了 A, (增加引用) ;
CMS 解决这个问题的思路被称为 Write Barrier 技术, 也就是在 对象做引用更新的时候, 会记录下这个数据需要被重新标记 (ReMark), 然后再 Remark 的时候通过 STW 来解决这些增量的问题;