1.直接内存的源码读写速度为啥快于堆上内存?
2.各种ByteBuffer解析
3.深入分析堆外内存 DirectByteBuffer & MappedByteBuffer
4.IBM WAS 内存溢出问题排查
5.深入理解DirectBuffer
直接内存的读写速度为啥快于堆上内存?
直接内存的读写速度为何快于堆上内存呢?
我们对比直接内存和堆内存的方法的汇编代码。可以看到,源码直接内存指令通常比堆内存指令要简洁,源码速度更快。源码
直接内存之所以速度更快,源码有三个主要原因:
首先,源码涨停监测源码直接内存使用DirectByteBuffer,源码它没有额外的源码offset字段,避免了一次不必要的源码读内存和计算操作。
其次,源码写数组时无需进行数组越界检查,源码这减少了时间开销。源码
最后,源码直接内存数组对象的源码地址是压缩指针,使用时需解压。源码但这种操作通常影响较小,而直接内存的读写速度优势明显。
各种ByteBuffer解析
ByteBuffer解析概览
在深入研究RocketMQ源码过程中,ByteBuffer频繁出现,起初让人困惑,但通过学习和理解,其核心概念逐渐明朗。本文将分享关于ByteBuffer的基础知识和常用操作。 ByteBuffer是Buffer的子类,它是一个字节缓冲区,可扩展到其他类型如IntBuffer和LongBuffer。Buffer的结构包括私有变量,如position、limit和capacity,它们之间满足mark <= position <= limit <= capacity的规则。 关键方法包括:设置limit和position为0,mark置0,obs简易源码用于读写转换;remaining()返回limit与position之间的差值,hasRemaining()则用于判断是否还有剩余空间。在实际操作中,flip方法非常重要,它在写入数据前后进行状态转换,确保正确读写。 ByteBuffer有堆内(HeapByteBuffer)和堆外(DirectByteBuffer)两种实现。HeapByteBuffer基于字节数组,而DirectByteBuffer则在直接内存中分配。MappedByteBuffer与FileChannel结合,通过mmap映射文件,提供内存映射功能。 在实际使用中,如写入文件,flip方法确保了数据正确写入堆外内存,避免了数据复制。MappedByteBuffer通过force()方法保证数据持久化,防止内存丢失。FileChannel和MappedByteBuffer虽然看似独立,但它们在操作上是相关的,尤其在读写分离的场景中,如RocketMQ设计中。 通过本文,希望能帮助读者更好地理解ByteBuffer的运作机制,下次遇到相关问题时能更加得心应手。持续关注公众号Hn技术随笔,获取更多技术分享。深入分析堆外内存 DirectByteBuffer & MappedByteBuffer
大家好,我是大明哥,一个专注于「死磕 Java」系列创作的硬核程序员。本文内容已收录在我的开锁宝源码技术网站:。
ByteBuffer有两种特殊类:DirectByteBuffer和MappedByteBuffer,它们的原理都是基于内存文件映射的。
ByteBuffer分为直接和间接两种。
我们先了解几个基本概念。
操作系统为什么要区分真实内存(物理内存)和虚拟内存呢?这是因为如果我们只使用物理内存会有很多问题。
对于常用的Linux操作系统而言,虚拟内存一般是4G,其中1G为系统内存,3G为应用程序内存。
进程使用的是虚拟内存,但我们数据还是存储在物理内存上,那么虚拟内存是怎样和物理内存对应起来的呢?答案是页表,虚拟内存和物理内存建立对应关系采用的是页表页映射的方式。
页表记录了虚拟内存每个页和物理内存之间的对应关系,具体如下:
它有两个栏位:有效位和路径。
当CPU寻址时,它有三种状态:
CPU访问虚拟内存地址过程如下:
下面是Linux进程的虚拟内存结构:
注意其中一块区域“Memory mapped region for shared libraries”,这块区域就是内存映射文件时将某一段虚拟地址和文件对象的某一部分建立映射关系,此时并没有拷贝数据到内存中,而是当进程代码第一次引用这段代码内的虚拟地址时,触发了缺页异常,这时候OS根据映射关系直接将文件的相关部分数据拷贝到进程的用户私有空间中去,当有操作第N页数据的时候重复这样的OS页面调度程序操作。这样就减少了文件拷贝到内核空间,再拷贝到用户空间,效率比标准IO高。
接下来,我们分析MappedByteBuffer和DirectByteBuffer的类图:
MappedByteBuffer是一个抽象类,DirectByteBuffer则是它的子类。
MappedByteBuffer作为抽象类,其实它本身还是智能灯源码非常简单的。定义如下:
在父类Buffer中有一个非常重要的属性address,这个属性表示分配堆外内存的地址,是为了在JNI调用GetDirectBufferAddress时提升它调用的速率。这个属性我们在后面会经常用到,到时候再分析。
MappedByteBuffer作为ByteBuffer的子类,它同时也是一个抽象类,相比ByteBuffer,它新增了三个方法:
与传统IO性能对比:
相比传统IO,MappedByteBuffer只有一个字,快!!!它之所以快,在于它采用了direct buffer(内存映射)的方式来读取文件内容。这种方式是直接调动系统底层的缓存,没有JVM,少了内核空间和用户空间之间的复制操作,所以效率大大提高了。那么它相比传统IO快了多少呢?下面我们来做个小实验。
通过更改size的数字,我们可以生成k,1M,M,M,1G五个文件,我们就这两个文件来对比MappedByteBuffer和传统IO读取文件内容的性能。
大明哥电脑是GB的MacBook Pro,对k,1M,M,购买源码被骗M,1G五个文件的测试结果如下:
绿色是传统IO读取文件的,蓝色是使用MappedByteBuffer来读取文件的,从图中我们可以看出,文件越大,两者读取速度差距越大,所以MappedByteBuffer一般适用于大文件的读取。
父类MappedByteBuffer做了基本的介绍,且与传统IO做了一个对比,这里就不对DirectByteBuffer做介绍了,咱们直接撸源码,撸了源码后我相信你对堆外内存会有更加深入的了解。
DirectByteBuffer是包访问级别,其定义如下:
DirectByteBuffer可以通过ByteBuffer.allocateDirect(int capacity)进行构造。
调用DirectByteBuffer构造函数:
这段代码中有三个方法非常重要:
下面就来逐个分析这三段代码。
这段代码有两个作用
maxMemory=VM.maxDirectMemory(),获取JVM允许申请的最大DirectByteBuffer的大小,该参数可通过XX:MaxDirectMemorySize来设置。这里需要注意的是-XX:MaxDirectMemorySize限制的是总cap,而不是真实的内存使用量,因为在页对齐的情况下,真实内存使用量和总cap是不同的。
tryReserveMemory()可以统计DirectByteBuffer占用总内存的大小,如果发现堆外内存无法再次分配DirectByteBuffer则会返回false,这个时候会调用jlra.tryHandlePendingReference()来进行会触发一次非堵塞的Reference#tryHandlePending(false),通过注释我们了解了该方法主要还是协助ReferenceHandler内部线程进行下一次pending的处理,内部主要是希望遇到Cleaner,然后调用Cleaner#clean()进行堆外内存的释放。
如果还不行的话那就只能调用System.gc();了,但是我们需要注意的是,调用System.gc();并不能马上就可以执行full gc,所以就有了下面的代码,下面代码的核心意思是,尝试9次,如果依然没有足够的堆外内存来进行分配的话,则会抛出异常OutOfMemoryError("Direct buffer memory")。每次尝试之前都会Thread.sleep(sleepTime),给系统足够的时间来进行full gc。
总体来说Bits.reserveMemory(size, cap)就是用来统计系统中DirectByteBuffer到底占用了多少,同时通过进行GC操作来保证有足够的内存空间来创建本次的DirectByteBuffer对象。所以对于堆外内存DirectByteBuffer我们依然可以不需要手动去释放内存,直接交给系统就可以了。还有一点需要注意的是,别设置-XX:+DisableExplicitGC,否则System.gc();就无效了。
到了这段代码我们就知道了,我们有足够的空间来创建DirectByteBuffer对象了.unsafe.allocateMemory(size)是一个native方法,它是在堆外内存(C_HEAP)中分配一块内存空间,并返回堆外内存的基地址。
这段代码其实就是创建一个Cleaner对象,该对象用于对DirectByteBuffer占用的堆外内存进行清理,调用create()来创建Cleaner对象,该对象接受两个参数:
调用Cleaner#clean()进行清理,该方法其实就是调用thunk#run(),也就是Deallocator#run():
方法很简单就是调用unsafe.freeMemory()释放指定堆外内存地址的内存空间,然后重新统计系统中DirectByteBuffer的大小情况。
Cleaner是PhantomReference的子类,PhantomReference是虚引用,熟悉JVM的小伙伴应该知道虚引用的作用是跟踪垃圾回收器收集对象的活动,当该对象被收集器回收时收到一个系统通知,所以Cleaner的作用就是能够保证JVM在回收DirectByteBuffer对象时,能够保证相对应的堆外内存也释放。
在创建DirectByteBuffer对象的时候,会new一个Cleaner对象,该对象是PhantomReference的子类,PhantomReference为虚引用,它的作用在于跟踪垃圾回收过程,并不会对对象的垃圾回收过程造成任何的影响。
当DirectByteBuffer对象从pending状态->enqueue状态,它会触发Cleaner#clean()。
在clean()方法中其实就是调用thunk.run(),该方法有DirectByteBuffer的内部类Deallocator来实现:
直接用unsafe.freeMemory()释放堆外内存了,这个address就是分配堆外内存的内存地址。
关于堆外内存DirectByteBuffer就介绍到这里,我相信小伙伴们一定有所收获。下面大明哥介绍堆内内存:HeapByteBuffer。
IBM WAS 内存溢出问题排查
IBM WAS 内存溢出问题排查案例分享引言 WAS(IBM WebSphere Application Server)作为一款成熟的大型企业级Web中间件产品,在国内大型商业银行Web服务领域广泛应用,以其可靠性与稳定性著称。然而,即使稳定性极高,生产运维中仍会遇到WAS应用问题,排查问题时常常感到棘手。本文将分享自己在应对WAS内存溢出方面的心得,帮助大家更优雅地解决内存溢出问题。 IBM JAVA内存管理 在解决内存溢出问题前,理解IBM JAVA内存管理至关重要。WAS使用的JAVA是内置的IBM JAVA,与Oracle Java在JVM、配置参数等方面存在差异。 IBM JAVA包含JDK、JRE、JVM三层,内存管理区域包括程序计数器、虚拟机栈、方法区、堆等。堆是JVM运行时内存中最大的区域,也是直接关联程序开发的部分,内存溢出主要发生在该区域。WAS支持的GC策略有多种,本文重点介绍“Generational Concurrent Garbage Collector”策略。 该策略将堆空间分为新区域(Nursery)和老区域(Tenured),对象创建后首先被分配到Nursery,存活的对象会移动到Tenured区域,减少GC时间,降低系统吞吐量下降的影响。 常见的WAS内存溢出原因 理解内存管理模型后,我们能清楚地知道发生内存溢出的原因。基本原因包括堆空间配置不当、内存泄漏、大对象或数组使用不当等。此外,本地操作系统内存不足、其他进程占用过高、DirectMemory使用不当也可能导致内存溢出。 如何优雅地应对WAS内存溢出 当发生内存溢出后,首先恢复生产,然后根据以下步骤进行分析:收集环境信息和日志,包括JVM内存使用情况、可疑的异常交易。
使用工具(如IBM Pattern Modeling and Analysis Tool for Java Garbage Collector、HeapAnalyzer、Thread and Monitor Dump Analyzer for Java)分析日志文件。
检查DirectByteBuffer内存区域以确认是否直接内存溢出。
在具体场景中应用 结合实际案例,当系统交易超时率升高时,首先通过监控发现内存异常,快速重启WAS应用恢复生产。接着,采集日志、分析Heapdump和Javacore文件,确认内存溢出原因。通过分析日志和工具结果,定位问题为某查询语句未对结果分页,一次性获取大量数据导致内存溢出。 预防或解决内存溢出问题 预防内存溢出的关键在于合理配置JVM参数、优化代码避免内存泄漏、限制大对象或数组使用、监控系统资源使用情况。通过这些方法,可以有效预防或解决WAS内存溢出问题。深入理解DirectBuffer
DirectBuffer在高性能场景中,因其堆外内存的特性,相较于ByteBuffer,能有效提升数据处理效率。本文将从源码角度深入解析DirectBuffer的原理和使用方式。
在Intel X架构下,用户态(Ring3)与内核态(Ring0)的划分保证了安全隔离。应用程序通过系统调用,将需要内核支持的任务委托给运行在Ring0的内核。创建DirectBuffer时,调用new DirectByteBuffer(int cap)的私有构造函数,它完成内存分配、大小记录和Cleaner对象的声明,以备后续内存清理。
使用DirectBuffer时,主要有putXXX和getXXX方法。putXXX如putInt,根据内存对齐和字节序,调用unsafe或Bits方法将数据写入。getXXX则根据对齐情况,通过相应方法读取数据。
内存回收有System.gc和Cleaner对象两种方式。System.gc会在内存不足且没有禁用显式GC时触发Full GC,尝试清理堆外内存。Cleaner对象则在DirectBuffer不再被引用时自动执行,释放堆外内存。
正确运用DirectBuffer,能够优化程序性能,减少GC的频繁发生。在高性能中间件中,它是一个实用且重要的工具。深入了解DirectBuffer的使用,对提高开发效率至关重要。