本文默认基于64位的JDK 8(其虚拟机实现是HotSpot),除非特别说明。
本文介绍JVM进程内5个主要内存区域中客体(变量、字段、对象等)之间的联系,5个主要内存区域是“Java虚拟机栈,本地方法栈,Java堆,方法区,显式调用JNI方法直接分配内存”,即不包括“程序计数器,JVM进程运行自身所需内存”。[1]
一、联系的本质
从内存和机器码的视角看,两块内存区域中客体之间的联系就是指针(概念引自“C/C++语言”,这里是广义化的,可以是直接内存地址,也可以是间接内存地址)。
从JVM的视角看,以上5块内存区域只不过是内存上的逻辑划分,其内客体之间的联系就是指针,只不过:
- 有些在Java语言视图下是Java对象引用,比如“在Java方法中有语句
Object obj = new Object()
,在Java语言视图下,Java虚拟机栈中的变量obj客体与Java堆中相应实例对象客体之间的指针是Java对象引用” - 其他的则不是,比如“在Java方法中有语句
long base = sun.misc.Unsafe.allocateMemory(size)
,在Java语言视图下,Java虚拟机栈中的变量base客体与‘显式调用JNI方法直接分配内存’中相应内存块客体之间的指针不是Java对象引用”
一个指针是Java对象引用须满足两个条件:
- [条件1]指针指向一个Java对象
- [条件2]保存指针值的变量是Java对象引用变量
这两个条件之间的关系是:
- 条件1不满足,条件2必然不满足,否则此时Java对象引用变量便是一个非法定义
- 条件1满足,条件2可能满足,也可能不满足,不满足的情形比如有“Java程序中一个long型变量保存一个Java对象的内存地址”,“本地方法实现中,执行上下文语境不是Java,一个C/C++语言指针变量保存一个Java对象的内存地址”
二、联系示意图和说明
在继续叙述之前,首先作三点说明:
- 对于“方法区”,本文只考虑“类元数据,即Klass对象”,不考虑“运行时常量池、即时编译器编译以后的代码等”
- Java堆内是Java对象,分为:普通实例对象,数组对象,Class对象
- 对于JNI方法,本地方法栈存放中间过程变量,直接操作目标内存块可以认为只位于“显式调用JNI方法直接分配内存”和“Java堆”,而不可以位于“Java虚拟机栈”,“本地方法栈”和“方法区”(虽然JNI方法理论上可以操作所在JVM进程内所有内存,但是直接操作“Java虚拟机栈”,“本地方法栈”和“方法区”中的内存块被认为不安全,这里不作考虑,笔者不清楚JDK本身是否对上述不安全行为有所限制)
2.1、联系示意图
联系示意图见图1。
图1
2.2、联系说明
- [1]和[2]:Java对象的对象头中的“Klass Pointer”,指向方法区中的“类元数据,即Klass对象”,该指针不是Java对象引用,因为条件1和2不满足;方法区中“类元数据”的“_java_mirror”指针,指向Java堆中对应的“java.lang.Class对象”,该指针不是Java对象引用,因为条件1满足而条件2不满足。比如“有一个自定义类me.dslztx.AObject,加载该类创建一个实例对象aObject之后,方法区存在一个对应于me.dslztx.AObject的Klass对象aKlass,在Java堆中存在一个对应于me.dslztx.AObject的Class对象aClz(即一个java.lang.Class类的实例对象),现在aObject实例对象的对象头中的Klass Pointer指向aKlass,aKlass中的“_java_mirror”指针指向aClz”
- [3]:部分为Java语言视图下的Java对象引用,示例见源代码1;其他部分则不是(可能条件1不满足,此时条件2必然不满足,比如“指向一个字段的内存地址”;可能条件1满足而条件2不满足,比如“一个long型变量保存一个Java对象的内存地址”),示例见源代码2
- [5]:部分为Java语言视图下的Java对象引用,示例见源代码3;其他部分则不是(可能条件1不满足,此时条件2必然不满足,比如“指向一个字段的内存地址”;可能条件1满足而条件2不满足,比如“一个long型变量保存一个Java对象的内存地址”),示例见源代码2
- [7]:没有一个指针是Java对象引用,条件2必然不满足(因为是非Java方法的执行上下文),条件1可能满足也可能不满足,示例见源代码4。这里有一点释疑:JNI方法声明中的输入参数和输出参数属于Java语言,如果相关变量是在本地方法栈中定义的,那条件2不是可能满足吗?答案是否的,因为在运行过程中,以上输入参数会被转换成适配JNI方法实现的输入参数,JNI方法实现的输出参数会被转换成适配以上输出参数,对于上述转换过程中的相关变量,笔者认为不存放在本地方法栈
- [4]、[6]、[8]:没有一个指针是Java对象引用,条件1必然不满足(否则这部分内存应该属于Java堆),此时条件2也必然不满足,示例见源代码5和源代码6
源代码1:
1 | public class Main { |
源代码2:
1 | public class Main { |
源代码3:
1 | public class Main { |
源代码4:
1 | UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapObject(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jobject e_h, jobject x_h)) |
源代码5:
1 | import sun.misc.Unsafe; |
源代码6:
1 | UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size)) |
参考文献
[1]《JVM进程内存分类》
[2]https://zhuanlan.zhihu.com/p/104725313
[3]https://www.sczyh30.com/posts/Java/jvm-klass-oop/
[4]https://zhuanlan.zhihu.com/p/104494807