详细总结Java堆栈内存、堆外内存、零拷贝浅析与代码实现

 更新时间:2021年05月14日 08:41:23   作者:JiangNanMax  
零拷贝,这是个耳熟能详的名词,是开发岗面试中经常提及的问题.最近在回顾Netty的基础原理,还是把NIO中关于堆外内存的知识点过了一遍,这里就针对堆栈内存 堆外内存和零拷贝这几个概念以及相关知识做一下记录,需要的朋友可以参考下

一、堆栈内存

堆栈内存,顾名思义,指的是堆内存以及栈内存,其中,堆内存是由Java GC进行管理的内存区域,而栈内存则是线程内存。关于栈内存,这里不去细说。以Hotspot为例,堆内存的简要结构如下图所示:

在这里插入图片描述

而堆栈的关系,我们可以通过一行简单的代码来理解:

public static void main(String[] args) {
    Object o = new Object();
}

上述代码主要完成了两件事,new Object( ) 在堆上开辟了一块内存,也就是说,new Object( )是分配在堆上的;而变量o,则是在线程main的栈上面的,它指向了new Object( ) 开辟的堆内存地址。简单来说,程序中创建的对象,都存储在堆内存中,栈内存包含对它的引用。

二、堆外内存

简单来说,除了堆栈内存,剩下的就都是堆外内存了(当然,这是从Java运行时内存的角度来看),堆外内存直接受操作系统管理,而不是虚拟机。而使用堆外内存的原因,主要有几点:

  • 一定程度上减少了GC,堆外内存是直接受操作系统管理的,而不是JVM,因此使用堆外内存的话,就可以保持一个比较小的堆内内存,减少垃圾回收对程序性能的影响。这一块,在Kafka中就应用得很好,感兴趣的同学可以去了解一下;
  • 还有一个更大的优点,就是提高IO操作的效率!这里就涉及用户态与内核态,以及内核缓冲区的概念,具体可以看笔者之前的一篇文章Java随笔记 - 内核缓冲区与进程缓冲区。其中,堆内内存其实就是用户进程的进程缓冲区,属于用户态,而堆外内存由操作系统管理,属于内核态。如果从堆内向磁盘写数据,数据会被先复制到堆外内存,即内核缓冲区,然后再由OS写入磁盘,但使用堆外内存的话则可以避免这个复制操作。

在这里插入图片描述

三、零拷贝

总结上述内容中对堆栈内存与堆外内存的说明,主要解决了两个疑问:“零拷贝”是从哪拷贝到哪?“零拷贝”是怎么优化掉这一拷贝操作的?

  • 用户进程需要像磁盘写数据时,需要将用户缓冲区(堆内内存)中的内容拷贝到内核缓冲区(堆外内存)中,操作系统再将内核缓冲区中的内容写进磁盘中;
  • 通过在用户进程中,直接申请堆外内存,存储其需要写进磁盘的数据,就能够省掉上述拷贝操作。

在Java中,提供了一些使用堆外内存以及DMA的方法,能够在很大程度上优化用户进程的IO效率。这里,给出一份拷贝文件的代码,分别使用BIO、NIO和使用堆外内存的NIO进行文件复制,简单对比其耗时。

这里我使用一个200MB左右的pdf文件进行拷贝操作,你可以另外指定更大的文件,文件越大对比越明显。这里我运行出来的延时,BIO的平均耗时1500ms上下,NIO耗时120ms左右, 使用堆外内存的NIO耗时100ms上下。

package top.jiangnanmax.nio;

import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

/**
 * @author jiangnanmax
 * @email jiangnanmax@gmail.com
 * @description CopyCompare
 * @date 2021/5/7
 **/

public class CopyCompare {

    public static void main(String[] args) throws Exception {
        String inputFile = "/tmp/nio/input/HyperLedger.pdf";
        String outputFile = "/tmp/nio/output/HyperLedger.pdf";

        long start = System.currentTimeMillis();

        nioCopyByDirectMem(inputFile, outputFile);

        long end = System.currentTimeMillis();

        System.out.println("cost time: " + (end - start) + " ms");

        deleteFile(outputFile);
    }

    /**
     * 使用传统IO进行文件复制
     *
     * 平均耗时 15** ms
     *
     * @param sourcePath
     * @param destPath
     */
    private static void bioCopy(String sourcePath, String destPath) throws Exception {
        File sourceFile = new File(sourcePath);
        File destFile = new File(destPath);
        if (!destFile.exists()) {
            destFile.createNewFile();
        }

        FileInputStream inputStream = new FileInputStream(sourceFile);
        FileOutputStream outputStream = new FileOutputStream(destFile);

        byte[] buffer = new byte[512];
        int lenRead;

        while ((lenRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, lenRead);
        }

        inputStream.close();
        outputStream.close();
    }

