【JVM原理探索】分析堆外内存(Direct Memory)使用和分析

简介: 【JVM原理探索】分析堆外内存(Direct Memory)使用和分析

堆外内存


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




相比于堆内内存有几个优势:


  1. 减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作(可能使用多线程或者时间片的方式,根本感觉不到)

堆外内存是直接受操作系统管理的,而不是JVM,因此使用堆外内存的话,就可以保持一个比较小的堆内内存,减少垃圾回收对程序性能的影响


  1. 就是提高IO操作的效率!这里就涉及用户态与内核态,以及内核缓冲区的概念,如果从堆内向磁盘写数据,数据会被先复制到堆外内存,即内核缓冲区,然后再由OS写入磁盘,但使用堆外内存的话则可以避免这个复制操作。


堆内内存其实就是用户进程的【进程缓冲区】,属于用户态;堆外内存由操作系统管理【内核缓冲区】,属于内核态。

image.png


自然也有不好的一面:


  1. 堆外内存难以控制,如果内存泄漏,那么很难排查
  2. 堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合
  3. 因为是操作系统的内存机制,所以需要通过本地方法进行分配,较为复杂和缓慢




直接内存使用


  1. 堆外内存通过java.nioByteBuffer来创建,调用allocateDirect方法申请即可。
  2. 可以通过设置-XX:MaxDirectMemorySize=10M控制堆外内存的大小。


堆外内存的垃圾回收


  1. 由于堆外内存并不直接控制于JVM,因此只能等到full GC的时候才能垃圾回收!Full GC,一般发生在年老代垃圾回收以及调用System.gc的时候,这样肯定不能满足我们的需求!
  2. 手动的控制回收堆外内存了!其中sun.nio其实是java.nio的内部实现。
package xing.test;
import java.nio.ByteBuffer;
import sun.nio.ch.DirectBuffer;
public class NonHeapTest {
  public static void clean(final ByteBuffer byteBuffer) { 
    if (byteBuffer.isDirect()) { 
      ((DirectBuffer)byteBuffer).cleaner().clean(); 
    } 
 } 
  public static void sleep(long i) { 
    try { 
       Thread.sleep(i); 
     }catch(Exception e) { 
       /*skip*/
     } 
  } 
  public static void main(String []args) throws Exception { 
      ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 200); 
      System.out.println("start"); 
      sleep(5000); 
      clean(buffer);//执行垃圾回收
//     System.gc();//执行Full gc进行垃圾回收
      System.out.println("end"); 
      sleep(5000); 
  } 
}
复制代码





零拷贝


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





实现方式


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;
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();
    }
}





相关文章
|
6天前
|
监控 Java Unix
6个Java 工具,轻松分析定位 JVM 问题 !
本文介绍了如何使用 JDK 自带工具查看和分析 JVM 的运行情况。通过编写一段测试代码(启动 10 个死循环线程,分配大量内存),结合常用工具如 `jps`、`jinfo`、`jstat`、`jstack`、`jvisualvm` 和 `jcmd` 等,详细展示了 JVM 参数配置、内存使用、线程状态及 GC 情况的监控方法。同时指出了一些常见问题,例如参数设置错误导致的内存异常,并通过实例说明了如何排查和解决。最后附上了官方文档链接,方便进一步学习。
|
1月前
|
存储 监控 Java
JVM实战—8.如何分析jstat统计来定位GC
本文详细介绍了使用jstat、jmap和jhat等工具分析JVM运行状况的方法,以及如何合理优化JVM性能。内容涵盖新生代与老年代对象增长速率、Young GC和Full GC的触发频率及耗时等关键指标的分析。通过模拟BI系统和计算系统的案例,展示了如何根据实际场景调整JVM参数以减少FGC频率,提升系统性能。最后汇总了常见问题及其解决方案,帮助开发者更好地理解和优化JVM运行状态。
JVM实战—8.如何分析jstat统计来定位GC
|
1月前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
174 29
JVM简介—1.Java内存区域
|
3月前
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
732 166
|
1月前
|
缓存 算法 Java
JVM实战—4.JVM垃圾回收器的原理和调优
本文详细探讨了JVM垃圾回收机制,包括新生代ParNew和老年代CMS垃圾回收器的工作原理与优化方法。内容涵盖ParNew的多线程特性、默认线程数设置及适用场景,CMS的四个阶段(初始标记、并发标记、重新标记、并发清理)及其性能分析,以及如何通过合理分配内存区域、调整参数(如-XX:SurvivorRatio、-XX:MaxTenuringThreshold等)来优化垃圾回收。此外,还结合电商大促案例,分析了系统高峰期的内存使用模型,并总结了YGC和FGC的触发条件与优化策略。最后,针对常见问题进行了汇总解答,强调了基于系统运行模型进行JVM参数调优的重要性。
109 10
JVM实战—4.JVM垃圾回收器的原理和调优
|
1月前
|
消息中间件 Java 应用服务中间件
JVM实战—2.JVM内存设置与对象分配流转
本文详细介绍了JVM内存管理的相关知识,包括:JVM内存划分原理、对象分配与流转、线上系统JVM内存设置、JVM参数优化、问题汇总。
JVM实战—2.JVM内存设置与对象分配流转
|
1月前
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
1月前
|
消息中间件 Java 应用服务中间件
JVM实战—1.Java代码的运行原理
本文介绍了Java代码的运行机制、JVM类加载机制、JVM内存区域及其作用、垃圾回收机制,并汇总了一些常见问题。
JVM实战—1.Java代码的运行原理
|
1月前
|
存储 Java
课时4:对象内存分析
接下来对对象实例化操作展开初步分析。在整个课程学习中,对象使用环节往往是最棘手的问题所在。
|
1月前
|
Java 编译器 Go
go的内存逃逸分析
内存逃逸分析是Go编译器在编译期间根据变量的类型和作用域,确定变量分配在堆上还是栈上的过程。如果变量需要分配在堆上,则称作内存逃逸。Go语言有自动内存管理(GC),开发者无需手动释放内存,但编译器需准确分配内存以优化性能。常见的内存逃逸场景包括返回局部变量的指针、使用`interface{}`动态类型、栈空间不足和闭包等。内存逃逸会影响性能,因为操作堆比栈慢,且增加GC压力。合理使用内存逃逸分析工具(如`-gcflags=-m`)有助于编写高效代码。

热门文章

最新文章

下一篇
oss创建bucket