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的堆区区分开来。
对于习惯在HotSpot(Sun公司开发的)虚拟机上开发和部署程序的开发者来说,很多人更愿意将之称为 永久代(或 持久代, Permanent Generation)。本质上两者并不等价,只是因为HotSpot虚拟机的设计团队把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。
方法区的大小可以用-XX:PermSize(初始大小)和-XX:MaxPermSize(最大值)来控制。一般默认固定为64M。
当方法区出现溢出时(通常是因为加载了大量的类或常量),将会出现OutOfMemeoryError,并提示是PermGen space,此时可以用-XX:PermSize和-XX:MaxPermSize来增大方法区的大小。
堆
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