学习JVM的基础知识点2

62 阅读9分钟

4.JVM的内存模型

1.JVM内存模型简介

JVM定义了不同运行时数据区,他们是用来执行应用程序的。某些区域随着JVM启动及销毁,另外一

些区域的数据是线程性独立的,随着线程创建和销毁。jvm内存模型总体架构图如下:(摘自oracle

官方网站)

image.png

JVM在执行Java程序时,会把它管理的内存划分为若干个的区域,每个区域都有自己的用途和创建

销毁时间。如下图所示,可以分为两大部分,线程私有区和共享区。下图是根据自己理解画的一个

JVM内存模型架构图:

image.png

JVM内存分为线程私有区和线程共享区

线程私有区

1、程序计数器

当同时进行的线程数超过CPU数或其内核数时,就要通过时间片轮询分派CPU的时间资源,不免发

生线程切换。这时,每个线程就需要一个属于自己的计数器来记录下一条要运行的指令。如果执行

的是JAVA方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为

空。

2、虚拟机栈

线程私有的,与线程在同一时间创建。管理JAVA方法执行的内存模型。每个方法执行时都会创建一

个桢栈来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决

定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机

栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则

抛出stackOverflflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出

OutofMemoryError。 使用jclasslib工具可以查看class类文件的结构。下图为栈帧结构图:

image.png

3、本地方法栈

与虚拟机栈作用相似。但它不是为Java方法服务的,而是本地方法(C语言)。由于规范对这块没有

强制要求,不同虚拟机实现方法不同。

线程共享区

1、方法区

线程共享的,用于存放被虚拟机加载的类的元数据信息,如常量、静态变量和即时编译器编译后的

代码。若要分代,算是永久代(老年代),以前类大多“static”的,很少被卸载或收集,现回收废弃

常量和无用的类。其中运行时常量池存放编译生成的各种常量。(如果hotspot虚拟机确定一个类

的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装

载该类的ClassLoader被回收)

2、堆

存放对象实例和数组,是垃圾回收的主要区域,分为新生代和老年代。刚创建的对象在新生代的

Eden区中,经过GC后进入新生代的S0区中,再经过GC进入新生代的S1区中,15次GC后仍存在就

进入老年代。这是按照一种回收机制进行划分的,不是固定的。若堆的空间不够实例分配,则

OutOfMemoryError。

image.png

image.png

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内存结构

image.png

方法区和对是所有线程共享的内存区域;而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堆分为新生代和老年代,

这样就可以根据各个年代的特点采用最适当的收集算法。