直接内存泄漏与JVM源码分析

直接内存(堆外内存)

直接内存泄漏与JVM源码分析_第1张图片

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

直接内存有哪些?

 使用了 Java 的 Unsafe 类,做了一些本地内存的操作;
 Netty 的直接内存(Direct Memory),底层会调用操作系统的 malloc 函数。
JNI 或者 JNA 程序,直接操纵了本地内存,比如一些加密库;
JNI 是 Java Native Interface 的缩写,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植。
JNA(Java Native Access )提供一组 Java 工具类用于在运行期间动态访问系统本地库(native library:如 Window 的 dll)而不需要编写任何 Native/JNI 代码。

下面通过代码来具体分析

  1. Unsafe 类,-XX:MaxDirectMemorySize 参数的大小限制对这种是无效的
/**
 * @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.
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);
    }
}

直接内存泄漏与JVM源码分析_第2张图片
直接内存泄漏与JVM源码分析_第3张图片
3.直接内存泄漏与JVM源码分析_第4张图片
如果这样配置参数,发现不会发生OOM。这里思考一下为什么138m的时候就不会发生oom了呢?

为什么要使用直接内存

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

直接内存的缺点

直接内存有很多好处,我们还是应该要了解它的缺点:
1、 堆外内存难以控制,如果内存泄漏,那么很难排查
2、 堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象比较适合。

直接内存案例和场景分析

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

package ex15;

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;

/**
 * @author  
 *
 *  -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);
            }
        }
    }
}

把上面的代码打成jar包部署到linux环境进行测试。我的虚拟机2核,4g内存

使用以下命令把程序跑起来:
java -cp ref-jvm3.jar -XX:+PrintGC -Xmx1G -Xmn1G -XX:+AlwaysPreTouch -XX:MaxMetaspaceSize=10M
-XX:MaxDirectMemorySize=10M ex15.LeakProblem
在这里插入图片描述

参数解释:
分别使用 Xmx、MaxMetaspaceSize、MaxDirectMemorySize 这三个参数限制了堆、元空间、直接内存的大小。
AlwaysPreTouch 这个参数,在 JVM 启动的时候,就把它所有的内存在操作系统分配了,默认情况下,此选项是禁用的,并且所有页面都在 JVM 堆空间填充时提交。我们为了减少内存动态分配的影响,把这个值设置为 True。
这个程序很快就打印一下显示,这个证明操作系统内存使用率,达到了 60%。
直接内存泄漏与JVM源码分析_第5张图片
通过 top 命令查看,确实有一个进程占用了很高的内存,
VIRT :virtual memory usage 虚拟内存
1、 进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据等
2、假如进程申请 100m 的内存,但实际只使用了 10m,那么它会增长 100m,而不是实际的使用量
RES :resident memory usage 常驻内存 达到了 1.4G
如果申请 100m 的内存,实际使用 10m,它只增长 10m,与 VIRT 相反
直接内存泄漏与JVM源码分析_第6张图片

常规排查方式

按照之前的排查方式,如果碰到内存占用过高,我们使用 top 命令来跟踪,然后使用 jmap –heap 来显示
直接内存泄漏与JVM源码分析_第7张图片
直接内存泄漏与JVM源码分析_第8张图片
我们发现这个 2741的 java 进程,占据的堆空间是比较小的,合计数远远小于 top 命令看到的 1.4G
我们怀疑是不是虚拟机栈占用过高。于是使用 jstack 命令来看下线程
直接内存泄漏与JVM源码分析_第9张图片
直接内存泄漏与JVM源码分析_第10张图片
发现也就那么 10 来个左右的线程,这块占用的空间肯定也不多。

jmap -histo 2741| head -20 显示占用内存最多的对象
直接内存泄漏与JVM源码分析_第11张图片
通过单位换算,发现内存大小不到1个G,还是差太多。
发现不了,我们前面用过 MAT,我们把内存 dump 下来,放到 MAT 中进行分析。
jmap -dump:live,format=b,file=heap.bin 2741
在这里插入图片描述
直接内存泄漏与JVM源码分析_第12张图片
发现MAT内存大小也对应不上。堆空间也好,其他空间也好,这些都没有说的那么大的内存 1.4G 左右。

使用工具排查

这种情况应该是发生了直接内存泄漏。、
如果你有时间,有兴趣,我推荐你使用 perf 这个工具,这个工具安装很容易,但是容易遇到操作系统内核一些功能没支持,
也分析不了,这里我就不多去花时间去分析:
Perf 安装:yum install perf

内存泄漏问题解决

这个程序可以访问服务器的 8888 端口,这将会把内存使用的阈值增加到 85%,我们的程序会逐渐把这部分内存占满
curl http://127.0.0.1:8888/

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

直接内存泄漏与JVM源码分析_第13张图片
直接内存泄漏与JVM源码分析_第14张图片
直接内存泄漏与JVM源码分析_第15张图片
直接内存泄漏与JVM源码分析_第16张图片
直接内存泄漏与JVM源码分析_第17张图片
由此可以定位到native修饰的本地方法,可见与直接内存有关。

问题修复

调用 close() 方法来主动释放,放置内存泄漏
直接内存泄漏与JVM源码分析_第18张图片
在这里插入图片描述
Top –p 查看,这个内存和堆内存的情况就比较符合
直接内存泄漏与JVM源码分析_第19张图片

问题得到解决。

直接内存总结

直接内存主要是通过 DirectByteBuffer 申请的内存,可以使用参数“MaxDirectMemorySize”来限制它的大小
其他堆外内存(直接内存),主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申请的内存。这种情况下没有任何参数能够阻挡它们,要么靠它自己去释放一些内存,要么等待操作系统对它来处理。所以如果你对操作系统底层以及内存分配使用不熟悉,最好不要使用这块,尤其是 Unsafe 或者其他 JNI 手段直接直接申请的内存.
那为什么要讲呢,EhCache 这种缓存框架,提供了多种策略,可以设定将数据存储在非堆上。
还有像 RocketMQ 都走了堆外分配,所以我们又必须要去了解他。
通过jvm,openjdk源码分析,直接内存

直接内存泄漏与JVM源码分析_第20张图片
在这里可以看到,值 新生代的最大值 = 新生代的最大值 - 一个 survivor 的大小。
为什么会这样,因为在新生代采用复制回收算法,一个幸存者区域是浪费的,所以实际空间最大大小要减去一个交换器的大小。
而老年代是没有空间浪费的,所以还是全区域。
也 得出 我们设置的-Xmx 的值里 减去 一个 survivor 的大小就是默认的堆外内存的大小

你可能感兴趣的:(jvm)