我所关注的Java问题

Java作为一个发展了多年的语言,由于历史包袱等原因,自然会有不少问题,这里讲的是Oracle Hotspot(其他的JVM会有些不同),我最关注的主要是以下几个:

1. 对大内存的支持
内存容量发展越来越大,而自然Java应用也会越多的面对大内存的场景,目前Java在大内存的情况下,有两个主要的问题:
* GC问题,CMS GC最大的问题是碎片,碎片所导致的不可预知的Full GC的行为是非常可怕的,我们都有好几个应用开始要在每天低峰的时候强制执行full gc来避免碎片问题,G1GC也仍然是有碎片问题的…
* 排查问题,大内存的情况下dump、分析目前的Hotspot支持的都非常糟糕。

2. 启动瞬间慢的问题
Hotspot在刚启动时是解释模式,逐步才编译为native代码,因此一定会出现启动瞬间慢的现象,这个问题很容易导致有些访问量很高的应用在启动瞬间会出现非常多的请求失败的现象,尽管目前Hotspot也做了很多的努力,例如TieredCompilation等。

3. coroutine
很多的Java应用在处理请求时都会有大量的访问后端的行为,例如访问数据库、调用其他应用等,由于是线程模式,会导致在支撑很高的并发时会比较容易达到瓶颈,而coroutine对于类似这样的场景,会有不小的帮助。
而更进一步,对于分布式的Java应用来说,在一次请求中可能会有大量的并行的后端调用,这种时候如果是线程模式也不是很好做。

4. 序列化/反序列化
这个算比较特殊的一个点,在一次请求要访问非常多后端的情况下,序列化/反序列化通常会成为很重要的一个CPU的消耗点,而根据我们以往的排查我们能看到主要的原因还是在序列化/反序列化中从对象到流,以及从流到对象的过程,所以如果能在这里有突破的话,会对分布式的Java应用有很大的帮助。

5. 字符串append
之前我们在分析Java应用的内存消耗时,会看到StringBuilder/StringBuffer.append造成的char[]数组的扩充造成了主要的内存消耗,而其实这个如果有改进是可以大幅减少内存消耗的压力的(在我们的很多应用上,我们可以看到在很高并发量时GC占据的CPU还是不少的)。

这里还有一个牛人写的JVM implementation challenges的pdf,感兴趣的也可以看看。

你有什么特别关心的Java问题呢,也可以回复我说说看。

=============================
欢迎关注微信公众号:hellojavacases

关于此微信号:
分享Java问题排查的Case、Java业界的动态和新技术、Java的一些小知识点Test,以及和大家一起讨论一些Java问题或场景,这里只有Java细节的分享,没有大道理、大架构和大框架。

公众号上发布的消息都存放在http://hellojava.info上。

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了。

=============================
题图来源于:http://www.searchsv.com.cn/upload/article/2012/2012-05-25-13-28-36.jpg
欢迎关注微信公众号:hellojavacases

关于此微信号:
分享Java问题排查的Case、Java业界的动态和新技术、Java的一些小知识点Test,以及和大家一起讨论一些Java问题或场景,这里只有Java细节的分享,没有大道理、大架构和大框架。

公众号上发布的消息都存放在http://hellojava.info上。

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

去年在排查很多java应用的问题时候,看到一些现象是程序员对自己写完的程序所运行的环境了解很少,导致排查问题的时候会比较折腾,因此想到了写这个系列的文章,程序要提供功能给最终用户使用,代码只是其中的一个部分,它还需要依赖jvm、os、服务器硬件、网络、负载均衡等等来共同完成,在这个系列的文章中,将重点关注除jvm外的几个部分,更多的也只是一个科普作用,由于os我使用的都是linux,这个系列的文章中讲到的os也都默认就是linux,这是这个系列的第一篇:CPU。

