JVM学习(一)——内存结构

709 阅读16分钟

其他更多java基础文章:
java基础学习(目录)


学习资料
Java虚拟机规范
方法区的Class信息,又称为永久代,是否属于Java堆?
JVM 内部原理(一)— 概述

运行时数据区域(Run-Time Data Areas)

Java虚拟机定义了在程序执行期间使用的各种运行时数据区域。其中一些数据区域是在Java虚拟机启动时创建的,仅在Java虚拟机退出时销毁。其他数据区域是每个线程。线程数据区域是在线程退出时创建和销毁线程时创建的。
JVM所管理的几个运行时数据区域:方法区、虚拟机栈、本地方法栈、堆、程序计数器,其中方法区和堆是由线程共享的数据区,其他几个是线程隔离的数据区。程序计数器,虚拟机栈,本地方法栈,随线程而生,线程亡而亡

线程独享

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享

  • 方法区

程序计数器/PC计数器(The pc Register)

程序计数器是一块较小的内存,他可以看做是当前线程所执行的行号指示器。每个Java虚拟机线程都有自己的程序计数器,通常程序计数器会在执行指令结束后增加,因此它需要保持下一将要执行指令的地址。 字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码的指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器则为空。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemotyError情况的区域

虚拟机栈(Java Virtual Machine Stacks)

每个线程都有自己的栈(stack),栈内以帧(frame)的形式保持着线程内执行的每个方法。栈是一个后进先出(LIFO)的数据结构,所以当前执行的方法在栈顶部。每次方法调用时,都会创建新的帧并且压入栈的顶部。当方法正常返回或抛出未捕获的异常时,帧或从栈顶移除。除了压入和移除帧对象的操作,栈没有其他直接的操作,因此帧对象分配在堆中,内存并不要求连续。对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。
在Java 虚拟机规范中,对虚拟机栈规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError 异常。

帧(Frame)

每次方法调用时,新的帧都会创建并被压入栈顶。当方法正常返回或抛出未捕获异常时,帧会从做退栈操作。详细的异常处理参加后面 异常表(Exception Table)部分。

每个帧都包括

  • 局部变量(Local variable)
  • 返回地址(Return value)
  • 操作数栈(Operand stack)
  • 动态链接(Dynamic Linking)
局部变量表(Local variable)

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。
Java虚拟机使用局部变量在方法调用上传递参数。在类方法调用中,任何参数都在从局部变量0开始的连续局部变量中传递。在实例方法调用中,局部变量0始终用于传递对调用实例方法的对象的引用(Java编程语言里的this)。随后,任何参数都在从局部变量1开始的连续局部变量中传递。

局部变量可以是:

  • boolean操作数栈
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

所有的类型都在本地变量数组中占一个槽,而 longdouble 会占两个连续的槽,因为它们有双倍宽度(64-bit 而不是 32-bit)。reference类型虚拟机规范没有明确说明它的长度,但一般来说,虚拟机实现至少都应当能从此引用中直接或者间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。

操作数栈(Operand stack)

每个帧包含一个后进先出(LIFO)堆栈,称为其操作数堆栈。帧的操作数堆栈的最大深度在编译时确定,并与帧相关的方法的代码一起提供。
虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:

begin  
iload_0    // push the int in local variable 0 ontothe stack  
iload_1    //push the int in local variable 1 onto the stack  
iadd       // pop two ints, add them, push result  
istore_2   // pop int, store into local variable 2  
end  

在这个字节码序列里,前两个指令iload_0iload_1将存储在局部变量中索引为0和1的整数压入操作数栈中,其后iadd指令从操作数栈中弹出那两个整数相加,再将结果压入操作数栈。第四条指令istore_2则从操作数栈中弹出结果,并把它存储到局部变量区索引为2的位置。下图详细表述了这个过程中局部变量和操作数栈的状态变化,图中没有使用的局部变量区和操作数栈区域以空白表示。

返回地址

方法的返回分为两种情况,一种是正常退出,退出后会根据方法的定义来决定是否要传返回值给上层的调用者,一种是异常导致的方法结束,这种情况是不会传返回值给上层的调用方法。

不过无论是那种方式的方法结束,在退出当前方法时都会跳转到当前方法被调用的位置,如果方法是正常退出的,则调用者的PC计数器的值就可以作为返回地址,如果是因为异常退出的,则是需要通过异常处理表来确定。

方法的的一次调用就对应着栈帧在虚拟机栈中的一次入栈出栈操作,因此方法退出时可能做的事情包括:恢复上层方法的局部变量表以及操作数栈,如果有返回值的话,就把返回值压入到调用者栈帧的操作数栈中,还会把PC计数器的值调整为方法调用入口的下一条指令。

动态链接(Dynamic Linking)

虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用可以看成是每个方法的间接引用。如果代表栈帧A的方法想调用代表栈帧B的方法,那么这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,但是因为符号引用并不是直接指向代表B方法的内存位置,所以在调用之前还必须要将符号引用转换为直接引用,然后通过直接引用才可以访问到真正的方法。

如果符号引用是在类加载阶段或者第一次使用的时候转化为直接应用,那么这种转换成为静态解析,如果是在运行期间转换为直接引用,那么这种转换就成为动态连接。

本地方法栈(Native Method Stack)

本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

堆(Heap)

堆是运行时分配类实例和数组内存的地方。数组和对象是不能存在栈里的,因为栈帧(frame)不是被设计用作此目的,一旦栈帧创建了,它的大小不可更改。帧只用来存储指向对中对象或数组的引用。与帧内本地变量数组里基本变量和引用不同,对象总是存储在堆内的,所以在方法结束前,它们不会被移除。而且,对象只能被垃圾回收器移除。

