在很长的一段时间里,我一直在思考一个问题,元空间到底在哪里?
现在的互联网,关于JVM,关于内存布局,关于优化JVM等知识,多如牛毛.
然而,元空间到底在哪里?堆外内存到底在哪里? 虽然有相关的文章谈及它们,但并不是我想要的答案,为了把它们掰扯清楚,让自己有一个较清晰的认知,不人云亦云, 自己对它们做了一点分析和研究,分享给大家,与大家一起交流.
依赖的包和测试代码如下所示
<dependency>
<groupId>org.jctools</groupId>
<artifactId>jctools-core</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
package com.infuq.memory;
import org.jctools.util.UnsafeAccess;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
import sun.misc.Unsafe;
public class AddressExample {
int value;
private AddressExample() {
this(20);
}
private AddressExample(int value) {
this.value = value;
}
public static void main(String[] args) throws Exception {
// 创建一个堆对象
AddressExample addressExample = new AddressExample();
long heap = VM.current().addressOf(addressExample);
System.out.println("heap address:\t 0x" + Long.toHexString(heap));
// 打印对象的布局(包括对象头等信息)
System.out.println(ClassLayout.parseInstance(addressExample).toPrintable());
// Class对象
long clazz = VM.current().addressOf(AddressExample.class);
System.out.println("clazz address:\t 0x" + Long.toHexString(clazz));
// 申请30M的直接内存
Unsafe unsafe = UnsafeAccess.UNSAFE;
long direct = unsafe.allocateMemory(30 * 1024 * 1024);
System.out.println("direct address:\t 0x" + Long.toHexString(direct));
// 每5秒打印一次对象的value属性值, value初始值20 .
while (true)
{
Thread.sleep(5000);
System.out.println(addressExample.value);
}
}
}
以上代码,借助第三方工具包,可以获取一个对象在堆内存的起始地址, 对象头信息, 以及申请30M的直接内存.
编译程序
javac -d . -classpath ".:./lib/*" AddressExample.java
运行程序
java -classpath ".:./lib/*"
-Xms50M -Xmx50M
-XX:MaxDirectMemorySize=32M
-XX:MetaspaceSize=12M
-XX:MaxMetaspaceSize=16M
-XX:-UseCompressedClassPointers
-XX:-UseCompressedOops
com.infuq.memory.AddressExample
根据输出信息可知
对象在堆中的地址=0x7f64890775a8
class对象的地址=0x7f6489076778
申请30M直接内存的地址=0x7f64661ff010
继续分析
查看进程的maps文件信息
由于对象在堆中的地址=0x7f64890775a8, 它处在图中所示的7f6489000000-7f648c200000空间范围内. 而这个范围的空间大小=(7f648c200000 减 7f6489000000) / 1024 / 1024 = 50M, 它等于我们运行程序时设置的堆空间大小 -Xms50M -Xmx50M.
class对象的地址=0x7f6489076778, 它也在堆空间7f6489000000-7f648c200000范围内.
继续分析
由于程序中申请了30M的直接内存,它的地址=0x7f64661ff010.
它处在上图中所示的7f64661ff000-7f6468000000空间范围内. 而这个范围的空间大小=(7f6468000000 减 7f64661ff000) / 1024 / 1024 = 30M, 它等于我们运行程序时申请的30M内存.
继续分析
在之前的打印中,打印出了对象头信息, 对象头中包含一个指针,这个指针指向元空间中的对象元信息.
指针地址=0x7f6488c00730
此处的指针地址涉及大小端问题, 需要从后向前读才是正确的指针地址
指针地址=0x7f6488c00730处在7f6488702000-7f6488e80000这个地址空间范围.而这个范围的空间大小=(7f6488e80000 减 7f6488702000) / 1024 / 1024 = 7M.
综上分析, 堆内存, 元空间, 直接内存 , 分别'分布在'Java进程的不同区域. 虽然元空间和直接内存都属于本地内存, 但它们都归属于Java进程里的空间.
读者要对进程虚拟地址空间有一定的了解
以上测试实验是在阿里云ECS上进行的, 由于某些原因, 无法继续实验, 接下来, 我在虚拟机VirtualBox上继续接下来的实验. 代码依然是上面的代码, 只是把代码放到VirtualBox上运行而已.
继续实验
再次重新运行程序,得到如下内容
根据之前的分析, 将上图中打印的地址归属到不同的区域上,可以得出如下概图
由于普通对象的对象头中包含指向元数据的指针, 因此可以看到图中, 有一个普通对象指向元数据的箭头.
我们读取了元数据的信息, 信息中有个指针指向了Class对象.
关于如何读取一个对象的内存信息, 后期讲解.
在Klass源码中定义了一个指向Class对象的属性.