跳到主要内容

10、JVM 调优实战 - JVM 直接内存概念分析

今天分析JVM直接内存问题:首先什么是直接内存。

1、直接内存(堆外内存)

 

直接内存有一种叫法,堆外内存。 直接内存( 堆外内存 ) 指的是 Java 应用程序通过直接方式从操作系统中申请的内存。这个差别与之前的堆、栈、方法区,那些内存都是经过了虚拟化。所 以严格来说,这里是指直接内存。

2、直接内存有哪些?

使用了 Java 的 Unsafe 类,做了一些本地内存的操作;

Netty 的直接内存( Direct Memory ),底层会调用操作系统的 malloc 函数;

JNI 或者 JNA 程序,直接操纵了本地内存,比如一些加密库;

JNI 是 Java Native Interface 的缩写,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植。

JNA( Java Native Access )提供一组 Java 工具类用于在运行期间动态访问系统本地库( native library :如 Window 的 dll )而不需要编写任何 Native/JNI 代码。 开发人员只要在一个 java 接口中描述目标 native library 的函数与结构, JNA 将自动实现 Java 接口到 native function 的映射。 JNA 是建立在 JNI 技术基础之上的一个 Java 类库,它使您可以方便地使用 java 直接访问动态链接库中的函数。 原来使用 JNI ,你必须手工用 C 写一个动态链接库,在 C 语言中映射 Java 的数据类型。

JNA 中,它提供了一个动态的 C 语言编写的转发器,可以自动实现 Java 和 C 的数据类型映射,你不再需要编写 C 动态链接库。 也许这也意味着,使用 JNA 技术比使用 JNI 技术调用动态链接库会有些微的性能损失。但总体影响不大,因为 JNA 也避免了 JNI 的一些平台配置的开销。

3、代码案例

(1)、 Unsafe 类,-XX:MaxDirectMemorySize 参数的大小限制对这种是无效的

import sun.misc.Unsafe;

import java.lang.reflect.Field;
/**
 * @author  
 * 参数无效:-XX:MaxDirectMemorySize=10m
 */
public class UnsafeDemo {
    public static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws Exception {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        long addr = unsafe.allocateMemory(100*_1MB);
    }
}

(2)、ByteBuffer 的这种方式,受到 MaxDirectMemorySize 参数的大小限制其实底层是

import java.nio.ByteBuffer;

/**
 * @author 
 * VM Args:-XX:MaxDirectMemorySize=100m
 * 限制最大直接内存大小100m
 * -XX:MaxDirectMemorySize=128m
 * -Xmx128m
 * -Xmx135m -Xmn100m -XX:SurvivorRatio=8
 * -Xmx138m -Xmn100m -XX:SurvivorRatio=8
 */
public class ByteBufferDemo {
    static ByteBuffer bb;
    public static void main(String[] args) throws Exception {
        //直接分配128M的直接内存
        bb = ByteBuffer.allocateDirect(128*1024*1024);
    }
}

 

4、为什么要使用直接内存

直接内存,其实就是不受 JVM 控制的内存。相比于堆内存有几个优势:

1、 减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作;

2、 加快了复制的速度因为堆内在flush到远程时,会先复制到直接内存(非堆内存),然后再发送,而堆外内存相当于省略掉了这个工作;

3、 可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现;

4、 可以扩展至更大的内存空间比如超过1TB甚至比主存还大的空间;

直接内存的另一面(负面)

直接内存有很多好处,我们还是应该要了解它的缺点:

1、 堆外内存难以控制,如果内存泄漏,那么很难排查;

2、 堆外内存相对来说,不适合存储很复杂的对象一般简单的对象比较适合;

5、直接内存案例和场景分析

内存泄漏案例

工作中经常会使用 Java 的 Zip 函数进行压缩和解压,这种操作在一些对传输性能较高的的场景经常会用到。

程序将会申请 1kb 的随机字符串,然后不停解压。为了避免让操作系统陷入假死状态,我们每次都会判断操作系统内存使用率,在达到 60% 的时候, 我们将挂起程序(不在解压,只不断的让线程休眠) 通过访问 8888 端口,将会把内存阈值提高到 85% 。

 

 

 

 

 

