JVM运行时数据区详解 - TongtongLan/Java GitHub Wiki

JVM运行时数据区(JVM Runtime Area)其实就是指JVM在运行期间,其对计算机内存空间的划分和分配。本文将通过以下几个话题来讨论JVM运行时数据区。

  • Topic 1. JVM运行时数据区里有什么?

  • Topic 2.栈帧是什么?栈帧里有什么?

  • Topic 3. 方法区是什么?方法区里有什么?

运行时数据区

JVM运行时数据区里有什么?

线程共享区

方法区(Method Area,别名Non-Heap)

方法区

用于存储已被虚拟机加载的类信息、常量、静态变量、**即时编译器编译后的代码?**等数据。JVM 规范中对此部分的限制非常宽松,除了和java堆一样不需要连续的内存和可选择固定大小或则会可扩展外,还可以选择不实现垃圾收集。这部分内存回收目标主要是针对常量池的回收和对类型的卸载,虽然回收效果一般,但确实必要。

  • 类变量

类变量( Class Variables 译者:就是类的静态变量,它只与类相关,所以称为类变量 )

  类变量被类的所有实例共享,即使没有类实例时你也可以访问它。这些变量只与类相关,所以在方法区中,它们成为类数据在逻辑上的一部分。在jvm使用一个类之前,它必须在方法区中为每个non-final类变量分配空间。

  • 常量

  常量(被声明为final的类变量)的处理方法则不同,每个常量都会在常量池中有一个拷贝。non-final类变量被存储在声明它的类信息内,而final类被存储在所有使用它的类信息内。

  • 方法信息

jvm必须保存所有方法的以下信息,同样域信息一样包括声明顺序

  方法名

  方法的返回类型(或 void)

  方法参数的数量和类型(有序的)

  方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的一个子集)除了abstract和native方法外,其他方法还有保存方法的字节码(bytecodes)操作数栈和方法栈帧的局部变量区的大小

  异常表

  • 字段信息(所有成员变量)

jvm必须在方法区中保存类型的所有域的相关信息以及域的声明顺序,

  域的相关信息包括:

  域名

  域类型

  域修饰符(public, private, protected,static,final volatile, transient的某个子集)

  • 类加载器的引用

  jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。

  jvm在动态链接的时候需要这个信息。当解析一个类型到另一个类型的引用的时候,jvm需要保证这两个类型的类加载器是相同的。这对jvm区分名字空间的方式是至关重要的。

  • Class类的引用

  jvm为每个加载的类型(译者:包括类和接口)都创建一个java.lang.Class的实例。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据联系起来。

  你可以通过Class类的一个静态方法得到这个实例的引用// A method declared in class java.lang.Class:

  public static Class forName(String className);

  假如你调用forName(“java.lang.Object”),你会得到与java.lang.Object对应的类对象。你甚至可以通过这个函数 得到任何包中的任何已加载的类引用,只要这个类能够被加载到当前的名字空间。如果jvm不能把类加载到当前名字空间,forName就会抛出ClassNotFoundException。

  (译者:熟悉COM的朋友一定会想到,在COM中也有一个称为 类对象(Class Object)的东东,这个类对象主要 是实现一种工厂模式,而java由于有了jvm这个中间 层,类对象可以很方便的提供更多的信息。这两种类对象 都是Singleton的)

  也可以通过任一对象的getClass()函数得到类对象的引用,getClass被声明在Object类中:

  // A method declared in class java.lang.Object:   public final Class getClass();

  例如,假如你有一个java.lang.Integer的对象引用,可以激活getClass()得到对应的类引用。

  通过类对象的引用,你可以在运行中获得相应类存储在方法区中的类型信息,下面是一些Class类提供的方法:

  // Some of the methods declared in class java.lang.Class:

  public String getName();

  public Class getSuperClass();

  public boolean isInterface();

  public Class[] getInterfaces();

  public ClassLoader getClassLoader();

  这些方法仅能返回已加载类的信息。getName()返回类的完整名,getSuperClass()返回父类的类对象,isInterface()判断是否是接口。getInterfaces()返回一组类对象,每个类对象对应一个直接父接口。如果没有,则返回一个长度为零的数组。

  getClassLoader()返回类加载器的引用,如果是由启动类加载器加载的则返回null。所有的这些信息都直接从方法区中获得。

  • 方法表

  为了提高访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,jvm的实现者还可以添加一些其他的数据结构,如方法表。jvm对每个加载的非虚拟类的类型信息中都添加了一个方法表,方法表是一组对类实例方法的直接引用(包括从父类继承的方法)。jvm可以通过方法表快速激活实例方法。(译者:这里的方法表与C++中的虚拟函数表一样,但java方法全都 是virtual的,自然也不用虚拟二字了。正像java宣称没有 指针了,其实java里全是指针。更安全只是加了更完备的检查机制,但这都是以牺牲效率为代价的,个人认为java的设计者 始终是把安全放在效率之上的,所有java才更适合于网络开发)

