1 运行时数据区域
在 JVM 中,JVM 内存主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈等。
按照与线程的关系也可以这么划分区域:
线程私有区域:一个线程拥有单独的一份内存区域。线程共享区域:被所有线程共享,且只有一份。
JVM运行时数据区概览
1.1 程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。
各线程之间独立存储,互不影响(线程私有)。
如果线程正在执行的是一个
Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。
此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
1.2 栈
1.2.1 Java虚拟机栈
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
1.2.1.1 局部变量表
比如方法中的变量,存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型) 和 returnAddress类型(它指向了一条字节码指令的地址);
1.2.1.2 操作数栈
存放java 方法执行的操作数的。
操作数栈本质上是JVM 执行引擎的一个工作区,也就是方法在执行,才会对操作数栈进行操作,如果代码不执行,操作数栈其实就是空的。
虚拟机栈的大小缺省为1M,可用参数
-Xss调整大小,例如-Xss256k.1.2.1.3 动态链接
部分符号引用在运行期间转化为直接引用,这种转化就是动态链接。
1.2.1.4 返回地址(方法出口)
正常返回(调用程序计数器中的地址作为返回):
- 恢复上层方法的
局部变量表和操作数栈; - 把
返回值(如果有的话)压入调用者栈帧的操作数栈中; - 调整
程序计数器的值以指向方法调用指令后面的一条指令。
异常的话(通过异常处理器表<非栈帧中的>来确定)。
1.2.2 本地方法栈
本地方法栈用于管理本地(Native)方法的调用。
本地方法并不是用Java实现的,而是由C语言实现的(比如Object.hashcode 方法)。
HotSpot 直接把本地方法栈和虚拟机栈合二为一。
1.3 堆
堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。
堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。堆一般设置成可伸缩的。
那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?
这和两个方面有关:对象的类型和在 Java 类中存在的位置。
Java 的对象可以分为
基本数据类型和普通对象。
- 对于
普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。 - 对于
基本数据类型来说(byte、short、int、long、float、double、char),有两种情况:- 当你在
方法体内声明了基本数据类型的对象,它就会在栈上直接分配。 - 其他情况,都是在
堆上分配。
- 当你在
1.3.1 堆大小参数
-Xms:堆的最小值;-Xmx:堆的最大值;-Xmn:新生代的大小;-XX:NewSize:新生代最小值;-XX:MaxNewSize:新生代最大值。
1.3.2 Young Generation
年轻代被分为三个部分:Eden、Survivor 1、Survivor 2(被称为from/to或s0/s1),默认比例是8:1:1。
- 大多数新创建的对象都位于
Eden内存空间中; - 当
Eden空间被对象填充时,执行Minor GC,并将所有Survivor对象移动到另一个Survivor空间中; Minor GC检查幸存者对象,并将它们移动到另一个Survivor空间。所以每次,一个Survivor空间总是空的;- 经过多次
GC 循环后存活下来的对象被移动到老年代。通常,这是通过设置年轻代对象的年龄阈值来实现的,然后他们才有资格提升到老年代。(当然还有其他晋升渠道,后面会讲。)
1.3.3 Old Generation
旧的一代内存包含那些经过许多轮小型 GC 后仍然存活的对象。
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝。
1.4 方法区(JDK1.7及以前)
方法区(Method Area)是可供各条线程共享的运行时内存区域。用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
1.4.1 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
Tips:
字面量:指字母、数字等构成的字符串或者数值常量,只可以以右值出现。所谓右值是指等号右边的值,如int a = 1, 这里的a是左值,1为右值。在这个例子中1就是一个字面量。符号引用:是相对于直接引用来说的,如类和接口的全限定名、字段名称和描述符、方法名称和描述符、方法句柄和方法类型、动态调用点和动态常量都是符号引用。Class 常量池:用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References),常量池一旦被装入内存就变成了运行时常量池,对应的符号引用在程序加载或运行时就会被转变为被加载内存区的代码的直接引用(即动态链接)。Java虚拟机对于Class文件每一部分(自然也包括常量池)的格式都有严格规定,如每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、加载和执行,但对于
运行时常量池,《Java虚拟机规范》并没有做任何细节的要求,不同提供商实现的虚拟机可以按照自己的需要来实现这个内存区域,不过一般来说,除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
1.5 元空间(JDK1.8及以后)
在JDK1.8中,Oracle最终删除了方法区,转而替代的方案是采用元空间,使得Hotspot与JRockit完美融合。另外,也能解决永久代更容易遇到内存溢出的问题。因为元空间存储位置是本地内存(堆外内存)。
早在
Java7版本中,Oracle就已经将永久代的静态变量和字符串常量池转移到了堆中。
其初始和最大值通过-XX:MetaspaceSize、-XX:MaxMetaspaceSize进行控制。
1.6 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起讲解。
如果使用了NIO,这块区域会被频繁使用。在Java堆内可以用directByteBuffer对象直接引用并操作。
这块内存不受Java堆大小限制,但受本机总内存的限制,通过-XX:MaxDirectMemorySize来设置(默认与堆内存最大值一样)。
1.7 版本变化
| JDK版本 | 是否有永久代,字符串常量池放在哪里? | 方法区逻辑上规范,由哪些实际的部分实现的? |
|---|---|---|
| jdk1.6及之前 | 有永久代,运行时常量池(包括字符串常量池),静态变量存放在永久代上 | 这个时期方法区在HotSpot中是由永久代来实现的,以至于这个时期说方法区就是指永久代 |
| jdk1.7 | 有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中; | 这个时期方法区在HotSpot中由永久代(类型信息、字段、方法、常量)和堆(字符串常量池、静态变量)共同实现 |
| jdk1.8及之后 | 取消永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中 | 这个时期方法区在HotSpot中由本地内存的元空间(类型信息、字段、方法、常量)和堆(字符串常量池、静态变量)共同实现 |

只有
HotSpot才有永久代的概念.
2 总体概览


