COPY Linux与JVM的内存关系分析

本文详细解析了Linux系统与JVM之间的内存关系,对比了一般进程与JVM进程在内存使用上的区别,通过两个具体案例分析了内存分配不当和内存泄漏问题,并提出了相应的解决方案。

在一些物理内存为8g的服务器上,主要运行一个Java服务,系统内存分配如下:Java服务的JVM堆大小设置为6g,一个监控进程占用大约600m,Linux自身使用大约800m。从表面上,物理内存应该是足够使用的;但实际运行的情况是,会发生大量使用SWAP(说明物理内存不够使用了),如下图所示。同时,由于SWAP和GC同时发生会致使JVM严重卡顿,所以我们要追问:内存究竟去哪儿了?

Linux SWAP发生Linux SWAP发生

要分析这个问题,理解JVM和操作系统之间的内存关系非常重要。接下来主要就Linux与JVM之间的内存关系进行一些分析。

一、Linux与进程内存模型

JVM以一个进程(Process)的身份运行在Linux系统上,了解Linux与进程的内存关系,是理解JVM与Linux内存的关系的基础。 下图给出了硬件、系统、进程三个层面的内存之间的概要关系。

Linux内存体系

从硬件上看,Linux系统的内存空间由两个部分构成:物理内存和SWAP(位于磁盘)。物理内存是Linux活动时使用的主要内存区域;当物理内存不够使用时,Linux会把一部分暂时不用的内存数据放到磁盘上的SWAP中去,以便腾出更多的可用内存空间;而当需要使用位于SWAP的数据时,必须先将其换回到内存中。

从Linux系统上看,除了引导系统的BIN区,整个内存空间主要被分成两个部分:内核内存(Kernel space)、用户内存(User space)。 内核内存是Linux自身使用的内存空间,主要提供给程序调度、内存分配、连接硬件资源等程序逻辑使用。用户内存是提供给各个进程主要空间,Linux给各个进程提供相同的虚拟内存空间;这使得进程之间相互独立,互不干扰。实现的方法是采用虚拟内存技术:给每一个进程一定虚拟内存空间,而只有当虚拟内存实际被使用时,才分配物理内存。如下图所示,对于32的Linux系统来说,一般将0~3G的虚拟内存空间分配做为用户空间,将3~4G的虚拟内存空间分配为内核空间;64位系统的划分情况是类似的。

Linux内核内存与用户内存空间

从进程的角度来看,进程能直接访问的用户内存(虚拟内存空间)被划分为5个部分:代码区、数据区、堆区、栈区、未使用区。代码区中存放应用程序的机器代码,运行过程中代码不能被修改,具有只读和固定大小的特点。数据区中存放了应用程序中的全局数据,静态数据和一些常量字符串等,其大小也是固定的。堆是运行时程序动态申请的空间,属于程序运行时直接申请、释放的内存资源。栈区用来存放函数的传入参数、临时变量,以及返回地址等数据。未使用区是分配新内存空间的预备区域。

二、进程与JVM内存模型

JVM本质就是一个进程,因此其内存模型也有进程的一般特点。但是,JVM又不是一个普通的进程,其在内存模型上有许多崭新的特点,主要原因有两个:1.JVM将许多本来属于操作系统管理范畴的东西,移植到了JVM内部,目的在于减少系统调用的次数;2. Java NIO,目的在于减少用于读写IO的系统调用的开销。 JVM进程与普通进程内存模型比较如下图:

JVM进程内存模型

需要说明的是,这个模型的并不是JVM内存使用的精确模型,更侧重于从操作系统的角度而省略了一些JVM的内部细节(尽管也很重要)。下面从用户内存和内核内存两个方面讲解JVM进程的内存特点。

1.用户内存

上图特别强调了JVM进程模型的代码区和数据区指的是JVM自身的,而非Java程序的。普通进程栈区,在JVM一般仅仅用做线程栈。JVM的堆区和普通进程的差别是最大的,下面具体详细说明:

首先是永久代。永久代本质上是Java程序的代码区和数据区。Java程序中类(class),会被加载到整个区域的不同数据结构中去,包括常量池、域、方法数据、方法体、构造函数、以及类中的专用方法、实例初始化、接口初始化等。这个区域对于操作系统来说,是堆的一个部分;而对于Java程序来说,这是容纳程序本身及静态资源的空间,使得JVM能够解释执行Java程序。

其次是新生代和老年代。新生代和老年代才是Java程序真正使用的堆空间,主要用于内存对象的存储;但是其管理方式和普通进程有本质的区别。

