JVM学习笔记(六)直接内存

直接内存


首先书上这样说:

  • 直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中农定义的内存区域。但是这部分内存也被频繁地使用。
  • 在JDK1.4 中新加入了NIO(NewInput/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
  • 本机直接内存的分配不会受到java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OOM异常。

直接内存(堆外内存)与堆内存比较
1.直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显。
2.直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显,但它的创建、销毁比普通Buffer慢。
3.因此直接内存使用于需要大内存空间且频繁访问的场合,不适用于频繁申请释放内存的场合。例如:有很大的数据需要存储,它的生命周期很长,或网络并发场景。

代码测试如下:

public class ByteBufferCompare {
    public static void main(String[] args) {
        allocateCompare();   //分配比较
        operateCompare();    //读写比较
    }

    /**
     * 直接内存 和 堆内存的 分配空间比较
     * 
     * 结论: 在数据量提升时,直接内存相比非直接内的申请,有很严重的性能问题
     * 
     */
    public static void allocateCompare(){
        int time = 10000000;    //操作次数                           


        long st = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {

            //ByteBuffer.allocate(int capacity)   分配一个新的字节缓冲区。
            ByteBuffer buffer = ByteBuffer.allocate(2);      //非直接内存分配申请     
        }
        long et = System.currentTimeMillis();

        System.out.println("在进行"+time+"次分配操作时,堆内存 分配耗时:" + (et-st) +"ms" );

        long st_heap = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            //ByteBuffer.allocateDirect(int capacity) 分配新的直接字节缓冲区。
            ByteBuffer buffer = ByteBuffer.allocateDirect(2); //直接内存分配申请
        }
        long et_direct = System.currentTimeMillis();

        System.out.println("在进行"+time+"次分配操作时,直接内存 分配耗时:" + (et_direct-st_heap) +"ms" );

    }

    /**
     * 直接内存 和 堆内存的 读写性能比较
     * 
     * 结论:直接内存在直接的IO 操作上,在频繁的读写时 会有显著的性能提升
     * 
     */
    public static void operateCompare(){
        int time = 1000000000;

        ByteBuffer buffer = ByteBuffer.allocate(2*time);  
        long st = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {

            //  putChar(char value) 用来写入 char 值的相对 put 方法
            buffer.putChar('a');
        }
        buffer.flip();
        for (int i = 0; i < time; i++) {
            buffer.getChar();
        }
        long et = System.currentTimeMillis();

        System.out.println("在进行"+time+"次读写操作时,非直接内存读写耗时:" + (et-st) +"ms");

        ByteBuffer buffer_d = ByteBuffer.allocateDirect(2*time);
        long st_direct = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {

            //  putChar(char value) 用来写入 char 值的相对 put 方法
            buffer_d.putChar('a');
        }
        buffer_d.flip();
        for (int i = 0; i < time; i++) {
            buffer_d.getChar();
        }
        long et_direct = System.currentTimeMillis();

        System.out.println("在进行"+time+"次读写操作时,直接内存读写耗时:" + (et_direct - st_direct) +"ms");
    }
}

输出:
在进行10000000次分配操作时,堆内存 分配耗时:12ms
在进行10000000次分配操作时,直接内存 分配耗时:8233ms
在进行1000000000次读写操作时,非直接内存读写耗时:4055ms
在进行1000000000次读写操作时,直接内存读写耗时:745ms

从数据流分析
非直接内存作用链:
本地IO –>直接内存–>非直接内存–>直接内存–>本地IO
直接内存作用链:
本地IO–>直接内存–>本地IO
例如:在进行堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。


探究:java中流的flush()方法
IO流中每一个类都实现了Closeable接口,它们进行资源操作之后都需要执行close()方法将流关闭 。但字节流与字符流的不同之处在于:字节流是直接与数据产生交互,而字符流在与数据交互之前要经过一个缓冲区
也就是说,字符流中的数据是暂存于缓冲区的,如果不将缓冲区中的数据真正地送达"目的地",结果就可能会和自己想的不太一样。要清空缓冲区中的数据(即将缓冲区中数据送达目的地)有两种办法:

public abstract void close() throws IOException
关闭流的同时将清空缓冲区中的数据,该抽象方法由具体的子类实现
public abstract void flush() throws IOException
不关闭流的话,使用此方法可以清空缓冲区中的数据

public class Writer_Flush_Test {
    public static void main(String[] args) throws IOException {
    File file = new File("D:\\hello.txt");
    BufferedWriter bWriter = new BufferedWriter(new FileWriter(file));
    bWriter.write("h");
    }
}

