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


    前 言
    🍉 作者简介:半旧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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    为什么直接内存读写效率高?使用阻塞式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 对方法区的实现称为元空间
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    输出结果。

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    /**
     * 禁用显式回收对直接内存的影响
     */
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

    在这里插入图片描述

    当在控制台输入回车,输出开始释放,再输入回车,这个占用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);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

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

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

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

    public static ByteBuffer allocateDirect(int capacity) {
          return new DirectByteBuffer(capacity);
    }
    
    • 1
    • 2
    • 3

    调用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;
    
    
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    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);
            }
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    那么垃圾回收的任务什么时候被执行的呢?看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;
                        }});
            }
        }
        //...
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

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

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

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

    总结直接内存分配、释放的的过程就是:通过调用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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

  • 相关阅读:
    对象存储解决方案:高性能分布式对象存储系统MinIO
    STM32CubeMX教程13 ADC - 单通道转换
    计算机毕业设计Java银行招聘系统设计(系统+程序+mysql数据库+Lw文档)
    行为型模式-中介者模式
    【Java】栈和队列的模拟实现(包括循环队列)
    docker简单实战
    excel用RAND函数、或者RAND.NV函数生成随机数、这两个函数的区别
    设计模式——2_A 访问者(Visitor)
    C++ - std::string字符串格式化方法总结
    SQLite 简介
  • 原文地址:https://blog.csdn.net/qq_41708993/article/details/125628024