直接内存

  • 1. 直接内存
  • 1.1 直接内存有哪些
  • 1.2 代码案例
  • 2. 为什么要使用直接内存
  • 3. 直接内存的缺点
  • 4. 直接内存案例和场景分析
  • 4.1 内存泄漏案例
  • 4.2 常规排查方式
  • 4.3 使用工具排查
  • 4.4 内存泄漏问题解决
  • 5. 堆外内存默认多大
  • 6. 直接内存总结


1. 直接内存

Java 应用程序通过直接方式从操作系统中申请的内存

1.1 直接内存有哪些

  1. 使用了 Java 的 Unsafe 类,做了一些本地内存的操作;
  2. Netty 的直接内存(Direct Memory),底层会调用操作系统的 malloc 函数;
  3. 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 的一些平台配置的开销。

1.2 代码案例

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

java nmt code内存很大_JVM


2、ByteBuffer 的这种方式,受到 MaxDirectMemorySize 参数的大小限制

java nmt code内存很大_java_02


其实底层是

java nmt code内存很大_jvm_03


java nmt code内存很大_JVM_04


java nmt code内存很大_JVM_05

2. 为什么要使用直接内存

直接内存,其实就是不受 JVM 控制的内存。相比于堆内存有几个优势:
1、减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作。
2、加快了复制的速度。因为堆内在 flush 到远程时,会先复制到直接内存(非堆内存),然后再发送,而堆外内存相当于省略掉了这个工作。
3、可以在进程间共享,减少 JVM 间的对象复制,使得 JVM 的分割部署更容易实现。
4、可以扩展至更大的内存空间。比如超过 1TB 甚至比主存还大的空间。

3. 直接内存的缺点

1、 堆外内存难以控制,如果内存泄漏,那么很难排查
2、 堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象比较适合。

4. 直接内存案例和场景分析

4.1 内存泄漏案例

  • 工作中经常会使用 Java 的 Zip 函数进行压缩和解压,这种操作在一些对传输性能较高的的场景经常会用到。
  • 程序将会申请 1kb 的随机字符串,然后不停解压。为了避免让操作系统陷入假死状态,我们每次都会判断操作系统内存使用率,在达到 60% 的时候,我们将挂起程序(不在解压,只不断的让线程休眠)
  • 通过访问 8888 端口,将会把内存阈值提高到 85%。
package demo;

import com.sun.management.OperatingSystemMXBean;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpServer;

import java.io.*;
import java.lang.management.ManagementFactory;
import java.net.InetSocketAddress;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 *
 *  -XX:+PrintGC -Xmx1G -Xmn1G
 *  -XX:+AlwaysPreTouch
 *  -XX:MaxMetaspaceSize=10M
 *  -XX:MaxDirectMemorySize=10M
 */
public class LeakProblem {
    /**
     * 构造随机的字符串
     */
    public static String randomString(int strLength) {
        Random rnd = ThreadLocalRandom.current();
        StringBuilder ret = new StringBuilder();
        for (int i = 0; i < strLength; i++) {
            boolean isChar = (rnd.nextInt(2) % 2 == 0);
            if (isChar) {
                int choice = rnd.nextInt(2) % 2 == 0 ? 65 : 97;
                ret.append((char) (choice + rnd.nextInt(26)));
            } else {
                ret.append(rnd.nextInt(10));
            }
        }
        return ret.toString();
    }
    //复制方法
    public static int copy(InputStream input, OutputStream output) throws IOException {
        long count = copyLarge(input, output);
        return count > 2147483647L ? -1 : (int) count;
    }
    //复制方法
    public static long copyLarge(InputStream input, OutputStream output) throws IOException {
        byte[] buffer = new byte[4096];
        long count = 0L;

        int n;
        for (; -1 != (n = input.read(buffer)); count += (long) n) {
            output.write(buffer, 0, n);
        }

        return count;
    }
    //解压
    public static String decompress(byte[] input) throws Exception {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        copy(new GZIPInputStream(new ByteArrayInputStream(input)), out);
        return new String(out.toByteArray());
    }
    //压缩
    public static byte[] compress(String str) throws Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        GZIPOutputStream gzip = new GZIPOutputStream(bos);
        try {
            gzip.write(str.getBytes());
            gzip.finish();
            byte[] b = bos.toByteArray();
            return b;
        }finally {
            try { gzip.close(); }catch (Exception ex ){}
            try { bos.close(); }catch (Exception ex ){}
        }
    }


    private static OperatingSystemMXBean osmxb = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();

    //通过MXbean来判断获取内存使用率(系统)
    public static int memoryLoad() {
        double totalvirtualMemory = osmxb.getTotalPhysicalMemorySize();
        double freePhysicalMemorySize = osmxb.getFreePhysicalMemorySize();

        double value = freePhysicalMemorySize / totalvirtualMemory;
        int percentMemoryLoad = (int) ((1 - value) * 100);
        return percentMemoryLoad;
    }


    private static volatile int RADIO = 60;

    public static void main(String[] args) throws Exception {
        //模拟一个http请求--提高内存阈值
        HttpServer server = HttpServer.create(new InetSocketAddress(8888), 0);
        HttpContext context = server.createContext("/");
        context.setHandler(exchange -> {
            try {
                RADIO = 85;
                String response = "OK!";
                exchange.sendResponseHeaders(200, response.getBytes().length);
                OutputStream os = exchange.getResponseBody();
                os.write(response.getBytes());
                os.close();
            } catch (Exception ex) {
            }
        });
        server.start();


        //构造1kb的随机字符串
        int BLOCK_SIZE = 1024;
        String str = randomString(BLOCK_SIZE / Byte.SIZE);
        //字符串进行压缩
        byte[] bytes = compress(str);
        for (; ; ) {
            int percent = memoryLoad();
            if (percent > RADIO) {//如果系统内存使用率达到阈值,则等待1s
                System.out.println("memory used >"+RADIO+"  hold 1s");
                Thread.sleep(1000);
            } else {
                //不断对字符串进行解压
                decompress(bytes);
                Thread.sleep(1);
            }
        }
    }
}
  • 程序打包上传到 CenterOS 的服务器中(服务器内存设置为 4G)
  • 使用以下命令把程序跑起来
    java -cp JVM.jar -XX:+PrintGC -Xmx1G -Xmn1G -XX:+AlwaysPreTouch -XX:MaxMetaspaceSize=10M -XX:MaxDirectMemorySize=10M demo.LeakProblem
  • 分别使用 Xmx、MaxMetaspaceSize、MaxDirectMemorySize 这三个参数限制了堆、元空间、直接内存的大小。
  • AlwaysPreTouch 这个参数,在 JVM 启动的时候,就把它所有的内存在操作系统分配了,默认情况下,此选项是禁用的,并且所有页面都在 JVM 堆空间填 充时提交。我们为了减少内存动态分配的影响,把这个值设置为 True。
  • 这个程序很快就打印一下显示,这个证明操作系统内存使用率,达到了 60%。
  • 通过 top 命令查看,确实有一个进程占用了很高的内存,
  • VIRT:virtual memory usage 虚拟内存
    1、 进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据等
    2、假如进程申请 100m 的内存,但实际只使用了 10m,那么VIRT = 100m,而不是实际的使用量
  • RES:resident memory usage 常驻内存 达到了 1.338G
    如果申请 100m 的内存,实际使用 10m,那么RES = 10m,与 VIRT 相反
  • 访问服务器的 8888 端口,将内存使用的阈值增加到 85%
    curl http://127.0.0.1:8888/