为了支持垃圾回收的机制,堆通常被分为三部分:

  • 新生代(Young Generation)
    • 通常分为 新生者(Eden)和 幸存者(Survivor)
  • 老年代(Old Generation/Tenured Generation)
  • 永久代(Permanent Generation)(JDK8中已移除

内存管理(Memory Management)

对象和数组不会被显式的移除,而是会被 GC 自动回收。通常的顺序是这样:

  1. 新的对象和数组被创建在新生代区
  2. 小的 GC 会发生在新生代,存活的对象会从 新生区(Eden)移到 幸存区(Survivor)
  3. 大的 GC ,通常会导致应用程序线程暂停,对象移动会发生在不同代之间。仍然存活的对象会从新生代被移动到老年代。
  4. 永久代的收集时刻都会在对老年代收集时发生。任何一代内存使用满了,会在两代同时发生收集。

关于新生代、老年代、GC的详细讲解请关注JVM学习后续文章

方法区(Method Area)

本文重点介绍方法区。因为jdk6,7,8中分别对方法区的实现永久代做了修改。

方法区介绍

JVM虚拟机规范中:

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization. The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous. A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.

在 Java 虚拟机中,方法区( Method Area) 是可供各条线程共享的运行时内存区域。方法区与传统语言中的编译代码储存区( Storage Area Of Compiled Code)或者操作系统进程的正文段( Text egment)的作用非常类似,它存储了每一个类的结构信息,例如运行时常量池( Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法

方法区在虚拟机启动的时候被创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集。这个版本的 Java 虚拟机规范也不限定实现方法区的内存位置和编译代码的管理策略。方法区的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。方法区在实际内存空间中可以是不连续的。

Java 虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段,对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段

如果方法区的内存空间不能满足内存分配请求,那 Java 虚拟机将抛出一个OutOfMemoryError异常。

总之,就是用来存储类的结构信息。它有一个别名叫做Non-Heap(非堆)。

永久代

永久代是HotSpot中方法区的实现。
平时,说到永久代(PermGen space)的时候往往将其和方法区不加区别。这么理解在一定角度也说的过去。 因为,JVM虚拟机规范只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 同时,大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。

虽然可以牵强的解释这种将方法区和永久带等同对待观点。但最终方法区和永久带还是不同的。一个是标准一个是实现。

JDK1.7之前的永久代

java7之前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变。这里有个在面试中经常问的问题,就是String.intern()方法,详情可以阅读之前我写的一篇文章java基础:String — 字符串常量池与intern(二)

JDK1.7的永久代

Highlights of Technology Changes in Java SE 7中,我们可以看到

In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.

在JDK 7中,interned strings不再分配在Java堆的永久代中,而是和应用程序创建的其他对象一样一起分配在Java堆的主要部分(称为年轻代和年老代)。此更改将导致主Java堆中的数据更多,而永久代中的数据更少,因此可能需要调整堆大小。由于这种变化,大多数应用程序在堆使用方面只会看到相对较小的差异,但是加载许多类或大量使用String.intern()方法的大型应用程序会看到更显著的差异。

以及方法区的Class信息,又称为永久代,是否属于Java堆? 中,R大的讲解:

Oracle JDK7 / OpenJDK 7的HotSpot VM是把Symbol的存储从PermGen移动到了native memory,并且把静态变量从instanceKlass末尾(位于PermGen内)移动到了java.lang.Class对象的末尾(位于普通Java heap内)。
“常量池”如果说的是runtime constant pool,这个还是在PermGen里;
“常量池”如果说的是SymbolTable / StringTable,这俩table自身原本就一直在native memory里,是它们所引用的东西在哪里更有意思。
上面说了,7是把SymbolTable引用的Symbol移动到了native memory,而StringTable引用的java.lang.String实例则从PermGen移动到了普通Java heap。

R大说的runtime constant pool即为运行常量池,SymbolTable 即为标识符表,StringTable即字符串常量池。从而我们可以得知,即 java7中,存储在永久代的部分数据就已经转移到Java Heap或者Native memory。但永久代仍存在于JDK 1.7中,并没有完全移除。

jdk1.8的元空间及永久代

JEP 122: Remove the Permanent Generation

Move part of the contents of the permanent generation in Hotspot to the Java heap and the remainder to native memory.
Hotspot's representation of Java classes (referred to here as class meta-data) is currently stored in a portion of the Java heap referred to as the permanent generation. In addition, interned Strings and class static variables are stored in the permanent generation. The permanent generation is managed by Hotspot and must have enough room for all the class meta-data, interned Strings and class statics used by the Java application.
The proposed implementation will allocate class meta-data in native memory and move interned Strings and class statics to the Java heap. Hotspot will explicitly allocate and free the native memory for the class meta-data. Allocation of new class meta-data would be limited by the amount of available native memory rather than fixed by the value of -XX:MaxPermSize, whether the default or specified on the command line.

  1. 移除了永久代(PermGen),替换为元空间(Metaspace);
  2. 永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
  3. 永久代中的 interned Strings 和 class static variables 转移到了 Java heap;
  4. 永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)

java8中,取消永久代,方法区存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中。

为什么移除永久代?
  1. 字符串存在永久代中,容易出现性能问题和内存溢出。
  2. 永久代大小不容易确定,PermSize指定太小容易造成永久代OOM
  3. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  4. Oracle 可能会将HotSpot 与 JRockit 合二为一。

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。 NIO类是一种基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个储存在Java堆中的DirectByteBuffer对象作为这块直接内存的引用进行操作,这样避免了java堆和navie堆中来回复制数据