异常状况:

  1. OutOfMemoryError:当方法区无法满足内存分配需求时抛出异常。

java堆(Heap)

对于大多数应用而言,JVM所管理内存中最大的一部分,在JVM启动时创建,唯一目的是存储Java对象实例,几乎所有的对象实例都在这里分配内存(Class对象被分配在方法区中)。Heap区域是垃圾收集管理器的主要区域。JVM 规范中规定,java堆是类似于磁盘空间的“物理上可以不连续、逻辑连续”的内存空间

异常状况:

  1. OutOfMemoryError:如果在堆中没有内存完成实例分配,并且堆也无法再扩展时抛出异常。

运行时常量池(与Class文件常量池不同)

《深入理解虚拟机》中提出运行时常量池是方法区的一部分(需要查看一下8中的新特性,好像有所改动)。用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。一般来讲,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于Class文件常量池的一个重要特征是具备动态性,并非预置入Class文件中常量池的内容才能进入运行时常量池,运行期间也可能将新的常量放入池中,最常用的是String类的intern()方法。

异常状况:

  1. OutOfMemoryError:当常量池无法再申请到内存时抛出异常。

线程私有区

虚拟机栈(VM stack)

虚拟机栈

描述java方法执行的内存模型:线程在调用每个java方法时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

虚拟机栈是一个先入后出栈,每一个线程创建时,JVM会为这个线程创建一个私有的虚拟机栈(虚拟机栈生命周期与线程相同),当线程调用某个对象的方法时,JVM会相应地创建一个栈帧放入虚拟机栈中,用于表示某个方法的调用,栈帧是用来存储数据和存储部分过程结果的数据结构,同时也被用于处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。

每个方法被调用和完成的过程,都对应一个栈帧从虚拟机上入栈和出栈的过程。线程在运行过程中,只有一个栈帧处于活跃状态,称为“当前活动栈帧”,且始终处于虚拟机栈的栈顶位置。

JVM 规范中规定了两种异常状况:

  1. StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度时抛出异常(例如无限递归)

  2. OutOfMemoryError:虚拟机栈动态扩展时无法申请到足够内存时抛出异常


本地方法栈(Native Method)

与虚拟机栈的区别是虚拟机栈为JVM执行java方法(即字节码)服务,而本地方法栈为虚拟机使用到的Native方法服务。用于存储线程调用本地方法时,本地方法的局部变量表、操作数栈等信息。有的虚拟机中将本地方法栈和虚拟机栈合二为一(例如 Hotspot虚拟机)

JVM 规范中规定了两种异常状况:

  1. StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度时抛出异常

  2. OutOfMemoryError:无法申请到足够内存时抛出异常


PC计数器 (唯一一个没有规定任何OutOfMemoryError情况的区域)

即程序计数器,一块很小的内存空间,存储下一条需要执行的字节码指令的地址,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。

JVM多线程是通过线程轮换并分配执行时间的方式来实现的。在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令,每个线程都有自己的PC,以保证各线程独立存在,互不影响。

如果当前执行的是java方法,其PC记录的是正在执行的虚拟机字节码指令的地址;如果执行的是本地方法(Native Method),则PC为空。