JVM内存详解

  • Post author:
  • Post category:其他


java虚拟机Java Virtual Machine简称jvm。我们工作中不直接于内存打交道,但是内存模型是成为架构师必备的基本知识,所以让我们来一起学习一下java的内存模型吧。

java内存模型一共分为五个部分,1.虚拟机栈。2.本地方法栈。3.程序计数器。4.方法区。5.堆。前3个是线程私有的,后2个是线程共享的。画出来就是下面这样的

model

下面就逐个的介绍一下每一个区域的作用以及特点。

1 虚拟机栈(VM Stack)

每一个线程都有自己的虚拟机栈,这是线程私有的,当线程执行方法的时候,就会在虚拟机栈中创建一个栈帧。每个栈帧里都有局部变量表,操作数栈,动态链接,返回地址和附加信息。

1.1 局部变量表

局部标量表 是一组变量值的存储空间,用于存放方法参数和局部变量。在Class 文件的方法表的Code属性的max_locals指定了该方法所需局部变量表的最大容量(在编译的时候就已经确定了)。


变量槽 (Variable Slot)

是局部变量表的最小单位,java虚拟机中并没有指明一个Slot所需要占用的内存空间大小。一个 Slot 可以存放 boolean、byte、char、short、int、float、reference 和 returnAddress 8种类型。其中 reference 表示对一个对象实例的引用,通过它可以得到对象在Java 堆中存放的起始地址的索引和该数据所属数据类型在方法区的类型信息。returnAddress 则指向了一条字节码指令的地址。 在32位虚拟机中,虚拟机之中使用了64位的内存空间去实现一个Slot,但假如在64位的java虚拟机之中使用了64位的内存空间去实现一个Slot,java虚拟机仍然要使用对齐或者补白的手段,让Slot在外观上看出来了与32位的java虚拟机之中是一致的。

为了节省栈帧空间,局部变量表中的 Slot 是可以重用的。

1.2 操作数栈

操作数栈(Operand Stack)也常称为操作栈。在Class 文件的Code 属性的 max_stacks 指定了执行过程中最大的栈深度。

和局部变量表一样,操作数栈也是以一个以字长为单位的数组。但是和局部变量表不同的是,局部变量表通过索引来访问,而操作数栈通过标准的栈操作—压栈和出栈—来访问的。

在HotSpot中,除了PC寄存器之外,再也没有包含其他任何的寄存器,并且之前曾经提及过,HotSpot中任何的操作都需要经过入栈和出栈来完成,Java 虚拟机的解释执行引擎称为”基于栈的执行引擎“,这里的栈就是指操作数栈。

举一个例子。比如下面这段代码

    public static void main(String[] args)  {

        int a = 2;
        int b = 4;

        int c = a+b;
   }

通过javap -c反编译出的字节码如下

  public static void main(java.lang.String[]);
    Code:
       0: iconst_2
       1: istore_1
       2: iconst_4
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: istore_3
       8: return
}

通过查jvm指令集可以知道上面9个步骤对应着(指令集请参考

JVM指令集



0 将int型(2)推送至栈顶

1 将栈顶int型数值存入第二个本地变量

2 将int型(4)推送至栈顶

3 将栈顶int型数值存入第三个本地变量

4 将第二个int型本地变量推送至栈顶

5 将第三个int型本地变量推送至栈顶

6 将栈顶两int型数值相加并将结果压入栈顶

7 将栈顶int型数值存入第四个本地变量

8 从当前方法返回void

1.3 动态链接

在Class文件中的常量持中存有大量的符号引用。字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分在类的加载阶段(解析)或第一次使用的时候就转化为了直接引用(指向数据所存地址的指针或句柄等),这种转化称为静态链接。而相反的,另一部分在运行期间转化为直接引用,就称为动态链接。

JVM编译一个java程序的时候,会得到程序中每个类(或者接口)的class文件,这些CLASS文件通过接口(harbor)符号相互关联,在JVM运行时,动态的将这些接口符号组织成链接CLASS的网。

CLASS包含一个很重要的部分,常量池,所有的接口符号就是保存在这里的。每个CLASS都有一个常量池,每个被JVM加载的CLASS有个自己的内部常量池,或者说运行时常量池。当CLASS刚刚被加载的时候,运行时常量池存储的就是刚刚说的接口符号(harbor),当程序运行到一个时间点,需要调用某个类时,JVM将解析运行时常量池的接口符号,根据符号引用查找到实体(每个CLASS在JVM加载后都会保存在方法区中的某个位置,在运行前不知道某个具体的CLASS将会被加载到内存中的具体地址),再把符号引用替换成直接引用的过程。又因为符号都保存在常量池中,这一步骤也称为常量池解析。

与那些在编译时进行链接的语言不同,Java类型的加载和链接过程都是在运行的时候进行的,这样虽然在类加载的时候稍微增加一些性能开销,但是却能为Java应用程序提供高度的灵活性。

1.4 返回地址

当一个方法开始执行以后,只有两种方法可以退出当前方法:

(1)当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。

(2)当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。

无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。

当方法返回时,可能进行3个操作:

1)恢复上层方法的局部变量表和操作数栈

