本篇主要介绍一下,JVM运行时数据区的内容。
概述
首先大概介绍一下下图所示的内容。JVM运行时数据区主要分为了两大部分的内容:线程共有的方法区(Method Area)和堆(Heap)、线程私有的虚拟机栈(VM Stack),本地方法栈(Native Method Stack)和程序计数器(Program Counter Register)。在数据区下面的执行引擎中又包含了:即时编译器(JITCompiler)和垃圾收集器(GC)。GC主要用于回收线程共享的区域(方法区和堆),对于私有的内存区域则方法执行完毕后系统自动释放。(在实际的程序当中,线程私有的内存区域会有很多份)
对于整个运行时数据区而言,外部交互的模块有执行引擎、本地库接口和类加载器。
下面分别介绍一下,各个内存区域的详细信息。
程序计数器(Program Counter Register [私有]):是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。① (此内存区域是唯一一个在JVM规范中没有规定任何OutOfMemoryError的区域)
JVM栈(Java Virtual Machine Stacks [私有]):每个方法在执行的时候都会同时创建一个栈帧。JVM栈中包含了:局部变量表、操作栈、动态链接、方法出口等信息。
本地方法栈(Native Method Stacks [私有]):其内存结构类似于JVM栈,不过使用到的是本地库接口。
堆(Heap [共享]):堆区域是JVM内存管理里面最大的一块,几乎所有通过new出来的对象都放在此区域。在GC的过程中,绝大部分的内存回收发生在此区域。
方法区(Method Area [共享]):方法区主要用于存储被JVM加载的类信息、常量、静态变量、即时编译后的代码等。
运行时常量池(Runtime Constant Pool [方法区组成部分]):用于存放编译器生成的各种字面量和符号引用。
Java堆
Java堆是应用程序最为关心的内存空间,几乎所有的对象实例都存放在堆中。并且Java中的GC都是JVM自动管理的,开发者不需要手动释放内存空间。
下面以HotSpot为例,介绍一下堆的内存结构。
从上面堆的结构图可以看出,HotSpot的堆结构包含了两大部分区域:新生代、老年代。新生代又分为:Eden区、from区、和to区,其中新创建的对象会存放在Eden区。当一次GC之后未被收集之后,对象将进入from区。from区和to区是对等的(分代收集/复制算法),其中只有一块区域可以使用。通常情况下,当GC次数达到15次之后,对象还存活的话,下一次GC时对象将进入老年代。如果新创建的对象在新生代存放不了的话,那么对象刚创建就在老年代中。
下面通过一段代码,看一下堆、方法区和栈的指向关系。
上述代码对应的内存引用关系如下所示:
Java栈
Java栈是一块线程私有的内存空间。线程执行的基本行为是函数调用,每次函数调用的数据都是通过Java栈传递的。
Java栈可以类比数据结构中的栈结构。每一次函数调用都会有一个对应的栈帧压入Java栈,每个函数调用的返回,都会有一个Java栈帧的弹出。一个栈帧中,至少要包含局部变量表、操作数栈和栈数据区几个部分。如下图所示:
JVM提供了参数-Xss来指定线程的最大栈空间,这个参数也决定了函数调用的最大深度。如果调用深度过大,则系统会抛出StackOverflowError。
局部变量表
局部变量表是栈帧的重要组成部分之一。它用于保存函数的参数、局部变量。局部变量表中的变量只对当前函数调用有效。
操作数栈
操作数栈也是栈帧的重要组成之一。它主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。如下图所示:
帧数据区
帧数据区主要用于支持常量池解析、正常方法返回和异常处理等。在帧数据区中保存着访问常量池的指针,方便程序访问常量池;异常处理表也是其重要组成的一部分。
栈上分配
栈上分配是JVM提供的一项优化技术。基本思想是,对于那些线程私有的对象,可以将它们打散分配在栈上,而不是在堆上。这样以来,当方法执行结束后,栈上的对象可以自动被释放,不需要GC的介入。下面看一段代码:
运行上述代码需要开启逃逸分析,运行结果如下(笔者1.8的JDK):
从上图可以看出,循环分配了近1.5G的内存空间,而GC只进行了1次,且只释放了1M多的空间。从而可以看出,对象基本上都是在栈上分配的。
方法区
方法区是线程共享的内存区域,它用于保存系统的类信息,比如类的字段、方法、常量池等。
在JDK6或JDK7中,方法区可以理解为永久区(Perm)。可以使用-XX:PermSize和-XX:MaxPermSize指定,默认情况下为64M。
在JDK8中,永久代被移除。取而代之的是元数据区,其大小可以使用-XX:MaxMetaspaceSize指定,它是一块堆外的直接内存。如果不指定大小,默认情况下它可以耗尽系统内存。
参考:《深入理解Java虚拟机》、《实战Java虚拟机》