天天看点

Java程序员也应该知道的系统知识系列之内存

作者:林昊

Java程序员也应该知道的系统知识系列之内存

上篇说到了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了。