写了多年代码,你却不知道的程序设计的5个底层逻辑( 四 )


写了多年代码,你却不知道的程序设计的5个底层逻辑

文章插图
 
  • 装入位 表示对于页是否在主存,如果地址页每页表示,数据还在磁盘
  • 存放位置 建立虚拟页和物理页的映射,用于地址转换,如果为null表示是一个未分配页
  • 修改位 用来存储数据是否修改过
  • 权限位 用来控制是否有读写权限
  • 禁止缓存位 主要用来保证 cache 主存 磁盘的数据一致性
内存映射
正常情况下,我们读取文件的流程为,先通过系统调用从磁盘读取数据,存入操作系统的内核缓冲区,然后在从内核缓冲区拷贝到用户空间,而内存映射,是将磁盘文件直接映射到用户的虚拟存储空间中,通过页表维护虚拟地址到磁盘的映射,通过内存映射的方式读取文件的好处有,因为减少了从内核缓冲区到用户空间的拷贝,直接从磁盘读取数据到内存,减少了系统调用的开销,对用户而言,仿佛直接操作的磁盘上的文件,另外由于使用了虚拟存储,所以不需要连续的主存空间来存储数据 。
写了多年代码,你却不知道的程序设计的5个底层逻辑

文章插图
 
在 Java 中,我们使用 MAppedByteBuffer 来实现内存映射,这是一个堆外内存,在映射完之后,并没有立即占有物理内存,而是访问数据页的时候,先查页表,发现还没加载,发起缺页异常,然后在从磁盘将数据加载进内存,所以一些对实时性要求很高的中间件,例如rocketmq,消息存储在一个大小为1G的文件中,为了加快读写速度,会将这个文件映射到内存后,在每个页写一比特数据,这样就可以把整个1G文件都加载进内存,在实际读写的时候就不会发生缺页了,这个在rocketmq内部叫做文件预热 。
下面我们贴一段 rocketmq 消息存储模块的代码,位于 MappedFile 类中,这个类是 rocketMq 消息存储的核心类感兴趣的可以自行研究,下面两个方法一个是创建文件映射,一个是预热文件,每预热 1000 个数据页,就让出 CPU 权限 。
private void init(final String fileName, final int fileSize) throws IOException { this.fileName = fileName; this.fileSize = fileSize; this.file = new File(fileName); this.fileFromOffset = Long.parseLong(this.file.getName()); boolean ok = false; ensureDirOK(this.file.getParent()); try { this.fileChannel = new RandomaccessFile(this.file, "rw").getChannel(); this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize); TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize); TOTAL_MAPPED_FILES.incrementAndGet(); ok = true; } catch (FileNotFoundException e) { log.error("create file channel " + this.fileName + " Failed. ", e); throw e; } catch (IOException e) { log.error("map file " + this.fileName + " Failed. ", e); throw e; } finally { if (!ok && this.fileChannel != null) { this.fileChannel.close(); } } }//文件预热,OS_PAGE_SIZE = 4kb 相当于每 4kb 就写一个 byte 0 ,将所有的页都加载到内存,真正使用的时候就不会发生缺页异常了 public void warmMappedFile(FlushDiskType type, int pages) { long beginTime = System.currentTimeMillis(); ByteBuffer byteBuffer = this.mappedByteBuffer.slice(); int flush = 0; long time = System.currentTimeMillis(); for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) { byteBuffer.put(i, (byte) 0); // force flush when flush disk type is sync if (type == FlushDiskType.SYNC_FLUSH) { if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) { flush = i; mappedByteBuffer.force(); } } // prevent gc if (j % 1000 == 0) { log.info("j={}, costTime={}", j, System.currentTimeMillis() - time); time = System.currentTimeMillis(); try { // 这里sleep(0),让线程让出 CPU 权限,供其他更高优先级的线程执行,此线程从运行中转换为就绪 Thread.sleep(0); } catch (InterruptedException e) { log.error("Interrupted", e); } } } // force flush when prepare load finished if (type == FlushDiskType.SYNC_FLUSH) { log.info("mapped file warm-up done, force to disk, mappedFile={}, costTime={}", this.getFileName(), System.currentTimeMillis() - beginTime); mappedByteBuffer.force(); } log.info("mapped file warm-up done. mappedFile={}, costTime={}", this.getFileName(), System.currentTimeMillis() - beginTime); this.mlock(); }JVM 中对象的内存布局
在linux中只要知道一个变量的起始地址就可以读出这个变量的值,因为从这个起始地址起前8位记录了变量的大小,也就是可以定位到结束地址,在 Java 中我们可以通过 Field.get(object) 的方式获取变量的值,也就是反射,最终是通过 UnSafe 类来实现的 。我们可以分析下具体代码 。
Field 对象的 getInt方法 先安全检查 ,然后调用 FieldAccessor @CallerSensitive public int getInt(Object obj) throws IllegalArgumentException, IllegalAccessException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, obj, modifiers); } } return getFieldAccessor(obj).getInt(obj); } 获取field在所在对象中的地址的偏移量 fieldoffset UnsafeFieldAccessorImpl(Field var1) { this.field = var1; if(Modifier.isStatic(var1.getModifiers())) { this.fieldOffset = unsafe.staticFieldOffset(var1); } else { this.fieldOffset = unsafe.objectFieldOffset(var1); } this.isFinal = Modifier.isFinal(var1.getModifiers()); } UnsafeStaticIntegerFieldAccessorImpl 调用unsafe中的方法 public int getInt(Object var1) throws IllegalArgumentException { return unsafe.getInt(this.base, this.fieldOffset); }


推荐阅读