在JVM使用的内存中,通常堆消耗的部分最多,但是JVM也会为内部操作分配一些内部。这些非堆内存就是原生内存。
应用中可以通过JNI的malloc()类似的方法或者是使用NIO的API分配。JVM使用的原生内存和堆内存的总量,就是一个应用总的内存占用(Footprint)。
1、测量内存占用
unix系统中,像top和ps这样的程序可以给出基本数据;
window系统中,可以使用perfmon或VMMap;
何种平台,都需要看看进程实际分配的内存(这与保留的内存完全不同)。分配内存(也叫提交内存),保留内存(有时也叫作虚拟内存)。
分配内存和保留内存区别:
通过示例说明,JVM必须告知操作系统,它的堆可能需要多达2GB的内存,所以会保留这么多内存:操作系统承诺,当JVM因为要增加堆而尝试分配额外的内存时,这些内存时可以获取到的。最初分配的内存时512MB,而且这就是实际用到的全部内存。也即是提交内存。提交内存的量随堆的重新调整而波动;特别是,提交内存会随着堆的增加而相应增加。
线程栈是个例外。JVM每次创建线程时,操作系统分配一些原生内存来保存线程栈,向线程提交更多内存(至少要等到线程退出)。线程栈是在创建时全部分配的。
2、原生NIO缓冲区
开发者 可以通过JNI调用来分配原生内存,但是如果NIO字节缓冲区是通过allocateDirect()方法创建的,则也会分配原生内存。从性能角度看,原生字节缓冲区非常重要,因为它们支持原生代码和Java代码在不复制的情况下共享数据。
调用allocateDirect()方法非常昂贵,所以应该尽可能重用直接字节缓冲区。理想的情况是,线程是独立的,而且每个线程持有一个直接字节缓存区作为线程局部变量。直接字节缓冲区的对象池可能更有用。
字节缓存区也可以切割管理。应用可以分配一个非常大的直接字节缓冲区,然后每个请求使用ByteBuffer类的slice()方法从中分配一部分。如果每次不能分配相同大小,这种方案也很难处理:会形成碎片。而且不像堆那样还可以对碎片进行压缩,字节缓冲区中的不同片段是无法压缩的,所以只有当所有片段大小都相同时,这种解决方案才好用。
直接字节缓冲区所分配的内存总量:可以通过设置-XX:MaxDirectMemorySize=N标志来指定。从JAVA7开始,这个标志默认值为0,以为没有限制(当然受制于地址空间大小,以及操作系统对进程的各种限制)。
3、原生内存跟踪
从java8开始,借助-XX:NativeMemoryTracking=off|summary|detail这个选项,JVM支持我们一窥它是如何分配原生内存的。原生内存跟踪(Native Memory Tracking,NMT)默认是关闭的(off)。如果开启了概要模式(summary)或详情模式(detail),可以随时通过jcmd命令获得原生内存的信息:
jcmd process_id VM.native_memory summary
如果JVM是使用-XX:+PrintNMTStatistics参数(默认false)启动的,它会在程序退出时打印原生内存分配信息。
示例:
package com.dxz.jvm; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; /** * 1堆溢出信息 * @VM args:-verbose:gc -Xms20M -Xmx20M -XX:+PrintGCDetails * * 2原生内存信息打印 * @VM args:-verbose:gc -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics -XX:NativeMemoryTracking=summary */ public class HeapOutOfMemory { public static void main(String[] args) { List<TestObject> cases = new ArrayList<TestObject>(); HeapOutOfMemory heapOutOfMemory = new HeapOutOfMemory(); int i = 0; while (i <10) { cases.add(heapOutOfMemory.new TestObject()); i++; } try { TimeUnit.SECONDS.sleep(60); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public class TestObject { private double a = 34.53; private Integer b = 9999999; } }
用jcmd
D:\workspace\study\target>jcmd 11532 VM.native_memory summary 11532: Native Memory Tracking: Total: reserved=1361449KB, committed=62425KB - Java Heap (reserved=20480KB, committed=20480KB) (mmap: reserved=20480KB, committed=20480KB) - Class (reserved=1062013KB, committed=10109KB) (classes #421) (malloc=5245KB #154) (mmap: reserved=1056768KB, committed=4864KB) - Thread (reserved=15423KB, committed=15423KB) (thread #16) (stack: reserved=15360KB, committed=15360KB) (malloc=45KB #82) (arena=18KB #30) - Code (reserved=249636KB, committed=2572KB) (malloc=36KB #314) (mmap: reserved=249600KB, committed=2536KB) - GC (reserved=6667KB, committed=6611KB) (malloc=5771KB #117) (mmap: reserved=896KB, committed=840KB) - Compiler (reserved=132KB, committed=132KB) (malloc=2KB #22) (arena=131KB #3) - Internal (reserved=5380KB, committed=5380KB) (malloc=5316KB #1365) (mmap: reserved=64KB, committed=64KB) - Symbol (reserved=1495KB, committed=1495KB) (malloc=943KB #111) (arena=552KB #1) - Native Memory Tracking (reserved=38KB, committed=38KB) (malloc=3KB #37) (tracking overhead=35KB) - Arena Chunk (reserved=185KB, committed=185KB) (malloc=185KB) D:\workspace\study\target>
说明:
Total: reserved=1361449KB(保留内存), committed=62425KB(提交内存) - Java Heap (reserved=20480KB(20MB,同参数设置一致), committed=20480KB) (mmap: reserved=20480KB, committed=20480KB)
堆使用情况:
Java Heap (reserved=20480KB, committed=20480KB) (mmap: reserved=20480KB, committed=20480KB)
保留内存为20MB与设置的相同。
- Class (reserved=1062013KB, committed=10109KB) (classes #421) (malloc=5245KB #154) (mmap: reserved=1056768KB, committed=4864KB)
用于保存类的元数据的原生内存。classes #421是实际用于保存程序中的421个类而占用的内存相比,JVM保留的内存要更多。
- Thread (reserved=15423KB, committed=15423KB)
(thread #16)
(stack: reserved=15360KB, committed=15360KB)
(malloc=45KB #82)
(arena=18KB #30)
thread #16,表示16个线程,分配的总内存有15423KB,平均一个线程是1MB。
- Code (reserved=249636KB, committed=2572KB) (malloc=36KB #314) (mmap: reserved=249600KB, committed=2536KB)
JIT的代码缓存:根据上面的类数量来的,所以内存占用不是很多。
- GC (reserved=6667KB, committed=6611KB) (malloc=5771KB #117) (mmap: reserved=896KB, committed=840KB)
GC算法的处理锁使用的一些堆外空间。
- Compiler (reserved=132KB, committed=132KB) (malloc=2KB #22) (arena=131KB #3)
这个区域是供编译器自身操作使用的,这与生成的代码放在代码缓存中是不同的。
- Symbol (reserved=1495KB, committed=1495KB) (malloc=943KB #111) (arena=552KB #1)
保留字符串(Interned String)的引用与符号表引用放在这里。
- Native Memory Tracking (reserved=38KB, committed=38KB) (malloc=3KB #37) (tracking overhead=35KB)
NMT本身的操作也需要一些空间。
NMT跟踪
NMT也支持跟踪内存分配随时间的变化情况。如果JVM在启动时启用了NMT,可以使用如下命令确定内存的基线使用情况:
jcmd process_id VM.native_memory baseline
4、针对不同操作系统优化JVM
JVM可以利用一些调优选项来优化操作系统内存的使用。
4.1、大页
java支持-XX:+UseLargepages选项。其默认值跟具体的操作系统配置有关。在windows上,必须在操作系统中启动大页。(默认使用常规页)
在linux上,UseLargepages默认不会启用,要支持大页,需要配置一下操作系统。
在Solaris上,不要什么操作系统方面配置,默认启用大页。
linux大页
linux透明页
大页大小
5、压缩的oop
oop:ordinary object pointer,即普通对象指针,JVM将其用作对象引用的句柄。