普通进程在运行时给内存对象分配空间时,比如C++执行new操作时,会触发一次分配内存空间的系统调用,由操作系统的线程根据对象的大小分配好空间后返回;同时,程序释放对象时,比如C++执行delete操作时,也会触发一次系统调用,通知操作系统对象所占用的空间已经可以回收。

JVM对内存的使用和一般进程不同。JVM向操作系统申请一整段内存区域(具体大小可以在JVM参数调节)作为Java程序的堆(分为新生代和老年代);当Java程序申请内存空间,比如执行new操作,JVM将在这段空间中按所需大小分配给Java程序,并且Java程序不负责通知JVM何时可以释放这个对象的空间,垃圾对象内存空间的回收由JVM进行。

JVM的内存管理方式的优点是显而易见的,包括:第一,减少系统调用的次数,JVM在给Java程序分配内存空间时不需要操作系统干预,仅仅在Java堆大小变化时需要向操作系统申请内存或通知回收,而普通程序每次内存空间的分配回收都需要系统调用参与;第二,减少内存泄漏,普通程序没有(或者没有及时)通知操作系统内存空间的释放是内存泄漏的重要原因之一,而由JVM统一管理,可以避免程序员带来的内存泄漏问题。

最后是未使用区,未使用区是分配新内存空间的预备区域。对于普通进程来说,这个区域被可用于堆和栈空间的申请及释放,每次堆内存分配都会使用这个区域,因此大小变动频繁;对于JVM进程来说,调整堆大小及线程栈时会使用该区域,而堆大小一般较少调整,因此大小相对稳定。操作系统会动态调整这个区域的大小,并且这个区域通常并没有被分配实际的物理内存,只是允许进程在这个区域申请堆或栈空间。

2.内核内存

应用程序通常不直接和内核内存打交道,内核内存由操作系统进行管理和使用;不过随着Linux对性能的关注及改进,一些新的特性使得应用程序可以使用内核内存,或者是映射到内核空间。Java NIO正是在这种背景下诞生的,其充分利用了Linux系统的新特性,提升了Java程序的IO性能。

JVM使用的内核内存

上图给出了Java NIO使用的内核内存在linux系统中的分布情况。nio buffer主要包括:nio使用各种channel时所使用的ByteBuffer、Java程序主动使用ByteBuffer.allocateDirector申请分配的Buffer。而在PageCache里面,nio使用的内存主要包括:FileChannel.map方式打开文件占用mapped、FileChannel.transferTo和FileChannel.transferFrom所需要的Cache(图中标示 nio file)。

通过JMX可以监控到NIO Buffer和 mapped 的使用情况,如下图所示。不过,FileChannel的实现是通过系统调用使用原生的PageCache,过程对于Java是透明的,无法监控到这部分内存的使用大小。

JMX监控NIO内存

Linux和Java NIO在内核内存上开辟空间给程序使用,主要是减少不要的复制,以减少IO操作系统调用的开销。例如,将磁盘文件的数据发送网卡,使用普通方法和NIO时,数据流动比较下图所示:

NIO使用内核内存的优势

将数据在内核内存和用户内存之间拷贝是比较消耗资源和时间的事情,而从上图我们可以看到,通过NIO的方式减少了2次内核内存和用户内存之间的数据拷贝。这是Java NIO高性能的重要机制之一(另一个是异步非阻塞)。

从上面可以看出,内核内存对于Java程序性能也非常重要,因此,在划分系统内存使用时候,一定要给内核留出一定可用空间。

三、案例分析

1.内存分配问题

通过上面的分析,省略比较小的区域,可以总结JVM占用的内存: JVM内存 ≈ Java永久代 + Java堆(新生代和老年代) + 线程栈+ Java NIO

回到文章开头提出的问题,原来的内存分配是:6g(java堆) + 600m(监控) + 800m(系统),剩余大约600m内存未分配。

现在分析这600m内存的分配情况:

(1)Linux保留大约200m,这部分是Linux正常运行的需要,

(2)Java服务的线程数量是160个,JVM默认的线程栈大小是1m,因此使用160m内存,

(3)Java NIO buffer,通过JMX查到最多占用了200m,

(4)Java服务使用NIO大量读写文件,需要使用PageCache,正如前面分析,这个暂时不好定量估算大小。

前三项加起来已经560m,因此可以断定Linux物理内存不够使用。

