1 Rss > Xmx
现象
生产环境下其实是很常见的,java
进程的内存超过了设置的Xmx
,这也很容易理解,因为Xmx
指的是堆内存的上限,而Rss(Resident set size)
是整个进程的内存,他不仅包括了堆内存,还包括了jvm
这个c++进程运行所需要的内存。
简单讲Rss
是总内存 = 堆内存 + 堆外内存,Xmx
只是限制的堆内存大小,接下来用一个简单的Main.java
为例
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Scanner sc = new Scanner(System.in);
while (true) {
int cmd = sc.nextInt();
switch (cmd) {
case 1:
for (int i = 0; i< 10_000_000; i++) {
list.add("i=" + i);
}
System.out.println("add finish, current size " + list.size());
break;
case 0:
list.clear();
System.out.println("clear finish");
default:
break;
}
}
}
}
首先使用java 8具体版本为8.0.412-zulu
$ javac Main.java
$ java -Xmx2g -Xms2g -XX:+PrintGC Main
## 此时查看内存占用,空壳程序大概占用32M内存
然后java进程的控制台输入1
回车,代码逻辑是在一个list中加入1kw个String
,连续输入两个1
,日志如下。
此时rss
- heap
= 200M
,这部分是堆外的diff,但是常见的堆外内存metaspace
compressedClassSpace
codeSpace
加起来远远不到200M,是jvm进程自己占用了较多的堆外内存,这部分很难追溯。
此时java
进程输入0
清理内存,但是因为没有gc所以内存无变化,然后我们使用指令强制GC:jcmd $(jps | grep Main | awk '{print $1}') GC.run
,发现rss
内存一点都没有减小,而jmap
查看堆内存已经只有不到100m了,那gc清理的内存为什么没有从RSS
减去呢。
多次创建和清理后,Rss
来到2.2G
触发GC后,内存仍是2.2G
,堆内存仅79M
2 如何才能归还
这是因为jvm申请到内存后,是不会轻易将内存归还回去OS的。大内存占用有哪些坏处呢?不归还,当然对jvm进程本身是百益无害的,但是较大的rss
可能会导致操作系统无法给其他进程分配内存,还有可能导致操作系统OOM
,被迫把jvm
进程给干掉。
因而如果操作系统内存远大于Xmx
堆内存的设置的话,那其实无所谓,如果操作系统内存只比Xmx
大一点点,那很有可能会在堆内存被撑大之后,无法归还OS,最后导致总Rss
超过了系统内存,最终被kill。
归还的方式有以下几个方向:
UseG1GC
并且设置Xms<Xmx
,尤其是jdk11
之后效果更佳- 调整
MAX_ARENA_SIZE
或使用Jemalloc
改善底层libc
的内存申请策略。
2.1 g1
修改启动指令-Xms200m -XX:+UseG1GC
指定Xms
只有200M,并且使用g1gc,重复上面的流程,最后进程RSS只有323M
$ java -Xmx2g -Xms200m -XX:+UseG1GC -XX:+PrintGC Main
从图中左侧看出,堆大小在gc的过程中不断调节,一开始young区200M,然后不够了,不断扩大最终2004M,在最后一次GC的时候大小又缩小到266M
,缩小的过程中会把内存归还操作系统。
扩缩的过程是伴随在gc之后的,所以对整体的性能影响不算大,但是肯定也有一点点影响。但是生产环境较少看到Xms < Xmx
的情况,一般都设置为相等,这个可能来自较早的流传下来的习惯,因为之前没有g1
的时候,是没有这个效果,所以设置为不同对整体没有任何收益,尤其是服务端进程。这个大家可以酌情去设置,做好灰度验证,最终适合自己业务场景,那么就可以使用这种启动参数。
2.2 MAX_ARENA_SIZE
ARENA
是linux的libc中的默认malloc实现(ptmalloc),分配内存时的概念,他是为了解决多线程分配内存的并发问题设置的,在多个(64位系统默认是核心数x8)空间上分配内存,这个空间就是ARENA
,较大的ARENA
数,会导致内存碎片较多,通过如下指令,改为 1 ,会加剧多线程内存分配的竞争问题,但是带来的好处是,可以在一个ARENA
分配,如果之前有用不到的内存,一定程度上可以复用。
$ export MALLOC_ARENA_MAX=1 && java -Xmx2g -Xms2g -XX:+PrintGC Main
这个参数,并不会使得内存可以归还OS,但是多次分配->清理->分配->清理后,的Rss总大小会比原来小一些,例如之前重复操作会导致Rss
大于2.1G
这里我们同样多次操作,最后只有1.9G
。
2.3 Jemalloc
jemalloc
是一种有着更好性能表现的malloc
实现,可以替换libc
的ptmalloc
,在内存分配上可以更好的避免内存碎片,提高内存利用率,一定程度上能缓解jvm占用过多内存。
$ wget https://github.com/jemalloc/jemalloc/archive/5.3.0.tar.gz
$ tar zxvf 5.3.0.tar.gz
$ cd jemalloc-5.3.0/
$ ./autogen.sh
$ ./configure --prefix=/usr/local/jemalloc-5.3.0 --enable-prof
$ make
$ make install
指定环境变量LD_PRELOAD
MALLOC_CONF
的同时,启动java进程,这个shell指令下,环境变量只对java进程生效,如果想要整个OS都替换为jemalloc
,也可以直接export LD_PRELOAD=/usr/local/jemalloc-5.3.0/lib/libjemalloc.so.2
。
$ LD_PRELOAD=/usr/local/jemalloc-5.3.0/lib/libjemalloc.so.2 MALLOC_CONF="prof:true,lg_prof_interval:20" java Main
pmap
查看确实使用了jemalloc.so.2
运行1 1 0 1 1 0,折腾一圈之后发现内存时1834M
,比之前的2.2G
也要少一点。
可以看到malloc
相关的两个策略,对于内存的缩小,表现非常有限,但是你会发现在网上搜索各种资料,最后都会指向这两者,因为他们确实还是有一点效果的,并且可能针对不同的程序环境,效果会有不同,只能说我这个简单场景下表现一般。
2.4 jemalloc的profile
上面构建的时候选了with-prof
参数才能进行profiling
,同时我们运行java进程的时候指定了MALLOC_CONF="prof:true,lg_prof_interval:20
,含义是porf
开启,然后lg_prof_interval
是采样的频率,每2^20字节,也就是1M内存,所以这时候看当前目录下,其实有几百个文件了。但是没到几千个,说明这个值也是个估值。
修改MALLOC_CONF="prof:true,lg_prof_interval:30
可以降低采样频率,30就是1G,也就是每申请1G会有一个文件,但是这个参数在上面2G的内存的时候,没有生成文件。实际的采样过程中,jemalloc
使用的是概率方法来决定是否记录采样信息,而不是严格按每 1 GiB 进行一次采样。
对于上述程序最后使用MALLOC_CONF="prof:true,lg_prof_interval:27
可以得到较少的文件,接下来对生成的heap
文件分析(在当前目录下生成的)
$ apt-get install graphviz
$ jeprof /root/.sdkman/candidates/java/8.0.412-zulu/bin/java jeprof.69365.0.i0.heap
$ top20