我们都知道在 Java
中每个对象都是存储在堆内存之中,那在程序运行过程当中,我们有应当如何判断获取一个对象所占用的内容空间大小呢?
直接进入正题,下面就让我们看一下应该如何查询对象的内存大小。
一、对象内存
1. 依赖导入
想要衡量一个 Java
对象在内存中具体的占用情况,默认 JDK
中并没有提供直观的查询方式,而 JOL
中则提供了一系列接口供于查询。
在使用之前需要在项目的 Maven
中引入下述依赖。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
2. 结构信息
通过 ClassLayout.parseClass()
可打印输出 Java
对象的基本结构等详情。
如下述即打印 HashMap
对象的结构与内存占用信息。
@Test
public void objDemo() {
String printable = ClassLayout.parseClass(HashMap.class).toPrintable();
System.out.println(printable);
}
运行示例程序可以看到输出的信息中包含的对象头与对象成员数据类型等信息。
java.util.HashMap object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) N/A
8 4 (object header: class) N/A
12 4 java.util.Set AbstractMap.keySet N/A
16 4 java.util.Collection AbstractMap.values N/A
20 4 int HashMap.size N/A
24 4 int HashMap.modCount N/A
28 4 int HashMap.threshold N/A
32 4 float HashMap.loadFactor N/A
36 4 java.util.HashMap.Node[] HashMap.table N/A
40 4 java.util.Set HashMap.entrySet N/A
44 4 (object alignment gap)
Instance size: 48 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
3. 内存占用
通过 GraphLayout.parseInstance()
即可获取一个对象的实际内存占用空间,其常见的方法参考下表。
方法 | 作用 |
---|---|
startAddress() | 输出对象的的内存起始地址。 |
endAddress() | 输出对象的的内存终止地址。 |
totalCount() | 输出对象的大小信息。 |
toPrintable() | 输出对象元素的详细内存使用情况。 |
@Test
public void jolDemo() {
Map<String, String> dataMap = new HashMap<>();
for (int i = 0; i < 5; i++) {
dataMap.put("key-" + i, "value-" + i);
}
long startAddress = GraphLayout.parseInstance(dataMap).startAddress();
System.out.println("Start address: " + startAddress);
long endAddress = GraphLayout.parseInstance(dataMap).endAddress();
System.out.println("End address: " + endAddress);
long totalCount = GraphLayout.parseInstance(dataMap).totalCount();
System.out.println("Total count: " + totalCount);
String printable = GraphLayout.parseInstance(dataMap).toPrintable();
System.out.println("Printable: " + printable);
}
二、运行内存
1. 服务信息
在 java.lang
包下提供了 Runtime
类可用于在程序运行期间获取服务运行载体信息,通俗的讲即获取服务部署的服务器信息与当前应用的 JVM
信息。
如下示例即通过 availableProcessors()
获取当前服务部署的服务器载体核心数,根据核心数的大小通过我们即可合理的设置线程等并发数量。
@Test
public void processorDemo() {
int processors = Runtime.getRuntime().availableProcessors();
System.out.println("Processors = " + processors);
}
2. Runtime
除了获取服务器核心数之外还可以获取当前应用的 JVM
内存使用信息,具体方法参考下表。
方法 | 作用 |
---|---|
freeMemory() | JVM 空闲内存区域,即未分配的内存空间。 |
maxMemory() | JVM 可以使用的最大内存大小。 |
totalMemory() | 当前已经占用的内存总量,包括了用于程序数据、堆、方法区等各种内存区域。 |
@Test
public void runtimeDemo() {
long heapSize = Runtime.getRuntime().totalMemory();
System.out.println("Heap size = " + heapSize);
long heapMaxSize = Runtime.getRuntime().maxMemory();
System.out.println("Heap max size = " + heapMaxSize);
long heapFreeSize = Runtime.getRuntime().freeMemory();
System.out.println("Heap free size = " + heapFreeSize);
}
三、直接内存
1. 内存信息
通过 sun.misc.SharedSecrets
类即可快速的获取当前应用已经申请的直接内存。
需要注意在 JDK 11
及之后的版本需要使用 jdk.internal.access.SharedSecrets
实现。
import sun.misc.SharedSecrets;
public class UsagePrinter {
@Test
public static void printDirectMemoryUsage() {
// JDK 8: "sun.misc.SharedSecrets"
// JDK 11: "jdk.internal.access.SharedSecrets"
long memoryUsed = SharedSecrets.getJavaNioAccess().getDirectBufferPool().getMemoryUsed();
memoryUsed = memoryUsed / 1024;
System.out.println("Direct memory = " + memoryUsed + " KB");
}
}
在 JDK 9
之后新增了模块特性,在 JDK 11
以及更新的版本中运行上述程序需添加下述参数。
# 1. Compile options
--add-exports java.base/jdk.internal.misc=ALL-UNNAMED
--add-exports java.base/jdk.internal.access=ALL-UNNAMED
# 2. VM options
--add-opens java.base/jdk.internal.misc=ALL-UNNAMED
--add-opens java.base/jdk.internal.access=ALL-UNNAMED
2. Cleaner
直接内存并没有提供显示的销毁方法,因此最常见的方式即通过反射方式进行,在之前 NIO
文章中的已详细介绍,这里不再具体描述,往期直达:Java NIO介绍。
public void freeDemo() throws Exception {
ByteBuffer buffer = ByteBuffer.allocateDirect(64);
UsagePrinter.printDirectMemoryUsage();
if (buffer.isDirect()) {
// The "DirectBuffer" is provided method "cleaner()" to return a cleaner
String directBufferCls = "sun.nio.ch.DirectBuffer";
Method cleanerMethod = Class.forName(directBufferCls).getMethod("cleaner");
Object cleaner = cleanerMethod.invoke(buffer);
// JDK 8: -> "sun.misc.Cleaner"
// JDK 11: -> "jdk.internal.ref.Cleaner"
String cleanerCls = "sun.misc.Cleaner";
// When we get "cleaner" then we can call "clean()" to free memory
Method cleanMethod = Class.forName(cleanerCls).getMethod("clean");
cleanMethod.invoke(cleaner);
}
UsagePrinter.printDirectMemoryUsage();
}
同理若在 JDK 11
之后运行上述需要在 VM options
添加下述配置。
--add-opens java.base/sun.nio.ch=ALL-UNNAMED
--add-opens java.base/jdk.internal.ref=ALL-UNNAMED