Java程序在运行时和CPU的关系是怎么样的,是怎么去使用和更充分的使用CPU,以及我们有什么办法能够来控制CPU呢,这是这篇文章中关注的几个重点,如果还有你想关注的,但这里也没提到的,以及本文中一些错误的地方,都欢迎回复下,:)

首先需要知道程序运行的机器的CPU状况,至少得了解下有几个CPU,CPU的型号,是不是开启了超线程(Hyper Thread)等,这些信息会对程序的执行性能有不小的影响(一个好一点的型号带来的性能和吞吐量提升是非常明显的,所以在看到各种不提供硬件环境说明的超NB的性能测试报告时,都不需要太惊讶),这些可以通过在机器上cat /proc/cpuinfo来获取,如果是虚拟机的话看到的信息可能会有些奇怪,例如有些虚拟机看到的会是virtual cpu等,这种就只能到宿主机上去查看了。

在/proc/cpuinfo里可以看到具体的cpu数量,cpu型号,HT是否开启可以根据physical id和core id来判断,如果看到两个processor的这两个id是一致的,那说明是开启了HT的,开启HT对于支撑高并发的场景而言,基本是有利无弊,一般来说可以提升60%左右(但做不到翻倍的提升,也不太可能做到)。

在cpuinfo中还会看到有flags这栏,感兴趣的可以多去了解下,有些会对应用的运行性能有很大的影响,当然,这些也可以通过cpu型号多进行一些了解。

在了解了机器的CPU硬件情况外,可以来看看Java程序执行的过程中和CPU的关系,Java程序是一个进程,多线程的方式执行,Java线程和OS线程可以是一对一的关系,也可以是多对一的关系,多对一这个更多的是因为以前的linux对多线程支持不好,所以可以认为现在通常情况下Java线程和OS线程都是一对一的关系。

既然是一对一的关系,因此线程具体获取CPU的执行时间片也由OS来控制(虚拟机的话会更复杂一点,这个就得讲到虚拟化的一些知识点了,这个之后再另写吧),默认情况下启动的进程可以使用机器上所有的CPU,但也可以通过taskset命令来控制一个进程可使用的CPU,甚至可以更精细的去控制到某个线程使用的CPU(因为在linux上线程在某种程度上也可以认为是“进程”)。

linux是分时调度的操作系统,处于Runnable状态的线程将根据时间片获得调度(具体os是怎么去调度的,怎么去保障多个core的cpu是比较均衡的,建议参看相应的操作系统方面的文章或书),Java程序在执行过程中根据执行的代码将进入不同的线程状态,具体可通过java自带的jstack来查看线程的状态,但jstack看到的线程状态不一定就是对的,例如很多时候会看到epollWait这样的代码对应的线程一直处于Runnable,其实更多的是因为jstack默认情况下只能看到Java栈上的状况,但Java代码执行时很多时候需要依赖native的代码,所以这种时候通常看到的线程状态就有可能是不对的,在比较新的java版本里,是可以通过jstack -m来直接把native stack和Java stack合在一起的,这种情况下就会准确很多。

因此可以认为在Java程序方面更多的是通过改变线程的状态来一定程度上干涉对cpu的使用,例如像disruptor这些号称更实时的调度,是靠极短的wait时间或直接的while(true)来保证线程大部分时候处于runnable,同样像sleep等也是很多代码用来主动释放cpu使用的方法。

如果想运行的线程获取更多的执行机会,看起来Java中提供的设置线程优先级的方法是比较合用的,但由于这个优先级的生效与否和os有很大的关系,并且不一定合理,所以不用比较好,对于实在是对应用运行很关键的线程,例如在通信密集的java程序中,nio的io处理线程可能非常关键,但通常nio的io处理线程数会很少,而其他线程数很多,如果都处于分时调度的情况下,那么有可能会导致有些情况下io处理线程得到的执行机会不够,对于这种特殊情况可以采用taskset来解决,可能有些同学看到这会想到那jvm是怎么让它的一些线程享有高的执行权限的呢,例如gc线程等,这个在之后讲内存的时候会讲到。

