最近与同事讨论了一下现在各大流行的语言及平台,有一个比较一致的观点,JVM平台在目前众多虚拟机平台中,已经是最成熟,最稳定的了。
JVM平台在内存管理,包括分配,GC等,已经非常完善。下面是关于JVM的内存的一些总结,包括逻辑视角上看来JVM的内存,从物理视角上来了解JVM内存的管理,最后再探讨一下JVM作为一个应用在Linux主机下与整个系统的内存之间关系。
1.逻辑视角
从逻辑视角上看,JVM中的内存可以分为下面6个部分,每个部分存储哪些信息在《深入Java虚拟机》一书中有非常详尽的说明。以前写的一篇文章对这些区存储Java程序中的哪些元素有些简要的说明。
Program Counter Register
Java Virtual Machine Stack
Heap
Method Area
Runtime Constant Pool
Native Method Stack
2.物理视角
在物理层面上JVM将内存分成几个区进行管理,它们只管理对应于上述逻辑层面上的Heap,Method Area。JVM stack的大小也是可调整的,但是它并没有包括在下面讨论的内存管理中。
具体各个区的分配及作用如下:
Young Generation 通常空间较小,因此收集算法需要快速,频繁且有效,若干次收集后仍然存活的对象,则移入Old Generation。
Old Generation 占用空间通常比Young要大很多,存放经过若干次收集后仍然存活的对象,某些大对象也直接在此区域分配。
Permanent Genetation Objects describing classes and methods,这个区域我认为对应于逻辑视角的Method Area.
其中Young区又分成几个区进行管理,各个区的作用如下:
Eden 初始化时产生的对象都存放在这个区,一些大对象除外,它们被直接分配的Old区。
From 存放至少经常一次收集仍然存活的对象,
To 与From 一个时间内只有一个区有对象。在收集过程中不断将仍然存活的对象移到另外一个空的区及Old区后,然后释放整个区的空间。
3.GC机制
Sun的Hotspot JVM中已经有非常多可选的GC算法了,不少GC算法已经非常智能。比如可以通过设置一个目标值,然后GC会自动调整适应以达到这个设定值,和Oracle 10g中的调优选项类似。
在Young 区达到某一阀值的时候启动minor gc收集,当Old及 Permanent达到某一个阀值的时候,启动major GC即full gc.
GC在对Young区和Old区会采用不同的算法,对于CPU个数的不同,又有串行收集和并行收集的不同。Young区较小,而且需要频繁运行,因此算法需要快速,有效。
对Young区的收集算法如下图示,其中标红X的是垃圾对象.
对于Old区收集算法在串行及并行时上都是采用Mark-Sweep-Compact方式,如下图所示
还有一种很特别的CMS(Current-Mark-Sweep)收集算法,它对Young区的收集与前面所讲的都一样,它对Old区的收集比一般的方法相比少了Compact这一操作,因此程序在Full GC时中断时间会缩短,缺点是收集完之后的内存块是不连续的,在minor GC时将Young区的对象移到Old区时,查找合适的内存块,需要花费更多一点的时间。它收集后的效果如下:
GC调优的策略在memoryManager_whitepaper一文中讲得非常清楚,在这里就不详细讲述了。下面给出一个通过jmap工具导出的JVM内存snopshoot的例子。
例子中有上述提到的各个区的大小等信息,还有其它一些GC使用的信息,这些可以在whitepaper中找到其对应的解释。
[admin@vm153f ~]$ /opt/java1/bin/jmap -heap 2457
Attaching to process ID 2457, please wait…
Debugger attached successfully.
Server compiler detected.
JVM version is 10.0-b23
using thread-local object allocation.
Mark Sweep Compact GC
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 1342177280 (1280.0MB)
NewSize = 335544320 (320.0MB)
MaxNewSize = 335544320 (320.0MB)
OldSize = 4194304 (4.0MB)
NewRatio = 8
SurvivorRatio = 8
PermSize = 100663296 (96.0MB)
MaxPermSize = 268435456 (256.0MB)
Heap Usage:
New Generation (Eden + 1 Survivor Space):
capacity = 301989888 (288.0MB)
used = 41946376 (40.00318145751953MB)
free = 260043512 (247.99681854248047MB)
13.889993561638725% used
Eden Space:
capacity = 268435456 (256.0MB)
used = 41946376 (40.00318145751953MB)
free = 226489080 (215.99681854248047MB)
15.626242756843567% used
From Space:
capacity = 33554432 (32.0MB)
used = 0 (0.0MB)
free = 33554432 (32.0MB)
0.0% used
To Space:
capacity = 33554432 (32.0MB)
used = 0 (0.0MB)
free = 33554432 (32.0MB)
0.0% used
tenured generation:
capacity = 1006632960 (960.0MB)
used = 153565120 (146.45111083984375MB)
free = 853067840 (813.5488891601562MB)
15.255324045817057% used
Perm Generation:
capacity = 100663296 (96.0MB)
used = 49601000 (47.303199768066406MB)
free = 51062296 (48.696800231933594MB)
49.274166425069176% used
在这里说一下JVM与主机内存的关系。JVM实际使用的内存包括堆内存,JVM stack使用的内存及java.nio.ByteBuffer等对象使用的内存。其中ByteBuffer使用的内存分为两种形式,如下图示。
总的来讲,JVM在管理内存上花费了很大的功夫,在10多年的演进中JVM得到了不断地完善,现在已经达到了非常高的可用性了。下一步JVM的主要演进方向应该是在多核时代如何达到更高的性能。如果说Java语言太老,不够灵活,已经进入晚年的话,JVM正处在青壮年时期,现在正是它的黄金年代。

