jvm的内存分配及运行机制(转)
http://www.cnblogs.com/200911/p/3922704.html
VM运行时数据区域:
根据《Java虚拟机规范(第二版)》的规定,JVM包括下列几个运行时区域:
我们思考几个问题:
1.jVM是怎么运行的?
2.JVM运行时内存是怎么分配的?
3.我们写的java代码(类,对象,方法,常量,变量等等)最终存放在哪个区?
VM运行时数据区域:
1.程序计数器(program Counter Register):
是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的 方式去实 现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个 计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核) 只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存 储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空 (Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2.java虚拟机栈(Java Virtual Machine Stacks):
里面存放的是本地变量表(存放了编译期可知的各种标量类型:Boolean,byte,char,short,int,float,long,double)、对象的引用(不是对象本身,仅仅是引用指针)、方法返回地址等。
虚拟栈中规定了两种异常状况:
- 如果线程请求的深度大于虚拟机所允许的深度,就会抛出stackoverflowerror异常,也就是栈溢出异常。在使用递归的调用方法的情况下,很容易抛出这个异常。
- 如果VM栈可以动态扩展,当扩展的时候无法申请到足够的内存空间,则抛出OutOfMemoryError异常,内存溢出。
3.本地方法栈(native method stacks)
这块区域在jvm运行内存中职责就相对比较少了。只是执行Native 方法。如果这个区的内存不足也是会抛出StackOverflowError 和 OutOfMemoryError 异常。
4.java 堆
这块区域是jvm中最大的一块区域了,java堆是被所有线程所共享的,也是GC主要的回收区,在jvm启动的时候就创建了。java堆的唯一的目的就是存放对象实例(所有new出来的对象)绝大部分对象的实例都是在这块区域分配。
从图中可以看出heap中还可以分为新生代(Young Generation)和老年代(Old Generation)。下面看这个图:
- 新 生代:GC每隔一段时间就会对新生代进行回收,在分配对象遇到内存不足的时候,先对新生代进行GC,当新生代GC后,无法满足内存空间的分配需求,才会对 整个对空间和方法区进行GC(FULL GC).而新生代又可以分为:一个Eden Space和两块相同大小的Survivor Space(s0,s1或From Survivor 和 To Survivor)正式图中所看到的。新生代中的E区和S区又有不同的职责。
- E区:GC触发比较频繁的区域,存储的是新new的对象,几乎所有对象都经过E区,如果多次GC仍然有存活的对象,就把存活的对象放到S区。
- S区:S区作为Eden区和old(老年代)的缓存。它是可以向老年代转移活动对象的实例.
- 老年代:用于存放多次新生代GC仍然活着的对象,如缓存对象。新建的对象也有可能直接进入老年代,主要有两种情况:①.大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。②.大的数组对象,切数组中无引用外部对象。
- 无 论对java堆如何划分,目的是为了更好的回收内存,或者是更快的分配内存;java的堆在物理空间上处于不连续的空间,但在逻辑上是连续的即可。虚拟机 堆内存空间是可扩展到的,可以通过-Xmx和-Xms控制,如果堆上无法分配内存空间,并且堆也无法再扩展到额时候,将会抛出 OutOfMemoryError异常。
5.方法区(Mehod Area)
方法区和堆一样也是线程共享的区域,它主要是用于存储被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据,不属于heap的一部分。 相对来说,GC行为在这个区域是相对比较少发生的,但并不是某些描述那样永久代不会发生GC。对于sun公司的HotSpot虚拟机来说。gc也会对这块 区域进行垃圾回收,这里的回收主要是常量池的回收和对类的卸载。
如果细分方法区的里面有为运行时常量池(Runtime Constant Pool),它主要存储Class文件中的版本、字段、方法、接口等描述信息。还 有一项信息是常量表(constant_pool table)用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是java语言并不要求常量一定只有编译期预先置入 Class的常量表的内容才能进入方法区常量池,运行期间才可以将新内容放入常量池(最典型的是String.intern()方法)
实战OutOfMemoryError:
除了程序计数器,其他在VMSpec中都描述了产生OutOfMemoryError(下称OOM)的情形,那我们就实战模拟一下,通过几段简单的代码,令对应的区域产生OOM异常以便加深认识,同时初步介绍一些与内存相关的虚拟机参数。
1.Java堆:
java 堆存放的是对象实例,因此只要不断建立对象,并且保证GCRoots到对象之间有可达路径即可产生OOM异常。测试中限制Java堆大小为20M,不可扩 展,通过参数-XX:+HeapDumpOnOutOfMemoryError让虚拟机在出现OOM异常的时候Dump出内存映像以便分析。
代码:
package com.lp.ecjtu; import java.util.ArrayList; public class JVMTestDemo_heap { public static void main(String[]args){ java.util.List<OOMObject>list = new ArrayList<OOMObject>(); while(true){ list.add(new OOMObject()); } } } /** * VMArgs:-Xms20m-Xmx20m-XX:+HeapDumpOnOutOfMemoryError * @author Administrator * */ class OOMObject{ }
运行结果: java.lang.OutOfMemoryError:Javaheapspace Dumpingheaptojava_pid3404.hprof... Heapdumpfilecreated[22045981bytesin0.663secs]
垃圾收集GC(GarbageCollection,下文简称GC):
总 结下:其中程序计数器、VM栈、本地方法栈随线程而生,随线程而灭;栈中的帧随着方法的进入退出,有条不紊的进行的出栈和入栈操作;每一个帧中分配多少内 存,基本上是在Class文件生成时就已知的(可能会由JIT动态晚期编译进行一些优化,但大体上可以认为是编译期可知
的),因此这几个区域的内存的分配和回收具备很高的确定性,因此在这几个区域不需要过多考虑回收问题。而java堆和方法区(包括运行时常量池)则不一样,我们必须等到程序实际运行期间才能知道会创建那些对象,这部分内存的回收和分配是动态的。
GC的历史远远比java来的久,在1960年诞生于MIT的Lisp(是一门真正的使用内存冬天分配和垃圾回收集)的语言。当Lisp在胚胎时期,人们在GC需要做的3件事情:
- 哪些内存需要回收
- 什么时候需要回收
- 怎么样回收
方法区的回收:
方法区即后文提到的永久代,很多人认为永久代是没有GC的,《Java虚拟机规范》中确实说过可以不要求虚拟机在这区实现GC,而且这区GC的“性价比”一般比较低:在堆中,尤其是在新生代,常规应用进行一次GC可以一般可以回收70%~95%的空间,而永久代的GC效率远小于此。虽然VMSpec不要求,但当前生产中的商业JVM都有实现永久代的GC,主要回收两部分内容:废弃常量与无用类。这两点回收思想与Java堆中的对象回收很类似,都是搜索是否存在引
用,常量的相对很简单,与对象类似的判定即可。而类的回收则比较苛刻,需要
满足下面3个条件:
1.该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
2.加载该类的ClassLoader已经被GC。
3.该类对应的java.lang.Class对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法。
在大量使用反射、动态代理、CGLib等bytecode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要JVM具备类卸载的支持以保证
永久代不会溢出。
垃圾收集算法
最基础的搜集算法是“标记-清除算法”(Mark-Sweep),如它的名字一样,算法分层“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,然后回收所有需要回收的对象,整个过程其实前一节讲对象标记判定的时候已经基本介绍完了。说它是最基础的收集算法原因是后续的收集算法都是基于这种思路并优化其缺点得到的。它的主要缺点有两个,一是效率问题,标记和清理两个过程效率都不高,二是空间问题,标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前触发另一次的垃圾搜集动作。
为了解决效率问题,一种称为“复制”(Copying)的搜集算法出现,它将用内存划分为两块,每次只使用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来整块内存空间一次过清理掉。这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点。
现在的商业虚拟机中都是用了这一种收集算法来回收新生代,IBM有专门研究表明新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的eden空间和2块较少的survivor空间,每次使用eden和其中一块survivor,当回收时将eden和survivor还存活的对象一次过拷贝到另外一块survivor空间上,然后清理掉eden和用过的survivor。SunHotspot虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有10%的内存是“浪费”的。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有10%以内的对象存活,当survivor空间不够用时,需要依赖其他内存(譬如老年代)进行分配担保(Handle Promotion)。
复制收集算法在对象存活率高的时候,效率有所下降。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。因此人们提出另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然一样,但后续步骤不是进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这端边界以外的内存。
当前商业虚拟机的垃圾收集都是采用“分代收集”(Generational Collecting)算法,这种算法并没有什么新的思想出现,只是根据对象不同的存活周期将内存划分为几块。一般是把Java堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,譬如新生代每次GC都有大批对象死去,只有少量存活,那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集。