Java IO 学习(六)Java的Direct Memory与IO
ByteBuffer的源码中有这样一段注释:
A byte buffer is either direct or non-direct. Given a direct byte buffer, the Java virtual machine will make a best effort to perform native I/O operations directly upon it. That is, it will attempt to avoid copying the buffer's content to (or from) an intermediate buffer before (or after) each invocation of one of the underlying operating system's native I/O operations.
大概意思是说ByteBuffer分为direct与heap两种,如果使用direct版本的ByteBuffer,JVM会尽可能的直接在这个ByteBuffer上做IO操作。从而省去了将数据在中间buffer上来回复制带来的开销。
看到这里你当然是一头雾水了,不过不要慌,本文会详尽的分析direct memory与IO之间的关系。
1.什么是direct memory?
Java应用程序执行时会启动一个Java进程,这个进程的用户地址空间可以被分成两份:JVM数据区 + direct memory。
通俗的说,JVM数据区就是Java代码可以直接操作的那部分内存,由heap/stack/pc/method area等组成,GC也工作在这一片区域里。
direct memory则是额外划分出来的一段内存区域,无法用Java代码直接操作,GC无法直接控制direct memory,全靠手工维护。
2. direct memory是怎么来的?
我们且来跟踪一下ByteBuffer.allocateDirect()方法的调用流程:
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
// Primary constructor
//
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);//记录已经申请了多少direct memory
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));//注册Cleaner
att = null;
}
其中比较重要的是调用了Unsafe.allocateMemory与Unsafe.setMemory这两个native方法来申请并初始化内存
我们且来跟踪一下这两个方法
Unsafe的实际实现位于src/share/vm/prims/unsafe.cpp
Unsafe.allocateMemory的实现则在这里:
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
UnsafeWrapper("Unsafe_AllocateMemory");
size_t sz = (size_t)size;
if (sz != (julong)size || size < ) {
THROW_0(vmSymbols::java_lang_IllegalArgumentException());
}
if (sz == ) {
return ;
}
//前面都是检查参数
sz = round_to(sz, HeapWordSize);//没找到round_to方法的定义,但是应该为了内存对齐而额外申请一点内存做padding
void* x = os::malloc(sz, mtInternal);//直接调用malloc
if (x == NULL) {
THROW_0(vmSymbols::java_lang_OutOfMemoryError());
}
//Copy::fill_to_words((HeapWord*)x, sz / HeapWordSize);
return addr_to_java(x);//将返回的内存地址转成long类型并返回给Java应用
UNSAFE_END
可以看到是直接调用了malloc方法来申请的一片内存空间
Unsafe.setMemory的实现在这里:
UNSAFE_ENTRY(void, Unsafe_SetMemory(JNIEnv *env, jobject unsafe, jlong addr, jlong size, jbyte value))
UnsafeWrapper("Unsafe_SetMemory");
size_t sz = (size_t)size;
if (sz != (julong)size || size < ) {
THROW(vmSymbols::java_lang_IllegalArgumentException());
}
//检查参数
char* p = (char*) addr_from_java(addr);//将从Java应用传来的long型变量强制转成char指针,现在p指向的就是那一块direct memory的起始位置了
Copy::fill_to_memory_atomic(p, sz, value);
UNSAFE_END
可以看到是调用了Copy::fill_to_memory_atomic方法来将指定的内存空间清空。
现在我们就明白了,这些direct memory,其实就跟一般的c语言编程里一样,是直接用malloc方法申请的。
JVM会将malloc方法的返回值(申请到的内存空间的首地址)转换成long类型的address变量,然后返还给Java应用程序。
Java应用程序在需要操作direct memory的时候,会调用native方法将address传给JVM,然后JVM就能对这块内存为所欲为了。
3. Java应用程序是如何访问direct memory的?
以DirectByteBuffer.get()方法为例
public byte get() {
return ((unsafe.getByte(ix(nextGetIndex()))));
}
逻辑看起来很简单,就是直接调用Unsafe的getByte方法来从指定的内存地址获取数据(偏移量已经给你算好了,只用取内存数据就行了)
有趣的是,我找了一圈没有发现Unsafe.getByte()方法的native实现,可能是因为这个方法太经常调用了,处于性能缘故JVM已经把它搞成intrinsics的了。
也就是说,跑在JVM内部的Java代码无法直接操作direct memory里的数据,需要经过Unsafe带来的中间层,而这必然也会带来一定的开销,所以操作direct memory比heap memory要慢一些。
4. 为什么说direct memory更加适合IO操作?
因为在JVM层面来看,所谓的direct memory就是在进程空间中申请的一段内存,而且指向direct memory的指针是固定不变的,因此可以直接用direct memory作为参数来执行各种系统调用,比方说read/pread/mmap等。
而为什么heap memory不能直接用于系统IO呢,因为GC会移动heap memory里的对象的位置。如果强行用heap memory来搞系统IO的话,IO操作的中途出现的GC会导致缓冲区位置移动,然后程序就跑飞了。
除非采用一定的手段将这个对象pin住,但是hotspot不提供单个对象层面的object pinning,一定要pin的话就只能暂时禁用gc了,也就是把整个Java堆都给pin住,这显然代价太高了。
总结一下就是:heap memory不可能直接用于系统IO,数据只能先读到direct memory里去,然后再复制到heap memory。
5. 实例说明
就用上一篇中提到的FileChannel.read()方法作为例子,而且使用heap memory作为缓冲区,其调用流程如下:
1. 先申请一块临时的direct memory
2. 调用native的FileDispatcherImpl.pread0或者FileDispatcherImpl.read0,将step1中申请的direct memory的地址传进去
3. jvm调用Linux提供的read或者pread系统调用,传入direct memory对应的内存空间指针,以及正在操作的fd
4. 触发中断,进程从用户态进入到内核态(1-3步全是在用户态中完成)
5. 操作系统检查kernel中维护的buffer cache是否有数据,如果没有,给磁盘发送命令,让磁盘将数据拷贝到buffer cache里
6. 操作系统将buffer cache中的数据复制到step3中传入的指针对应的内存里
7. 触发中断,进程从内核态退回到用户态(5-6步全在内核态中完成)
8. FileDispatcherImpl.pread0或者FileDispatcherImpl.read0方法返回,此时临时创建的direct memory中已经有用户需要的数据了
9. 将direct memory里的数据复制到heap memory中(这中间又要调用Unsafe里的一些方法,例如copyMemory)
10. 现在heap memory中终于有我们想要的数据了。
总结一下,数据的流转过程是:hard disk -> kernel buffer cache -> direct memory -> heap memory
中间调用了一次系统调用,触发了两次中断。
流程看起来相当复杂,有优化的办法吗?当然是有的:
a. 可以直接使用direct memory作为缓冲区,这样就砍掉了direct memory -> heap memory的耗费
b. 也可以使用内存映射文件,也就是FileChannel.map,砍掉中间的kernel buffer cache这一段
参考资料
Java NIO中,关于DirectBuffer,HeapBuffer的疑问?
Java IO 学习(六)Java的Direct Memory与IO的更多相关文章
- Hbase深入学习(六) Java操作HBase
Hbase深入学习(六) ―― Java操作HBase 本文讲述如何用hbase shell命令和hbase java api对hbase服务器进行操作. 先看以下读取一行记录hbase是如何进行工作 ...
- java虚拟机学习-触摸java常量池(13-1)
java虚拟机学习-深入理解JVM(1) java虚拟机学习-慢慢琢磨JVM(2) java虚拟机学习-慢慢琢磨JVM(2-1)ClassLoader的工作机制 java虚拟机学习-JVM内存管理:深 ...
- java基础学习总结——java环境变量配置(转)
只为成功找方法,不为失败找借口! 永不放弃,一切皆有可能!!! java基础学习总结——java环境变量配置 前言 学习java的第一步就要搭建java的学习环境,首先是要安装 JDK,JDK安装好之 ...
- java web 学习六(servlet开发2)
一.ServletConfig讲解 1.1.配置Servlet初始化参数 在Servlet的配置文件web.xml中,可以使用一个或多个<init-param>标签为servlet配置一些 ...
- JAVA NIO学习一:NIO简介、NIO&IO的主要区别
在前面学习了IO之后,今天我们开始进入NIO学习环节,首先我们会NIO做一个简单的介绍,让大家认识NIO,然后会和IO进行一个对比认识进行区分.好了,下面我们就开始学习: 一.NIO简介 1.概述 从 ...
- 多线程编程学习六(Java 中的阻塞队列).
介绍 阻塞队列(BlockingQueue)是指当队列满时,队列会阻塞插入元素的线程,直到队列不满:当队列空时,队列会阻塞获得元素的线程,直到队列变非空.阻塞队列就是生产者用来存放元素.消费者用来获取 ...
- JAVA多线程学习六-守护线程
java中的守护程序线程是一个服务提供程序线程,它为用户线程提供服务. 它的生命依赖于用户线程,即当所有用户线程都死掉时,JVM会自动终止该线程. 有许多java守护程序线程自动运行,例如 gc,fi ...
- Java基础学习(一)---Java初识
一.Java介绍 关于Java的诞生和发展网上比较多,在此就不再赘述了,可以参考http://i.cnblogs.com/EditArticles.aspx?postid=4050233. 1.1 J ...
- java并发系列(六)-----Java并发:volatile关键字解析
在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性.可见性和有序性.只要有一条原则没有被保证,就有可能会导致程序运行不正确.volatile关键字 被用来保证可见性 ...
随机推荐
- 内存释放free函数的异常问题
本次在实际应用中遇到一个问题,首先是定义了一个指针,然后这个指针指向某一个地址,但是这个地址不是用malloc分配的.如果后面用free去释放这个指针会产生什么现象. 首先看下指针的声明和使用 uin ...
- ios tcpdump
转载 前提条件:机器要破解,cydia能打开 需要工具1.openssh2.tcpdump 安装工具方法:1.连接网络,打开cydia2.确认Cydia设置为开发者模式(管理->设置->开 ...
- 大中型 UGC 平台的反垃圾(anti-spam)工作
本文来自网易云社区 随着互联网技术的日渐发展,相继诞生了垂直社区.社交平台.短视频应用.网络直播等越来越多样的产品.但在内容爆炸式增长的同时,海量UGC中也夹杂着各种违规垃圾信息,包括垃圾广告.诈骗信 ...
- yum源是什么
我们安装软件的时候需要下载软件,将很多软件放在一起就是源.软件安装包的来源. 所以yum源就是软件安装包来源. 如果我们是在线的,他会在网上给你下载安装包,如果是离线的没有网络.那么就只能配置本地的y ...
- 对于进程没杀死占用内存和cpu行为的方法
在跑机器学习或者深度学习的过程中有可能遇到没杀死进程的情况,但是程序的入口又没关掉,尤其是我使用jupyter从远程Linux映射到windows浏览器跑程序的时候 对于上面的问题, 首先运行 hto ...
- Day4 自定义控件/ListView/RecyclerView
创建自定义控件 引入布局 在新增的title.xml中创建一个自定义的标题栏: <LinearLayout xmlns:android="http://schemas.android. ...
- HDU 4747 Mex ( 线段树好题 + 思路 )
参考:http://www.cnblogs.com/oyking/p/3323306.html 相当不错的思路,膜拜之~ 个人理解改日补充. #include <cstdio> #incl ...
- __PRETTY_FUNCTION__,__func__,__FUNCTION__
今天在看苹果的官方demo的时候,发现这个打印调用方法的参数,很是好奇,遂bing了一番. NSLog(@"----------------%s",__PRETTY_FUNCTIO ...
- 那些牛掰的 HTML5的API(二)
1:视频播放器 2:地理定位 我们的支持html5 的浏览器给我们提供一个接口(api),可以用来获取你当前的位置. 主要是通过geolocation(地理位置),对象 ,去访问硬件,来获取到经纬度. ...
- 2013年 ACMICPC 杭州赛区H题
思路:树状数组统计.待验证,不知道是否对. #include<cstdio> #include<cstring> #include<cmath> #include& ...