作者:林昊
上篇说到了Java程序和CPU的关系,对于多数实现的较好的Java应用程序而言,基本上随着CPU的核数增加或能力提升,系统能够支撑的并发量就可以稳步上升,但对于内存而言,是否也是这样呢,这篇我们就来看看Java程序和内存的关系。
和CPU一样,我们首先要知道机器上的内存的硬件状况,在linux下,可以通过dmidecode | grep -A16 “Memory Device$”命令来查看机器插了多少根内存条,以及每根内存条的具体型号,内存条的具体型号对Java应用的运行性能也会有些影响,但一般来说不会有CPU那么明显。
要查看机器上内存的使用状况,可通过free -m来查看,这个时候常见的第一个问题是看到free值很小,就认为内存不够用了,但其实真正可用的内存是free+buffers+cached,os为了提升运行性能,会利用一些内存来做cache,以提升诸如读写文件的速度等。
当free不够的时候,os会根据一个系统值来决定是释放buffers/cached还是使用swap,如果swap没开启就不用判断了,如果swap开启了,那么vm.swappiness这个值就非常关键了,这个值是一个倾向值的意思,值越大表示越倾向于使用swap,越小表示越倾向于释放buffers/cached,对于响应时间敏感的应用而言,只要用到swap了,通常对响应时间的影响都会很明显,而且swappiness默认是60,意味着默认其实是倾向于使用swap的,因此对于这类系统建议最好是关闭swap,毕竟对于集群型的应用来说,通常都是宁可接受内存不够用的情况下机器挂掉,也不能接受响应时间变慢。
对于cached的内存区域,可以执行echo 3 > /proc/sys/vm/drop_caches来强制释放,这种在某些情况下可能会需要用,例如希望把还在cache里的文件内容刷到磁盘。
对于swap区域,可以通过执行swapoff -a来强制刷掉,如果需要再开启,可以执行swapon -a。
除了os利用内存来提升运行性能外,cpu也同样借助它的各级cache来提升运行速度,多核之后,UMA的方式导致系统总线带宽会比较吃紧,而NUMA是解决这个的一种好的方式,关于NUMA具体是什么就不在这里讲了,需要知道下的是默认通常是不打开NUMA的,从我们的一些测试来看,有些CPU型号在是否打开NUMA的情况下应用的性能会相差一倍,不过大部分的CPU型号里打开NUMA的提升大概会在20%–30%左右,如果OS没打开NUMA,其实在Java启动参数上设置了-XX:+UseNuma也是没什么用的,可以用numactl -H来查看NUMA是否打开,但由于打开NUMA的话对应用跑在同一个NUMA Node上要求还是比较高的,因此在虚拟机类的场景中为了追求CPU搭配的灵活性以及维护的简便性,通常就只能放弃NUMA了。
要看运行的Java进程消耗的内存,可以用ps aux | grep java或具体的pid、或top -p [pid]也可以看,可以看到的是有两列内存的信息,一列是VIRT,一列是RES。
VIRT表示的是此进程占用的地址空间的大小,地址空间在32bit的os上的上限是3G,在64bit可以认为是无限大,当地址空间不够用的时候,Java进程会直接crash,在crash的log里会有java.lang.OutOfMemoryError: Out of swap space的信息,Java进程在启动时会根据-Xms + -XX:PermSize先申请好相应大小的地址空间,在创建线程等的时候也会直接申请好-Xss对应大小的地址空间,所以创建了很多线程的情况下可以看到VIRT会很高,
RES表示的是此进程具体占用的内存的大小,这个地方很容易产生两个疑问:
1. 为什么看到的RES值大于或小于了-Xmx的设置;
Java应用在刚启动,或者说还没有到触发Full GC之前,只有当真正需要使用内存才会去占用实际的内存,否则只是占据了地址空间,因此看到的RES值有可能会小于-Xmx的值;
而对于一个运行了一段时间且触发过CMS GC/Full GC的Java应用而言,则很有可能看到的RES大于了-Xmx的值,原因在于Java除了-Xmx会占用相应的内存外,Perm Gen、C Heap(CodeCache、Direct Memory、线程、对象结构、GC等)也要占据一些内存,所以看到的RES大于-Xmx也很正常。
2. 为什么GC后RES的值没下降相应的数值;
这个的原因在于GC后JVM并不会把内存释放给OS,而是会占着继续用。
Java程序在运行中过程,除了Direct Memory、直接用Unsafe操作、或间接的使用Deflater等的会涉及到C Heap,更多的是去JVM Heap中申请内存,并且由于JVM包装掉了,所以Java程序员在写代码的时候很容易由于错误的使用API或数据结构导致内存的浪费,这通常是为什么很多C的高手(注意:这里说的是C的高手)写的代码效率会比普通的Java程序员写的高不少的一个原因之一,而回收也由JVM来控制,这个系列的文章主要是科普下系统方面的知识,JVM的一些就不在这里写了,在之前的一些PPT或文章里也写过很多次关于JVM的内存管理,同样关于怎么去查Java程序在JVM Heap和C Heap里的消耗,之前也写过不少的文章,就不在这里写了,毕竟这些多数和系统关系就不算大了。
关于内存资源这块,Java程序倒不一定是越多越好,内存越大,通常也就意味着GC的负担越重,而GC的时候通常应用是全暂停的(除了CMS是Almost Concurrently外),但也不能太小,太小的话运行时会比较明显的暴露出来,因为会导致非常频繁的GC(到底多频繁算频繁呢,从目前的经验来看,ygc尽可能能在3s+一次,fgc或cms gc的话最好在10分钟以上),而太频繁的GC会导致CPU大部分时候都耗了执行GC上,应用能够支撑的并发量自然就会不够,够用就OK,在排除内存泄露等因素外,可以看看在Full GC后实际需要占用的内存大小,一般来说只要确保给Java进程留有的空间比这个需要常驻的大小大一定比例就OK(不过到底大多少还真不好说,凭经验吧),不要因为机器内存有多(相对而言,现在多数机器在内存这块都是比较够的),就给Java分配更多的内存,否则一次较长时间的暂停搞不好就回导致极大的杯具,所以内存资源这块和CPU不太一样,我的观点一向是够用并留有一定空间就OK,而不用去追求用满,当然如果能充分有效的利用多余的内存提升性能当然是OK的,例如cache什么的。
从内存资源的状况可以看到,随着硬件的不断发展,将来对Java应用而言,会有个悲催的现象是,CPU用的比较满,但机器的内存资源浪费的比较严重,针对这个问题,看来后面必须专门写一篇来讲讲虚拟化。
说到这了,顺带说下上篇文章留下的一个话题,就是GC这种线程在执行的时候是怎么确保占有足够的时间片,这个的原因是GC在执行的时候其他的线程其实都是处于暂停状态(其实这话不太准确),GC要执行前,JVM会先将一个内存页设为只读,而在所有有引用关系赋值的地方,JVM在编译代码时都会先插入一个检查某个内存页的状态的代码,而因为之前GC已经把这个内存页状态设为了只读,所以当其他线程的代码走到这个地方的时候,会抛出异常,从而导致线程进入一个blocked的状态,就不会来抢占GC线程需要的CPU了。