JVM内存组成和GC机制

按照官方的说法:

Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。
在JVM中堆之外的内存称为非堆内存(Non-heap memory)。

可以看出JVM主要管理两种类型的内存:非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给 自己用的,所有方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法 的代码都在非堆内存中。

JVM内存组成

方法栈&本地方法栈

线程创建时产生,方法执行时生成栈帧。在Sun JDK中没有本地方法栈,只有每个线程单独的方法栈。此栈大小可以用-Xss来控制。

栈溢出会出现StackOverflowError异常,在Java中因为对象都是存在堆中,所以造成此错误一般是因为方法调用的递归深度太大(一般是产生了循环递归),可以根据输出的错误堆栈信息排查代码中的问题。
若在申请线程过多时出现OutOfMemeoryError,是因为无法为新的线程申请到栈内存,这时可以尝试降低堆内存(-Xms、-Xmx),从而给操作系统预留更多的内存。

方法区

方法区是JVM中所有线程共享的内存区,存储已被虚拟机加载的类信息常量静态变量、即时编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java的堆区区分开来。
JDK7之前(JDK1.2~JDK6),方法区以永久代实现,GC被扩展到方法区。永久代的大小可以用-XX:PermSize和-XX:MaxPermSize来进行调整。一般默认固定为64M。
JDK7中开始移除永久代,将字符串常量和类引用移到了堆中。但在JDK7中永久代依然存在,在加载足够多的类信息时也会出现OOM。但是在运行时调用String.intern()增加字符串常量不会使永久代OOM了(JDK7以前是会OOM的)。
JDK8之后永久代被完全移除,方法区的数据存储在与堆不相连的本地内存区域,称为元空间。元空间最大的问题就是碎片化。元空间默认情况下最大为主机可分配内存大小,但是可以通过-XX:MaxMetaspaceSize来限制其增长。

java代码中所有的new操作都会在堆中申请内存,这是JVM运行时的主要内存区域。
JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指 定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx相等以避免在每次GC后调整堆的大小。

通过-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析。出现heap space的OOM可能是因为内存溢出或者内存泄漏。如果是内存溢出,则需进一步使用工具分析泄漏对象,从而找到代码漏洞。如果是内存溢出,则需要根据物理内存调整-Xms与-Xmx的值。

堆内存分配和回收

根据GC分代收集策略,将堆内存分为新生代(New Generation)与老年代(Old Generation)。其中,新生代又分为Eden和两个Survivor。如下图所示:

新生代的大小可以用虚拟机参数-Xmn来设置,Sun官方推荐的配置为整个堆的3/8。也可以用-XX:NewRatio来设置新生代与老年代的比例(-XX:NewRatio=4,则新生代与年老代所占比值为1:4,新生代占整个堆栈的1/5)。

其中新生代的两个Survivor分别称为from space和to space,这两块空间交替使用,每次只使用了其中的一个,也就是说新生代中真正在使用的是Eden空间和一个Survivor空间。HotSpot虚拟机默认的Eden和Survivor的大小比例是8:1,也就是说每次新生代中可用的空间为总新生代容量的90%,还有10%是用于下次发生MinorGC(在下面会详细讲到)时收集还存活着的对象。可以使用-XX:SurvivorRatio来设置新生代中Eden区与Survivor区的大小比值,默认为8(例如-XX:SurvivorRatio=4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个新生代的1/6)。

对象优先在Eden分配

大多数情况下对象在新生代的Eden区中分配,当Eden中没有足够的空间时,虚拟机就会发起一次MinorGC。

MinorGC发起时,会将Eden和当前正在使用的Survivor空间中还存活着的对象一次性的复制到另一个Survivor空间中,然后将当前Eden和当前的Survivor清空,开始使用另一个Survivor。当然,如果Survivor的空间不够,从Eden和正在使用的Survivor中回收的对象无法存到Survivor中,则会将活着的对象交给老年代进行分配担保(Handle Promotion)。

大对象直接进入老年代

所谓大对象,就是需要大量连续内存空间的Java对象,最典型的就是那种很长的字符串及数组。当出现大对象时,会直接进入老年代。而如果老年代空间满,则会触发一次FullGC,与MinorGC相比,FullGC是一个非常重量级的操作。所以写程序时应该避免使用一些“朝生夕死”的“短命大对象”,这些大对象直接进入老年代而后就不再使用,从而容易触发FullGC,降低程序的性能。

虚拟机提供了一个-XX:PretenureSizeThreshold参数,另大于这个设置值的对象直接在脑年代中分配。这样做的原因是避免频繁的MinorGC中在Eden区和两个Survivor区之间发生大量耗时的内存拷贝。(这个参数必须以字节为单位,如-XX:PretenureSizeThreshold=3145728,表示3MB,而不能直接写3M)。

长期存活的对象进入老年代

当一个存活的对象在MinorGC中第一次被拷贝到Survivor中时,其年龄会被设置为1。此后,每次这个对象熬过一次MinorGC时,其年龄就会加1,默认情况下,当对象的年龄达到15时,将会被拷贝进老年代,从而减少MinorGC中Eden和两个Survivor之间的内存拷贝。

对象年龄的阈值可以用虚拟机参数-XX:MaxTenuringThreshold来设置,默认为1。

动态对象年龄判定

虚拟机并不是一定要等到对象的年龄达到MaxTenuringThreshold才将其移动到老年代。如果Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold的年龄。

JVM的GC参数

参考

http://unixboy.iteye.com/blog/174173
http://www.cnblogs.com/redcreen/archive/2011/05/04/2036387.html