4.JVM的内存模型
1.JVM内存模型简介
JVM定义了不同运行时数据区,他们是用来执行应用程序的。某些区域随着JVM启动及销毁,另外一
些区域的数据是线程性独立的,随着线程创建和销毁。jvm内存模型总体架构图如下:(摘自oracle
官方网站)
JVM在执行Java程序时,会把它管理的内存划分为若干个的区域,每个区域都有自己的用途和创建
销毁时间。如下图所示,可以分为两大部分,线程私有区和共享区。下图是根据自己理解画的一个
JVM内存模型架构图:
JVM内存分为线程私有区和线程共享区
线程私有区
1、程序计数器
当同时进行的线程数超过CPU数或其内核数时,就要通过时间片轮询分派CPU的时间资源,不免发
生线程切换。这时,每个线程就需要一个属于自己的计数器来记录下一条要运行的指令。如果执行
的是JAVA方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为
空。
2、虚拟机栈
线程私有的,与线程在同一时间创建。管理JAVA方法执行的内存模型。每个方法执行时都会创建一
个桢栈来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决
定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机
栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则
抛出stackOverflflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出
OutofMemoryError。 使用jclasslib工具可以查看class类文件的结构。下图为栈帧结构图:
3、本地方法栈
与虚拟机栈作用相似。但它不是为Java方法服务的,而是本地方法(C语言)。由于规范对这块没有
强制要求,不同虚拟机实现方法不同。
线程共享区
1、方法区
线程共享的,用于存放被虚拟机加载的类的元数据信息,如常量、静态变量和即时编译器编译后的
代码。若要分代,算是永久代(老年代),以前类大多“static”的,很少被卸载或收集,现回收废弃
常量和无用的类。其中运行时常量池存放编译生成的各种常量。(如果hotspot虚拟机确定一个类
的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装
载该类的ClassLoader被回收)
2、堆
存放对象实例和数组,是垃圾回收的主要区域,分为新生代和老年代。刚创建的对象在新生代的
Eden区中,经过GC后进入新生代的S0区中,再经过GC进入新生代的S1区中,15次GC后仍存在就
进入老年代。这是按照一种回收机制进行划分的,不是固定的。若堆的空间不够实例分配,则
OutOfMemoryError。
5、堆和栈的区别
栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;堆
是存储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在
区域不连续,会有碎片。
1、功能不同
栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变
量,还是类变量,它们指向的对象都存储在堆内存中。
2、共享性不同
栈内存是线程私有的。 堆内存是所有线程共有的。
3、异常错误不同
如果栈内存或者堆内存不足都会抛出异常。 栈空间不足:java.lang.StackOverFlowError。 堆空间
不足:java.lang.OutOfMemoryError。
4、空间大小
栈的空间大小远远小于堆的。
6、 什么时候会触发FullGC
除直接调用System.gc外,触发Full GC执行的情况有如下四种。
1. 旧生代空间不足 旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不
足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起
的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不
要创建过大的对象及数组。
2. Permanet Generation空间满 PermanetGeneration中存放的为一些class的信息等,当系统中
要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采
用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信
息: java.lang.OutOfMemoryError: PermGen space 为避免Perm Gen占满造成Full GC现象,可
采用的方法为增大Perm Gen空间或转为使用CMS GC。
3. CMS GC时出现promotion failed和concurrent mode failure 对于采用CMS进行旧生代GC的
程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当
这两种状况出现时可能会触发Full GC。 promotionfailed是在进行Minor GC时,survivor space放
不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行
CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。 应对措施为:增大
survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由
于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-
XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
4. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间 这是一个较为复杂的触发
情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor
GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩
余空间,那么就直接触发Full GC。 例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生
代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,
则执行Full GC。 当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上
面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触
发对旧生代的回收。 除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默
认情况下会一小时执行一次Full GC。可通过在启动时通过- java
Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+
DisableExplicitGC来禁止RMI调用System.gc。
7、Java内存结构
方法区和对是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有
的内存区域。
Java堆(Heap)
,是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内
存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实
例都在这里分配内存。
方法区(Method Area),方法区(Method Area)与Java堆一样,是各个线程共享的内存区
域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数
据。
程序计数器(Program Counter Register),程序计数器(Program Counter Register)是一块
较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是
线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方
法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态
链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机
栈中从入栈到出栈的过程。
本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发
挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服
务,而本地方法栈则是为虚拟机使用到的Native方法服务。
8、垃圾收集算法
GC最基础的算法有三种:
标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般
都采用分代收集算法。
标记 -清除算法,
“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清
除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
复制算法,
**“复制”(Copying)的收集算法,**它将可用内存按容量划分为大小相等的两块,每次
只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后
再把已使用过的内存空间一次清理掉。
标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行
清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,
这样就可以根据各个年代的特点采用最适当的收集算法。