三种了解使用内存的方法
#java #性能 #jmx

我想解决什么任务

最近我试图知道某些操作期间的应用程序废物多少。
我要测量的操作是数据快照创建。
该应用程序存储了一个用对象的操作序列,并定期创建对象快照。
因此,如果我们想在任何时间点都知道对象的状态,我们将不会从头到尾重播所有操作,而是从最近的快照中获取对象的状态,而只播放有限的操作,从此快照到我们的操作点。

显然,我们想在堆内存中存储尽可能多的快照,但是在现实世界中,我们的内存有限。我们应该限制快照的内存量,并且我们传统上通过通过应用程序属性设置阈值来进行此操作。

下一步,我们要为此属性提供一些有意义的默认值。
这里的问题是,我们无法预测价值,因为不同的客户有不同的:环境,数据,快照的大小等。
这意味着我们需要有更多数据来做出决定。

最后,我们决定记录在快照创建之前和之后占据了多少堆,并以这样的方式结束:

public Snapshot buildSnapshot() {
    long usedMemory = calculateUsedMemory();
    try {
        return doBuildSnapshot();
    } finally {
        log.debug("Used memory: " + (calculateUsedMemory() - usedMemory));
    }
}

冷0

如果您Google“ Java在运行时使用了内存”,则会看到此方法:Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()及其不同的变体。

让我们仔细看看这种方法返回我们。

用于总存储器:

返回Java虚拟机中的总内存量。该方法返回的值可能会随着时间而变化,具体取决于主机环境。

免费内存:

返回Java虚拟机中的免费内存量。调用GC方法可能会导致增加freememory返回的值。

换句话说,总内存可能包含不再需要的对象的引用。该对象将在垃圾收集后释放,但是我们对GC没有任何控制权,也不知道何时执行。

因此我们需要找到一些更好的解决方案。

JMX方式

JMX来自 Java管理扩展。我们可能会使用这些内置扩展名来获取有关运行时内部设备的不同信息。

java.lang.management.ManagementFactory中,我们可能会找到很多用于我们喜欢的任务的工厂方法,但是我们对MemoryMXBean getMemoryMXBean()最感兴趣。

该方法返回manageble bean我们可以用来请求堆内存使用。

private static final MemoryMXBean MEMORY_MX_BEAN = ManagementFactory.getMemoryMXBean();

private long calculateUsedMemory() {
    return  MEMORY_MX_BEAN.getHeapMemoryUsage().getUsed();
}

到目前为止,很好,但是上面的方法仅在某个特定时间显示整个应用程序的内存,我们仍然需要找出应用程序浪费在存储快照时的内存。

让我们从另一点看一下任务,我们用来完成操作的内存。

换句话说,从内存使用的角度来看,代码的效果有多有效?
知道这一点和GC信息,我们可以预测默认值。

秘密JMX方式

JMX具有两个ThreadMXBean接口,java.lang.management.ThreadMXBeancom.sun.management.ThreadMXBean
两者都可以用来获取有关线程的信息。

第一个是公开的,并由ManagementFactory返回,但没有任何与内存有关的方法。

第二个声明2种出色的方法:

  • long getThreadAllocatedBytes(long id)-返回一个内存总量的近似值,以字节为单位,分配在堆内存中的每个线程中,其ID在输入数组ID中。
  • long getCurrentThreadAllocatedBytes()-返回内存总量的近似值,以字节为单位,分配给当前线程中的堆内存。 超过com.sun.management.ThreadMXBean扩展了java.lang.management.ThreadMXBean,因此我们可以使用该接口而不是第一个接口。

但是您找不到任何返回这些接口的方法。

如果会查看ThreadMXBean层次结构,我可能会看到,在我的JDK中,两个接口都是由单个类实现的。
ThreadMxBean hierarchy

我已经使用了Amazon Correto 11,但是在其他JDK中使用了相同的图片(在Eclipse TemurinAzul上检查)。

现在我的代码看起来像这样:

private final LongSupplier allocatedByThread = initAllocatedMemoryProvider(); 

private static LongSupplier initAllocatedMemoryProvider() {  

    ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();  
    if (threadMXBean instanceof com.sun.management.ThreadMXBean) {  
        com.sun.management.ThreadMXBean casted = (com.sun.management.ThreadMXBean) threadMXBean;  
        return casted::getCurrentThreadAllocatedBytes;  
    }  
    return () -> 0;  
}

private long calculateUsedMemory() {
    return initAllocatedMemoryProvider().getAsLong();
}

问题仅在于jdk.management模块中的com.sun.management.ThreadMXBean,因此,如果客户使用JRE运行我们的应用程序,我们将不会在类路径中使用该界面。该应用程序将与NoClassDefFoundError混在一起,但我们可以使用反射API进行修复。

private static LongSupplier initAllocatedMemoryProvider() {  
    try {  
        Class<?> internalIntf = Class.forName("com.sun.management.ThreadMXBean");  
        ThreadMXBean bean = ManagementFactory.getThreadMXBean();  
        if (!internalIntf.isAssignableFrom(bean.getClass())) {  
            // Attempts to get the interface from PlatformMXBean
            Class<?> pmo = Class.forName("java.lang.management.PlatformManagedObject");  
            Method m = ManagementFactory.class.getMethod("getPlatformMXBean", Class.class, pmo);  
            bean = (ThreadMXBean) m.invoke(null, internalIntf);  
            if (bean == null) {  
                throw new UnsupportedOperationException("No way to access private ThreadMXBean");  
            }  
        }  

        ThreadMXBean allocMxBean = bean;  
        Method allocMxBeanGetter = internalIntf.getMethod("getCurrentThreadAllocatedBytes");  

        return () -> (long)allocMxBeanGetter.invoke(allocMxBean);
    } catch (Exception e) {  
        return () -> 0;  
    } 
}

结论

您可能会使用JMX访问不同的运行时信息,但是如果您稍微深入研究,您可能会发现宝藏。

您可以在GitHub上找到代码。

如果您发现该帖子有帮助,请支持我和:
Buy Me A Coffee