• 自动内存管理
    • 分配内存
    • 回收内存

对象的内存分配在大方向上看就是在堆上分配(也可能经过JIT编译后被拆散为标量类型,间接地在栈上分配),细节取决于当前使用的是哪种垃圾收集器组合,以及虚拟机中与内存相关参数设置。

对象优先在Eden分配

书中案例

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

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
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;

/**
* JVM参数:<br>
* -verbose:gc -Xms20M -Xmx20m -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
* <br>
* 参数解释:<br>
* -Xms、Xmx:分配用来设置进程堆内存的最小和最大。<br>
* 此处限制堆大小为20M,不可扩展<br>
* -Xmn:用来设置堆内新生代的大小。<br>
* 通过这个值我们也可以得到老生代的大小:-Xmx减去-Xmn<br>
* -其中10M分配给新生代,剩下10M给老年代<br>
* -XX:SurvivorRatio=8<br>
* 新生代中Eden区与一个Survivor区的空间比例8:1<br>
*
*
* @author 2bai
*
*/
public class Allocation {

private static final int _1M = 1024 * 1024;

public static void main(String[] args) {
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
System.out.println(gc.getObjectName());
}
testAllocation();
}

@SuppressWarnings("unused")
public static void testAllocation() {
byte[] allocation1 = new byte[2 * _1M];
System.out.println("allocation1");
byte[] allocation2 = new byte[2 * _1M];
System.out.println("allocation2");
byte[] allocation3 = new byte[2 * _1M];
System.out.println("allocation3");
byte[] allocation4 = new byte[4 * _1M];
System.out.println("allocation4");
}
}

新生代可用空间为9216K=Eden区8192K+一个Survivor区1024K

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java.lang:type=GarbageCollector,name=Copy
java.lang:type=GarbageCollector,name=MarkSweepCompact
allocation1
allocation2
allocation3
[GC (Allocation Failure) [DefNew: 6979K->289K(9216K), 0.0053855 secs] 6979K->6433K(19456K), 0.0054286 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
allocation4
Heap
def new generation total 9216K, used 4625K
eden space 8192K, 52% used
from space 1024K, 28% used
to space 1024K, 0% used
tenured generation total 10240K, used 6144K
the space 10240K, 60% used
Metaspace used 2761K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 304K, capacity 386K, committed 512K, reserved 1048576K

可以看出,allocation1、allocation2和allocation3分配成功,分配allocation4时会发生一次Minor GC,新生代6979K变为278K,而总内存占用量则几乎没有减少。这次GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB,剩余空间已不足以分配allocation4所需的4MB内存。GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入Survivor空间(Survivor空间只有1MB大小),所以只好通过分配担保机制提前转移到老年代去。

GC结束后,4M的allocation4对象顺利分配在Eden中,因此程序执行完的结果是Eden区占用4MB(allocation4),Survivor空间,老年代占用6MB(allocation1、allocation2和allocation3)。


实测案例一

上面是《深入理解JVM虚拟机》中的内容,但在实际运行中(JDK1.8,虚拟机参数:-verbose:gc -Xms20M -Xmx20m -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8)时,GC日志却发生了变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java.lang:type=GarbageCollector,name=PS Scavenge
java.lang:type=GarbageCollector,name=PS MarkSweep
allocation1
allocation2
allocation3
allocation4
Heap
PSYoungGen total 9216K, used 7144K
eden space 8192K, 87% used
from space 1024K, 0% used
to space 1024K, 0% used
ParOldGen total 10240K, used 4096K
object space 10240K, 40% used
Metaspace used 2761K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 304K, capacity 386K, committed 512K, reserved 1048576K

由日志可见,4个对象均分配成功(allocation1、allocation2和allocation3分配在Eden中,allocation4直接进入到了老年代),没有发生GC。

这是因为在分配了allocation1、allocation2和allocation3后,Eden区实际占用内存是大于6M的(JVM运行时也会占用一定堆空间),剩余空间不足,无法将allocation4分配到Eden区,这时hotspot会根据分配失败策略尝试在old区分配,只要request的size>=eden_size / 2即满足在old区进行allocation的条件,4M正好等于8M(Eden区10M,8:1)的一半,所以会尝试在old区分配;恰好old区有10M的free空间,所以allocation4直接分配到了old区,没有执行GC。


实测案例二

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
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;

/**
* -verbose:gc -Xms20M -Xmx20m -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* 参数解释:<br>
* -Xms、Xmx:分配用来设置进程堆内存的最小和最大。<br>
* 此处限制堆大小为20M,不可扩展<br>
* -Xmn:用来设置堆内新生代的大小。<br>
* 通过这个值我们也可以得到老生代的大小:-Xmx减去-Xmn<br>
* -其中10M分配给新生代,剩下10M给老年代<br>
* -XX:SurvivorRatio=8<br>
* 新生代中Eden区与一个Survivor区的空间比例8:1<br>
*
*
* @author 2bai
*
*/
public class Allocation {

private static final int _1M = 1024 * 1024;

public static void main(String[] args) {
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
System.out.println(gc.getObjectName());
}
testAllocation();
}

@SuppressWarnings("unused")
public static void testAllocation(){
byte[] allocation1 = new byte[2 * _1M];
System.out.println("allocation1");
byte[] allocation2 = new byte[2 * _1M];
System.out.println("allocation2");
byte[] allocation3 = new byte[2 * _1M];
System.out.println("allocation3");
byte[] allocation4 = new byte[1 * _1M];
System.out.println("allocation4");
byte[] allocation5 = new byte[3 * _1M];
System.out.println("allocation5");
}
}

