前言
之前在看netty的时候,不断的提到直接内存与零拷贝,所以就想看看JVM与直接内存的之间的关系;
环境准备
系统:macOS 11+
Jdk:jdk1.8
内存:16G
直接内存分析
1.直接内存与heap空间、meta空间之间的关系
Code-1:申请直接内存
//-Xmx2g -XX:+PrintGCDetails
public class BufferTest1 {
private static final int BUFFER = 1024 * 1024 * 1024;//1GB
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000l);
Scanner scanner = new Scanner(System.in);
System.out.println("请求指示:");
scanner.next();
//直接分配本地内存空间
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
System.out.println("直接内存分配完毕,请求指示!");
scanner.next();
System.out.println("直接内存开始释放!");
byteBuffer = null;
System.gc();
scanner.next();
}
}
可以发现直接内存 heap空间与meta空间都是没有影响的,但是对应进程占用的内存是有直接影响的。
2.直接内存最大内存
Code-2:多申请几个直接内存
//-Xmx2g -XX:+PrintGCDetails
public class BufferTest2 {
private static final int BUFFER = 1024 * 1024 * 1024;//1GB
private static final int BUFFER2 = 512 * 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000l);
Scanner scanner = new Scanner(System.in);
System.out.println("请求指示:");
scanner.next();
//直接分配本地内存空间1
ByteBuffer byteBuffer1 = ByteBuffer.allocateDirect(BUFFER);
System.out.println("直接内存1分配完毕,请求指示!");
scanner.next();
ByteBuffer byteBuffer2 = ByteBuffer.allocateDirect(BUFFER2);
System.out.println("直接内存2分配完毕,请求指示!");
scanner.next();
ByteBuffer byteBuffer3 = ByteBuffer.allocateDirect(BUFFER2);
System.out.println("直接内存3分配完毕,请求指示!");
scanner.next();
System.out.println("直接内存开始释放!");
byteBuffer1 = null;
byteBuffer2 = null;
byteBuffer3 = null;
System.gc();
scanner.next();
}
}
请求指示:
1
直接内存1分配完毕,请求指示!
2
直接内存2分配完毕,请求指示!
3
[GC (System.gc()) [PSYoungGen: 10486K->1966K(76288K)] 10486K->1974K(251392K), 0.0027432 secs] [Times: user=0.01 sys=0.01, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 1966K->0K(76288K)] [ParOldGen: 8K->1801K(175104K)] 1974K->1801K(251392K), [Metaspace: 4178K->4178K(1056768K)], 0.0069622 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:695)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.kaikela.demo2.home.BufferTest2.main(BufferTest2.java:42)
Heap
PSYoungGen total 76288K, used 1748K [0x0000000795580000, 0x000000079aa80000, 0x00000007c0000000)
eden space 65536K, 2% used [0x0000000795580000,0x0000000795735018,0x0000000799580000)
from space 10752K, 0% used [0x0000000799580000,0x0000000799580000,0x000000079a000000)
to space 10752K, 0% used [0x000000079a000000,0x000000079a000000,0x000000079aa80000)
ParOldGen total 175104K, used 1801K [0x0000000740000000, 0x000000074ab00000, 0x0000000795580000)
object space 175104K, 1% used [0x0000000740000000,0x00000007401c2490,0x000000074ab00000)
Metaspace used 4218K, capacity 4674K, committed 4864K, reserved 1056768K
class space used 466K, capacity 494K, committed 512K, reserved 1048576K
Process finished with exit code 1
可以看出,直接内存大小不能超过jvm的最大限制(-Xmx),当然也可以限制直接内存的大小(-XX:MaxDirectMemorySize)。比如设置-XX:MaxDirectMemorySize=1g,那么上面的代码在执行第二次空间申请的时候就会报OOM。
3.直接内存与GC
上面的例子可以发现,我们在主动进行GC的时候,那我们可以设想一个场景,我们申请了一个很大直接内存,然后开始大量的创建对象,那么整个JVM内存的表现是怎么样的
Code-3:直接内存与GC
// -Xmx2g -XX:+PrintGCDetails
public class BufferTest {
private static final int BUFFER = 1700 * 1024 * 1024;//1.6GB
public static void main(String[] args) throws InterruptedException {
Scanner scanner = new Scanner(System.in);
System.out.println("请求指示:");
scanner.next();
//直接分配本地内存空间
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
System.out.println("直接内存分配完毕,请求指示!");
scanner.next();
List<TestObj> bigObj = new ArrayList<>();
for (int n = 0; n < 5000000; n++) {
bigObj.add(new TestObj(n));
}
System.out.println(bigObj);
scanner.next();
System.out.println("直接内存开始释放!");
byteBuffer = null;
System.gc();
scanner.next();
}
}
可以发现当创建对象的时候,发生GC的时候,申请的直接内存也会被回收。
请求指示:
1
直接内存分配完毕,请求指示!
2
[GC (Allocation Failure) [PSYoungGen: 65536K->10728K(76288K)] 65536K->34945K(251392K), 0.0276737 secs] [Times: user=0.17 sys=0.02, real=0.03 secs]
[GC (Allocation Failure) [PSYoungGen: 76264K->10749K(76288K)] 100481K->92821K(251392K), 0.0606339 secs] [Times: user=0.41 sys=0.04, real=0.07 secs]
[GC (Allocation Failure) [PSYoungGen: 68029K->10747K(76288K)] 150101K->142462K(251392K), 0.0662871 secs] [Times: user=0.47 sys=0.03, real=0.06 secs]
[Full GC (Ergonomics) [PSYoungGen: 10747K->0K(76288K)] [ParOldGen: 131715K->141458K(285184K)] 142462K->141458K(361472K), [Metaspace: 9355K->9355K(1058816K)], 1.4972584 secs] [Times: user=8.48 sys=0.09, real=1.50 secs]
[GC (Allocation Failure) [PSYoungGen: 65536K->10752K(128512K)] 206994K->203458K(413696K), 0.0623276 secs] [Times: user=0.34 sys=0.05, real=0.06 secs]
1
直接内存开始释放!
[GC (System.gc()) [PSYoungGen: 101794K->10752K(128512K)] 294500K->288159K(413696K), 0.0895192 secs] [Times: user=0.55 sys=0.07, real=0.09 secs]
[Full GC (System.gc()) [PSYoungGen: 10752K->0K(128512K)] [ParOldGen: 277407K->260989K(285184K)] 288159K->260989K(413696K), [Metaspace: 9526K->9526K(1058816K)], 1.1158031 secs] [Times: user=6.84 sys=0.02, real=1.12 secs]
Heap
PSYoungGen total 128512K, used 3498K [0x0000000795580000, 0x000000079e080000, 0x00000007c0000000)
eden space 117760K, 2% used [0x0000000795580000,0x00000007958ea8a0,0x000000079c880000)
from space 10752K, 0% used [0x000000079d580000,0x000000079d580000,0x000000079e000000)
to space 512K, 0% used [0x000000079e000000,0x000000079e000000,0x000000079e080000)
ParOldGen total 285184K, used 260989K [0x0000000740000000, 0x0000000751680000, 0x0000000795580000)
object space 285184K, 91% used [0x0000000740000000,0x000000074fedf5b8,0x0000000751680000)
Metaspace used 9540K, capacity 10054K, committed 10240K, reserved 1058816K
class space used 1103K, capacity 1211K, committed 1280K, reserved 1048576K
4.直接内存+heap+metaspace=进程内存
根据code-3的例子,我们翻过来思考,如果先将heap空间用完,在去申请直接内存是否还能够成功?
Code-4
public class BufferTest3 {
private static final int BUFFER = 1740 * 1024 * 1024;//1.6GB
public static void main(String[] args) throws InterruptedException {
Scanner scanner = new Scanner(System.in);
System.out.println("请求指示:");
scanner.next();
List<TestObj> bigObj = new ArrayList<>();
for (int n = 0; n < 5000000; n++) {
bigObj.add(new TestObj(n));
}
System.out.println(bigObj);
scanner.next();
//直接分配本地内存空间
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
System.out.println("直接内存分配完毕,请求指示!");
scanner.next();
System.out.println("直接内存开始释放!");
byteBuffer = null;
System.gc();
scanner.next();
}
}
可以看到java进程消耗的内存是3.6g已经超过了Xmx配置的大小,不过gc之后一样会被回收。
总结
1、直接内存申请的总和大小不能超过-Xmx配置的大小
2、直接内存申请的总和大小不能超过-XX:MaxDirectMemorySize配置的大小
3、直接内存申请的总和不能超过系统允许的最大值
4、gc可以将直接内存进行回收
5、直接内存虽然要小于-Xmx配置的大小,但是直接内存+heap+metaspace会大于-Xmx配置的大小