堆中对象分配&布局&访问

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人想出来。

对象的创建

Java中创建对象(例如克隆、反序列化)通常仅仅是一个new关键字。当虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在运行时常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析、和初始化过。如果没有必须先执行相应的类加载过程

类加载检查通过后,虚拟机将为新生对象分配内存,对象所需内存的大小在类加载完成后便可完全确定。对象内存的分配方式有指针碰撞空闲列表两种,选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定,或者说所采用的垃圾收集器采用的哪种或哪几种垃圾收集算法决定

  • 指针碰撞,假设堆内存绝对规整,使用过的内存放在一边空闲内存放在另一边,中间放着一个指针作为分界点的指示器,给对象分配内存时将该指针向空闲空间那边挪一段与对象大小相等的距离。
  • 空闲列表,堆内存不规则,已使用的内存和空闲内存相互交错,这时虚拟机就必须维护一个列表,用于记录哪些内存是可用的,在分配内存时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

对象的创建在虚拟机中是非常频繁的行为,在并发情况下是非线程安全的。解决线程安全问题有两种方案:

  • 对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
  • 把内存分配的动作按照线程划分在不同的空间中进行,为每个线程在堆中预先分配一小块内存TLAB(本地线程分配缓冲),线程分配内存时在TLAB上分配,当TLAB用完并分配新的TLAB时才需要进行同步锁定。虚拟机通过-XX:+/-UseTLAB参数来设置是否使用TLAB。

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,但不包括对象头。如果使用TLAB该过程可以提前至TLAB分配时进行。该操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用。接下来虚拟机要对对象进行必要的设置,类如对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄、是否使用偏向锁等这些存在对象头中的信息。执行new指令后会接着执行<init>方法,把对象按照开发着的意愿进行初始化。

对象的内存布局

在HotSpot虚拟机中对象在内存中存储布局分为对象头、实例数据对齐填充3块区域。

对象头包括用于存储对象自身的运行时数据类型指针运行时数据包括哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳类型指针——对象指向它的类元数据的指针,虚拟机通过该指针来确定这个对象是哪个类的实例。但并不是所有虚拟机在对象数据上保留类型指针,或者做查找对象的元数据信息不一定要经过对象本身。另外如果对象是一个数组,在对象头中必须有一块用于记录数组长度的数据,这样虚拟机可以通过对象元数据信息确定Java对象大小,但从数组的元数据中却无法确定数组的大小

对象头

对象头在hotspotC++源码里的注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Bit‐format of an object header (most significant first, big endian layout below):
//
// 32 bits:
// ‐‐‐‐‐‐‐‐
// hash:25 ‐‐‐‐‐‐‐‐‐‐‐‐>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (CMS free block)
// PromotedObject*:29 ‐‐‐‐‐‐‐‐‐‐>| promo_bits:3 ‐‐‐‐‐>| (CMS promoted object)
//
// 64 bits:
// ‐‐‐‐‐‐‐‐
// unused:25 hash:31 ‐‐>| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| promo_bits:3 ‐‐‐‐‐>| (CMS promoted object)
// size:64 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (CMS free block)
//
// unused:25 hash:31 ‐‐>| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ‐‐‐‐‐>| (COOPs && CMS promoted object)
// unused:21 size:35 ‐‐>| cms_free:1 unused:7 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (COOPs && CMS free block)

实例数据是对象真正存储有效信息,也是程序代码中所定义的各种类型的字段内容从父类继承的和在子类中定义的都需要记录。存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中的定义顺序影响。HotSpot默认分配策略是相同宽度的字段总是被分到一起。在满足该条件的情况下父类中定义的变量会出现在子类之前。如果CompactFields参数值为true,子类中较窄的变量也可能会插入到父类变量的空隙之中。

对齐填充并不是必然存在的,仅仅起占位符的作用HotSpot VM自动内存管理系统要求对象的起始地址必须是8字节整倍数,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问定位

对象的访问需要通过栈上的引用对象(reference)数据来操作堆上的具体对象。由于reference类型在虚拟机规范中只定义了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,对象的访问方式由虚拟机实现而定,目前主要的访问方式有使用句柄直接指针两种。

使用句柄访问的话,在堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据对象类型数据各自的具体地址信息。
通过句柄访问对象

使用直接指针访问的话,堆中对象的布局就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象的地址
通过直接指针访问对象

两种对象访问方式各有优势,使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改;使用直接指针访问方式的最大好处就是速度更快,节省了一次指针定位的时间开销,HotSpot虚拟机使用的直接指针方式进行对象的访问;

