JVM运行时内存结构
-
元空间(方法区):线程共享,用于存放虚拟机加载的类信息、运行时常量池、静态变量、类的方法定义、属性等所有字节码指令。
-
运行时常量池程序运行期间,JVM会将字节码文件中的常量池加载到内存中,存放在运行时常量池中
-
字节码文件中的常量池:存储的是在编译期间解析的各种类型、方法、字段等的符号引用,以及字面量等常量值,以支持程序的动态链接和执行
-
-
堆:线程共享,存放对象实例
-
栈:线程私有。每个线程有一个栈,每个方法对应一个栈帧(stack frame),方法调用对应于栈帧的入栈和出栈
-
java虚拟机栈:为虚拟机上执行java方法服务
-
本地方法栈:为虚拟机上执行native方法服务
-
-
PC/程序计数器:线程私有,PC中存储的引用,指向下一条即将执行的指令
内存溢出
理论上会出现OOM的区域:(除了pc应该都有可能出现)
-
堆空间:无法创建对象,且堆空间无法扩展 java.lang.OutofMemoryError:Java heap space
- 出现原因
- 存在内存泄漏,导致无用对象长时间占用内存空间
- 代码中存在大对象分配
- 解决方案
- 加大堆内存-Xmx
- 找到内存泄漏的地方,进行代码优化,及时销毁不需要的对象
- 优化大对象的加载方式
- 出现原因
-
栈区:
- 出现原因
- 虚拟机栈区:如果线程调用深度大于虚拟机允许的最大深度,抛出stackOverFlowError(递归调用)
- 多数java虚拟机都支持动态扩展虚拟机栈的大小,直到内存不足时,抛出oom。(创建大量的线程)
- 解决方案
- 增加栈内存-Xss
- 优化代码,减少递归调用
- 出现原因
-
元空间
- 出现原因
- 使用大量动态生成类的框架(动态代理技术、热部署工具等)
- 程序代码中大量使用反射,反射在大量使用时,因为使用缓存的原因,会导致ClassLoader和它引用的Class等对象不能被回收
- 出现原因
内存泄漏
内存泄漏指的是,程序在申请内存后,无法释放已申请的空间
栈帧
jvm中,方法执行时的数据结构。栈帧的核心结构是:局部变量表和操作数栈。每个方法对应一个栈帧。
-
局部变量表
局部变量表是用于存放方法的参数和方法内部定义的局部变量,都是基本数据类型或对象引用。表内的元素不能互相赋值,必须通过操作数栈完成。
非静态方法的局部变量表当中索引为0的位置在还没有进行初始化操作,就是存uninitialized_this变量;如果已经进行了初始化操作,就是存储this变量,参数依次存储在索引1开始的位置。而静态方法中参数依次存储在索引0开始的位置
局部变量表的容量以slot为最小单位,一个slot可以存储一个32位的数据,所以一个局部变量占用的不一定是一个slot
-
操作数栈
方法开始执行时,操作数栈是空的,随着字节码指令的执行,会有数据入栈和出栈。操作数栈中的数据类型,必须要与当前需要执行的字节码指令匹配。
-
一个引用【支持动态连接】
栈帧中会存一个引用:指向当前方法所属类的运行时常量池
多态:
通过栈帧中的引用找到当前类的运行时常量池后,对被调用方法进行符号解析,拿到被调用方法的描述符
invokevirtual 指令在运行时的解析过程可以分为以下几步:【多态的原理】
- ①、找到操作数栈顶的元素所指向的对象的实际类型,记作 C。
- ②、如果在类型 C 中找到与常量池中的描述符匹配的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找结束;否则返回
java.lang.IllegalAccessError
异常。 - ③、否则,按照继承关系从下往上一次对 C 的各个父类进行第二步的搜索和验证。
- ④、如果始终没有找到合适的方法,则抛出
java.lang.AbstractMethodError
异常
-
返回地址
正常退出:一定是执行了某种RETURN指令。可能会有返回值传递给上层调用者
一般来说,方法正常退出的时候,PC 计数器的值会作为返回地址,栈帧中很可能会保存这个计数器的值,异常退出时则不会。
栈帧映射
java6以后引入了栈帧映射,用于jvm快速校验字节码,加速了类型安全检查过程。是验证数据,不直接参与方法执行。但是栈帧是运行时数据结构的一部分。
stackMapTable中存储了一系列的栈帧映射【此刻栈、local的数据变化情况】。每个映射实例,都反映了与上一个映射之间的变化关系,如下:
在1.6之前,是将所有可能得分支都列出来,进行类校验。1.6引入了栈帧映射,可快速进行类型安全检查。jvm优化后,规定每个条件分支后必须有一个映射表示栈帧的变化
stackMapTable在字节码阶段就会生成,用于运行时快速进行类的校验,如下图,展示了一个静态方法以及其字节码
fun t(flag: Boolean) {
var num = 0
if (flag) {
num = 1
} else {
num = 2
}
println(num)
}
// access flags 0x19
public final static t(Z)V
L0
LINENUMBER 16 L0
ICONST_0
ISTORE 1 //将num赋值为0
L1
LINENUMBER 17 L1
ILOAD 0
IFEQ L2
L3
LINENUMBER 18 L3
ICONST_1
ISTORE 1 //将num赋值为1 [假如后续我们删除这行指令]
GOTO L4
L2
LINENUMBER 20 L2
FRAME APPEND [I] //栈帧扩充一个local I类型变量 此时栈帧:[I, I][]
ICONST_2
ISTORE 1 //将num赋值为2
L4
LINENUMBER 22 L4
FRAME SAME //标识栈帧保持不变,此时栈帧为:[I, I][]
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ILOAD 1
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L5
LINENUMBER 23 L5
RETURN
L6
LOCALVARIABLE num I L1 L6 1
LOCALVARIABLE flag Z L0 L6 0
MAXSTACK = 2
MAXLOCALS = 2
为什么要有类校验
jvm加载类后,会进行校验,主要进行元数据验证(类文件相关信息)、字节码验证、控制流验证等,为了保证类的运行时符合安全规范,避免潜在风险。
这其中,字节码验证是要确保字节码能够被正确执行,这其中会进行类型安全检查。
类型安全:通过任何分支到达同一指令处,操作数栈和本地变量的数据类型需要始终保持一致,避免出现类型错误。该检查就是使用stackmapTable进行的。
实例说明类型安全校验的重要性:为了保证程序运行的正确性
如上述代码中,调用这个t(true)方法,正常运行应该输出1
如果删除if分支中对于num赋值的指令,继续运行就会报错,verifyError,提示栈帧映射不一致。如果关闭校验(运行时假如-noverify),程序能正常运行,但是显然并不符合逻辑预期。
asm如何写stackMapTable实例
MethodVisitor.visitFrame()用于写入栈帧映射,在每个跳转分支后的visitLabel调用之后调用。
public void visitFrame(int type,
int numLocal,
Object[] local,
int numStack,
Object[] stack)
详解visitFrame方法参数:
-
type
- 取值:
- F_NEW:ClassWriter被指定为扩展栈帧映射时使用
- F_SAME:栈帧相较于之前没有变化,并且栈为空
- F_SAME!:局部变量表没有变化,栈上有一个元素
- F_APPEND:栈帧相较于之前,局部变量表上向后扩充1,2,3个元素
- F_CHOP:栈帧相较于之前,局部变量表上少了1,2,2个元素,栈为空
- F_FULL:表示完整的栈帧内容【尽可能不用,不然会导致信心臃肿】
- 取值:
-
numLocal:变化的局部变量的个数
-
object[] local:变化的局部变量元素,类型描述符,如果这个slot为空(不是为空对象)则用TOP代替。该数组的长度为numLocal
-
numstack 如上
-
object[] stack 如上
ASM自动计算frame
构建ClassWriter时,可以设定flags属性,来决定当前类中需要自动计算什么
/**
flag:
COMPUTE_FRAMES:自动计算栈帧映射、stack和local的最大容量值
COMPUTE_MAXS: 自动计算stack和local的最大容量值
0:什么都不自动计算
*/
public ClassWriter(final ClassReader classReader, final int flags) {
super(/* latest api = */ Opcodes.ASM9);
symbolTable = classReader == null ? new SymbolTable(this) : new SymbolTable(this, classReader);
if ((flags & COMPUTE_FRAMES) != 0) {
this.compute = MethodWriter.COMPUTE_ALL_FRAMES;
} else if ((flags & COMPUTE_MAXS) != 0) {
this.compute = MethodWriter.COMPUTE_MAX_STACK_AND_LOCAL;
} else {
this.compute = MethodWriter.COMPUTE_NOTHING;
}
}