上周重新为某一个版本部署一套服务器,在服务器上co出release以后,照常脚本start,服务器跑起来。一天来下,没什么事,可是第二天早上刚来,客户端开发就告诉我服务器跪了,top看了一眼,果然悲剧了,cpu已经稳定在700%了。测试服只是一个很low的云主机,所以肯定是跪在这里了,发生了什么呢,我确认了一遍代码,没问题,那么只能是运行环境的问题了。所以开始检查启动脚本,对比了其他服务器的启动脚本也是一样的,无奈了,重启一把好了,再等一天看看是不是会有问题。
第二天来还是一样的事情,700%的cpu,说明问题是一直可以重现的,那么肯定是哪里出了问题,用了jstack,jmap看来看去,没发现有什么问题。最后一招了,请教别人。一问,原来是一个已知的bug,启动项-XX:+ParallelRefProcEnabled的原因。去掉了这个参数跑了几天果然没问题了。
本着好奇心求知欲的原则,决定去搞清楚问什么会这样。首先去Oracle的网站去查查原因,可以从搜索结果看出这个bug确实很久以前就被提出了,原来这个参数在单核的机器上使用CMS时不会回收weak reference,导致最后跪掉。
之前的服务器都是多核的,所以已知没发生这个问题。既然是gc出了问题,那就找出当时的gclog来看看,根据故障发生的时间,定位到gclog,故障发生时如下:
2014-08-12T06:40:48.665+0800: 75677.941: [GC 75677.941: [ParNew Desired survivor size 26836992 bytes, new threshold 1 (max 14) - age 1: 47400272 bytes, 47400272 total : 471872K->52416K(471872K), 0.1390260 secs] 1223447K->854043K(2044736K), 0.1393060 secs] [Times: user=1.03 sys=0.00, real=0.14 secs]
2014-08-12T06:40:48.809+0800: 75678.085: [GC [1 CMS-initial-mark: 801627K(1572864K)] 861108K(2044736K), 0.1160480 secs] [Times: user=0.14 sys=0.00, real=0.11 secs] 2014-08-12T06:40:48.926+0800: 75678.202: [CMS-concurrent-mark-start] 2014-08-12T06:40:49.100+0800: 75678.376: [CMS-concurrent-mark: 0.174/0.174 secs] [Times: user=0.62 sys=0.00, real=0.17 secs] 2014-08-12T06:40:49.100+0800: 75678.376: [CMS-concurrent-preclean-start] 2014-08-12T06:40:49.105+0800: 75678.381: [CMS-concurrent-preclean: 0.005/0.005 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 2014-08-12T06:40:49.105+0800: 75678.381: [CMS-concurrent-abortable-preclean-start] CMS: abort preclean due to time 2014-08-12T06:40:54.123+0800: 75683.399: [CMS-concurrent-abortable-preclean: 4.115/5.018 secs] [Times: user=5.21 sys=0.00, real=5.02 secs] 2014-08-12T06:40:54.125+0800: 75683.401: [GC[YG occupancy: 190655 K (471872 K)]75683.401: [Rescan (parallel) , 0.1389620 secs]75683.541: [weak refs processing
可以看出CMS的gc有问题,虽然问题解决了,但是觉得自己对gc方面的东西了解的太少了,所以决定重新复习一下。我想作为一名java程序员,了解gc应该是必须要做,深入了解gc的工作方式可以写出更好地java代码。
gc有一个很重要的概念:“stop-the-world”。当stop-the-world发生时,除了gc所需的线程以外,所有线程都处于等待状态,直到gc任务完成。gc优化很多时候就是指减少stop-the-world发生的时间。
java程序中不能显示的分配和注销内存,好处是程序员不需要关心内存的操作,减少了很多可能发生的错误,坏处是程序中的内存可能变得不可控,或是因为gc而严重影响程序的性能。(类似System.gc()这样的代码永远不应该出现在java程序中,它只是建议jvm进行gc,但不一定进行gc,而且还会严重影响性能,真是好没有意义的发明-_-|||)
gc在以下两个前提(假设hypotheses)下会被触发:(个人理解,满足这两个条件才能触发gc,但不是满足这两个条件就一定能触发)
1、大多数对象会很快变得不可达
2、只有很少的由老对象(创建时间较长)指向新生对象的引用
这两个前提称之为“弱年代假设(weak generational hypothesis)”。为了强化这一假设,HotSpot虚拟机将其物理上划分为两个代:新生代(yong generation)和老年代(old generation)。
新生代:绝大多数的新创建对象都会被分配到这里,由于新生代中的新创建对象大部分很快就会变成不可达的,所以这里的对象频繁的创建又消失。对象从新生代消失的过程称为“minor GC”。
老年代:对象在新生代中没有变成不可达,也就是存活下来的对象会被拷贝到这里。老年代占用的空间要比新生代大,由于其空间较大,所以发生在老年代的gc次数要少的多。对象从老年代中消息的过程称为“major GC”(或者“full GC”),如下图所示。
上图中持久代(permanent generation)也被称为方法区(method area),用来保存类常量和字符串常量。这个区域也可能会发生gc,并且持久代发生的gc也被算做major GC。
这样的gc过程,会有一个问题,就是当一个老年代的对象需要引用一个新生代的对象时,新生代gc的时候会怎么办?为了解决这个问题,老年代中存在一个“card table”,它是一个512byte大小的空间,所有老年代对象指向新生代对象的引用都会被记录在这个表中,当新生代发生gc时,只需要查询card table来确定是否回收,而不用查询整个老年代。这个card table由一个write barrier来管理。write barrier给gc带来了极大的性能提升,虽然带来了一些开销,典型的空间换取时间,使gc性能大幅提升。
未完待续。。。