暂时理解为上一节对象的内存布局中讲的对象头中的类型指针就是对象类型数据的指针,当然这是使用直接指针访问对象的情况;在通过句柄访问对象的情况下就不存在对象头中的类型指针了。

对象大小与指针压缩

对象大小可以用jol-­core包查看,引入依赖:

1
2
3
4
5
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
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
28
public class JOLSample {
public static void main(String[] args) {
{
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
{
System.out.println("***********************");
ClassLayout layout = ClassLayout.parseInstance(new int[]{});
System.out.println(layout.toPrintable());
}
{
System.out.println("***********************");
ClassLayout layout = ClassLayout.parseInstance(new A());
System.out.println(layout.toPrintable());
}
}

public static class A {
int id; // 4B
String name; // 4B 如果关闭压缩‐XX:‐UseCompressedOops,则占用8B
byte aByte; // 1B
Object object; // 4B 如果关闭压缩‐XX:‐UseCompressedOops,则占用8B
}
}

在执行时设置VM参数,‐XX:+UseCompressedOops默认开启的压缩所有指针,‐XX:+UseCompressedClassPointers默认开启压缩对象头里类型指针Klass Pointer。运行结果如下,VALUE打印的是对象头的信息,且由于计算机是小端模式,所以先打印的低位再打印的高位,所以要颠倒过来看,故对于第一个obj的对象头信息00000000 00000000 00000000 00000001,可明显看出是无锁状态,对于用synchronized (obj)加锁的第二个打印的obj对象头信息00000011 00100111 11110101 11101000,可明显看出是轻量级锁,虽然通常说锁是先从偏向锁再到轻量级锁,但JVM做了优化进行了锁的推迟,默认推迟大概4s多一点,为了避免无谓的大量的偏向锁向轻量级锁转换的开销,可通过-XX:BiasedLockingStartupDelay=0禁用延迟

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) e8 f5 27 03 (11101000 11110101 00100111 00000011) (52950504)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

***********************
[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 0 int [I.<elements> N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

***********************
com.eleven.icode.jvm.JOLSample$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 63 cc 00 f8 (01100011 11001100 00000000 11111000) (-134165405)
12 4 int A.id 0
16 1 byte A.aByte 0
17 3 (alignment/padding gap)
20 4 java.lang.String A.name null
24 4 java.lang.Object A.object null
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力。为了减少64位平台下内存的消耗,所以启用指针压缩功能

JVM32位地址最大支持4G内存(2的32次方),可通过对对象指针压缩编码解码方式进行优化,使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G) 。

堆内存小于4G时,不需要启用指针压缩jvm会直接去除高32位地址,即使用低虚拟地址空间;堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址;

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

进行延时后,很明显看到第一个打印的obj对象头为:00000000 00000000 00000000 00000101,这里为什么第一个obj也变成了偏向锁呢,且无偏向信息,此时对象是处于匿名偏向,可偏向状态,第二个打印的obj对象头:00000010 10010001 01001000 00000101,加上锁后再打印对象头信息,发现马上就有偏向信息了,当退出同步块后对象头中锁标记依然是偏向锁

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
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 58 59 03 (00000101 01011000 01011001 00000011) (56186885)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 58 59 03 (00000101 01011000 01011001 00000011) (56186885)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
hashCode对锁的影响

当一个对象已经计算过identity hash code它就无法进入偏向锁状态;当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的则它的偏向锁会被撤销,且锁会膨胀为重量锁

Identity hash code未被覆写java.lang.Object.hashCode()java.lang.System.identityHashCode(Object)返回的值。若一个对象覆盖hashCode方法,仍想获得它的内存地址计算的Hash值可调用identityHashCode方法。

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
obj.hashCode();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}

开始锁是处于偏向锁的状态,当调用锁对象objhashCode方法后,锁从偏向锁退出变为无锁状态,当通过synchronizedobj加锁时,锁变直接为了轻量级锁,即使退出了同步块对象头中锁标志变成无锁状态,而非偏向锁。

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
28
29
30
31
32
33
34
35
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 db ec 2d (00000001 11011011 11101100 00101101) (770497281)
4 4 (object header) 18 00 00 00 (00011000 00000000 00000000 00000000) (24)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) c8 f0 96 02 (11001000 11110000 10010110 00000010) (43446472)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 db ec 2d (00000001 11011011 11101100 00101101) (770497281)
4 4 (object header) 18 00 00 00 (00011000 00000000 00000000 00000000) (24)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

