1.记一次源码追踪分析,方法源t方法从Java到JNI,方法源t方法再到JVM的方法源t方法C++:fileChannel.map()为什么快;源码分析map方法,put方法
2.idea debug进入HashMap源码时传参不正确?
3.为什么HashMap是方法源t方法线程不安全的
记一次源码追踪分析,从Java到JNI,方法源t方法再到JVM的方法源t方法hashset源码解读C++:fileChannel.map()为什么快;源码分析map方法,put方法
前言
在系统IO相关的方法源t方法系统调用有read/write,mmap,方法源t方法sendfile等这些。方法源t方法
其中read/write是方法源t方法普通的读写,每次都需要将buffer从用户空间拷贝到内核空间;
而mmap使用的方法源t方法是内存映射,会将磁盘文件对应的方法源t方法页映射(拷贝)到内核空间的page cache,并记录到用户进程的方法源t方法页表中,使得用户空间也可以像操作用户空间一样操作该文件的方法源t方法映射,最后再由操作系统来讲该映射(脏页)回写到磁盘;
sendfile则使用的方法源t方法是零拷贝技术,在mmap的基础上,当发送数据的时候只拷贝fd和offset等元数据信息,而将数据主体直接拷贝至protocol buffer,实现了内核数据零冗余的零拷贝技术
本文地址:/post//
问题/目的问题1Java中哪些API使用到了mmap问题2怎么知道该API使用到了mmap,如何追踪程序的系统调用目的1源码中分析验证,从Java到JNI,jdbctokenstore源码再到C++:fileChannel.map()使用的是系统调用mmap目的2源码验证分析:调用mmapedByteBuffer.put(Byte[])时JVM在搞些什么?mmap比普通的read/write快在哪?揭晓答案1mmap在Java NIO中的体现/使用看一个例子
// 1GBpublic static final int _GB = 1**;File file = new File("filename");FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();MappedByteBuffer mmapedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB);for (int i = 0; i < _GB; i++) { count++;mmapedByteBuffer.put((byte)0);}其中fileChannel.map()底层使用的就是系统调用mmap,函数签名为: public abstract MappedByteBuffer map(MapMode mode,long position, long size)throws IOException
答案2程序执行的系统调用追踪/** * @author Tptogiar * @description * @date /5/ - : */public class TestMappedByteBuffer{ public static final int _4kb = 4*;public static final int _GB= 1**;public static void main(String[] args) throws IOException, InterruptedException { // 为了方便在日志中找到本段代码的开始位置和结束位置,这里利用文件io来打开始标记FileInputStream startInput = null;try { startInput = new FileInputStream("start1.txt");startInput.read();} catch (IOException e) { e.printStackTrace();}File file = new File("filename");FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB); //我们想分析的语句问题2for (int i = 0; i < _GB; i++) { map.put((byte)0); // 下文中需要分析的语句目的2}// 打结束标记FileInputStream endInput = null;try { endInput = new FileInputStream("end.txt");endInput.read();} catch (IOException e) { e.printStackTrace();}}}把上面这段代码编译后把“.class”文件拉到linux执行,并用linux上的strace工具记录其系统调用日志,拿到日志文件我们可以在日志中看到以下信息(关于怎么拿到日志可以参照我的博文:无(代写)):
注:日志有多行,这里只选取我们关注的
// ...// 看到了我们打的开始标志openat(AT_FDCWD, "start1.txt", O_RDONLY) = -1 ENOENT (No such file or directory)// ... // 打开文件,文件描述符fd为6openat(AT_FDCWD, "filename", O_RDWR|O_CREAT, ) = 6// 判断文件状态fstat(6, { st_mode=S_IFREG|, st_size=, ...}) = 0// ... // 判断文件状态fstat(6, { st_mode=S_IFREG|, st_size=, ...}) = 0// 进行内存映射mmap(NULL, , PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0) = 0x7f2fd6cd// ...// 程序退出exit(0)// 看到了我们打的结束标志openat(AT_FDCWD, "end.txt", O_RDONLY) = -1 ENOENT (No such file or directory)在上面程序的系统调用日志中我们确实看到了我们打的开始标志,结束标志。在开始标志和结束标志之间我们看到了我们的文件"filename"确实被打开了,文件描述符fd = 6;在打开文件后紧接着又执行了系统调用mmap,这一点我们Java代码一致,这样,我们就验证了我们答案1中的结论,可以开始我们的下文了
源码追踪分析,从Java到JNI,再到JVM的C++目的1寻源之旅:fileChannel.map()我们知道我们执行Java代码fileChannel.map()确实会在底层调用系统调用,那怎么在源码中得到验证呢?怎么落脚于源码进行分析呢?下面开始我们的寻源之旅
FileChannelImpl.map() 注:由于代码较长,这里代码中略去了一些我们不关注的,比如异常捕获等
public MappedByteBuffer map(MapMode mode, long position, long size)throws IOException{ // ...try { // ...synchronized (positionLock) { // ...long mapPosition = position - pagePosition;mapSize = size + pagePosition;try { // !我们要找的语句就在这!addr = map0(imode,getpart源码 mapPosition, mapSize);} catch (OutOfMemoryError x) { // 如果内存不足,先尝试进行GCSystem.gc();try { Thread.sleep();} catch (InterruptedException y) { Thread.currentThread().interrupt();}try { // 再次试着mmapaddr = map0(imode, mapPosition, mapSize);} catch (OutOfMemoryError y) { // After a second OOME, failthrow new IOException("Map failed", y);}}} // ...} finally { // ...}}上面函数源码中真正执行mmap的语句是在addr = map0(imode, mapPosition, mapSize),于是我们寻着这里继续追踪
FileChannelImpl.map0()
// Creates a new mappingprivate native long map0(int prot, long position, long length)throws IOException;可以看到,该方法是一个native方法,所以后面的源码我们需要到这个FileChannelImpl.class对应的fileChannelImpl.c中去看,所以我们需要去找到JDK的源码
在JDK源码中我们找到fileChannelImpl.c文件
fileChannelImpl.c 根据JNI的对应规则,我们找到该文件内对应的Java_sun_nio_ch_FileChannelImpl_map0方法,其源码如下:
JNIEXPORT jlong JNICALLJava_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this, jint prot, jlong off, jlong len){ void *mapAddress = 0;jobject fdo = (*env)->GetObjectField(env, this, chan_fd);jint fd = fdval(env, fdo);int protections = 0;int flags = 0;if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) { protections = PROT_READ;flags = MAP_SHARED;} else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) { protections = PROT_WRITE | PROT_READ;flags = MAP_SHARED;} else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) { protections =PROT_WRITE | PROT_READ;flags = MAP_PRIVATE;}// !我们要找的语句就在这里!mapAddress = mmap(0,/* Let OS decide location */len,/* Number of bytes to map */protections,/* File permissions */flags,/* Changes are shared */fd, /* File descriptor of mapped file */off); /* Offset into file */if (mapAddress == MAP_FAILED) { if (errno == ENOMEM) { JNU_ThrowOutOfMemoryError(env, "Map failed");return IOS_THROWN;}return handle(env, -1, "Map failed");}return ((jlong) (unsigned long) mapAddress);}我们要找的语句就上面代码中的mapAddress = mmap(0,len,protections,flags,fd,off),至于为什么不是直接的mmap,而是mmap,是因为这里的mmap是一个宏,在文件上方有其定义,如下:
#define mmap mmap至此,我们就在源码中得到验证了我们问题2中的结论:fileChannelImpl.map()底层使用的是mmap系统调用
目的2寻源之旅:mmapedByteBuffer.put(Byte[ ])接着我们来看看当我们调用mmapedByteBuffer.put(Byte[])JVM底层在搞些什么动作
MappedByteBuffer ?首先我们得知道,当我们执行MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB)时,实际返回的对象是DirectByteBuffer类的实例,因为MappedByteBuffer为抽象类,且只有DirectByteBuffer继承了它,看下面两图就明白了
DirectByteBuffer 于是我们找到DirectByteBuffer内的put(Byte[ ])方法
public ByteBuffer put(byte x) { unsafe.putByte(ix(nextPutIndex()), ((x)));return this;}可以看到该方法内实际是调用Unsafe类内的putByte方法来实现功能的,所以我们还得去看Unsafe类
Unsafe.class
public native voidputByte(long address, byte x);该方法在Unsafe内是一个native方法,所以所以我们还得去看unsafe.cpp文件内对应的treeselect源码实现
unsafe.cpp
在JDK源码中,我们找到unsafe.cpp
在这份源码内,没有使用JNI内普通加前缀的方法来形成对应关系
不过我们还是能顺着源码的蛛丝轨迹找到我们要找的方法
注意到源码中有这样的注册机制,所以我们可以知道我们要找的代码就是上图中标注的代码
顺藤摸瓜,我们就找到了该方法的定义
UNSAFE_ENTRY(void, Unsafe_SetNative##Type(JNIEnv *env, jobject unsafe, jlong addr, java_type x)) \UnsafeWrapper("Unsafe_SetNative"#Type); \JavaThread* t = JavaThread::current(); \t->set_doing_unsafe_access(true); \void* p = addr_from_java(addr); \*(volatile native_type*)p = x; \t->set_doing_unsafe_access(false); \UNSAFE_END \该方法内主要的逻辑语句就是以下两句:
/** * @author Tptogiar * @description * @date /5/ - : */public class TestMappedByteBuffer{ public static final int _4kb = 4*;public static final int _GB= 1**;public static void main(String[] args) throws IOException, InterruptedException { // 为了方便在日志中找到本段代码的开始位置和结束位置,这里利用文件io来打开始标记FileInputStream startInput = null;try { startInput = new FileInputStream("start1.txt");startInput.read();} catch (IOException e) { e.printStackTrace();}File file = new File("filename");FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB); //我们想分析的语句问题2for (int i = 0; i < _GB; i++) { map.put((byte)0); // 下文中需要分析的语句目的2}// 打结束标记FileInputStream endInput = null;try { endInput = new FileInputStream("end.txt");endInput.read();} catch (IOException e) { e.printStackTrace();}}}0至此,我们就知道:其实我们调用mmapedByteBuffer.put(Byte[ ])时,JVM底层并不需要涉及到系统调用(这里也可以用strace工具追踪从而得到验证)。也就是说通过mmap映射的空间在内核空间和用户空间是共享的,我们在用户空间只需要像平时使用用户空间那样就行了————获取地址,设置值,而不涉及用户态,内核态的切换
总结fileChannelImpl.map()底层用调用系统函数mmap
fileChannelImpl.map()返回的其实不是MappedByteBuffer类对象,而是DirectByteBuffer类对象
在linux上可以通过strace来追踪系统调用
JNI中“.class”文件内方法与“.cpp”文件内函数的对应关系不止是前缀对应的方法,还可以是注册的方式,这一点的追寻代码的时候有很大帮助
directByteBuffer.put()方法底层并没有涉及系统调用,也就不需要涉及切态的性能开销(其底层知识执行获取地址,设置值的操作),所以mmap的性能就比普通读写read/write好
...
原文:/post/idea debug进入HashMap源码时传参不正确?
我测试了下面的代码:分别在这四个位置打了断点以监控程序的运行情况,debug后,进入第一次断点的geekhub源码位置为:
与题主说的情况一致,而没有进入我的第一个断点进行输出,而后F9:
发现还是在put文件,经多次F9之后,可以看出来,其实java的jvm在启动的时候,在底层也自行调用的put方法,将jvm所需要的一些动态库、jar包put到某个map之中,具体是哪个map看不出来。要等到jvm底层将所有东西准备好后,才进行main函数。
jvm准备需要put多少次我就不数了,现在我先把put的断点取消,让程序debug到我的第一个断点处:
这个时候将put方法打上断点,F9发现:
奇怪的key值增加了,它将我的classes编译目录丢进去了,继续F9,和上一步差不多,再再次F9,终于来了:
继续F9,终于到达了我的第二个断点:
继续F9,这次没有put奇怪的东西了:
继续:
最后:
然后程序退出:
综上,jvm在启动的时候会在程序背后隐式地将一些配置啊什么的通过put方法放到某些地方,不用关心,你遇到的情况是正常的也是正确的
为什么HashMap是线程不安全的
这是《Java程序员进阶之路》专栏的第篇,我们来聊聊为什么HashMap是线程不安全的。、多线程下扩容会死循环众所周知,HashMap是通过拉链法来解决哈希冲突的,也就是当哈希冲突时,会将相同哈希值的键值对通过链表的形式存放起来。
JDK7时,采用的是头部插入的方式来存放链表的,也就是下一个冲突的键值对会放在上一个键值对的前面(同一位置上的新元素被放在链表的头部)。扩容的时候就有可能导致出现环形链表,造成死循环。
resize方法的源码:
//newCapacity为新的容量voidresize(intnewCapacity){ //小数组,临时过度下Entry[]oldTable=table;//扩容前的容量intoldCapacity=oldTable.length;//MAXIMUM_CAPACITY为最大容量,2的次方=1<<if(oldCapacity==MAXIMUM_CAPACITY){ //容量调整为Integer的最大值0x7fffffff(十六进制)=2的次方-1threshold=Integer.MAX_VALUE;return;}//初始化一个新的数组(大容量)Entry[]newTable=newEntry[newCapacity];//把小数组的元素转移到大数组中transfer(newTable,initHashSeedAsNeeded(newCapacity));//引用新的大数组table=newTable;//重新计算阈值threshold=(int)Math.min(newCapacity*loadFactor,MAXIMUM_CAPACITY+1);}transfer方法用来转移,将小数组的元素拷贝到新的数组中。
voidtransfer(Entry[]newTable,booleanrehash){ //新的容量intnewCapacity=newTable.length;//遍历小数组for(Entry<K,V>e:table){ while(null!=e){ //拉链法,相同key上的不同值Entry<K,V>next=e.next;//是否需要重新计算hashif(rehash){ e.hash=null==e.key?0:hash(e.key);}//根据大数组的容量,和键的hash计算元素在数组中的下标inti=indexFor(e.hash,newCapacity);//同一位置上的新元素被放在链表的头部e.next=newTable[i];//放在新的数组上newTable[i]=e;//链表上的下一个元素e=next;}}}注意e.next=newTable[i]和newTable[i]=e这两行代码,就会将同一位置上的新元素被放在链表的头部。
扩容前的样子假如是下面这样子。
那么正常扩容后就是下面这样子。
假设现在有两个线程同时进行扩容,线程A在执行到newTable[i]=e;被挂起,此时线程A中:e=3、next=7、e.next=null
线程B开始执行,并且完成了数据转移。
此时,7的next为3,3的next为null。
随后线程A获得CPU时间片继续执行newTable[i]=e,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下:
执行下一轮循环,此时e=7,原本线程A中7的next为5,但由于table是线程A和线程B共享的,而线程B顺利执行完后,7的next变成了3,那么此时线程A中,7的next也为3了。
采用头部插入的方式,变成了下面这样子:
好像也没什么问题,此时next=3,e=3。
进行下一轮循环,但此时,由于线程B将3的next变为了null,所以此轮循环应该是最后一轮了。
接下来当执行完e.next=newTable[i]即3.next=7后,3和7之间就相互链接了,执行完newTable[i]=e后,3被头插法重新插入到链表中,执行结果如下图所示:
套娃开始,元素5也就成了弃婴,惨~~~
不过,JDK8时已经修复了这个问题,扩容时会保持链表原来的顺序,参照HashMap扩容机制的这一篇。
、多线程下put会导致元素丢失正常情况下,当发生哈希冲突时,HashMap是这样的:
但多线程同时执行put操作时,如果计算出来的索引位置是相同的,那会造成前一个key被后一个key覆盖,从而导致元素的丢失。
put的源码:
finalVputVal(inthash,Kkey,Vvalue,booleanonlyIfAbsent,booleanevict){ Node<K,V>[]tab;Node<K,V>p;intn,i;//步骤①:tab为空则创建if((tab=table)==null||(n=tab.length)==0)n=(tab=resize()).length;//步骤②:计算index,并对null做处理if((p=tab[i=(n-1)&hash])==null)tab[i]=newNode(hash,key,value,null);else{ Node<K,V>e;Kk;//步骤③:节点key存在,直接覆盖valueif(p.hash==hash&&((k=p.key)==key||(key!=null&&key.equals(k))))e=p;//步骤④:判断该链为红黑树elseif(pinstanceofTreeNode)e=((TreeNode<K,V>)p).putTreeVal(this,tab,hash,key,value);//步骤⑤:该链为链表else{ for(intbinCount=0;;++binCount){ if((e=p.next)==null){ p.next=newNode(hash,key,value,null);//链表长度大于8转换为红黑树进行处理if(binCount>=TREEIFY_THRESHOLD-1)//-1for1sttreeifyBin(tab,hash);break;}//key已经存在直接覆盖valueif(e.hash==hash&&((k=e.key)==key||(key!=null&&key.equals(k))))break;p=e;}}//步骤⑥、直接覆盖if(e!=null){ //existingmappingforkeyVoldValue=e.value;if(!onlyIfAbsent||oldValue==null)e.value=value;afterNodeAccess(e);returnoldValue;}}++modCount;//步骤⑦:超过最大容量就扩容if(++size>threshold)resize();afterNodeInsertion(evict);returnnull;}问题发生在步骤②这里:
if((p=tab[i=(n-1)&hash])==null)tab[i]=newNode(hash,key,value,null);两个线程都执行了if语句,假设线程A先执行了tab[i]=newNode(hash,key,value,null),那table是这样的:
接着,线程B执行了tab[i]=newNode(hash,key,value,null),那table是这样的:
3被干掉了。
、put和get并发时会导致get到null线程A执行put时,因为元素个数超出阈值而出现扩容,线程B此时执行get,有可能导致这个问题。
注意来看resize源码:
finalNode<K,V>[]resize(){ Node<K,V>[]oldTab=table;intoldCap=(oldTab==null)?0:oldTab.length;intoldThr=threshold;intnewCap,newThr=0;if(oldCap>0){ //超过最大值就不再扩充了,就只好随你碰撞去吧if(oldCap>=MAXIMUM_CAPACITY){ threshold=Integer.MAX_VALUE;returnoldTab;}//没超过最大值,就扩充为原来的2倍elseif((newCap=oldCap<<1)<MAXIMUM_CAPACITY&&oldCap>=DEFAULT_INITIAL_CAPACITY)newThr=oldThr<<1;//doublethreshold}elseif(oldThr>0)//initialcapacitywasplacedinthresholdnewCap=oldThr;else{ //zeroinitialthresholdsignifiesusingdefaultsnewCap=DEFAULT_INITIAL_CAPACITY;newThr=(int)(DEFAULT_LOAD_FACTOR*DEFAULT_INITIAL_CAPACITY);}//计算新的resize上限if(newThr==0){ floatft=(float)newCap*loadFactor;newThr=(newCap<MAXIMUM_CAPACITY&&ft<(float)MAXIMUM_CAPACITY?(int)ft:Integer.MAX_VALUE);}threshold=newThr;@SuppressWarnings({ "rawtypes","unchecked"})Node<K,V>[]newTab=(Node<K,V>[])newNode[newCap];table=newTab;}线程A执行完table=newTab之后,线程B中的table此时也发生了变化,此时去get的时候当然会get到null了,因为元素还没有转移。
为了便于大家更系统化地学习Java,二哥已经将《Java程序员进阶之路》专栏开源到GitHub上了,大家只需轻轻地star一下,就可以和所有的小伙伴一起打怪升级了。
GitHub地址:/itwanger/toBeBetterJavaer
2024-12-23 01:42
2024-12-23 01:07
2024-12-23 00:54
2024-12-23 00:52
2024-12-23 00:32
2024-12-23 00:22
2024-12-22 23:44
2024-12-22 23:27