6、 linux环境运行:;

 

使用以下命令把程序跑起来:

java -cp ref-jvm3.jar -XX:+PrintGC -Xmx1G -Xmn1G -XX:+AlwaysPreTouch -XX:MaxMetaspaceSize=10M -XX:MaxDirectMemorySize=10Mex15.LeakProblem

 

7、 参数解释:;

分别使用 Xmx 、 MaxMetaspaceSize 、 MaxDirectMemorySize 这三个参数限制了堆、元空间、直接内存的大小。

AlwaysPreTouch 这个参数,在 JVM 启动的时候,就把它所有的内存在操作系统分配了,默认情况下,此选项是禁用的,并且所有页面都在 JVM 堆空间填 充时提交。我们为了减少内存动态分配的影响,把这个值设置为 True 。

这个程序很快就打印一下显示,这个证明操作系统内存使用率,达到了 60% 。

 

8、 通过top命令查看,确实有一个进程占用了很高的内存,;

VIRT virtual memory usage 虚拟内存

(1)、 进程 “ 需要的 ” 虚拟内存大小,包括进程使用的库、代码、数据等

(2)、假如进程申请 100m 的内存,但实际只使用了 10m ,那么它会增长 100m ,而不是实际的使用量

RES resident memory usage 常驻内存 达到了 1.5G

如果申请 100m 的内存,实际使用 10m ,它只增长 10m ,与 VIRT 相反

 

9、常规排查方式

按照之前的排查方式,如果碰到内存占用过高,我们使用 top 命令来跟踪,然后使用 jmap –heap 来显示

 

 

10、 我们发现这个3468的java进程,占据的堆空间是比较小的,合计数远远小于top命令看到的1.5G;

我们怀疑是不是虚拟机栈占用过高。于是使用 jstack 命令来看下线程

 

 

11、 发现也就那么10来个左右的线程,这块占用的空间肯定也不多;

jmap -histo 3468 | head -20 显示占用内存最多的对象

 

12、 发现这个才20多M,没有达到1.5G发现不了,我们前面学过MAT,我们把内存dump下来,放到MAT中进行分析;

 

 

发现没什么问题?堆空间也好,其他空间也好,这些都没有说的那么大的内存 1.5G 左右。

13、使用工具排查

这种情况应该是发生了直接内存泄漏。 如果要跟踪本地内存的使用情况,一般需要使用 NMT

NMT

NativeMemoryTracking ,是用来追踪 Native 内存的使用情况。通过在启动参数上加入 -XX:NativeMemoryTracking=detail 就可以启用。使用 jcmd ( jdk 自

带)命令,就可查看内存分配。

Native Memory Tracking (NMT) 是 Hotspot VM 用来分析 VM 内部内存使用情况的一个功能。我们可以利用 jcmd ( jdk 自带)这个工具来访问 NMT 的数据。

NMT 必须先通过 VM 启动参数中打开,不过要注意的是,打开 NMT 会带来 5%-10% 的性能损耗。

在服务器上重新运行程序:

java

-cp

ref-jvm3.jar

-XX:+PrintGC

-Xmx1G

-Xmn1G

-XX:+AlwaysPreTouch

-XX:MaxMetaspaceSize=10M

-XX:MaxDirectMemorySize=10M

-XX:NativeMemoryTracking=detail ex15.LeakProblem

 

14、 jcmd$pidVM.native_memorysummary;

可惜的是,这个工具并一样很烂,看到我们这种泄漏的场景。下面这点小小的空间,是不能和 1~2GB 的内存占用相比的。

 

15、 其实问题排查到这里,很明显了,这块的问题排查超出了一般java程序员的范畴了(说白了就是你做到这点就OK了,继续排查就是在干操作系统和其他的语言相关的问题排查了),如果你有时间,有兴趣,我推荐你使用perf这个工具,这个工具安装很容易,但是容易遇到操作系统内核一些功能没支持,也分析不了,这里我就不多去花时间去分析Perf安装:yuminstallperf;

到此直接内存的概念和示例就分析完了,大家一定要自己模拟测试几次。下篇我们实战分析直接内存的内存泄漏问题!