    /**
     * 使用NIO进行文件复制,但不使用堆外内存
     *
     * 平均耗时 1** ms, 比BIO直接快了一个数量级???
     *
     * @param sourcePath
     * @param destPath
     */
    private static void nioCopy(String sourcePath, String destPath) throws Exception {
        File sourceFile = new File(sourcePath);
        File destFile = new File(destPath);
        if (!destFile.exists()) {
            destFile.createNewFile();
        }

        FileInputStream inputStream = new FileInputStream(sourceFile);
        FileOutputStream outputStream = new FileOutputStream(destFile);

        FileChannel inputChannel = inputStream.getChannel();
        FileChannel outputChannel = outputStream.getChannel();

        // transferFrom底层调用的应该是sendfile
        // 直接在两个文件描述符之间进行了数据传输
        // DMA

        outputChannel.transferFrom(inputChannel, 0, inputChannel.size());

        inputChannel.close();
        outputChannel.close();
        inputStream.close();
        outputStream.close();

    }

    /**
     * 使用NIO进行文件复制,并使用堆外内存
     *
     * 平均耗时100ms上下,比没使用堆外内存的NIO快一点
     *
     * @param sourcePath
     * @param destPath
     */
    private static void nioCopyByDirectMem(String sourcePath, String destPath) throws Exception {
        File sourceFile = new File(sourcePath);
        File destFile = new File(destPath);
        if (!destFile.exists()) {
            destFile.createNewFile();
        }

        FileInputStream inputStream = new FileInputStream(sourceFile);
        FileOutputStream outputStream = new FileOutputStream(destFile);

        FileChannel inputChannel = inputStream.getChannel();
        FileChannel outputChannel = outputStream.getChannel();

        MappedByteBuffer buffer = inputChannel.map(FileChannel.MapMode.READ_ONLY, 0, inputChannel.size());

        outputChannel.write(buffer);

        inputChannel.close();
        outputChannel.close();
        inputStream.close();
        outputStream.close();

    }

    /**
     * 删除目标文件
     *
     * @param target
     */
    private static void deleteFile(String target) {
        File file = new File(target);
        file.delete();
    }

}

到此这篇关于详细总结Java堆栈内存、堆外内存、零拷贝浅析与代码实现的文章就介绍到这了,更多相关Java堆栈内存 堆外内存 零拷贝浅析内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java实现的日期时间转换工具类完整示例

    java实现的日期时间转换工具类完整示例

    这篇文章主要介绍了java实现的日期时间转换工具类,结合完整实例形式分析了java针对日期时间常见的转换、计算、格式化等相关操作与封装技巧,需要的朋友可以参考下
    2019-10-10
  • Spring的Bean生命周期之BeanDefinition详解

    Spring的Bean生命周期之BeanDefinition详解

    这篇文章主要介绍了Spring的Bean生命周期之BeanDefinition详解,在spring bean创建过程 依赖 BeanDefinition 中的信息处理bean的生产,BeanDefinition 是 Spring Framework 中定义 Bean 的配置元信息接口,需要的朋友可以参考下
    2023-12-12
  • MyBatis-Plus 快速入门案例(小白教程)

    MyBatis-Plus 快速入门案例(小白教程)

    这篇文章主要介绍了MyBatis-Plus 快速入门案例(小白教程),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-08-08
  • java调用shell命令并获取执行结果的示例

    java调用shell命令并获取执行结果的示例

    今天小编就为大家分享一篇java调用shell命令并获取执行结果的示例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-07-07
  • Java多线程中的Exchanger应用简析

    Java多线程中的Exchanger应用简析

    这篇文章主要介绍了Java多线程中的Exchanger应用简析,Exchanger提供了一个同步点exchange方法,两个线程调用exchange方法时,无论调用时间先后,两个线程会互相等到线程到达exchange方法调用点,此时两个线程可以交换数据,将本线程产出数据传递给对方,需要的朋友可以参考下
    2023-12-12
  • 基于spring @Cacheable 注解的spel表达式解析执行逻辑

    基于spring @Cacheable 注解的spel表达式解析执行逻辑

    这篇文章主要介绍了spring @Cacheable 注解的spel表达式解析执行逻辑,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • 详解Mybatis的分页插件

    详解Mybatis的分页插件

    这篇文章主要介绍了详解Mybatis的分页插件,在 Mybatis中,如何对数据进行分页是一个非常常见的问题,现在,我们可以通过使用 Mybatis 的分页插件来实现对数据的分页,需要的朋友可以参考下
    2023-05-05
  • java如何调用kettle设置变量和参数

    java如何调用kettle设置变量和参数

    文章简要介绍了如何在Java中调用Kettle,并重点讨论了变量和参数的区别,以及在Java代码中如何正确设置和使用这些变量,避免覆盖Kettle中已设置的变量,作者分享了个人经验,并鼓励大家参考和使用脚本之家
    2025-01-01
  • java使用wait和notify实现线程通信

    java使用wait和notify实现线程通信

    这篇文章主要为大家详细介绍了java如何使用wait和notify实现线程之间通信,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2023-10-10
  • SpringBoot整合Kafka工具类的详细代码

    SpringBoot整合Kafka工具类的详细代码

    Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者在网站中的所有动作流数据,这篇文章主要介绍了SpringBoot整合Kafka工具类的代码详解,需要的朋友可以参考下
    2022-09-09

最新评论