JVM运行时数据区域
文章目录
1. 运行时数据区图解
从上图可以看出,运行时数据区主要分为5个部分
- 程序计数器
- 本地方法栈
- 虚拟机栈
- 方法区
- 堆
其中程序计数器、本地方法栈和虚拟机栈是线程私有的,方法区和堆是线程共享的。
何为线程私有?
线程私有数据区域生命周期与线程相同,依赖用户线程的启动/结束而创建/销毁(在 Hotspot VM 内,每个线程都与操作系统的本地线程直接映射,因此这部分内存区域的存/否跟随本地线程的生/死对应)。
何为线程共享?
线程共享区域随虚拟机的启动/关闭而创建/销毁。
2. 程序计数器
2.1 程序计数器是什么
一块较小的内存空间,可以理解为当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器。
2.2 为什么要每个线程都要有一个独立的程序计数器
java的多线程是通过切换、分配处理器处理的,任何时刻,一个处理器都只会处理一个线程中的指令,为了能够让线程切换后‘指示器’恢复到正确的执行位置,所以每个线程都要有一个独立的程序计数器,互不干扰。
2.3 程序计数器有什么特点
- 正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。
- 这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。
3. 虚拟机栈
3.1 虚拟机栈是什么
我们通常所说的栈,就是虚拟机栈,它是描述 java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame),每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
3.2 栈帧是什么
栈帧是方法运行时的基础数据结构。主要存储数据如下:
- 局部变量表
- 操作数栈
- 动态链接
- 方法出口信息
栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
3.3 局部变量表
局部变量表存放了编译期可知的
- 各种基本数据类型:8中基本数据类型
- 对象引用:reference类型,不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置。
- returnAddress:指向了一条字节码指令的地址
局部变量表所需的内存空间在编译期间完成分配,方法运行期间不会改变局部变量表的大小。
3.4 栈有什么特点
- 对于栈来说不存在垃圾回收问题,因为方法结束后,对应的栈帧也会销毁。
- 栈先进先出
4. 本地方法栈
本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用的方式以及数据结构并没有强制规定,因此虚拟机可以自由实现它,甚至 Sun HotSpot 中直接将这两个区域合二为一。
5. 堆
5.1 堆是什么
对于大多数应用来说,堆是 Java 虚拟机所管理内存中最大的一块,它被所有的线程共享,此区域存放的唯一目的就是存放对象实例。但是随着 JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有对象实例分配在堆上也不是那么“绝对”了。
堆是垃圾收集器管理的主要区域,因此被称为“GC堆”。
站在垃圾收集器的角度来看,可以把内存分为新生代与老年代,默认情况下 Young : Old = 1 : 2,这个可以通过–XX:NewRatio
参数进行设定。
其中,新生代 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 From Survivor 和RTo To Survivor。默认情况下 Eden : From : To = 8 : 1 : 1 ,该值可以通过参数 –XX:SurvivorRatio
参数进行设定。
虚拟机每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
前面说过,可以使用 Xms 与 Xmx 来指定堆的最小与最大空间。如果 Xms 小于 Xmx,堆的大小不会直接扩展到上限,而是留着一部分等待内存需求不断增长时,再分配给新生代。Vritual空间便是这部分保留的内存区域。
5.2 堆的常见配置
-
-Xms20M
:最小20M -
-Xmx20M
:最大20M -
-XX:NewSize=10M
:新生代最小值 -
-XX:MaxNewSize=10M
:新生代最大值 -
-Xmn10M
:新生代大小,-XX:NewSize=10M
,-XX:MaxNewSize=10M
的快捷设置 -
-XX:NewRatio=n
:老年代与新生代的比例,eg:2,即代表 老年代:新生代 = 2:1 -
-XX:SurviorRatio=n
:Eden区与Survivior区的比例,eg:8,即代表 Eden:S0:S1=8:1:1 -
-XX:MaxTenuringThreshold=n
:在新生代中对象存活次数(经过Minor GC的次数)后仍然存活,就会晋升到旧生代
5.3 堆有什么特点
- 被所有线程共享
- 存放对象实例
- 垃圾收集器管理的主要区域
- 按照分代收集器可分为新生代和老年代
5.4 为什么存在两个Survivor?
这个在看到‘内存分配’时在进行学习。
6. 方法区
6.1 什么是方法区
被所有线程共享, 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,他还有一个别名:非堆(Non-Heap)。
在HotSpot虚拟机上,方法区也就是我们常说的永久代(Permanent Generation),但是对于其他的虚拟机来说,并不存在这一概念。使用永久带来实现方法区并不是一个好的主意,因为永久代有 -XX:MaxPermSize 的上限,极少数方法,如**String.intern()**可能会引起内存溢出的问题。
并不是说被称为永久代,GC就不会对该区域进行管理,只是较少管理而已。这个区域内存回收的目标主要是针对常量池的回收和对类型的卸载。
方法区中还有一个运行时常量池,用于存储编译生成的字面量和符号引用
6.2 方法区有什么特点
- 被所有线程共享
- 存储定义的方法的信息
- 有大小限制 -XX:MaxPermSize
- GC较少回收该区域
- 还有一个运行时常量池,用户存储编译后的字面量和符号引用
6.3 字面量、符号引用
-
字面量:基本数据类型以及进制,String字符串。
int a = 8
中的8和String a = "hello"
中的hello都是字面量。 -
符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。个人理解:符号引用可以看作是一个虚拟地址,只有在JVM加载完类,确认了字面量的地址,就会将这个符号引用(字符串)解析成直接引用(指针)
7. 常量池
名称 | 存放数据 | 内存位置 |
---|---|---|
class文件常量池(Constant Pool Table) | 用于存放编译器生成的各种字面量和符号引用,这部分在类被加载后,进入方法区的运行时常量池中存放 | .class文件 |
运行时常量池(Run Constant Pool) | 在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用 | 方法区 |
全局字符串常量池 | 在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例 | JDK1.7前(方法区),后(堆) |