细心的人会发现,引言中给出两个服务器,一个SWAP最多占用了2.16g,另外一个SWAP最多占用了871m;但是,似乎我们的内存缺口没有那么大。事实上,这是由于SWAP和GC同时进行造成的,从下图可以看到,SWAP的使用和长时间的GC在同一时刻发生。

Linux SWAPJVM GC

SWAP和GC同时发生会导致GC时间很长,JVM严重卡顿,极端的情况下会导致服务崩溃。原因如下:JVM进行GC时,时需要对相应堆分区的已用内存进行遍历;假如GC的时候,有堆的一部分内容被交换到SWAP中,遍历到这部分的时候就需要将其交换回内存,同时由于内存空间不足,就需要把内存中堆的另外一部分换到SWAP中去;于是在遍历堆分区的过程中,(极端情况下)会把整个堆分区轮流往SWAP写一遍。Linux对SWAP的回收是滞后的,我们就会看到大量SWAP占用。

上述问题,可以通过减少堆大小,或者增加物理内存解决。

因此,我们得出一个结论:部署Java服务的Linux系统,在内存分配上,需要避免SWAP的使用;具体如何分配需要综合考虑不同场景下JVM对Java永久代 、Java堆(新生代和老年代)、线程栈、Java NIO所使用内存的需求。

2.内存泄漏问题

另一个案例是,8g内存的服务器,Linux使用800m,监控进程使用600m,堆大小设置4g;系统可用内存有2.5g左右,但是也发生了大量的SWAP占用。

分析这个问题如下:

(1)在这个场景中, Java永久代 、Java堆(新生代和老年代)、线程栈所用内存基本是固定的,因此,占用内存过多的原因就定位在Java NIO上。

(2)根据前面的模型,Java NIO使用的内存主要分布在Linux内核内存的System区和PageCache区。查看监控的记录,如下图,我们可以看到发生SWAP之前,也就是物理内存不够使用的时候,PageCache急剧缩小。因此,可以定位在System区的Java NIO Buffer发生内存泄漏。

Linux SWAPLinux SWAP

(3)由于NIO的DirectByteBuffer需要在GC的后期被回收,因此连续申请DirectByteBuffer的程序,通常需要调用System.gc(),避免长时间不发生FullGC导致引用在old区的DirectByteBuffer内存泄漏。分析到此,可以推断有两种可能的原因:第一,Java程序没有在必要的时候调用System.gc();第二,System.gc()被禁用。 (4)最后是要排查JVM启动参数和Java程序的DirectByteBuffer使用情况。在本例中,查看JVM启动参数,发现启用了-XX:+DisableExplicitGC导致System.gc()被禁用。

四、总结

本文详细分析了Linux与JVM的内存关系,比较了一般进程与JVM进程使用内存的异同点,理解这些特性将对Linux系统内存分配、JVM调优、Java程序优化有帮助。限于篇幅关系仅仅列举两个案例,希望起到抛砖引玉的作用。

参考

  1. http://www.ibm.com/developerworks/library/j-nativememory-linux/
  2. http://www.ibm.com/developerworks/library/l-kernel-memory-access/
  3. http://www.ibm.com/developerworks/library/j-zerocopy/
  4. 《深入分析Java Web技术内幕》
