在Linux下使用
top
命令进行Java进程状态的查看,对于%Mem
这一列很多人都以为是通过-Xmx/-Xms设置的堆大小,这是对Linux 中进程分配的内存的错误理解。本文从操作系统内核以及用户进程JVM之间的关系上进行一定的说明。
了解内存类型
在Linux下输入top
命令并回车后可以下按f键可以看到详细说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
* 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
本文首次发布于 LiuShuo’s Blog, 转载请保留原文链接.