0%

JVM进程内5个主要内存区域中客体之间的联系

本文默认基于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. [条件1]指针指向一个Java对象
  2. [条件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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {

public static void main(String[] args) {

AObject aObject = new AObject();

BObject bObject = new BObject(aObject);
}
}

class AObject {

}

class BObject {
// Java堆中BObject实例对象内a字段客体与Java堆中AObject实例对象之间的指针是Java对象引用
AObject a;

public BObject(AObject a) {
this.a = a;
}
}

源代码2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Main {

public static void main(String[] args) {
AObject aObject = new AObject();

//obtainAddressJNI是一个待实现的获取对象内存地址的JNI方法

// Java虚拟机栈中aAddressTmp局部变量客体与Java堆中AObject实例对象之间的指针不是Java对象引用
long aAddressTmp = obtainAddressJNI(aObject);

BObject bObject = new BObject(aAddressTmp);
}
}

class AObject {

}

class BObject {
// Java堆中BObject实例对象内aAddress字段客体与Java堆中AObject实例对象之间的指针不是Java对象引用
long aAddress;

public BObject(long aAddress) {
this.aAddress = aAddress;
}
}

源代码3:

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {

public static void main(String[] args) {

// Java虚拟机栈中aObject局部变量客体与Java堆中AObject实例对象之间的指针是Java对象引用
AObject aObject = new AObject();
}
}

class AObject {

}

源代码4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapObject(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jobject e_h, jobject x_h))
UnsafeWrapper("Unsafe_CompareAndSwapObject");
oop x = JNIHandles::resolve(x_h); // 新值
oop e = JNIHandles::resolve(e_h); // 预期值
oop p = JNIHandles::resolve(obj);

// addr变量虽然指向了Java堆中的一个对象,但是在本地方法语境中不是Java对象引用
HeapWord* addr = (HeapWord *)index_oop_from_field_offset_long(p, offset);// 在内存中的具体位置
oop res = oopDesc::atomic_compare_exchange_oop(x, addr, e, true);// 调用了另一个方法
jboolean success = (res == e); // 如果返回的res等于e,则判定满足compare条件(说明res应该为内存中的当
前值),但实际上会有ABA的问题
if (success) // success为true时,说明此时已经交换成功(调用的是最底层的cmpxchg指令)
update_barrier_set((void*)addr, x); // 每次Reference类型数据写操作时,都会产生一个Write Barrier暂时>中
断操作,配合垃圾收集器
return success;
UNSAFE_END

源代码5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sun.misc.Unsafe;

public class Main {

private static final Unsafe unsafe = Unsafe.getUnsafe();

public static void main(String[] args) {

// Java虚拟机栈中base局部变量客体与“显式调用JNI方法直接分配内存”中一个内存块的指针不是Java对象引用
long base = unsafe.allocateMemory(1 * 1024 * 1024);

CObject cObject = new CObject(base);
}
}

class CObject {
// Java堆中CObject实例对象内address字段客体与“显式调用JNI方法直接分配内存”中一个内存块的指针不是Java对象引用
long address;

public CObject(long address) {
this.address = address;
}
}

源代码6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
UnsafeWrapper("Unsafe_AllocateMemory");
size_t sz = (size_t)size;
if (sz != (julong)size || size < 0) {
THROW_0(vmSymbols::java_lang_IllegalArgumentException());
}
if (sz == 0) {
return 0;
}

// sz局部变量客体与“显式调用JNI方法直接分配内存”中一个内存块的指针不是Java对象引用
sz = round_to(sz, HeapWordSize);
void* x = os::malloc(sz, mtInternal);
if (x == NULL) {
THROW_0(vmSymbols::java_lang_OutOfMemoryError());
}
//Copy::fill_to_words((HeapWord*)x, sz / HeapWordSize);
return addr_to_java(x);
UNSAFE_END

参考文献

[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

您的支持将鼓励我继续分享!