研究了一波JVM,自己把手头的资料做一些整理。
一,JVM演变史
图出处:https://www.cnblogs.com/xiaofuge/p/14244755.html
图中大概可以看出一个梗概,那就是方法区(永久代)的逐渐消亡,从主内存中逐渐变到本地内存中。
Hotspot中 方法区的变化:
- jdk1.6及之前:有永久代(permanent generation),静态变量存放在 永久代上。
- jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中。
- jdk1.8及之后:无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍留在堆空间。
那为什么要这么做呢?
原来方法区存储了类的元数据信息和各种常量,而且他也受GC的管理,而GC的目标理应当是对这些类型的卸载和常量的回收。但由于这些数据被类的实例引用,卸载条件变得复杂且严格,回收不当会导致堆中的类实例失去元数据信息和常量信息。因此,回收方法区内存不是一件简单高效的事情,往往GC在做无用功。另外随着应用规模的变大,各种框架的引入,尤其是使用了反射,动态代理等字节码生成技术的框架,对于方法区的大小设置无法把控,会导致方法区内存占用越来越大,最终OOM,那把方法区剔除出来是当务之急。
二,JVM模型拆检
JVM内存模型可以分为两个部分,堆和方法区(元空间)是所有线程共有的,而虚拟机栈,本地方法栈和程序计数器则是线程私有的。为了形象说明,参见下图:
图出处:https://www.cnblogs.com/yanl55555/p/13323128.html
1. 程序计数器
- 较小的内存空间、线程私有,记录当前线程所执行的字节码行号。
- 在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。
- 这一块区域没有任何 OOM 定义
2. 本地方法栈(Native Stack)
与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务,普通开发可以忽略。JDK1.8 HotSpot虚拟机直接就把本地方法栈和虚拟机栈合二为一。
3. 虚拟机栈(JVM Stack)
-
java方法执行的内存模型——栈帧:用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。生命周期与线程相同,无线程安全的顾虑,因为都是线程单独私有的。
-
-Xss设置每个线程堆栈的大小。一般情况下256K是足够了。
局部变量表:
-
存放方法参数和方法内部定义的局部变量(包括对象引用)。注意如果是成员变量,或者定义在方法外对象的引用,它们存储在堆中,涉及到线程共有的都在堆中,如果都是私有,也就没线程安全这一说了。
-
局部变量表的容量以变量槽slot为最小单位,每个变量槽都可以存储32位长度的内存空间,例如boolean、byte、char、short、int、float、reference。其中64位长度的long和double类型的数据会占用2个空间,同时slot支持复用。
-
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。
操作数栈:
- 一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈。方法开始执行时,这个方法的操作数栈是空的。
- 操作数栈的每个位置上可以保存一个java虚拟机中定义的任意数据类型的值,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。
- 所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值。
- 在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push) /出栈(pop)
- 在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传递
图出处:https://blog.csdn.net/dyangel2013/article/details/106588217
动态链接:
-
运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking) 。比如: invokedynamic指令
-
Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
图出处:https://blog.csdn.net/dyangel2013/article/details/106588217
方法返回地址:
当一个方法开始执行以后,只有两种方法可以退出当前方法:
-
当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口,一般来说,调用者的PC计数器可以作为返回地址。
-
当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口,返回地址要通过异常处理器表来确定。
-
当方法返回时,有三个操作:
- 恢复上层方法的局部变量表和操作数栈。
- 把返回值压入调用者调用者栈帧的操作数栈,异常返回时没有返回值。
- 调整PC计数器的值以指向方法调用指令后面的一条指令。