[JVM系列]二、一文彻底搞懂 JVM运行时数据区 和 JVM内存结构

1,205 阅读12分钟

JVM运行时数据区

Run-Time Data Areas

在上一篇文章中类加载过程中,装载阶段的第2,3步可以发现有运行时数据方法区等名词 装载阶段的第2步将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 第3步在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口 说白了就是类文件被类装载器装载进来之后,类中的内容(比如变量,常量,方法,对象等这些数据得 要有个去处,也就是要存储起来,存储的位置肯定是在JVM中有对应的空间)

官网概述

官网:docs.oracle.com/javase/spec…

The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.

图解

详解

1. 方法区(Method Area)

  1. 方法区是各个线程共享的内存区域,在虚拟机启动时创建

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is created on virtual machine start-up.

  1. 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来

  2. 用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据

  3. 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

此时回看装载阶段的第2步,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 如果这时候把从Class文件到装载的第(1)和(2)步合并起来理解的话,可以画个图

值得说明的:

JVM运行时数据区是一种规范,真正的实现: 在JDK 8中是Metaspace,在JDK6或7中是Perm Space

2. 堆(Heap)

  1. Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享

  2. Java对象实例以及数组都在堆上分配。

  3. 当堆内存无法满足分配时,将抛出OutOfMemoryError异常

此时回看装载阶段的第3步,在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法 区中这些数据的访问入口。

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的

3. 虚拟机栈(Java Virtual Machine Stacks)

经过上面的分析,类加载机制的装载过程已经完成,后续的链接,初始化也会相应的生效。后续做啥呢?肯定是Use使用咯。那怎样才能被使用到?换句话说里面内容怎样才能被执行?比如通过主函数main调 用其他方法,这种方式实际上是main线程执行之后调用的方法,即要想使用里面的各种内容,得 要以线程为单位,执行相应的方法才行。那一个线程执行的状态如何维护?一个线程可以执行多 少个方法?这样的关系怎么维护呢?

  1. 虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建

  2. 每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。

  3. 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

图解栈和栈帧:

void a(){ 
    b();
}
void b(){ 
    c();
} 
void c(){
}

4. 栈帧(Frames)

官网参考:docs.oracle.com/javase/spec…

  1. 栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。

  2. 每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。

局部变量表

方法中定义的局部变量以及方法的参数存放在这张表中, 就是一个表格,是以数组的方式存储的。

局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使 用。

操作数栈

以压栈和出栈的方式存储操作数

和局部变量表组合使用,用于方法中数据的运算过程支持

动态链接(引用)

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调 用过程中的动态连接(Dynamic Linking)。

Dynamic linking translates these symbolic method references into concrete method references, loading classes as necessary to resolve as-yet-undefined symbols, and translates variable accesses into appropriate offsets in storage structures associated with the run-time location of these variables.

就是链接--->解析(把符号引用变成直接引用)

在类加载过程中的解析阶段已经做过了,为什么在方法调用过程中又做一次呢?

类加载是只能把确定类型的符号引用变成直接引用,是一个静态的解析,还有一些不确定的没转化(多态)。

比如Person().eat()方法在加载的时候是不确定的,要等子类实现了之后才能继续解析

方法返回地址

当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇到异常,并且这个异常没有在方法体内得到处理。

5. 程序计数器(The pc Register)

我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据 CPU调度来的。 假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获 得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到 的位置。

  1. 如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址。
  2. 如果正在执行的是Native方法,则这个计数器为空。

6. 本地方法栈

本地方法栈和虚拟机栈非常相似。不同在于:

如果当前线程执行的方法是Native类型的,我们也压入java虚拟机栈吗?(比如Object里面native修饰(c语言)的hashCode()方法。)

答:不是的,这些方法会在本地方法栈中执行。

那如果在Java方法执行的时候调用native的方法呢?

答:创建一个动态链接,压入隔壁的本地方法栈。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowErrorOutOfMemoryError异常。

注:虚拟机栈,本地方法栈,程序计数器是每个进程有一套的,是线程安全的

常见情况分析

1. 虚拟机栈指向堆

如果在栈帧中有一个变量,类型为引用类型,比如Object obj=new Object(),这时候就是典型的栈中元素指向堆中的对象。