<think>我们正在讨论JVM内存映射文件的读取流程。根据提供的引用,特别是引用[2]和引用[4]中的信息,我们可以总结如下: 内存映射文件(Memory Mapped Files)是Java NIO提供的一种机制,它允许将文件直接映射到进程的虚拟地址空间(即用户空间的私有地址空间)。这样,应用程序就可以像访问普通内存一样访问文件,而不需要通过传统的I/O操作(如read/write系统调用)来读写文件。这种机制可以显著提高I/O性能,尤其是对于大文件的访问。 流程如下: 1. **文件映射**:首先,通过`FileChannel.map()`方法将文件的一个区域映射到进程的虚拟地址空间。该方法会返回一个`MappedByteBuffer`对象,该对象就是映射区域的直接内存缓冲区。 2. **建立映射关系**:在操作系统中,这个映射操作是通过系统调用`mmap`(在Linux中)完成的。`mmap`会在进程的用户空间创建一个虚拟内存区域,并将该区域文件建立映射关系。注意,此时并没有立即将文件内容加载到物理内存中。 3. **读取数据**:当应用程序通过`MappedByteBuffer`访问文件的某个位置时,如果该位置的数据尚未加载到内存中,则会发生缺页中断(page fault)。操作系统会负责将相应的文件页(通常是4KB大小)从磁盘加载到物理内存中(此时会涉及磁盘I/O)。这个过程对应用程序是透明的,应用程序就像在访问普通内存一样。 4. **直接访问**:一旦文件页被加载到物理内存,后续对同一区域的访问将直接操作物理内存,无需再次从磁盘读取(除非页面被换出)。如果文件被修改,操作系统会负责在适当的时候将修改写回磁盘(也可以手动调用`MappedByteBuffer.force()`方法强制写回)。 5. **解除映射**:当不再需要映射时,可以通过`FileChannel.unmap()`方法(或者等待垃圾回收器回收`MappedByteBuffer`对象时自动解除映射)来解除映射关系。解除映射后,进程将不能再访问该内存区域,操作系统会回收相关的资源(如页表项等)。 值得注意的是,内存映射文件属于直接内存(Direct Memory),也就是堆外内存,不受JVM堆大小的限制,但受操作系统的虚拟内存大小的限制。 引用[2]特别指出,内存映射文件相比标准IO操作,它不需要将数据读取到OS内核缓冲区(即不需要经过内核空间的缓冲区),而是直接将用户空间的虚拟地址映射到文件,因此少了一次从内核缓冲区到用户缓冲区的数据拷贝(即零拷贝技术的一种应用)。 因此,JVM内存映射文件的读取流程可以概括为:建立映射(通过`mmap`系统调用)→按需加载(通过缺页中断)→直接访问(在用户空间操作内存)。 下面用简单的步骤描述: 1. 应用程序调用`FileChannel.map()`方法,指定映射模式(只读、读写等)和映射区域。 2. JVM内部通过本地方法调用操作系统的`mmap`系统调用,建立文件到用户空间内存的映射。 3. 返回一个`MappedByteBuffer`对象,该对象指向映射的内存区域。 4. 当读取该缓冲区的数据时,如果数据尚未加载,则触发缺页中断,由操作系统加载相应的文件数据到物理内存。 5. 应用程序直接通过`MappedByteBuffer`读取内存中的数据,无需经过JVM堆。 引用[4]中也提到了直接内存(包括内存映射文件)在提高I/O性能方面的作用,特别是避免了数据在Java堆和本地内存之间的复制。 **注意**:内存映射文件的写操作也是类似的,对缓冲区的修改会由操作系统在后台写回文件(除非显式调用`force()`方法,否则不会立即写回)。 下面生成相关问题: </think>### JVM内存映射文件的读取流程 JVM内存映射文件(Memory Mapped Files)的读取流程基于操作系统提供的虚拟内存机制,通过直接建立文件进程虚拟地址空间的映射来实现高效访问。以下是详细流程: 1. **建立文件映射关系** 通过`FileChannel.map()`方法调用系统级`mmap`函数,将目标文件的指定区域映射到进程的虚拟地址空间。此时操作系统仅在页表中创建映射条目,**并不立即加载文件数据到物理内存**(称为"懒加载")[^2][^4]。 2. **触发缺页中断** 当Java程序通过`MappedByteBuffer`访问映射区域时,CPU检测到虚拟地址尚未关联物理内存,触发**缺页中断(Page Fault)**。操作系统挂起当前线程,由内核处理中断[^3]。 3. **数据加载到物理内存** 内核根据映射关系定位文件位置,将对应文件块(通常4KB大小)从磁盘**直接加载到物理内存的页帧**中,并更新页表条目指向该物理页。**不经过JVM堆或内核缓冲区**,减少数据拷贝[^2][^5]。 4. **应用程序直接访问** 缺页中断处理完成后,CPU重新执行内存访问指令。此时进程可直接通过虚拟地址**读写物理内存中的文件数据**,如同操作普通内存数组: ```java MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize); byte data = buffer.get(1024); // 直接访问物理内存数据 ``` 5. **内存管理优化** - **预读机制**:操作系统可能预加载相邻文件块到内存 - **页面置换**:内存不足时,LRU等算法自动将冷数据页写回磁盘 - **同步控制**:调用`buffer.force()`强制将修改刷盘[^4] ### 关键技术优势 - **零拷贝(Zero-copy)**:消除JVMOS内核间的数据复制 - **堆外内存**:不受`-Xmx`限制,规避GC停顿[^1][^5] - **原子页操作**:文件修改以内存页为单位,保证操作原子性[^2] ### 流程示意图 ``` Java进程虚拟地址空间 ┌─────────────────────┐ │ MappedByteBuffer ├──┐ └─────────────────────┘ │ mmap系统调用 ▼ 页表映射 │ 缺页中断时加载文件块 │ ▼ 磁盘文件 ────────────► 物理内存页帧 ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值