hashCode调用在synchronized代码块中执行时,锁直接从偏向锁变成了重量级锁退出同步块后锁状态还是处于重量级锁的状态。

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
obj.hashCode();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
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
28
29
30
31
32
33
34
35
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 58 7d 03 (00000101 01011000 01111101 00000011) (58546181)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0a cd 3e 26 (00001010 11001101 00111110 00100110) (641649930)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0a cd 3e 26 (00001010 11001101 00111110 00100110) (641649930)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

对象内存分配

对象内存分配流程

对象栈上分配

JAVA中对象都是在堆上分配,当对象没有被引用时,需要依靠GC进行回收,若对象数量较多时,会给GC带来较大压力,也间接影响了应用的性能。为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。若不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。

JDK7之后默认开启逃逸分析,可以通过-XX:+/-DoEscapeAnalysis开启或关闭逃逸分析来优化对象内存分配位置,使其通过标量替换优先分配在栈上;

标量替换:通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被该方法使用的成员变量所代替,这些代替的成员变量在栈帧寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。通过-XX:+EliminateAllocations参数开启标量替换,JDK7之后默认开启

标量不可被进一步分解的量,如int,long等基本数据类型以及reference类型等,聚合量是可以被进一步分解的量,也可以使用jmap -histo查看创建对象的数量加以验证;

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
/**
* 栈上分配,标量替换
* 代码调用了1亿次alloc(),如果是分配到堆上,大概需要1GB以上堆空间,如果堆空间小于该值,必然会触发GC。
* <p>
* 使用如下参数不会发生GC
* -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
* 使用如下参数都会发生大量GC
* -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
* -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
*/
public class AllotOnStack {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));
}

private static void alloc() {
User user = new User();
user.setName("Test User");
user.setAge(15);
}
}
在Eden区分配对象

大多数情况下对象在新生代中Eden区分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC

  • Minor GC/Young GC:指发生新生代的的垃圾收集动作Minor GC非常频繁回收速度一般也比较快
  • Major GC/Full GC:一般会回收老年代年轻代方法区的垃圾Major GC的速度一般会比Minor GC10倍以上

Eden区满了会触发minor gc,99%以上的对象可能都会成为垃圾被回收掉,剩余存活对象会被挪到空的那块survivor区,下次Eden区满了后又会触发minor gc,把Eden区survivor区垃圾对象回收,把剩余存活对象一次性挪动到另外一块空的survivor区。

新生代的对象都是朝生夕死存活时间很短,故JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可,JVM默认开启-XX:+UseAdaptiveSizePolicy参数,会导致这个8:1:1比例自动变化。

通过-XX:+PrintGCDetails参数打赢GC日志详情,一下代码可以看到即使程序什么也不做,eden区内存也几乎已经被分配完全。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
byte[] allocation = new byte[60000 * 1024];
}

Heap
PSYoungGen total 76288K, used 65245K [0x000000076b180000, 0x0000000770680000, 0x00000007c0000000)
eden space 65536K, 99% used [0x000000076b180000,0x000000076f137638,0x000000076f180000)
from space 10752K, 0% used [0x000000076fc00000,0x000000076fc00000,0x0000000770680000)
to space 10752K, 0% used [0x000000076f180000,0x000000076f180000,0x000000076fc00000)
ParOldGen total 175104K, used 0K [0x00000006c1400000, 0x00000006cbf00000, 0x000000076b180000)
object space 175104K, 0% used [0x00000006c1400000,0x00000006c1400000,0x00000006cbf00000)
Metaspace used 3445K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K

若再分配一个8M的内存,Eden区没有足够空间进行分配,明显的看到进行了一次Minor GC,由于survivor空间只有10752K明显不足以放下allocation1,只好把新生代的对象提前转移到老年代中去,老年代上的空间足够存放allocation1,故不会出现Full GC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
byte[] allocation = new byte[60000 * 1024];
byte[] allocation2 = new byte[8000 * 1024];
}
[GC (Allocation Failure) [PSYoungGen: 63934K->776K(76288K)] 63934K->60784K(251392K), 0.0312953 secs] [Times: user=0.11 sys=0.00, real=0.03 secs]
Heap
PSYoungGen total 76288K, used 9431K [0x000000076b180000, 0x0000000774680000, 0x00000007c0000000)
eden space 65536K, 13% used [0x000000076b180000,0x000000076b9f3ef8,0x000000076f180000)
from space 10752K, 7% used [0x000000076f180000,0x000000076f242020,0x000000076fc00000)
to space 10752K, 0% used [0x0000000773c00000,0x0000000773c00000,0x0000000774680000)
ParOldGen total 175104K, used 60008K [0x00000006c1400000, 0x00000006cbf00000, 0x000000076b180000)
object space 175104K, 34% used [0x00000006c1400000,0x00000006c4e9a010,0x00000006cbf00000)
Metaspace used 3446K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K