输出日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java.lang:type=GarbageCollector,name=PS Scavenge
java.lang:type=GarbageCollector,name=PS MarkSweep
allocation1
allocation2
allocation3
allocation4
[GC (Allocation Failure) [PSYoungGen: 8003K->416K(9216K)] 8003K->7592K(19456K), 0.0095503 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 416K->0K(9216K)] [ParOldGen: 7176K->7456K(10240K)] 7592K->7456K(19456K), [Metaspace: 2755K->2755K(1056768K)], 0.0049244 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
allocation5
Heap
PSYoungGen total 9216K, used 3313K
eden space 8192K, 40% used
from space 1024K, 0% used
to space 1024K, 0% used
ParOldGen total 10240K, used 7456K
object space 10240K, 72% used
Metaspace used 2762K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 304K, capacity 386K, committed 512K, reserved 1048576K

由日志可见,发生了两次GC,一次Minor GC,一次Full GC。

分配了allocation1、allocation2、allocation3和allocation4后,Eden区占用已经超过了7M,尝试分配allocation5(3M)时,不满足在old区分配的条件(3M<4M(8/2)),只能让Eden区发生Minor GC已满足分配allocation5的需求,于是就触发了GC Cause为“Allocation Failure”的Minor GC,所以Eden区那些7M多的对象就被promote到了old区(因为survivor区太小了,所以只剩余416K的东西在Survivor区);然后紧接着

parallel scavenge会检查old gen的剩余容量是否满足历次晋升的条件,如果不满足就会进行一次Full GC。

默认收集器

Default garbage collectors:

  • Java 7 - Parallel GC
  • Java 8 - Parallel GC
  • Java 9 - G1 GC
  • Java10- G1 GC

参考资料

Difference between -XX:UseParallelGC and -XX:+UseParNewGC

Parallel Scavenge垃圾收集器认定对象是大对象的条件是什么?

大对象直接进入老年代

  • 大对象
    • 需要大量连续内存空间的Java对象
      • 很长的字符串以及数组

写程序时需避免“短命大对象”,经常出现大对象很容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间来分配。

JVM参数

-XX:PretenureSizeThreshold

  • 作用:新对象申请的内存空间大于这个设置值直接分配在老年代。
  • 单位:Byte
  • 目的:避免在Eden区及两个Survivor区之间发生大量的内存复制
  • 有效收集器:Serial / ParNew
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
package com.super2bai.jvm.gc;

import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;

/**
* -verbose:gc -Xms20M -Xmx20m -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728 -XX:+UseSerialGC
* 参数解释:<br>
* -Xms、Xmx:分配用来设置进程堆内存的最小和最大。<br>
* 此处限制堆大小为20M,不可扩展<br>
* -Xmn:用来设置堆内新生代的大小。<br>
* 通过这个值我们也可以得到老生代的大小:-Xmx减去-Xmn<br>
* -其中10M分配给新生代,剩下10M给老年代<br>
* -XX:SurvivorRatio=8<br>
* 新生代中Eden区与一个Survivor区的空间比例8:1<br>
* -XX:PretenureSizeThreshold=3145728<br>
* 超过3M的大对象直接分配到old区<br>
* -XX:+UseSerialGC<br>
* JDK1.8默认PS GC,需修改为ServialGC,否则无法测试<br>
*
* @author 2bai
*
*/
public class Allocation {

private static final int _1M = 1024 * 1024;

public static void main(String[] args) {
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
System.out.println(gc.getObjectName());
}
testPretenureSizeThreshold();
}

public static void testPretenureSizeThreshold(){
byte[] allocation1 = new byte[4 * _1M];
}
}

日志:

1
2
3
4
5
6
7
8
9
10
11
java.lang:type=GarbageCollector,name=Copy
java.lang:type=GarbageCollector,name=MarkSweepCompact
Heap
def new generation total 9216K, used 999K
eden space 8192K, 12% used
from space 1024K, 0% used
to space 1024K, 0% used
tenured generation total 10240K, used 4096K
the space 10240K, 40% used
Metaspace used 2761K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 304K, capacity 386K, committed 512K, reserved 1048576K

Eden区几乎没有被使用,而老年代的10M空间被使用了40%,也就是4M的allocation对象直接就分配在老年代中。

长期存活的对象将进入老年代

概述

基于分代收集思想,那么GC时就必须能识别不同年龄代的对象,所以虚拟机给每个对象定义了一个对象年龄计数器。

运行原理

如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。

对象在Survivor区中每经历过一次Minor GC,年龄就加1,当年龄到达设置值后,就会被晋升到老年代中。

JVM参数

-XX:MaxTenuringThreshold:对象晋升到年代的年龄阈()值,默认15。

如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率。该参数只有在串行GC时才有效.

问题

Major GC 和 Full GC 是一个概念吗?

基本上可以认为是的。

Major GC 主要描述的是在老年代的内存回收,触发老年代垃圾回收的其中一个原因是新生代在垃圾后将到达一定年龄的对象复制到老年代,实际上可以看出Major GC产生的之前多伴随着 Minor GC 的产生。

我们不应该去关心到底应该是叫 Major GC 还是 Full GC,这些术语无论是在 JVM 规范还是在垃圾收集研究论文中都没有正式的定义,大家应该关注当前的 GC 是否停止了所有应用程序的线程,还是能够并发的处理而不用停掉应用程序的线程。

参考资料

Difference between -XX:UseParallelGC and -XX:+UseParNewGC

Parallel Scavenge垃圾收集器认定对象是大对象的条件是什么?

JDK6 Download

Minor GC、Major GC和Full GC之间的区别