在运行的过程中,可以通过top -H来查看运行的java应用的每个线程耗cpu的状况,如果你看到的现象是消耗cpu的线程不断的变化,且每个线程消耗的cpu都不多,那说明应用的运行状况是不错的(当然,这种情况通常可能也有优化空间),反之如果总是看到有个别线程消耗的cpu比例比较高,就需要查查是什么原因了,可通过看到的线程id做十六进制转化,然后对应到jstack出来的线程信息的nid上,看看堆栈具体做的动作,更好的办法则是通过perf这样的工具来查看,但默认版本的perf是无法统计经过c2编译优化后的java代码的cpu消耗的,会统计到?里,所以对查问题帮助就会显得小了很多,这个要支持的话得修改perf和jvm才行。

对于Java程序而言,更多的需要考虑的是如何能充分的发挥cpu,而对于多数Java应用而言,通常其他硬件资源都不太会成为瓶颈,所以写的Java程序在随着并发量上升的情况下,应该尽可能去做到跑满cpu。

top后按数字1可以看到每个cpu core的消耗状况,主要会有us sy ni id wa hi si st这几个指标,分别对应用户态的消耗 系统内核的消耗 调过ni值的进程的cpu us的消耗 cpu空闲 iowait的消耗 硬中断响应的消耗 软中断响应的消耗 被其他虚拟机借用的消耗,要跑满cpu,不是其中的一个cpu core的id这个值接近0,而应该是所有的cpu core的id都接近0才是合理的,并且对于Java程序而言,应该尽可能让us这个值跑的比较高。

有些应用可能会出现即使并发量上涨,但cpu的id也一直降不太下去或者说us上不来,在排查了不是其他硬件资源的瓶颈问题外,通常可能会是以下的一些原因:
1.处理的线程数不够,多数请求处理的过程中并不是全部耗cpu的,例如锁等待、io事件等待等等,这种时候线程会进入blocked或waiting等状态,如运行的线程不够多,就会导致cpu us上不去,现在的java应用从请求进来一直到真正的处理,通常会经历多个线程池,因此在查这类问题的时候一定要清楚整个处理过程,然后看看每个过程的线程数是不是够用,判断是否够用还是比较容易的,jstack如果看到所有的线程都是在执行应用相应的代码,而不是线程池类的wait等时,那基本就说明线程数可能不够用了,这种情况下可以尝试增加线程池的线程数试试,看看cpu的利用率能否上去,但这个地方不太好折腾的是线程数到底设置为多大合适,太小的话cpu不能充分使用,太大则可能造成全部运行起来后存活的线程比较多,导致占用的Java堆内存也增加,从而形成频繁的gc,问题更严重,所以一直以来线程池的线程数设置多大都是个痛苦的问题,基本上得靠经验以及在不同硬件情况下的测试来设置,动态的貌似一直没找到什么好的办法,如果大家有知道的,拜托推荐下,:)

2.另外一种状况是线程池的线程数加大了,但仍然没什么作用,这种通常有可能是处理的过程中各并发线程处理过程中串行的部分耗时较长,例如需要同一把锁等场景,这种通过jstack通常也能看出,需要做的则是引入各种高并发的技巧,尤其是无锁数据结构或锁粒度的控制来解决。

通常情况下,如果能把上面两问题都解决好,那么基本上是可以在随着并发量上升的情况下,把cpu也充分利用起来的,如果还碰到了诡异的其中某个cpu的消耗一直比其他的高不少而成为瓶颈的现象,则有可能是网卡中断等的处理造成的,这种可以看看我之前写的一篇关于网卡中断的文章。

=============================
欢迎关注微信公众号:hellojavacases

关于此微信号:
分享Java问题排查的Case、Java业界的动态和新技术、Java的一些小知识点Test,以及和大家一起讨论一些Java问题或场景,这里只有Java细节的分享,没有大道理、大架构和大框架。

公众号上发布的消息都存放在http://hellojava.info上。