10 07 2020

运行时数据区域

JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。

JVM 所管理的内存会包括以下几个运行时数据区域:

Java 堆

Java 堆(Java Heap) 是虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

为什么是几乎呢?从实现角度来看,随着 Java 语言的发展,现在已经能看到一些迹象表明日后可能出现值类型的支持,即使只考虑现在,由于JIT编译器的发展和”逃逸分析”技术的逐渐成熟,栈上分配、标量替换等优化技术使得对象一定分配在堆上这件事情已经变得不那么绝对了。

方法区

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

Java 虚拟机栈

Java 虚拟机栈(JVM Stacks),与程序计数器一样也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

内存中的栈(stacks)、堆(heap)和方法区(method area)的用法

通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中的栈空间。

数据类型包括 boolean、byte、char、short、int、float、double。

引用对象是 reference 类型,它并不等同于对象本身,可能是一个指向对象起始地址的指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置

现场保护现场恢复源于汇编中主程序和子程序之间的调用和返回,和CPU中断机制有关。
主程序和子程序通常是分别编制的,所以它们所使用的寄存器往往会发生冲突。如果主程序在调用子程序之前的某个寄存器内容在从子程序返回后还有用,而子程序又恰好使用了同一个寄存器,这就破坏了该寄存器的原有内容,因而造成程序运行错误,这是不允许的。为避免这种错误的发生,在一进入子程序后,就应该把子程序所需要使用的寄存器内容保存在堆栈中,此过程称作现场保护。在退出子程序前把寄存器内容恢复原状,此过程称作现场恢复。现场保护与现场恢复分别使用压栈和弹出指令实现。

而通过new关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域。

由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老年代,再具体一点可以分为 Eden、Survivor(又可分为From Survivor和To Survivor)、Tenured。

方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码缓存等数据。

运行时常量池(Runtime Constant Pool)是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这些内容会在类加载后存放到方法区的运行时常量池中。

栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过 JVM 的启动参数来进行调整,栈空间用光了会引发 StackOverflowError 异常,而堆和常量池空间不足则会引发 OutOfMemoryError 异常。

举个例子:
  1. String str = new String("hello");

上面的语句中变量 str 放在栈上,用 new 创建出来的字符串对象放在堆上,而“hello”这个字面量是放在方法区的。

补充

运行时常量池相当于 Class 文件常量池具有动态性,Java 语言并不要求常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中,String 类的 intern() 方法就是这样的。

查看以下代码执行结果在java7之前和之后的版本中运行结果是否一致:

  1. String s1 = new StringBuilder("go")
  2. .append("od").toString();
  3. System.out.println(s1.intern() == s1);
  4. String s2 = new StringBuilder("ja")
  5. .append("va").toString();
  6. System.out.println(s2.intern() == s2);

运行结果:
java7 之前:false,false.
java7 之后:true false
原因:对于”s1.intern() == s1”,java7之前,由于运行时常量池存放在方法区内,变量s1通过new关键字创建的对象,所以s1是堆空间的引用;而s1.intern()返回的是方法区中的运行时常量池的引用,因此结果是false;java7之后运行时常量池存放在堆中,因此”s1=new StringBuilder(“go”).append(“od”).toString()”会将”good”直接放到运行时常量池,同时返回该引用给到s1;而调用”s1.intern()”返回的同样是运行时常量池中该对象的引用,所以是true。

对于”s2.intern() == s2”,是因为在编译后java常量池中已经”java”对象,所以”s2.intern()”返回的始终都是方法区常量池的对象引用,所以结果都是false。

延伸阅读
  1. JVM 运行时数据区域
  2. JVM 基础
  3. JVM 基础故障处理工具
发表评论