首页 > 代码库 > java学习-----jvm的内存分配及运行机制
java学习-----jvm的内存分配及运行机制
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:JavaheapspaceDumpingheaptojava_pid3404.hprof...Heapdumpfilecreated[22045981bytesin0.663secs]
垃圾收集GC(GarbageCollection,下文简称GC):
GC的历史远远比java来的久,在1960年诞生于MIT的Lisp(是一门真正的使用内存冬天分配和垃圾回收集)的语言。当Lisp在胚胎时期,人们在GC需要做的3件事情:
- 哪些内存需要回收
- 什么时候需要回收
- 怎么样回收
总 结下:其中程序计数器、VM栈、本地方法栈随线程而生,随线程而灭;栈中的帧随着方法的进入退出,有条不紊的进行的出栈和入栈操作;每一个帧中分配多少内 存,基本上是在Class文件生成时就已知的(可能会由JIT动态晚期编译进行一些优化,但大体上可以认为是编译期可知
的),因此这几个区域的内存的分配和回收具备很高的确定性,因此在这几个区域不需要过多考虑回收问题。而java堆和方法区(包括运行时常量池)则不一样,我们必须等到程序实际运行期间才能知道会创建那些对象,这部分内存的回收和分配是动态的。