前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【JVM调优实战100例】05——方法区调优实战(下)

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

作者头像
用户10127530
发布2022-10-26 18:26:28
4340
发布2022-10-26 18:26:28
举报
文章被收录于专栏:半旧的技术栈半旧的技术栈

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

7.8 直接内存

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

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

代码语言:javascript
复制
/**
 * 演示 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进行垃圾回收,可能导致内存泄漏问题。运行如下代码。

代码语言:javascript
复制
/**
 * 演示直接内存溢出
 */
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 对方法区的实现称为元空间
    }
}

输出结果。

代码语言:javascript
复制
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)

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

代码语言:javascript
复制
/**
 * 禁用显式回收对直接内存的影响
 */
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对象,演示直接内存分配的底层原理。

代码语言:javascript
复制
/**
 * 直接内存分配的底层原理: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对象。

代码语言:javascript
复制
public static ByteBuffer allocateDirect(int capacity) {
      return new DirectByteBuffer(capacity);
}

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

代码语言:javascript
复制
    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。

代码语言:javascript
复制
  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源码。

代码语言:javascript
复制
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创建,过程就清楚了。

代码语言:javascript
复制
 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。

代码语言:javascript
复制
/**
 * 禁用显式回收对直接内存的影响
 */
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对象来分配、回收内存。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-07-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 7.8 直接内存
  • 7.9 直接内存的内存溢出问题
  • 7.10 禁用显式垃圾回收对直接内存的影响
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档