Java虚拟机运行时内存数据区域

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

程序计数器

  程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成,这类内存区域为“线程私有”的内存。
  程序计数器可以从多线程运行方式来理解,例如一个双核的CPU,运行10个线程,要知道CPU每个核心在一个时刻只能运行一个线程,要运行这10个线程,是通过轮流切换时间片来实现。就像CPU执行了一下A线程,过一会儿又切换到B线程执行,这时候A线程处于暂停状态。当B线程执行完毕,要切换到A线程时,执行A线程要从哪行代码开始执行?所以线程切换之前,就要类似把这代码的行号记录下来,等一下切换回来时,从刚才暂停的地方继续执行。所以引入程序计数器,就是记录线程执行的位置。

Java虚拟机栈

  与程序计数器一样样, Java虚拟机栈也是线程私有的,它的生命周期与线程相同。
  Java虚拟机提供参数-Xss来指定线程的最大栈空间,这个参数也直接决定了函数调用的最大深度。
  Java开发中,每当我们在程序中使用new生成一个对象,对象的引用存放在栈里,而对象实体是存放在堆里。Java栈上的所有数据都是私有的.任何线程都不能访问另一个线程的栈数据。所以我们不用 考虑多线程情况下栈数据访问同步的问题。

栈帧

  每当启动一个线程时, JVM就为线程分配一个 Java栈, 栈是以帧为单位保存当前线程的运行状态。虚拟机栈描述的是 Java方法执行的内存模型: 每个方法在执行的时都会创建一个栈帧(StackFrame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息. 每一个方法从调用直至执行完成的过程, 就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

桟帧和函数调用

  当函数返同时,栈帧从 Java栈中被弾出. Java方法有两种返回函数的方式, 一种是正常的函数返同,使用return指令;另外一种是抛出异常。 不管使用哪种方式,都会导致栈帧被弹出。

局部变量区

  局部变量区被组织为以一个字长为单位,从 0开始计数的数组, 类型为short、 byte和 char 的值在存入数组前要被转换成 int值,而long和 double在数组中占据连续的两项,在访问局部变量中的1ong或double时,只需取出连续两项的第一项的索引值即可。
  局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。

操作数栈

  操作数栈和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的。可把操作数栈理解为存储计算时临时数据的存储区域。

本地方法栈

  本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native方法服务。本地方法栈区域也会抛出 StackOverflowError和 OutOfMemoryError异常。

  对于大多数应用来说, Java堆是 Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域, 在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例和数组都在这里分配内存。为什么不是说所有呢?因为在逃逸分析、栈上分配这样的优化技术的存在,所以也就不是那么绝对了。

  从内存同收的角度来看,由于现在的垃圾收集器基本都采用分代收集算法,所以 Java堆中还可以细分为:新生代老年代,而新生代的垃圾回收为了防止空间碎片又采用复制算法,所以新生代空间又分为:Eden空间、 From Survivor空间, To Survivor空间。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区也就是所谓的ThreadLoca1
Java虚拟机堆内存空间

新生代

  新生代的布局,分为3个独立区域:

  • Eden 大多数新对象分配在这里(不是是所有,因为大对象可能直接分配到老年代)。在一次YoungGC后Eden几乎总是空的。
  • Survivor(From和To) 这里存放的对象至少经历了一次YoungGC,它们在提升至老年代之前还有一次被收集的机会。

  Eden和Survivor的大小比例默认是8:2,因为新生代中的对象98%是“朝生夕死”的,只有很少的对象存活下来,所以Survivor的空间可以小一点。

方法区

  方法区与 Java堆一样,是各个线程共享的内存区域,它用于存储被虚松机加裁的类信息、常量、静态变量、即时编译器编译后的代码等数据。在JDK1.6和JDK1.7中,方法区可以理解为“永久代”(PermGen)。在JDK1.8中,永久区的概念被废除,取而代之的是元数据区(metaspace),与永久区不同的是,如果不指定大小,默认情况下,虚拟机会耗尽所有可用系统内存。

运行时常量池

  运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载进入方法区的运行时常量池中存放。

直接内存

  直 接内存并不是虚拟机运行时数据区的一部分,也不是 Java虚拟机规范中定义的内存区域。 但是这部分内存也被频繁地使用, 而且也可能导致0utofMemoryError异常出现。在 JDK1.4中新加人了 NIO类,引入了一种基于通道(channe1)与缓冲区(Bufner)的I/0方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在 Java堆中的 DirectByteBuffer对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能, 因为避免了在 Java堆和 Native堆中来回复制数据。

堆、栈和方法区的关系

  下面通过一个简单的示例,来展示Java堆、栈和方法区之间的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SimpleHeap {
private int id;
public SimpleHeap(int id) {
this.id = id
}
public void show() {
System.out.println("my id is " + id);
}
public static void main(String [] args) {
SimpleHeap s1 = new SimpleHeap(1);
SimpleHeap s2 = new SimpleHeap(2);
s1.show();
s2.show();
}
}

堆、方法区和栈的关系

参考资料

  • 《深入理解Java虚拟机:JVM高级特性与最佳实践》
  • 《实战Java虚拟机,JVM故障诊断与性能优化》
  • 《HotSpot实战》