4.2 常规排查方式

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

  • 这个 2879 的 java 进程,占据的堆空间是1G,合计数小于 top 命令看到的 2.25G
  • 使用 jstack 命令来看下此线程的虚拟机栈占用情况
  • java nmt code内存很大_Java_07


  • java nmt code内存很大_JVM_08


  • java nmt code内存很大_jvm_09

  • 发现只有 10 来个线程,这块占用的空间肯定也不多。
  • jmap -histo 2879 | head -20 显示占用内存最多的对象
  • java nmt code内存很大_java_10

  • 全部加起来只有20多M,没有达到2.25G
  • jmap -dump:live,format=b,file=heap.bin 2879,把内存 dump 下来,放到 MAT 中进行分析。
  • java nmt code内存很大_java nmt code内存很大_11

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

4.3 使用工具排查

  • NativeMemoryTracking,是用来追踪 Native 内存的使用情况。通过在启动参数上加入 -XX:NativeMemoryTracking=detail 就可以启用。使用 jcmd (jdk 自带)命令,就可查看内存分配。
  • Native Memory Tracking (NMT) 是 Hotspot VM 用来分析 VM 内部内存使用情况的一个功能。我们可以利用 jcmd(jdk 自带)这个工具来访问 NMT 的数据。
  • NMT 必须先通过 VM 启动参数中打开,不过要注意的是,打开 NMT 会带来 5%-10%的性能损耗。
  • 在服务器上重新运行程序:
    java -cp JVM.jar -XX:+PrintGC -Xmx1G -Xmn1G -XX:+AlwaysPreTouch -XX:MaxMetaspaceSize=10M -XX:MaxDirectMemorySize=10M -XX:NativeMemoryTracking=detail demo.LeakProblem

jcmd 3344 VM.native_memory summary

java nmt code内存很大_java_12


java nmt code内存很大_java_13

  • 这个值远没有达到1.338G的内存占用。

4.4 内存泄漏问题解决

  • 问题关键点
    GZIPInputStream 使用 Inflater 申请堆外内存、我们没有调用 close() 方法来主动释放。如果忘记关闭,Inflater 对象的生命会延续到下一次 GC,有一点类似堆内的弱引用。在此过程中,堆外内存会一直增长。

java nmt code内存很大_java_14


java nmt code内存很大_java_15


java nmt code内存很大_java_16

  • 问题修复
    调用 close() 方法来主动释放,放置内存泄漏
  • 修改之后,重新启动
  • 问题得到解决

5. 堆外内存默认多大

没有显示的设置-XX:MaxDirectMemorySize 参数的情况下,通过 ByteBuffer 能够分配的直接内存空间大小就是堆的最大的 可使用 的大小。
堆的最大的可使用的大小= 堆的最大值- 一个 Survivor 的大小(浪费的空间)

  • VM 参数配置:-Xmx135m -Xmn100m -XX:SurvivorRatio=8
  • 程序报错,堆的最大的可使用的大小=135-10m=125m ,不能分配 128M 的对象
  • VM 参数配置:-Xmx138m -Xmn100m -XX:SurvivorRatio=8
  • 堆的最大的可使用的大小=138-10m=128m ,刚好可以分配 128M 的对象

6. 直接内存总结

  • 直接内存主要是通过 DirectByteBuffer 申请的内存,可以使用参数“MaxDirectMemorySize”来限制它的大小
  • 其他堆外内存(直接内存),主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申请的内存。这种情况下没有任何参数能够阻挡它们,要么靠它自己去释 放一些内存,要么等待操作系统对它来处理。所以如果你对操作系统底层以及内存分配使用不熟悉,最好不要使用这块,尤其是 Unsafe 或者其他 JNI 手段直接直接申请的内存。