Java运行时内存监控


我们都知道在 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

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录