C/C++程序员和Java程序员最大的区别之一就是对内存管理的工作,Java程序员不需要管理内存,因为有JVM帮助管理。所以JVM的所谓开发必然涉及内存的分配和管理。我们这里尽可能地简化描述内存分配和管理,只描述和GC算法相关的部分。本质上来说,了解这一部分内容越多,特别是了解JVM如何与操作系统交互的部分,越容易对JVM调优。
JVM作为内存分配的管理器,一定涉及如何与内存交互。那么JVM是如何管理内存的?实际上内存管理的算法很多,简单来说JVM从操作系统申请一块内存,然后根据不同的GC算法进行管理。下面以Linux为例看一下JVM是如何做的。
首先JVM先通过操作系统的 系统调用 (system call)进行内存的申请,典型的就是mmap。在这里提一个问题,众所周知glibc提供了我们常用的内存管理函数如malloc/free/realloc/memcopy/memset等。为什么JVM不直接使用这些函数?glibc里面的malloc也是通过mmap等系统调用来完成内存的分配,之后glibc再对已经分配到的内存进行管理。GC算法实现了一套自己的管理方式,所以再基于malloc/free实现效率肯定不高。mmap必须以PAGE_SIZE为单位进行映射,而内存也只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行以PAGE_SIZE的倍数大小进行映射。还要注意一点,操作系统对内存的分配管理典型地分为两个阶段: 保留 (reserve)和 提交 (commit)。保留阶段告知系统从某一地址开始到后面的dwSize大小的连续虚拟内存需要供程序使用,进程其他分配内存的操作不得使用这段内存;提交阶段将虚拟地址映射到对应的真实物理内存中,这样这块内存就可以正常使用。
对于保留和提交,Windows在使用VirtualAlloc分配内存时传递不同的参数MEM_RESERVE/MEM_COMMIT,Linux在mmap保留内存时使用MAP_PRIVATE|MAP_NORESERVE|MAP_ANONYMOUS,提交内存时使用MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS。其中MAP_NORESERVE指不要为这个映射保留交换空间,MAP_FIXED使用指定的映射起始地址。
在JVM中我们还看到了使用类库函数malloc/free的地方。这和JVM内存管理策略有关,JVM内部也有很多数据需要在堆中分配,而这和Java堆空间没有关系,所以直接使用类库函数。另外需要提一下JVM推荐使用jemalloc替代glibc,原因是其效率更高。
JVM中常见的对象类型有以下6种:
·ResourceObj:线程有一个资源空间(Resource Area),一般ResourceObj都位于这里。定义资源空间的目的是对JVM其他功能的支持,如CFG、在C1/C2优化时可能需要访问运行时信息(这些信息可以保存在线程的资源区)。
·StackObj:栈对象,声明的对象使用栈管理。其实栈对象并不提供任何功能,且禁止New/Delete操作。对象分配在线程栈中,或者使用自定义的栈容器进行管理。
·ValueObj:值对象,该对象在堆对象需要进行嵌套时使用,简单地说就是对象分配的位置和宿主对象(即拥有这个ValueObj对象的对象)是一样的。
·AllStatic:静态对象,全局对象,只有一个。值得一提的是C++中静态对象的初始化并没有通过规范保证,可能会有一个问题,就是两个静态对象相互依赖,那么在初始化的时候可能出错。JVM中的很多静态对象的初始化,都是显式调用静态初始化函数。
·MetaspaceObj:元对象,比如InstanceKlass这样的元数据就是元对象。
·CHeapObj:这是堆空间的对象,由new/delete/free/malloc管理。其包含的内容很多,比如Java对象、InstanceOop(后面提到的G1对象分配出来的对象)。除了Java对象,还有其他的对象也在堆中。
JVM中为了准确描述这些堆中的对象,以方便对JVM进行优化,所以又定义了更具体的子类型,代码如下所示:
hotspot/src/share/vm/memory/allocation.hpp // JVM中使用的内存类型 mtJavaHeap = 0x00, // Java堆 mtClass = 0x01, // JVM中Java类 mtThread = 0x02, // JVM中线程对象 mtThreadStack = 0x03, mtCode = 0x04, // JVM中生成的编译代码 mtGC = 0x05, // GC的内存 mtCompiler = 0x06, // 编译器使用的内存 mtInternal = 0x07, // JVM中内部使用的类型,不属于上述任何类型 mtOther = 0x08, // 不是由JVM使用的内存 mtSymbol = 0x09, // 符号表使用的内存 mtNMT = 0x0A, // NMT使用的内存 mtClassShared = 0x0B, // 共享类数据 mtChunk = 0x0C, // Chunk用于缓存 mtTest = 0x0D, mtTracing = 0x0E, mtNone = 0x0F,
这些信息描述了JVM使用内存的情况,这一部分信息能够帮助定位JVM本身运行时出现的问题,我们将在最后的附录B中通过本地内存跟踪(Native Memory Tracking)来进一步解读这些信息。