执行Minor GC后,后面分配的对象如果能够存在eden区的话,还是会在eden区分配内存,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
byte[] allocation1 = new byte[60000 * 1024];
byte[] allocation2 = new byte[8000 * 1024];
byte[] allocation3 = new byte[8000 * 1024];
byte[] allocation4 = new byte[8000 * 1024];
byte[] allocation5 = new byte[8000 * 1024];
byte[] allocation6 = new byte[8000 * 1024];
}

[GC (Allocation Failure) [PSYoungGen: 63934K->776K(76288K)] 63934K->60784K(251392K), 0.0255463 secs] [Times: user=0.09 sys=0.05, real=0.03 secs]
Heap
PSYoungGen total 76288K, used 42715K [0x000000076b180000, 0x0000000774680000, 0x00000007c0000000)
eden space 65536K, 63% used [0x000000076b180000,0x000000076da74e78,0x000000076f180000)
from space 10752K, 7% used [0x000000076f180000,0x000000076f242020,0x000000076fc00000)
to space 10752K, 0% used [0x0000000773c00000,0x0000000773c00000,0x0000000774680000)
ParOldGen total 175104K, used 60008K [0x00000006c1400000, 0x00000006cbf00000, 0x000000076b180000)
object space 175104K, 34% used [0x00000006c1400000,0x00000006c4e9a010,0x00000006cbf00000)
Metaspace used 3446K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
大对象直接进入老年代

大对象直接分配到老年代的目的是为了避免大对象分配内存时的复制操作而降低效率,JVM参数-XX:PretenureSizeThreshold单位字节,可以设置大对象的大小若对象超过设置大小会直接进入老年代,该参数只在SerialParNew两个收集器下有效。

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
/**
* -XX:+PrintGCDetails
* 通过如下参数让大对象直接分配在老年代
* -XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC
*/
public class GCTest {
public static void main(String[] args) {
byte[] allocation1 = new byte[60000 * 1024];
byte[] allocation2 = new byte[8000 * 1024];
byte[] allocation3 = new byte[8000 * 1024];
byte[] allocation4 = new byte[8000 * 1024];
byte[] allocation5 = new byte[8000 * 1024];
byte[] allocation6 = new byte[8000 * 1024];
}
}

Heap
def new generation total 78656K, used 5596K [0x00000006c1400000, 0x00000006c6950000, 0x00000007162a0000)
eden space 69952K, 8% used [0x00000006c1400000, 0x00000006c19772a8, 0x00000006c5850000)
from space 8704K, 0% used [0x00000006c5850000, 0x00000006c5850000, 0x00000006c60d0000)
to space 8704K, 0% used [0x00000006c60d0000, 0x00000006c60d0000, 0x00000006c6950000)
tenured generation total 174784K, used 100000K [0x00000007162a0000, 0x0000000720d50000, 0x00000007c0000000)
the space 174784K, 57% used [0x00000007162a0000, 0x000000071c448060, 0x000000071c448200, 0x0000000720d50000)
Metaspace used 3446K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
对象动态年龄判断

一批对象的总大小大于这块Survivor区域内存大小的50%,可以通过-XX:TargetSurvivorRatio参数指定,则此时大于等于这批对象年龄最大值的对象,直接进入老年代,例:Survivor区域里现在有一批对象,年龄1+年龄2+年龄n多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在Minor GC之后触发

老年代空间分配担保机制

年轻代每次Minor GC之前JVM都会计算老年代剩余可用空间,若可用空间小于年轻代里现有的所有对象大小之和,包括垃圾对象,就会检查-XX:-HandlePromotionFailure参数是否设置,jdk1.8默认设置,若已设置则检查老年代可用内存大小,判断是否大于之前每一次Minor GC后进入老年代的对象的平均大小。若小于参数未设置,则触发一次Full GC,若回收完还是没有足够空间存放新的对象则发生OOM,若Minor GC之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,也会触发Full GCFull GC完之后如果还是没有空间放Minor GC之后的存活对象,则也会发生OOM。

老年代空间分匹配担保机制