比如像上面这段程序,h字符是不会被写入到文件中的。因为在程序运行结束时,数据仍然是放在缓冲区中,并没有真正送达文件。
那么问题来了,我们知道了flush方法,也知道了字符流与字节流的差别在于要经过一个缓冲区,那缓冲区是什么?
范例:使用字节流不关闭执行

public class OutputStreamDemo05 {    
public static void main(String[] args) throws Exception {   // 异常抛出,  不处理    
// 第1步:使用File类找到一个文件    
     File f = new File("d:" + File.separator + "test.txt"); // 声明File  对象    
// 第2步:通过子类实例化父类对象    
     OutputStream out = null;            
// 准备好一个输出的对象    
     out = new FileOutputStream(f);      
// 通过对象多态性进行实例化    
// 第3步:进行写操作    
     String str = "Hello World!!!";      
// 准备一个字符串    
     byte b[] = str.getBytes();          
// 字符串转byte数组    
     out.write(b);                      
// 将内容输出    
 // 第4步:关闭输出流    
    // out.close();                  
// 此时没有关闭    
        }    
    }   

输出结果:

Hello World!!!

此时没有关闭字节流操作,但是文件中也依然存在了输出的内容,证明字节流是直接操作文件本身的。而下面继续使用字符流完成,再观察效果。
范例:使用字符流不关闭执行

public class WriterDemo03 {    
    public static void main(String[] args) throws Exception { // 异常抛出,  不处理    
        // 第1步:使用File类找到一个文件    
        File f = new File("d:" + File.separator + "test.txt");// 声明File 对象    
        // 第2步:通过子类实例化父类对象    
        Writer out = null;                 
// 准备好一个输出的对象    
        out = new FileWriter(f);            
// 通过对象多态性进行实例化    
        // 第3步:进行写操作    
        String str = "Hello World!!!";      
// 准备一个字符串    
        out.write(str);                    
// 将内容输出    
        // 第4步:关闭输出流    
        // out.close();                   
// 此时没有关闭    
    }    
}  

程序运行后会发现文件中没有任何内容,这是因为字符流操作时使用了缓冲区,而在关闭字符流时会强制性地将缓冲区中的内容进行输出,但是如果程序没有关闭,则缓冲区中的内容是无法输出的,所以得出结论:字符流使用了缓冲区,而字节流没有使用缓冲区。
提问:什么叫缓冲区?在很多地方都碰到缓冲区这个名词,那么到底什么是缓冲区?又有什么作用呢?
回答:缓冲区可以简单地理解为一段内存区域。可以简单地把缓冲区理解为一段特殊的内存。某些情况下,如果一个程序频繁地操作一个资源(如文件或数据库),则性能会很低,此时为了提升性能,就可以将一部分数据暂时读入到内存的一块区域之中,以后直接从此区域中读取数据即可,因为读取内存速度会比较快,这样可以提升程序的性能。在字符流的操作中,所有的字符都是在内存中形成的,在输出前会将所有的内容暂时保存在内存之中,所以使用了缓冲区暂存数据。如果想在不关闭时也可以将字符流的内容全部输出,则可以使用Writer类中的flush()方法完成。
所以,在这个延伸之中,我们很明显可以发现缓冲区运用了直接内存

提问:使用字节流好还是字符流好?学习完字节流和字符流的基本操作后,已经大概地明白了操作流程的各个区别,那么在开发中是使用字节流好还是字符流好呢?
回答:使用字节流更好。在回答之前,先为读者讲解这样的一个概念,所有的文件在硬盘或在传输时都是以字节的方式进行的,包括图片等都是按字节的方式存储的,而字符是只有在内存中才会形成,所以在开发中,字节流使用较为广泛。


本机直接内存溢出
DirectMemory容量可通过**-XX:MaxDirectMemorySize指定**,如果不指定,则默认与Java堆的最大值(-Xmx指定)一
样。下面代码越过了DirectByteBuffer类,直接通过反射获取Unsafe实例并进行内存分配(Unsafe类的getUnsafe()
方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。因为,虽
然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是
通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。

public class DirectMemoryOOM {  
	private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) throws Exception {
	Field unsafeField = Unsafe.class.getDeclaredFields()[0];  
	unsafeField.setAccessible(true);  
	Unsafe unsafe = (Unsafe) unsafeField.get(null);  
	while (true) {  
            unsafe.allocateMemory(_1MB);  
        }
    }
} 


由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常。如果读者发现OOM
之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。

你可能感兴趣的:(JVM)