垃圾收集器与内存分配策略(四)
内存分配和回收策略(一)
Java中的自动内存管理,分别是给对象分配内存以及回收分配给对象的内存。回收内存,之前已经说了虚拟机的一些垃圾收集器以及运作原理,接下来就是讨论如何给对象分配内存。
对象的内存分配,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配规则不是百分百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,以及虚拟机与内存的相关参数配置。(垃圾收集器+内存相关参数配置)
1、对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
/**
* -verbose:gc 输出虚拟机中GC的详细情况
* -Xms20M Java堆最大20M
* -Xmx20M Java堆最小20M(说明不可扩展)
* -Xmn10M 10M分给新生代
* -XX:+PrintGCDetails
* -XX:SurvivorRatio=8 新生代Eden区与一个Survivor区的空间比例是8:1
*
* @author wcj
* @date 2019/9/23 14:50
*/
public class MinorGC {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] b1, b2, b3, b4;
b1 = new byte[2 * _1MB];
b2 = new byte[2 * _1MB];
b3 = new byte[2 * _1MB];
//第四次分配时,容量不够了,会触发一个Minor GC
b4 = new byte[4 * _1MB];
}
}
输出结果如下:
Heap
PSYoungGen total 9216K, used 8154K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
eden space 8192K, 99% used [0x00000007bf600000,0x00000007bfdf6be8,0x00000007bfe00000)
from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
to space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
ParOldGen total 10240K, used 4096K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
object space 10240K, 40% used [0x00000007bec00000,0x00000007bf000010,0x00000007bf600000)
Metaspace used 3273K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K
在上述的代码中,会尝试分配3个2MB大小和一个4MB大小的对象,从输出结果中可以看出“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代总可用空间为9216KB(Eden区+1个Survivor区的总容量)。
当执行到b4的时候会发生一次Minor GC,因为给b4分配内存时,发现Eden已经被占用了6MB,剩余的空间不足以分配b4的内存,因此发生了Minor GC。GC期间发生已有的b1-b3无法放入Survivor空间(因为剩余的Survivor空间只有1MB大小),所以通过分配担保转移到了老年代。
在GC结束后,b4顺利的分配到了Eden中,因此只需结果是Eden被b4占用4MB,Survivor空闲,老年代被b1-b3占用6MB。
Minor GC和Full GC的区别
- 新生代GC(Minor GC):是指发生在新生代的垃圾收集动作,因为Java对象大多数都是“朝生夕死”,所以Minor非常频繁,一般回收速度也很快
- 老年代GC(Major GC/Full GC):是指发生在老年代的GC。Major GC的速度一般会比Minor GC慢10倍以上
2、大对象直接进入老年代
大对象一般是指需要大量连续内存存储的对象,比如很长的字符串或数组。-XX:PretenureSizeThreshold参数,当大于这个设置值的对象直接会在老年代进行分配。其目的是避免Eden区及两个Survivor区之间发生大量的内存复制(新生代采用复制算法收集内存)。
注意:此参数只适用于Serial和ParNew收集器有效,Parallel Scavenge收集器无法识别此参数,JDK1.8默认使用的是Parallel Scavenge收集器
/**
* -XX:+UseSerialGC 使用SerialGC垃圾收集器
* -verbose:gc 输出虚拟机中GC的详细情况
* -Xms20M Java堆最大20M
* -Xmx20M Java堆最小20M(说明不可扩展)
* -Xmn10M 10M分给新生代
* -XX:+PrintGCDetails
* -XX:SurvivorRatio=8 新生代Eden区与一个Survivor区的空间比例是8:1
* -XX:PretenureSizeThreshold=3145728 只适用于SerialGC和ParNew收集器
*
* @author wcj
* @date 2019/9/23 16:56
*/
public class BigObjectGC {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] b;
b = new byte[4 * _1MB];
}
}
输出结果如下:
Heap
def new generation total 9216K, used 1845K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
eden space 8192K, 22% used [0x00000007bec00000, 0x00000007bedcd450, 0x00000007bf400000)
from space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
to space 1024K, 0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
tenured generation total 10240K, used 4096K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
the space 10240K, 40% used [0x00000007bf600000, 0x00000007bfa00010, 0x00000007bfa00200, 0x00000007c0000000)
Metaspace used 3167K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 347K, capacity 388K, committed 512K, reserved 1048576K
可以看到,“the space 10240K, 40% used”,老年代中的10MB空间被使用了40%,也就是b4被直接分配到老年代中,可知,在SerialGC收集器下,超过3145728KB的对象都会直接在老年代中。
3、长期存活的对象直接进入老年代
虚拟机采用分代收集思想来管理内存,虚拟机是通过对象年龄(Age)计数器来区分哪些放在新生代,哪些属于老年代。如果对象在Eden创建并经过第一次Minor GC后仍然存活,并且被Survivor容纳,那么就移动到Survivor空间,并且Age=1。该对象每在Survivor中熬过一次Minor GC,age就+1,当age=15时(默认),就会放到老年代中。这个晋升的阈值,可以通过参数-XX:MaxTenuringThreshold设置。
/**
* -XX:+UseSerialGC 使用SerialGC垃圾收集器
* -verbose:gc 输出虚拟机中GC的详细情况
* -Xms20M Java堆最大20M
* -Xmx20M Java堆最小20M(说明不可扩展)
* -Xmn10M 10M分给新生代
* -XX:+PrintGCDetails
* -XX:SurvivorRatio=8 新生代Eden区与一个Survivor区的空间比例是8:1
* -XX:MaxTenuringThreshold=15 当age=15时,放入老年代
* -XX:+PrintTenuringDistribution 打印对象年龄的日志
*
* @author wcj
* @date 2019/9/23 16:56
*/
public class LongSurvivalObjectGC {
//1024字节*1024字节=1MB
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] b1, b2, b3;
b1 = new byte[_1MB / 4];
b2 = new byte[4 * _1MB];
b3 = new byte[4 * _1MB];
b3 = null;
b3 = new byte[4 * _1MB];
}
}
当XX:MaxTenuringThreshold=1时运行结果如下:
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age 1: 682400 bytes, 682400 total
: 6032K->666K(9216K), 0.0031150 secs] 6032K->4762K(19456K), 0.0031372 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
: 4762K->0K(9216K), 0.0008103 secs] 8858K->4745K(19456K), 0.0008244 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4260K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
eden space 8192K, 52% used [0x00000007bec00000, 0x00000007bf0290f0, 0x00000007bf400000)
from space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
to space 1024K, 0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
tenured generation total 10240K, used 4745K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
the space 10240K, 46% used [0x00000007bf600000, 0x00000007bfaa2620, 0x00000007bfaa2800, 0x00000007c0000000)
Metaspace used 3192K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
当方法执行时,b1需要256KB内存,Survivor空间可以容纳。当MaxTenuringThreshold=1时,b1会在第二次GC时进入老年代,新生代变为了0KB。