一、JVM内存分配情况
我们知道,Java内存分配的时候,堆中1/3是年轻代(新生代),2/3是老年代,我们现再设置如下JVM启动参数
-Xms9M -Xmx9M -XX:+PrintGCDetails
然后运行代码
public class HeapTest {
public static void main(String[] args) {
//堆内存占用测试
}
}
结果如下
Heap
PSYoungGen total 2560K, used 955K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 46% used [0x00000000ffd00000,0x00000000ffdeec80,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
Metaspace used 2537K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 270K, capacity 386K, committed 512K, reserved 1048576K
我们的堆内存大概有9*1024=9216KB,然后新生代为(PSYoungGen)2560KB,老年代(ParOldGen)有7168K,占比跟上面说的1/3是年轻代,2/3是老年代差不多,然后元空间(Metaspace)用了2537K,元空间就是方法区,方法区用的内存是直接内存。
并且我们还可以知道,就算没有执行任何逻辑,堆中eden区还是会有一定的占用,那是因为HeadTest以及一些
JVM本来的类产生的对象。
二、测试JVM内存回收
大多数情况下,对象在年轻代中Eden区分配,当Eden区中没有足够的空间进行分配时,虚拟机将发起一次Minor GC,在老年代没有足够空间的时候才会进行一次Full GC,我们先看一下Minor GC和Full GC有啥区别:
Minor GC,在老年代没有足够空间的时候才会进行一次Full GC,我们先看一下Minor GC和Full GC有啥区别:
Minor GC/Young GC:指发生新生代的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。Minor GC/Young GC:指发生新生代的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
Major GC/Full GC:一般会回收老年代,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。
为什么Full GC会比Minor GC慢10倍以上,这里简单说明下,因为新生代垃圾回收一般用的算法时复制算法,而老年代一般用的时标记整理算法,所以比较慢,这个在以后会说到。
下面来触发一下垃圾回收,JVM配置参数和代码如下:
-Xms9M -Xmx9M -XX:+PrintGCDetails
1、先分配1000KB内存
public class HeapTest {
public static void main(String[] args) {
//1、先分配1000KB内存
byte[] k1 = new byte[1000*1024];
}
}
运行结果如下
Heap
PSYoungGen total 2560K, used 1955K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 95% used [0x00000000ffd00000,0x00000000ffee8ce8,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
Metaspace used 2537K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 270K, capacity 386K, committed 512K, reserved 1048576K
新生代:eden:95%;from:0%;to:0%;
老年代:0%;
2、再分配200KB内存
public class HeapTest {
public static void main(String[] args) {
//1、先分配1000KB内存
byte[] k1 = new byte[1000*1024];
//2、再分配200KB内存
byte[] k2 = new byte[200*1024];
}
}
运行结果:
[GC (Allocation Failure) [PSYoungGen: 1913K->504K(2560K)] 1913K->1648K(9728K), 0.0018048 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 2560K, used 664K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 7% used [0x00000000ffd00000,0x00000000ffd28370,0x00000000fff00000)
from space 512K, 98% used [0x00000000fff00000,0x00000000fff7e010,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 1144K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 15% used [0x00000000ff600000,0x00000000ff71e010,0x00000000ffd00000)
Metaspace used 2538K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 270K, capacity 386K, committed 512K, reserved 1048576K
新生代:eden:10%;from:98%;to:0%;
老年代:15%;
此时因为Eden已经放不下了,所以触发了一次minor GC,然后将一部分内存移动到了from区,但是发现移动到from区后还是放不下,所以就移动到了老年代。
3、再分配2K内存
public class HeapTest {
public static void main(String[] args) {
//1、先分配1000KB内存
byte[] k1 = new byte[1000*1024];
//2、再分配200KB内存
byte[] k2 = new byte[200*1024];
//3、再分配2KB内存
byte[] k3 = new byte[2*1024];
}
}
运行结果如下
[GC (Allocation Failure) [PSYoungGen: 1913K->504K(2560K)] 1913K->1608K(9728K), 0.0020718 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 2560K, used 766K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 12% used [0x00000000ffd00000,0x00000000ffd41a00,0x00000000fff00000)
from space 512K, 98% used [0x00000000fff00000,0x00000000fff7e030,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 1104K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 15% used [0x00000000ff600000,0x00000000ff714010,0x00000000ffd00000)
Metaspace used 2538K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 270K, capacity 386K, committed 512K, reserved 1048576K
新生代:eden:12%;from:98%;to:0%;
老年代:15%;
表明新分配的对象如果Eden区内存还足够的话,还是会在eden区分配内存的。
4、再分配8000KB内存,看是否会触发Full GC
public class HeapTest {
public static void main(String[] args) {
//1、先分配1000KB内存
byte[] k1 = new byte[1000*1024];
//2、再分配200KB内存
byte[] k2 = new byte[200*1024];
//3、再分配2KB内存
byte[] k3 = new byte[2*1024];
//4、再分配8000KB内存
byte[] k4 = new byte[8000*1024];
}
}
运行结果
[GC (Allocation Failure) [PSYoungGen: 1913K->504K(2560K)] 1913K->1624K(9728K), 0.0024726 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 745K->496K(2560K)] 1865K->1828K(9728K), 0.0012383 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 496K->480K(2560K)] 1828K->1852K(9728K), 0.0006963 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 480K->0K(2560K)] [ParOldGen: 1372K->1772K(7168K)] 1852K->1772K(9728K), [Metaspace: 2531K->2531K(1056768K)], 0.0068447 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 1772K->1772K(9728K), 0.0004366 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 1772K->1761K(7168K)] 1772K->1761K(9728K), [Metaspace: 2531K->2531K(1056768K)], 0.0067124 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.suibibk.jvm.HeapTest.main(HeapTest.java:13)
Heap
PSYoungGen total 2560K, used 60K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0f1f8,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 1761K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 24% used [0x00000000ff600000,0x00000000ff7b8748,0x00000000ffd00000)
Metaspace used 2563K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 273K, capacity 386K, committed 512K, reserved 1048576K
根据结果可以知道,先触发了Minor GC,后面老年代也不够了就触发了Full GC,并且抛出了OutOfMemoryError。为什么都触发了Full GC还会抛出OutOfMemoryError异常呢?
是因为对于强引用,当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
那么我们要换一种方法来测试了,我们知道Java有一种引用叫做软引用,来描述一些还有用但是并非必须的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题。
5、用软引用来测试Full GC并且不触发OutOfMemoryError。
因为我内存分配的总共就只有9M,所以测试值比较难找,如果对象分配内存太大,那么就算有软引用也还是不够。
不使用软引用的情况
public class HeapTest {
public static void main(String[] args) {
//1、先分配1000KB内存
//SoftReference<byte[]> k1 = new SoftReference<byte[]>(new byte[1000*1024]);
byte[] k1 = new byte[1000*1024];
//2、再分配200KB内存
byte[] k2 = new byte[200*1024];
//3、再分配2KB内存
byte[] k3 = new byte[2*1024];
//4、再分配6400KB内存
byte[] k4 = new byte[6400*1024];
}
}
运行结果
[GC (Allocation Failure) [PSYoungGen: 1913K->504K(2560K)] 1913K->1640K(9728K), 0.0016796 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 745K->488K(2560K)] 1881K->1872K(9728K), 0.0011202 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 488K->480K(2560K)] 1872K->1864K(9728K), 0.0008974 secs] [Times: user=0.03 sys=0.02, real=0.01 secs]
[Full GC (Allocation Failure) [PSYoungGen: 480K->0K(2560K)] [ParOldGen: 1384K->1772K(7168K)] 1864K->1772K(9728K), [Metaspace: 2531K->2531K(1056768K)], 0.0065214 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 1772K->1772K(9728K), 0.0003934 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 1772K->1761K(7168K)] 1772K->1761K(9728K), [Metaspace: 2531K->2531K(1056768K)], 0.0072175 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.suibibk.jvm.HeapTest.main(HeapTest.java:17)
Heap
PSYoungGen total 2560K, used 60K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0f1f8,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 1761K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 24% used [0x00000000ff600000,0x00000000ff7b8748,0x00000000ffd00000)
Metaspace used 2562K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 273K, capacity 386K, committed 512K, reserved 1048576K
可以看到,触发了OutOfMemoryError。
然后使用软引用
public class HeapTest {
public static void main(String[] args) {
//1、先分配1000KB内存
SoftReference<byte[]> k1 = new SoftReference<byte[]>(new byte[1000*1024]);
//byte[] k1 = new byte[1000*1024];
//2、再分配200KB内存
byte[] k2 = new byte[200*1024];
//3、再分配2KB内存
byte[] k3 = new byte[2*1024];
//4、再分配6400KB内存
byte[] k4 = new byte[6400*1024];
}
}
我们的预计是再Full GC后发现内存不够的情况下,会把软引用k1给回收,然后就不会导致OutOfMemoryError,运行测试看看
[GC (Allocation Failure) [PSYoungGen: 1914K->504K(2560K)] 1914K->1640K(9728K), 0.0020576 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 745K->480K(2560K)] 1881K->1816K(9728K), 0.0012011 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 480K->496K(2560K)] 1816K->1848K(9728K), 0.0005865 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 1352K->1772K(7168K)] 1848K->1772K(9728K), [Metaspace: 2531K->2531K(1056768K)], 0.0065809 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 1772K->1772K(9728K), 0.0003726 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 1772K->762K(7168K)] 1772K->762K(9728K), [Metaspace: 2531K->2531K(1056768K)], 0.0060668 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 2560K, used 20K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 1% used [0x00000000ffd00000,0x00000000ffd05360,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 7162K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 99% used [0x00000000ff600000,0x00000000ffcfe830,0x00000000ffd00000)
Metaspace used 2537K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 270K, capacity 386K, committed 512K, reserved 1048576K
果然,跟预计的一样,没有导致OutOfMemoryError。
三、总结
- 1、新对象会先放在新生代的Eden区
- 2、再Eden区满了后会触发Minor GC
- 3、如果新生代触发了Minor GC后还是放不下则会将对象放入老年代中
- 4、如果老年代也满了就会触发Full GC
- 5、触发Full GC的时候如果内存还不够并且没有软引用对象则会报OutOfMemoryError,若有软引用会把软引用强制清理,若清理后内存够就不会报OutOfMemoryError,否则还是会爆OutOfMemoryError。