当前位置:网站首页>【JVM调优实战100例】05——方法区调优实战(下)

【JVM调优实战100例】05——方法区调优实战(下)

2022-07-06 17:11:00 半旧518

前 言
作者简介:半旧518,长跑型选手,立志坚持写10年博客,专注于java后端
专栏简介:实战案例驱动介绍JVM知识,教你用JVM排除故障、评估代码、优化性能
文章简介:介绍方法区概念、帮助你深入理结直接内存

7.8 直接内存

直接内存由操作系统来管理。常见于NIO,用于数据缓冲,读写性能很高,分配回收花销较高。

使用以下代码来比较使用传统方式读写与NIO读写的区别,注意第一次启动读写性能会较差,需多运行几次,计算平均值。

/**
 * 演示 ByteBuffer 作用
 */
public class Demo1_9 {
    static final String FROM = "F:\\博客\\谷粒学院实践项目.md";
    static final String TO = "F:\\谷粒学院实践项目.md";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // io 用时:1535.586957 1766.963399 1359.240226
        directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
    }

    private static void directBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
             FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
            while (true) {
                int len = from.read(bb);
                if (len == -1) {
                    break;
                }
                bb.flip();
                to.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
    }

    private static void io() {
        long start = System.nanoTime();
        try (FileInputStream from = new FileInputStream(FROM);
             FileOutputStream to = new FileOutputStream(TO);
        ) {
            byte[] buf = new byte[_1Mb];
            while (true) {
                int len = from.read(buf);
                if (len == -1) {
                    break;
                }
                to.write(buf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io 用时:" + (end - start) / 1000_000.0);
    }
}

为什么直接内存读写效率高?使用阻塞式io进行读写cpu和内存的变化如下图。很显然,从系统缓存区将文件复制到java缓存区是一个耗时且不必要的复制。

在这里插入图片描述

使用Nio进行读写cpu和内存的变化如下图。操作系统在allocateDirect()方法执行时会分配一块直接内存,这部分内存java代码和系统都可以进行访问。

在这里插入图片描述

7.9 直接内存的内存溢出问题

直接内存direct memory并不由jvm进行垃圾回收,可能导致内存泄漏问题。运行如下代码。

/**
 * 演示直接内存溢出
 */
public class Demo1_10 {
    static int _100Mb = 1024 * 1024 * 100;

    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
        }
        // 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
        //                  jdk8 对方法区的实现称为元空间
    }
}

输出结果。

72
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:695)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at cn.itcast.jvm.t1.direct.Demo1_10.main(Demo1_10.java:19)

直接内存的底层回收机制是怎样的呢?运行以下代码。

/**
 * 禁用显式回收对直接内存的影响
 */
public class Demo1_26 {
    static int _1Gb = 1024 * 1024 * 1024;

    /*
     * -XX:+DisableExplicitGC 显式的
     */
    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,Full GC
        System.in.read();
    }
}

在控制台输出分配完毕后,从后台的任务管理器可以看到其内存占用情况。

在这里插入图片描述

当在控制台输入回车,输出开始释放,再输入回车,这个占用1个G的内存的进程就被清理了。是不是意味着java的gc操作发生了作用呢?

下面我们来解析上面直接内存回收的过程。Unsafe是jdk底层的一个类,用于内存分配,内存回收等,一般普通程序员无需使用,这里我们通过反射获取Unsafe对象,演示直接内存分配的底层原理。

/**
 * 直接内存分配的底层原理:Unsafe
 */
public class Demo1_27 {
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.in.read();

        // 释放内存
        unsafe.freeMemory(base);
        System.in.read();
    }

    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

运行代码,在任务管理器观察jdk进程内存占用发现,内存占用会在allocateMemory()后增加1G,在freeMemory()后恢复。因此,直接内存的回收其实不是由jvm虚拟机完成,而是通过Unsafe对象调用freeMemory()完成。

下面查看ByteBuffer类的源码来验证我们的观点。

allocateDirect()中返回一个DirectByteBuffer对象。

public static ByteBuffer allocateDirect(int capacity) {
      return new DirectByteBuffer(capacity);
}

调用Unsafe中allocateMemory()来实现申请内存,新建Cleaner对象来释放内存。

    DirectByteBuffer(int cap) {                   // package-private

        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;



    }

cleaner中关联的Deallocator是什么?点进去看发现它实现了Runnable,是回调任务对象,在run方法中调用了Unsafe的freeMemory。

  private static class Deallocator
        implements Runnable
    {

        private static Unsafe unsafe = Unsafe.getUnsafe();

        private long address;
        private long size;
        private int capacity;

        private Deallocator(long address, long size, int capacity) {
            assert (address != 0);
            this.address = address;
            this.size = size;
            this.capacity = capacity;
        }

        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }

    }

那么垃圾回收的任务什么时候被执行的呢?看Cleaner源码。

public class Cleaner
    extends PhantomReference<Object> {
    //...
    public void clean() {
        if (!remove(this))
            return;
        try {
            thunk.run();
        } catch (final Throwable x) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null)
                            new Error("Cleaner terminated abnormally", x)
                                .printStackTrace();
                        System.exit(1);
                        return null;
                    }});
        }
    }
    //...
    }

原来Cleaner是java中的虚引用类型,当它的绑定的对象被垃圾回收时,会触发虚引用的clean()方法,执行回调方法run()。

下面回过头看DirectByteBuffer类中的Cleaner创建,过程就清楚了。

 cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

总结直接内存分配、释放的的过程就是:通过调用Unsafe的allocateMemory来分配直接内存,通过创建虚引用对象Cleaner对象,将DirectoryByteBuffer与回调任务绑定,当Directory被垃圾回收时,会自动执行Cleaner的clean()方法,来调用Unsafe的freeMemory()释放内存。

7.10 禁用显式垃圾回收对直接内存的影响

在java中可以采用System.gc()来显式的建议jvm进行垃圾回收,但这种垃圾回收方式是Full GC,既会进行新生代的回收,也会进行老年代的回收。可能会影响程序性能。为了避免程序员误用,可以使用-XX +DisableExplctGC 来禁用显示的垃圾回收。

在禁用了显式垃圾回收后再次运行Demo1_26。

/**
 * 禁用显式回收对直接内存的影响
 */
public class Demo1_26 {
    static int _1Gb = 1024 * 1024 * 1024;

    /*
     * -XX:+DisableExplicitGC 显式的
     */
    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,Full GC
        System.in.read();
    }
}

以上代码的直接内存并没有被回收,这是因为显式的垃圾回收失效。bytebuffer不会被垃圾回收,进而导致直接内存无法被释放,只有在程序被动进行Full GC时进行垃圾回收。如果在程序需要频繁使用直接内存的情况,我们可以收到使用Unsafe对象来分配、回收内存。

原网站

版权声明
本文为[半旧518]所创,转载请带上原文链接,感谢
https://blog.csdn.net/qq_41708993/article/details/125628024