一、运行时数据区域
1.1.java运行时数据区域划分
Java虚拟机所管理的内存包括以下几个数据区域:
- 程序计数器
- 线程私有,每个线程会单独分配一个
- 不会出现OOM异常
- 用于保存当前线程的执行位置
- 虚拟机栈
- 线程私有,每个线程都会分配一个
- 该区域会出现两种异常
- StackOverflowError异常: 线程请求的栈深度大于虚拟机栈所允许的深度
- OOM:如果虚拟机栈可以动态扩展,当扩展无法申请到足够的内存时,会发生OOM异常
- 本地方法栈
- 与虚拟机栈类似,只不过是用于执行本地方法
- 堆
- 线程共享
- 用于存放对象实例
- 是垃圾收集器管理的主要区域,也成为GC堆
- 可能发生OOM
- 方法区
- 线程共享
- 用于存储被jvm加载的class信息,主要包括类信息、方法信息、静态变量、常量、即时编辑器编译后的代码
- 可能会发生OOM
- 运行时常量池
- 方法区的一部分,用于存放class常量池信息,主要是编译期生成的各种字面量和符号引用呢.
- 可能会发生OOM
- 直接内存
- 不是虚拟机规范中的一部分,但是会被使用到.
- 可能会发生OOM
1.2.jdk1.6,1.7,1.8运行时数据区域区别
二、内存分配和回收策略
2.1.对象
2.1.1.对象的创建过程
2.1.1.1.对象实例化的方式
java对象实例化的方式主要有以下几种:
- 通过new 语句创建
- 通过工厂方法返回,如String.valueOf(“”),本质还是new 语句创建
- 反射创建,通过java.lang.Class或者java.lang.reflect.Constructor类的newInstance()方法.
- 调用对象的clone方法
- 通过I/O ,反序列化得到
2.1.1.2.对象实例化的步骤
以new指令为例, new Object(),实例化过程分为以下几步:
- 检查类: 检查当前类是否已经在方法区存在,并且检查当前类是否已被加载、解析、初始化,如果没有,执行类加载过程.
- 分配内存: 如果类已经加载,并且已经解析、初始化完成,那么当前类对应的实例所需要的内存空间大小就已经确定,这时虚拟机需要在堆中为对象分配空间,分配空间的方式有两种:
- 指针碰撞
- 空闲列表
- 初始化内存空间: 内存分配完成后,虚拟机需要将分配到的内存都初始化为零值. 这些零值是不同类型对应不同的默认值. 如private int a = 0;
- 设置对象头信息: 如类的元数据、
- 调用方法,执行java代码中默认的值 如private int a =10;
2.1.1.3.对象内存分配原理
Java对象的分配,根据其过程,分为快速分配和慢速分配两种过程.
以new指令为例,其源码在bytecodeInterpreter.cpp文件中,如下.
CASE(_new): {
u2 index = Bytes::get_Java_u2(pc+1);
ConstantPool* constants = istate->method()->constants();
if (!constants->tag_at(index).is_unresolved_klass()) {
// Make sure klass is initialized and doesn't have a finalizer
Klass* entry = constants->resolved_klass_at(index);
InstanceKlass* ik = InstanceKlass::cast(entry);
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;
if (UseTLAB) {
result = (oop) THREAD->tlab().allocate(obj_size);
}
// Disable non-TLAB-based fast-path, because profiling requires that all
// allocations go through InterpreterRuntime::_new() if THREAD->tlab().allocate
// returns NULL.
#ifndef CC_INTERP_PROFILE
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()) {
if (Atomic::cmpxchg(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) {
goto retry;
}
result = (oop) compare_to;
}
}
#endif
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(ik);
// Must prevent reordering of stores for object initialization
// with stores that publish the new object.
OrderAccess::storestore();
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);
// Must prevent reordering of stores for object initialization
// with stores that publish the new object.
OrderAccess::storestore();
SET_STACK_OBJECT(THREAD->vm_result(), 0);
THREAD->set_vm_result(NULL);
UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
}
根据源码,得出流程图如下:
在jvm中,从对象的分配过程,可以将其分为快速分配过程和慢速分配过程.
下面根据上面的流程详细介绍下这两种分配过程.
2.1.1.3.1.快速分配
进入快速分配的条件
u2 index = Bytes::get_Java_u2(pc+1);
ConstantPool* constants = istate->method()->constants();
if (!constants->tag_at(index).is_unresolved_klass()) {
Klass* entry = constants->resolved_klass_at(index);
InstanceKlass* ik = InstanceKlass::cast(entry);
if (ik->is_initialized() && ik->can_be_fastpath_allocated() ) {
}
能够进入快速分配需要以下条件:
- 当前类已经解析
- 当前类已经初始化
- can_be_fastpath_allocated 为true
前两个好理解,我们看下can_be_fastpath_allocated,源码在instanceKlass.hpp
// This bit is initialized in classFileParser.cpp.
// It is false under any of the following conditions:
// - the class is abstract (including any interface)
// - the class has a finalizer (if !RegisterFinalizersAtInit)
// - the class size is larger than FastAllocateSizeLimit
// - the class is java/lang/Class, which cannot be allocated directly
bool can_be_fastpath_allocated() const {
return !layout_helper_needs_slow_path(layout_helper());
}
如果有以下这几种情况之一,该函数返回false:
- 当前类是抽象类、接口
- 当前类包含一个finalizer,即当前类是否实现了finalize方法
- 当前类大小是否超过FastAllocateSizeLimit,默认128k
- 当前类是否是java.lang.Class,该类不能直接分配内存
获取对象大小
一个类加载、解析完成后,其对应的实例所需的内存空间大小是固定的.
是否可以TLAB分配
默认可以.
可以通过参数-XX:+UseTLAB/-XX:-UseTLAB来开启/关闭TLAB模式.
Eden区尝试指针碰撞分配
bool need_zero = !ZeroTLAB;
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()) {
if (Atomic::cmpxchg(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) {
goto retry;
}
result = (oop) compare_to;
}
}
这块主要是通过指针碰撞模式,进行当前实例对象的内存空间分配,流程如下:
- 获取当前eden区的空闲起始地址,HeapWord* compare_to = *Universe::heap()->top_addr();
- 计算偏移地址 ; HeapWord* new_top = compare_to + obj_size;
- 如果当前地址小于eden区空闲区域的终止地址,即有空间进行分配,通过CAS的方式进行设置碰撞指针
- 如果CAS失败,重试.
变量need_zero:
bool need_zero = !ZeroTLAB;
//gc_globals.hpp定义默认值 \
product(bool, ZeroTLAB, false, \
"Zero out the newly created TLAB")
该变量默认为true,可以通过 -XX:+ZeroTLAB/-XX:-ZeroTLAB 设置ZeroTLAB 的值
- need_zero 如果为true,在之后的对象实例空间分配成功后,会对内存空间进行初始化,设置变量的默认值.
- 如果为false,则不设置
如果在eden区域分配内存成功,则进行内存空间初始化,否则进入慢速分配流程.
空间初始化
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(ik);
// Must prevent reordering of stores for object initialization
// with stores that publish the new object.
OrderAccess::storestore();
SET_STACK_OBJECT(result, 0);
UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
初始化流程如下:
- need_zero如果为true,这是空间默认值
- UseBiasedLocking: -XX:-UseBiasedLocking关闭
- true:开启偏向锁优化
- false:关闭偏向锁
- 设置对齐数据(后面会有对齐/补齐的概念)
- 设置当前实例的class引用
2.1.1.3.2.慢速分配
IRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* thread, ConstantPool* pool, int index))
Klass* k = pool->klass_at(index, CHECK);
InstanceKlass* klass = InstanceKlass::cast(k);
// Make sure we are not instantiating an abstract klass
klass->check_valid_for_instantiation(true, CHECK);
// Make sure klass is initialized
klass->initialize(CHECK);
// At this point the class may not be fully initialized
// because of recursive initialization. If it is fully
// initialized & has_finalized is not set, we rewrite
// it into its fast version (Note: no locking is needed
// here since this is an atomic byte write and can be
// done more than once).
//
// Note: In case of classes with has_finalized we don't
// rewrite since that saves us an extra check in
// the fast version which then would call the
// slow version anyway (and do a call back into
// Java).
// If we have a breakpoint, then we don't rewrite
// because the _breakpoint bytecode would be lost.
oop obj = klass->allocate_instance(CHECK);
thread->set_vm_result(obj);
IRT_END
//instanceKlass.cpp
instanceOop InstanceKlass::allocate_instance(TRAPS) {
bool has_finalizer_flag = has_finalizer(); // Query before possible GC
int size = size_helper(); // Query before forming handle.
instanceOop i;
i = (instanceOop)Universe::heap()->obj_allocate(this, size, CHECK_NULL);
if (has_finalizer_flag && !RegisterFinalizersAtInit) {
i = register_finalizer(i, CHECK_NULL);
}
return i;
}
//collectHeap.cpp
oop CollectedHeap::obj_allocate(Klass* klass, int size, TRAPS) {
ObjAllocator allocator(klass, size, THREAD);
return allocator.allocate();
}
//ObjAllocator 继承了MemAllocator
oop MemAllocator::allocate() const {
oop obj = NULL;
{
Allocation allocation(*this, &obj);
HeapWord* mem = mem_allocate(allocation);
if (mem != NULL) {
obj = initialize(mem);
}
}
return obj;
}
HeapWord* MemAllocator::mem_allocate(Allocation& allocation) const {
if (UseTLAB) {
HeapWord* result = allocate_inside_tlab(allocation);
if (result != NULL) {
return result;
}
}
return allocate_outside_tlab(allocation);
}
HeapWord* MemAllocator::allocate_outside_tlab(Allocation& allocation) const {
allocation._allocated_outside_tlab = true;
HeapWord* mem = _heap->mem_allocate(_word_size, &allocation._overhead_limit_exceeded);
if (mem == NULL) {
return mem;
}
NOT_PRODUCT(_heap->check_for_non_bad_heap_word_value(mem, _word_size));
size_t size_in_bytes = _word_size * HeapWordSize;
_thread->incr_allocated_bytes(size_in_bytes);
return mem;
}
//g1CollectedHeap.cpp
HeapWord*
G1CollectedHeap::mem_allocate(size_t word_size,
bool* gc_overhead_limit_was_exceeded) {
assert_heap_not_locked_and_not_at_safepoint();
if (is_humongous(word_size)) {
return attempt_allocation_humongous(word_size);
}
size_t dummy = 0;
return attempt_allocation(word_size, word_size, &dummy);
}
inline HeapWord* G1CollectedHeap::attempt_allocation(size_t min_word_size,
size_t desired_word_size,
size_t* actual_word_size) {
assert_heap_not_locked_and_not_at_safepoint();
assert(!is_humongous(desired_word_size), "attempt_allocation() should not "
"be called for humongous allocation requests");
HeapWord* result = _allocator->attempt_allocation(min_word_size, desired_word_size, actual_word_size);
if (result == NULL) {
*actual_word_size = desired_word_size;
result = attempt_allocation_slow(desired_word_size);
}
assert_heap_not_locked();
if (result != NULL) {
assert(*actual_word_size != 0, "Actual size must have been set here");
dirty_young_block(result, *actual_word_size);
} else {
*actual_word_size = 0;
}
return result;
}
HeapWord* G1CollectedHeap::attempt_allocation_slow(size_t word_size) {
ResourceMark rm; // For retrieving the thread names in log messages.
// Make sure you read the note in attempt_allocation_humongous().
assert_heap_not_locked_and_not_at_safepoint();
assert(!is_humongous(word_size), "attempt_allocation_slow() should not "
"be called for humongous allocation requests");
// We should only get here after the first-level allocation attempt
// (attempt_allocation()) failed to allocate.
// We will loop until a) we manage to successfully perform the
// allocation or b) we successfully schedule a collection which
// fails to perform the allocation. b) is the only case when we'll
// return NULL.
HeapWord* result = NULL;
for (uint try_count = 1, gclocker_retry_count = 0; /* we'll return */; try_count += 1) {
bool should_try_gc;
uint gc_count_before;
{
MutexLockerEx x(Heap_lock);
result = _allocator->attempt_allocation_locked(word_size);
if (result != NULL) {
return result;
}
// If the GCLocker is active and we are bound for a GC, try expanding young gen.
// This is different to when only GCLocker::needs_gc() is set: try to avoid
// waiting because the GCLocker is active to not wait too long.
if (GCLocker::is_active_and_needs_gc() && g1_policy()->can_expand_young_list()) {
// No need for an ergo message here, can_expand_young_list() does this when
// it returns true.
result = _allocator->attempt_allocation_force(word_size);
if (result != NULL) {
return result;
}
}
// Only try a GC if the GCLocker does not signal the need for a GC. Wait until
// the GCLocker initiated GC has been performed and then retry. This includes
// the case when the GC Locker is not active but has not been performed.
should_try_gc = !GCLocker::needs_gc();
// Read the GC count while still holding the Heap_lock.
gc_count_before = total_collections();
}
if (should_try_gc) {
bool succeeded;
result = do_collection_pause(word_size, gc_count_before, &succeeded,
GCCause::_g1_inc_collection_pause);
if (result != NULL) {
assert(succeeded, "only way to get back a non-NULL result");
log_trace(gc, alloc)("%s: Successfully scheduled collection returning " PTR_FORMAT,
Thread::current()->name(), p2i(result));
return result;
}
if (succeeded) {
// We successfully scheduled a collection which failed to allocate. No
// point in trying to allocate further. We'll just return NULL.
log_trace(gc, alloc)("%s: Successfully scheduled collection failing to allocate "
SIZE_FORMAT " words", Thread::current()->name(), word_size);
return NULL;
}
log_trace(gc, alloc)("%s: Unsuccessfully scheduled collection allocating " SIZE_FORMAT " words",
Thread::current()->name(), word_size);
} else {
// Failed to schedule a collection.
if (gclocker_retry_count > GCLockerRetryAllocationCount) {
log_warning(gc, alloc)("%s: Retried waiting for GCLocker too often allocating "
SIZE_FORMAT " words", Thread::current()->name(), word_size);
return NULL;
}
log_trace(gc, alloc)("%s: Stall until clear", Thread::current()->name());
// The GCLocker is either active or the GCLocker initiated
// GC has not yet been performed. Stall until it is and
// then retry the allocation.
GCLocker::stall_until_clear();
gclocker_retry_count += 1;
}
// We can reach here if we were unsuccessful in scheduling a
// collection (because another thread beat us to it) or if we were
// stalled due to the GC locker. In either can we should retry the
// allocation attempt in case another thread successfully
// performed a collection and reclaimed enough space. We do the
// first attempt (without holding the Heap_lock) here and the
// follow-on attempt will be at the start of the next loop
// iteration (after taking the Heap_lock).
size_t dummy = 0;
result = _allocator->attempt_allocation(word_size, word_size, &dummy);
if (result != NULL) {
return result;
}
// Give a warning if we seem to be looping forever.
if ((QueuedAllocationWarningCount > 0) &&
(try_count % QueuedAllocationWarningCount == 0)) {
log_warning(gc, alloc)("%s: Retried allocation %u times for " SIZE_FORMAT " words",
Thread::current()->name(), try_count, word_size);
}
}
ShouldNotReachHere();
return NULL;
}
根据代码,梳理大致流程图如下:
在这里就不对源码进行详细介绍了.
2.1.2.对象的内存布局
上图就是java对象在内存中的布局,主要分为三部分:
- 对象头:对象头又分为以下两个部分
- Mark Word
- 类型指针
- 实例数据
- 对齐数据
在正式说这些内容之前,先来说一个知识点,指针压缩.
2.1.2.1.指针压缩
先介绍几个背景知识:
-
操作系统物理内存的基本单位是字节(Byte),一个字节有8个二进制位.每一个内存地址指向一个字节,该地址就是物理地址.
-
虚拟地址:
- 32位操作系统 最大寻址,会有2^ 32个地址,每个地址代表1byte,所以最大内存为: 2^ 32 * 2^3 = 2^2 * 2^10 * 2^10 * 2^10 * 2^3 = 4*1024 10241024 byte = 4G
- 64位操作系统,由于只使用了低位48位,所以最大寻址,会有2^ 48个地址,每个地址代表1byte,所以最大内存为: 2^ 48 * 2^3 = 2^8 * 2^10 * 2^10 * 2^10 * 2^10 * 2^3 = 2561024 102410241024 byte = 256TB
-
指针:
- 在32位操作系统,一个指针大小为4byte
- 在64位操作系统,一个指针大小为8byte
-
jvm中对象采用8字节对齐,也就是说一个对象的大小一定是8字节的整数倍.
如果java对象是8byte的整数倍,我们现在假设目前有三个对象,大小为: 16byte,32byte,64byte,假设内存地址的起始为: 0x00 000,那么这三个java对象在内存的位置如下:
|0x00 000 --- 0x10 000 | ---------------- 0x30 000 | ----------------- 0x70 000 |
| 对象一 16byte |对象二 32byte。 | 对象三 64byte。 |
我们发现,由于java对象都是8byte的整数倍,所以对象的起始位置一定是 xxxx 000 到 xxxx 000 ,后三位都是零.
假设我们现在使用一种机制.
程序使用地址->程序地址*8 = cpu地址
那么我们32位系统,可以使用的最大内存将变成4G* 8 = 32G
这就是指针压缩的原理.
开始指针压缩时,我们程序中的一个地址就代表一个8byte的开始地址.
特别注意:
- 当堆内存小于4G时,不需要指针压缩
- 当堆内存大于32G时,压缩指针会失效,因为指针压缩的情况下,最大内存位32G
2.1.2.2.Mark Word
Mark Word大小固定:
- 32位系统: Mark Word 32位
- 64位系统: Mark Word 64位
2.1.2.3.类型指针
大小:
- 32位系统:4字节
- 64位系统
- 开启指针压缩,4字节
- 不开启指针压缩: 8 字节
2.1.2.4.对齐数据
2.1.2.4.1、基本数据和引用数据占用空间大小
- 引用数据: word size
- byte: 1byte
- boolean : 1byte
- char: 2bytes
- short:2bytes
- int :4bytes
- float:4bytes
- double:8bytes
- long:8bytes
2.1.2.4.2、对齐(Alignment)和补齐(Padding)
- 对齐: 任何对象都是以8bytes的粒度来对齐
- 补齐:补齐的粒度时4bytes
new Object()产生的对象的大小是多少呢?12 bytes的header,但对齐必须是8的倍数,还有4 bytes的alignment,所以对象的大小是16 bytes.
,JVM分配内存空间一次最少分配8 bytes,对象中字段对齐的最小粒度为4 bytes。
即:如果只有char 和boolean ,会补齐1bytes
2.1.2.4.3.对齐规则
2.1.2.4.3.1.重排序
public class RecordObject {
private byte a;
private boolean b;
private char c;
private int e;
private float f;
}
com.source.jvm.data.RecordObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800f2ea
12 4 int RecordObject.e 0
16 4 float RecordObject.f 0.0
20 2 char RecordObject.c
22 1 byte RecordObject.a 0
23 1 boolean RecordObject.b false
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
2.1.2.4.3.2 重排序的特殊情况
为了避免空间浪费,一般情况下,field分配的优先依次顺序是:double > long > int > float > char > short > byte > boolean > object reference。
基本的原则是:尽可能先分配占用空间大的类型(除了object reference)。这里的尽可能有两层含义:
在同等优先级情况下,按这个顺序分配
@Data
public class RecordObject {
private byte a;
private boolean b;
private char c;
private int e;
private int e1;
private float f;
private double g;
private long h;
}
com.source.jvm.data.RecordObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800f2ea
12 4 int RecordObject.e 0
16 8 double RecordObject.g 0.0
24 8 long RecordObject.h 0
32 4 int RecordObject.e1 0
36 4 float RecordObject.f 0.0
40 2 char RecordObject.c
42 1 byte RecordObject.a 0
43 1 boolean RecordObject.b false
44 4 (object alignment gap)
Instance size: 48 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
在考虑补齐(padding)的情况下,排在后面的类型可能比排在前面的优先级更高
从上面的例子可以看出,在header后面如果有int总会先出现int,这就是 Alignment和Padding共同作用的结果.
JVM每次最少分配8 bytes的空间,而header的大小是12。
也就是说,已经分配了16 bytes的空间了,如果严格按照前面说的那个顺序,最先分配一个double类型的field,就需要在这之前先分配4 bytes的空间来补齐,也就这4 bytes的空间就白白浪费了。
这中情况下,
<=Padding Size(4 bytes)的类型的优先级就高于大小>Padding Size的类型了。
而在所有大小<=Padding Size的类型中,int的优先级又是最高的,所以header后的第一个field是int。
以下情况会产生内部补齐:
@Data
public class RecordObject {
// private byte a;
private boolean b;
private char c;
// private int e;
// private int e1;
// private float f;
// private double g;
private long h;
private Object obj;
}
com.source.jvm.data.RecordObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800f2ea
12 2 char RecordObject.c
14 1 boolean RecordObject.b false
15 1 (alignment/padding gap)
16 8 long RecordObject.h 0
24 4 java.lang.Object RecordObject.obj null
28 4 (object alignment gap)
Instance size: 32 bytes
Space losses: 1 bytes internal + 4 bytes external = 5 bytes total
可以看到header后的4个bytes空间分配情况,在所有大小<=Padding Size的类型中,char优先级最高,其次是boolean,加起来只有3bytes,需要补齐1byte,接下来再分配一个8bytes存储long,
一个4bytes存储obj 指针,之后补齐4bytes
2.1.2.4.3.3、子类和父类
- 子类和父类的field永远不会混在一起,并且父类分配完之后才会分配子类
- 父类的最后一个字段与子类的第一个字段以一个Padding Size(4bytes)来对齐
例:
public class ParentObject {
private char a;
}
public class SonObject extends ParentObject {
private int b;
private long d;
}
com.source.jvm.data.SonObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c181
12 2 char ParentObject.a
14 2 (alignment/padding gap)
16 8 long SonObject.d 0
24 4 int SonObject.b 0
28 4 (object alignment gap)
Instance size: 32 bytes
Space losses: 2 bytes internal + 4 bytes external = 6 bytes total
从上面可以看出不会把子类的int放到header后.并且父类的最后一个字段加了2bytes Padding数据
2.1.2.4.3.4.数组
数组也是对象,但数组的header中包含有一个int类型的length值,又多占了4 bytes的空间,所以数组的header大小是16 bytes。
System.out.println(ClassLayout.parseInstance(new Object[1]).toPrintable());
输出:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800234c
12 4 (array length) 1
12 4 (alignment/padding gap)
16 4 java.lang.Object Object;.<elements> N/A
20 4 (object alignment gap)
Instance size: 24 bytes
Space losses: 4 bytes internal + 4 bytes external = 8 bytes total
2.1.3.对象的访问定位
jvm中对象的访问是通过句柄的方式.
通过句柄访问的最大好处是,对象指针的地址是稳定的,因为jvm经常发生垃圾回收,一般垃圾回收都会伴随着内存整理,需要移动对象,当对象地址改变时,只需要同步改变句柄中指向的对象地址,相比于改变对象中指针的地址来说,效率很高.
三、垃圾回收器
3.1.垃圾回收算法理论基础
3.1.1.jvm如何判断对象已死,可回收
目前有两种比较常见的垃圾标记算法,分别为引用计数算法和可达性分析算法(也称根搜索算法).
3.1.1.1.引用计数算法
原理:
给每个对象设置一个计数器,初始值为0,如果该对象被另外一个对象引用,就对该计数器加1.当一个对象的计数器为0时,说明该对象没有被其他对象引用,可以被垃圾回收器回收.
存在问题:
循环引用,无法释放无用对象,如下图:
假设:
在某一时刻,存在以下引用关系:
对象A->对象B->对象C->对象D->对象A
经过一段时间:
对象A.B=null,对象A不在对对象B有引用关系.那么对象B,C,D将变成无用对象,应该被回收,
但是,对象B,C,D之间存在循环引用,导致这三者的计数器都不为0,不会被垃圾收集器认为是垃圾,进行回收,进而造成内存碎片.
3.1.1.2.可达性分析算法
原理:
可达性分析算法是以根对象集合为起点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达,如果不可达,则说明该对象已经死亡,便将其标记为垃圾.
如下图:
可以做为根对象的有以下五种元素:
- Java栈内的对象引用
- 本地方法栈中的对象引用
- 运行时常量池中的对象引用
- 方法区中类静态属性的对象引用
- 方法区中的Class对象
jvm中使用该算法来标记死亡对象.
3.1.2.并发可达性分析算法实现-三色抽象标记算法
3.1.2.1.原理
在三色抽象标记算法中,回收器将根对象之外对象分为三种状态:
- 白色: 新的对象、未被扫描到的对象
- 灰色: 正在扫描的对象,回收器正在扫描其属性引用
- 黑色: 已经完成扫描的对象.
工作流程如下:
- 扫描开始前,当前所有对象都为白色状态
- 当回收器扫描到某一个对象时,将其设置为灰色状态,然后开始扫描其属性引用.
- 如果当前对象的所有属性已经扫描完成,则设置当前对象为黑色
- 扫描结束,目前对象只会存在黑色状态和白色状态,回收器将白色状态的对象进行回收,释放内存
3.1.2.2.三色标记存在的问题
3.1.2.2.1.多标
原因:
在标记过程中,处于灰色状态的对象被引用对象断开引用,导致该对象,甚至该对象所关联的对象应该被回收,但并未在本次垃圾回收过程中被回收.
如下图:
- 在垃圾回收器处理B对象时,A对象断开了对B对象的引用.
- B对象完成扫描标记后,变成黑色
这样就导致了B应该被回收,但是未被回收,这样就会造成B、C、D对象形成浮动垃圾.
解决:
可以忽略,下次回收时,将会将这些浮动垃圾进行回收.
3.1.2.2.2.漏标
原因:
在标记过程中,已经处于黑色的对象新增了对白色对象的引用.
如下图所示:
- 对象B已经完成扫描标记,变成黑色,本次垃圾回收不会被回收
- 对象B新增对F对象引用,且对象E断开对对象F的引用
解决:
对新增引用关系进行记录,然后进行重新标记.
3.1.2.3.漏标的两种解决方案
3.1.2.3.1.两种解决方案
黑色对象被误标为白色,需要同时满足以下两个条件:
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
要解决并发扫描时的对象消失问题,只需要破坏这两个条件的任意一个即可.
由此分别产生了两种解决方案:
- 增量更新(Incremental Update): 破坏第一个条件,当黑色对象新增指向白色对象的引用关系时,就将该引用记录下来,等并发扫描结束后,再将记录的这些引用关系中的黑色对象作为根,重新扫描.
- 原始快照(Snapshot At The Beginning ,SATB):破坏第二个条件,当灰色对象指向白色对象的引用被删除时,记录下来,等并发扫描结束,将这些引用记录的灰色对象作为根,进行再次扫描.
后面要说的并发垃圾收集CMS和G1,其中
- CMS是通过增量更新
- G1是通过原始快照
来解决漏标问题.
3.1.2.3.2.如何记录新的引用-记忆集
从上面可以看到,解决漏标的思路是保存变更的引用记录,不同的是增量更新是保存变更后的,SATB是保存变更前的.
那么在jvm是如何保存这些引用记录的呢?
答案是记忆集的实现卡表.
什么是记忆集?
记忆集是用于记录分代间指针的数据结构,其中所记录的是从堆中一个空间指向另一个空间的指针来源.
记忆集记录的是指针来源而非目标,主要有以下原因:
- 在移动式回收器中,一旦目标对象被复制或者提升,回收器便可根据记忆集更新回收相关指针的来源.
- 在连续两次回收过程之间,某个回收相关指针的来源域可能会被修改,如果记录回收相关的来源而非目标,则回收器可以仅对回收时刻该指针域所引用的对象进行处理,而无需关心它曾经指向了哪些对象
如图所示,假如在执行young gc前,执行了d.e=null.那么在gc扫描时,
因为只扫描新生代,从根节点来看E对象是不可达的,但是它其实还被老年代的对象所引用,应该是可达的.所以这是我们通过记忆集,记录有哪些引用是从老年代到新生代的,
再基于此判断现存再不可达对象是否是真正不可达的.
记忆集是一种思想,在jvm中具体实现是通过卡表来实现的.
卡表最简单的实现就是数组.
如图所示,可以为当前分区维护一个内存数组array和一个当前内存分区的起始地址,当前一个数组元素对应一块固定大小内存的首地址,在hotspot中是512byte,
那么就可以根据那么下标index对应的内存地址为:
起始地址+index*512byte
当下标index对应的内存空间存在,别的分区到当前内存的引用时,就将array[index]=1;
当垃圾回收时,就可以通过遍历array来找到值为1的下标,进而确定其对应的内存地址进行扫描.
那么何时去更新这个卡表的值的?
jvm中是通过写屏障来实现.
3.1.2.3.3.如何保证更新记忆集是安全的-写屏障
什么是写屏障.
写屏障可以看作是jvm层面对引用类型字段赋值这个操作的aop.
伪代码:
void setValue(oop field ,oop newValue){
//赋值操作前可以加入操作
field = newValue;
//赋值操作后可以加入操作
}
在hotspot中,是通过赋值操作后进行插入操作,叫做写后屏障,完成对卡表状态的更新.
这样就安全了吗?
在并发场景下还有伪共享的问题,不了解伪贡献的,可以看下另外一篇单独介绍伪贡献的文章.
做以下假设:
- 处理器的缓存行(Cache Line)大小为64字节.
- 卡表一个元素占用1字节,每个元素对应内存页大小为512byte
那么一个CacheLine可以读取 64个元素,对应内存为64*512byte = 32KB.
也就是说,当多个线程同时操作这块32KB内存时,他们读取到CacheLine的卡表信息有可能是相同的,这样在更新对应卡表元素时,就会有伪共享问题,导致cache Line相互失效,进而引发性能问题.
为了解决这个问题,hotspot在更新卡表前加入了条件判断:如果当前卡表对应的元素已经设置为1,就不进行更新.
3.1.3.常见的垃圾回收算法思想
常见的垃圾回收算法思想主要有以下几种:
- 标记-清除:
- 过程:分为标记阶段、清除阶段
- 不足
- 效率问题,两个过程效率都不高
- 会产生大量的内存碎片
- 标记-复制
- 原理:将内存分为大小相等的两块,每次都使用其中一块,当这块用完后,进行垃圾回收,将存活对象复制到另外一块,在该块接着进行内存分配,并将当前内存清理.
- 不足:浪费空间
- 好处:进行了空间整理,并且运行高效,因为空闲内存是连续的,分配内存时,按顺序分配即可.
- 标记-整理
- 过程: 与标记-清除一样,但是不会对可回收对象进行清理,而是将存活对象都向一端进行移动,然后将剩余空间进行清理.
- 分代收集
- 根据对象存活周期的不同,将内存划分为几块,然后选择合适的垃圾回收算法进行回收.
3.2.垃圾回收器
3.2.1.常见垃圾回收器简介
Hotspot中主要的垃圾集收集器以及其特点:
- 新生代
- Serial
- 单线程
- 标记-复制算法思想
- 整个流程需要STW
- 针对客户端模式的虚拟机是一种很好的选择
- ParNew
- 多线程的Serial
- 标记-复制算法思想
- 整个流程需要STW
- Parallel Scavenge
- 吞吐量优先收集器
- 多线程垃圾收集器
- 整个流程需要STW
- 标记-复制算法思想
- Serial
- 老年代
- Serial Old
- 单线程
- 标记-整理算法思想
- 整个流程需要STW
- Parallel Old
- Parallel Scavenge老年代版本
- 多线程并发收集
- 标记-整理算法思想
- CMS
- 以获取最短回收停顿时间为目标的收集器后面详细介绍
- Serial Old
- G1:老年代、新生代都可以回收的垃圾收集器.
3.2.1.1.常用垃圾收集器组合
经常组合使用的垃圾收集器:
- ParNew + CMS
- ParNew + Serial Old(jdk 1.9中废弃)
- Parallel Scavenge+ Parallel Old
- Parallel Scavenge+ Serial Old
- Serial +CMS(jdk 1.9中废弃)
- Serial + Serial Old (jdk 1.9中废弃)
另外:
Serial Old 会作为CMS收集器发生失败时的后备预案,当并发收集发生Concurrent Mode Failure时使用.
3.2.2.CMS垃圾回收器
3.2.2.1.CMS介绍
设计目标:
获取最短回收停顿时间的垃圾收集器.
算法思想:
基于标记-清除算法实现.
垃圾回收流程:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
其中初始标记、重新标记过程需要STW.
3.2.2.2.CMS存在的不足
CMS虽然是一款优秀的垃圾收集器,主要体现在并发收集、低停顿.但是它也存在着一些问题.
3.2.2.2.1.对处理器资源敏感
CMS默认启动的回收线程是:
(处理器核心数量+3)/4
处理器核心数量越多,CMS回收线程对用户程序影响越小.
3.2.2.2.2.浮动垃圾
CMS无法处理浮动垃圾.
CMS在工作时,用户程序不会停止运行,则需要给用户程序保留足够的内存,所以在老年代使用超过一定比例时,就会触发垃圾收集:
- jdk1.5: 68%
- jdk1.6: 92%
设置参数: -XX:CMSInitiatingOccupancyFraction
如果保留的内存不够用户程序分配内存,则会出现Concurrent Mode Failure,这时jvm就会暂停用户程序,采用Serial Old进行老年代的垃圾回收.
3.2.2.2.3.空间碎片
CMS是基于标记-清除算法思想实现的,该算法思想的一个非常突出的缺点就是垃圾收集结束时会有内存空间碎片.
当空间碎片过多时,就会导致大对象分配失败率提高,进而表现为老年代还有很多剩余空间,但是无法找到足够连续的空间去分配大对象,使得不提前触发Full GC,这样Full GC的频率就会变高,而Full GC是比较耗时的.
CMS提供了以下两种解决办法.
-XX:+UseCMSCompactAtFullCollection
该参数开启后,CMS会在full gc时,对老年代进行空间整理,因为需要移动对象,并且不能并发执行,这样会导致停顿时间变长.
-XX:CMSFullGCsBeforeCompaction
该参数表示,在经历了几次不整理空间的Full GC后,下次进入Full GC前会先进行空间碎片整理,该值默认为0,即每次进入Full GC前都进行碎片整理.
3.2.3.G1垃圾回收器
3.2.3.1.原理
内存区域划分
-
将堆划分成多个大小相等的独立区域Region.
- Region大小: 由参数-XX:G1HeapRegionSize设定,取值范围1MB-32MB
- Region标记:
- E: Eden 空间
- S: Survivor空间
- O: Old空间,老年代
- H: Humongous空间,当超过标准Region 50%大小的对象会放到该空间.G1的大多数行为,把H Region当作老年代处理.
特点:
- 垃圾收集的目标不再是整个新生代、老年代或者堆,而是一组Region集合(可以同时包括老年代、新生代)
- 可以让用户设定允许的收集停顿时间(-XX:MaxGCPauseMillis ,默认200ms),G1会在该时间内,尽量完成最大的垃圾回收收益
- 基于标记-整理垃圾收集算法,垃圾收集完成后不会产生内存空间碎片.提供规整的可用内存.
垃圾回收过程
- 初始标记:从GCRoot标记直接关联的对象,需要STW
- 并发标记:不需要STW,接着扫描堆内对象
- 最终标记: 从SATB记录扫描,需要STW
- 筛选回收:更新各个region的统计数据,对各个Region的回收价值、成本进行排序,根据用户设定的期望停顿时间,来决定回收哪些Region.需要STW
- 先进行复制,将存活对象复制到新的Region
- 对旧的Region进行清理.