2)把返回值压入调用者调用者栈帧的操作数栈

3)调整 PC 计数器的值以指向方法调用指令后面的一条指令

1.5 附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与高度相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。

2 本地方法栈

该区域与虚拟机栈所发挥的作用非常相似,只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的本地操作系统(Native)方法服务。

本地方法是非java语言实现,比如C语言实现。而我们需要在java程序中使用这个C语言的方法的功能。

我们知道java是高级编程语言,当对一些底层的如操作系统或某些硬件交换信息时,我们使用java来编程实现起来不容易,再者使用java来编程效率也很低下。这就不得不需要调用本地方法来解决这一问题。

当一个线程调用一个本地方法时,本地方法又回调虚拟机中的另一个Java方法。这幅图展示了java虚拟机内部线程运行的全景图。一个线程可能在整个生命周期中都执行Java方法,操作他的Java栈;或者他可能毫无障碍地在Java栈和本地方法栈之间跳转。

该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。图中的本地方法栈显示为 一个连续的内存空间。假设这是一个C语言栈,期间有两个C函数,他们都以包围在虚线中的灰色块表示。第一个C函数被第二个Java方法当做本地方法调用, 而这个C函数又调用了第二个C函数。之后第二个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过 本地方法接口回调了一个Java方法(第三个Java方法)。最终这个Java方法又调用了一个Java方法。栈帧的结构就如下图所示

native

3 程序计数器

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

2.如果正在执行的是Native 方法,则这个技术器值为空(Undefined)

3.此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

  public static void main(java.lang.String[]);
    Code:
       0: iconst_2
       1: istore_1
       2: iconst_4
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: istore_3
       8: return
}

这里再次用到上面的反编译code。大家看下在每个指令集前面都有一个数字,那个数字就是字节码指令的偏移地址,程序计数器记录的就是前面这个数据。用来记录当前程序执行到哪一行了。

4 方法区

方法区是系统分配的一个内存逻辑区域,是一块所有线程共享的内存区域,用来存储类型信息(类型信息可以理解为类的描述信息(类的全限定名,访问修饰符,字段,方法等)),方法区的大小决定了系统可以包含多少个类,如果系统类太多,方法区内存不够会导致方法区溢出,虚拟机同样会抛出内存溢出信息。

4.1 特点:

1.方法区是线程安全的,由于所有的线程都共享方法区,所以方法区里的数据访问必须被设计成线程安全的。例如,假如同时有两个线程都企图访问方法区中的同一个类,而这个类还没有被装入jvm,那么只允许一个线程去装在它,而其他线程必须等待。

2.方法区的大小不必是固定的,jvm可根据应用需要动态调整,同时,方法区也不一定是连续的,方法区可以在一个堆(甚至是jvm自己的堆)中自由分配。

3.方法区也可被垃圾收集,当某个类不在被使用时,jvm将卸载这个类,进行垃圾收集。

4.2 方法区存放内容:

1.类的全限定名(类的全路径名)。

2.类的直接超类的权全限定名(如果这个类是Object,则它没有超类)。

3.类的类型(类或接口)。

4.类的访问修饰符,public,abstract,final等。

5.类的直接接口全限定名的有序列表。

6.常量池(字段,方法信息,静态变量,类型引用(class))等

4.3 jdk8与jdk6和jdk7的区别


在jdk1.6和1.7 中方法区就是永久代可以用


-XX:PermSize来设置初始化方法区的大小

-XX:MaxPermSize来设置方法区的最大大小


在jdk1.8 中方法区被称为元数据区


-XX:MetaspaceSize初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize最大空间,默认是没有限制的。

5 堆

Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。这个区域是用来存放对象实例的,几乎所有对象实例都会在这里分配内存。堆是Java垃圾收集器管理的主要区域(GC堆),垃圾收集器实现了对象的自动销毁。

为了垃圾收集的方便,Java堆可以细分为:新生代和老年代,新生代又可以分为Eden空间,From Survivor空间,To Survivor空间等。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。

6 小结

  1. 虚拟机栈,本地方法栈和程序计数器是线程私有的。方法区和堆是线程共享的
  2. 堆内存,方法区,本地方法栈和虚拟机栈都会发生内存溢出。(直接内存也会发生内存溢出)
  3. 方法区管理class文件、静态对象、属性等
  4. 堆可以分为新生代和老年代,新生代可以分为eden区,From Survivor区和To Survivor区



版权声明:本文为niunai112原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。