2. 方法区指向堆

 private static Object obj=new Object(); //类变量

3. 堆指向方法区

注意,方法区中会包含类的信息,堆中会有对象,那怎么知道对象是哪个类创建的呢?怎么记录?这就需要了解一个Java对象的具体信息咯。

Java对象内存模型:

一个Java对象在内存中包括3个部分:对象头实例数据对齐填充

对象头中的Class Pointer,指向方法区,方法区会存储类的信息(类的源数据)。

JVM内存模型

上面讨论的JVM运行时数据区,描述的程序运行起来的状态,是逻辑上存在的概念,物理上不存在,也就是可以理解为运行时数据区仅仅是JVM规范。

上面对运行时数据区描述了很多,其实重点存储数据的是堆和方法区(非堆),所以内存的设计也着重从 这两方面展开(注意这两块区域都是线程共享的)。

落到真实的物理内存,也就是内存条里面是,应该怎么划分呢?

可以这样理解,JVM运行时数据区是一种规范,而JVM内存模式是对该规范的实现 (此处只考虑方法区和堆的内存模型。)

图解

  • 一块是非堆区,一块是堆区

  • 堆区分为两大块,一个是Old区,一个是Young区

  • Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区

  • S0和S1一样大,也可以叫From和To

Young区和Old区

  • 也叫新生代和老年代

  • Young区和Old区将堆内存划分为两个部分。

  • 一个对象内部,会维护对象年龄的大小,也就是在对象头中的mark word中记录一个age值,记录经过几次gc后还没被回收。

  • 刚创建的对象age=0,被放在Young区(若对象太大,在Young区放不下,则会直接放到Old区)如果经过一次gc,还没被回收掉,那么age就+1。

  • 默认情况下,临界区是15次,超过15次,也就是16次gc还没被回收时,就到了Old区老年代。

  • 新生代和老年代一般来说 是2:1

若只有Young区和Old区会出现的问题

在新生代中创建了许多person对象

GC后,部分person对象被回收,部分留下,产生空间碎片。

这时,如果需要完整的三个单元的对象,因为空间碎片化的问题,没得为其分配内存,又会导致一次GC,有没有办法解决?

Eden区,Surivor区

为了解决这个问题,就需要Eden区,和Surivor区

将Young区划分为Eden区Surivor区

对象创建后分配在Eden区,如果在Eden区没有空间分配后,把Eden区内容转到Surivor区,这样Eden就可以随时准备接受新对象。

但是这样Surivor区也可能出现空间碎片,使得空间利用率低。

那就搞两个Surivor区,S0,S1。将S区分为S0和S1也可称为To Survivor和From Survivor两个部分,执行垃圾回收的时候Eden区域不能被回收的对象被放入到空的Survivor(也就是To Survivor,同时Eden区域的内存会在垃圾回收的过程中全部释放),另一个Survivor(即From Survivor)里不能被回收的对象也会被放入这个survivor(即To Survivor),然后To Survivor 和 From Survivor的标记会互换,始终保证一个survivor是空的。

利用这两个区来回交换,来保证某一块S区的空间永远都是空的,另外一块会被使用,浪费Surivor区的一半空间,追求内存的连续性。

在新生代中,Eden:S0:S1 的比例一般是 8:1:1

一个对象创建的过程

Minor GC:新生代垃圾回收

Major GC:老年代 垃圾回收

Full GC:新生代+老年代垃圾回收

常见问题

什么需要Survivor区?只有Eden不行吗?

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。

这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了 Full GC)。 老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。 执行时间长有什么坏处?频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。

可能你会说,那就对老年代的空间进行增加或者较少咯。 假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一 旦发生Full GC,执行所需要的时间更长。 假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。

所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

新生代中Eden:S1:S2为什么是8:1:1?

新生代中的可用内存:复制算法用来担保的内存为9:1

可用内存中Eden:S1区为8:1 即新生代中Eden:S1:S2 = 8:1:1

现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象大概98%是 “朝生夕死”的


JVM系列文章:

[JVM系列]三、一文搞懂JVM垃圾回收

[JVM系列]二、一文彻底搞懂 JVM运行时数据区 和 JVM内存结构

[JVM系列]一、源码->类文件->JVM过程详解(类文件解读/类加载机制/类加载器)