Linux和JVM内存

Memory Allocation for JVM in Linux

Posted by S.L on April 17, 2019

在Linux下使用top命令进行Java进程状态的查看,对于%Mem这一列很多人都以为是通过-Xmx/-Xms设置的堆大小,这是对Linux 中进程分配的内存的错误理解。本文从操作系统内核以及用户进程JVM之间的关系上进行一定的说明。

了解内存类型

在Linux下输入top命令并回车后可以下按f键可以看到详细说明:

* A: PID        = Process Id
* E: USER       = User Name
* H: PR         = Priority
* I: NI         = Nice value
* O: VIRT       = Virtual Image (kb)
* Q: RES        = Resident size (kb)
* T: SHR        = Shared Mem size (kb)
* W: S          = Process Status
* K: %CPU       = CPU usage
* N: %MEM       = Memory usage (RES)
* M: TIME+      = CPU Time, hundredths
b: PPID       = Parent Process Pid
c: RUSER      = Real user name
d: UID        = User Id
f: GROUP      = Group Name
g: TTY        = Controlling Tty
j: P          = Last used cpu (SMP)
p: SWAP       = Swapped size (kb)
l: TIME       = CPU Time
r: CODE       = Code size (kb)
s: DATA       = Data+Stack size (kb)
u: nFLT       = Page Fault count
v: nDRT       = Dirty Pages count
y: WCHAN      = Sleeping in Function
z: Flags      = Task Flags <sched.h>
* X: COMMAND    = Command name/line

需要我们关注的和「内存」相关的属性如下:

  • O: VIRT (Virtual Image) - 进程使用的总虚拟内存 (virtual memory) 大小,包括进程的程序码、资料和共享程序库再加上被置换 (swap out) 的空间。公式:VIRT = SWAP + RES
  • p: SWAP (Swapped size) - 进程被置换的虚拟内存空间大小。
  • Q: RES (Resident size) - 常住内存,这个就是进程真正使用的RAM大小,也是进程「非被置换」的「实体内存」大小。公式:RES = CODE + DATA
  • r: CODE (Code size) - 进程的代码在实体内存占用空间大小,也叫作 text resident set (TRS)。
  • s: DATA (Data+Stack size) - 进程占用实体内存中的非代码部分大小,也叫作 data resident set (DRS)。如果top没有显示,按f键可以显示出来。这一块是真正的该程序要求的数据空间,是真正在运行中要使用的。
  • t: SHR (Shared Mem size) - 进程使用的共享内存大小,即可以和其他进程共享的内存空间。
  • N: %MEM (Memory usage) - 进程占用「实体内存」大小对系统总实体内存大小的比例,以百分比显示。

Linux与内存模型

JVM以一个进程(Process)的身份运行在Linux系统上,了解Linux与进程的内存关系,是理解JVM与Linux内存的关系的基础。

它给出了硬件、系统、进程三个层面的内存之间的概要关系。

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

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

从进程的角度来看,进程能直接访问的用户内存(虚拟内存空间)被划分为5个部分:代码区、数据区、堆区、栈区、未使用区。

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

进程与JVM模型

JVM本质就是一个进程,因此其内存模型也有进程的一般特点。但是,JVM又不是一个普通的进程,其在内存模型上有许多崭新的特点,主要原因有两个:

  • JVM将许多本来属于操作系统管理范畴的东西,移植到了JVM内部,目的在于减少系统调用的次数
  • Java NIO,目的在于减少用于读写IO的系统调用的开销

用户内存

JVM进程与普通进程的内存模型的对比:

  • 永久代本质上是Java程序的代码区和数据区,比如.class文件会被加载到这个区域的不同数据结构中去,包括常量池、域、方法数据、方法体、构造函数等
  • 永久代对于操作系统来说是堆的一部分,对于Java程序来说是容纳程序本身以及静态资源的空间
  • 新生代和老年代是Java程序真正可以使用的堆空间,主要用于内存对象的存储
  • 和普通进程管理内存有本质的区别:JVM内部在已经申请好的堆内存空间中进行内存申请和释放的管理,即垃圾回收,其他程序如C++在申请和释放内存时都交给操作系统来执行
  • JVM管理内存有效降低系统的调用次数,在给Java程序分配内存空间时无需操作系统干扰
  • 只有在堆大小改变时才需要向操作系统申请内存或通知回收

内核内存

  • 应用程序通常不直接和内核内存打交道,内核内存由操作系统进行管理和使用
  • Java NIO使用了内核内存或者映射到内核空间
  • JVM的NIO Buffer主要包括:NIO使用各种Channel时所使用的ByteBuffer、ByteBuffer.allocateDirect()申请分配的Buffer
  • PageCache里,NIO使用的内存主要包括FileChannel.map方式打开文件占用mapped和FileChannel.transferTo()/from()
  • Linux和JavaNIO在内核内存上开辟空间给程序使用,主要是减少不必要的复制,减少IO操作系统调用的开销
  • 内核内存对于Java程序性能也非常重要,因此,在划分系统内存使用时候,一定要给内核留出一定可用空间

References

本文首次发布于 S.L’s Blog, 作者 @stuartlau , 转载请保留原文链接.