正好今天是愚人节,就来说点骗子的东西吧~
时不时的我就会听见有人抱怨说,他的HotSpot JVM不停的在垃圾回收,可是每次回收完后堆却还是满的。当他们发现这是因为JVM的内存已经不够了之后,通常会问这么个问题,为什么JVM不抛一个OutOfMemoryError(OOME)呢?毕竟来说,由于内存不足,我的程序都已经没法继续跑了,对吧?
先说重要的,如果你运气好的话,你永远不会发现你的JVM其实在你身上下了个庞氏骗局的套。它会一直告诉你,你的内存是无限的,就只管去用就好了。JVM的垃圾回收器会一直维持这么个错觉,在内存这一亩三分地上,啥事都好着呢。
然而在这个领域里可不止这一个庞氏骗局而已。操作系统处理内存也是这么副德性。你会不停的分配本地内存,直到最后触发了虚拟内存的交换,这下你就惨了——虽然程序没有完全停止,不过也差不多了,因为磁盘的速度跟内存比起来差太多了。
为了维持这个错觉,垃圾回收器会使用一个叫安全点(Safe Point)的咒语来冻结时间,应用程序根本不知道发生了什么。然而你的程序停止的时间长点是无所谓,但对于使用你的应用程序的人来说,时间可不是冻结的。如果你的应用程序的存活数据很多的话,垃圾回收器得费很大劲来维持这个错觉。你的程序可能不知道时间冻结得有多频繁,多久,但你的用户肯定知道!
由于你的程序相信你的JVM,而你的JVM也一直很努力,很英勇地(甚至有点傻)工作,来维持这种错觉,最后演出终于露馅的时候,你会想,为什么我的应用程序没有抛一个OOME出来呢?
使用ParNew新生代回收算法(和CMS配套使用)
我们来看一段GC日志,来看下能不能搞清楚是怎么回事,我们从一段大概10秒的日志的开头看起。
85.578: [GC 85.578: [ParNew: 17024K->2110K(19136K), 0.0352412 secs] 113097K->106307K(126912K), 0.0353280 secs]
这是一个正常完成的新生代并行回收的过程,通常这是由于新生代的eden区内存分配失败触发的。来看下里面的数据:
- 所有的存活对象占用的空间是
106307K
. - Survivor区的已使用空间是
2110K
- 这说明老生代中的对象占用的空间是
104197K
(106307-2110)
我们再进一步的分析下:
- 堆的总大小是
126912K
- 其中新生代的大小是
19136K
. - 这意味着老生代是
107776K
.
再稍微计算一下我们会发现,老生代是104197K/107776K也就是已经使用了97%了,这已经相当危险了!
CMS上场了
下面的一组日志表明,前面的ParNew回收是在一次CMS周期里执行的,而这次CMS已经完成了。不过这次CMS周期结束后紧接着又是一次CMS。为什么呢,因为前面那次CMS只回收了104197K-101397K = 2800K内存,这大概只是老生代的2.5%,于是只能继续GC了,但这暴露出一个严重的问题!
86.306: [CMS-concurrent-abortable-preclean: 0.744/1.546 secs]
86.306: [GC[YG occupancy: 10649 K (19136 K)]86.306: [Rescan (parallel) , 0.0039103 secs]86.310: [weak refs processing, 0.0005278 secs] [1 CMS-remark: 104196K(107776K)] 114846K(126912K), 0.0045393 secs]
86.311: [CMS-concurrent-sweep-start]
86.366: [CMS-concurrent-sweep: 0.055/0.055 secs]
86.366: [CMS-concurrent-reset-start]
86.367: [CMS-concurrent-reset: 0.001/0.001 secs]
86.808: [GC [1 CMS-initial-mark: 101397K(107776K)] 119665K(126912K), 0.0156781 secs]
看来在这样的情况下,一个并发模式失败(Concurrent Mode Failure)的错误是必不可少的。
接下来是Concurrent Mode Failure
下面这段日志说明,对于垃圾回收器来说,糟糕的事情发生了,CMS concurrent-mark刚准备开始工作,而讨厌的ParNew又想把一堆数据提升到老生代来,但是现在空间已经不够了。
86.824: [CMS-concurrent-mark-start]
86.875: [GC 86.875: [ParNew: 19134K->19134K(19136K), 0.0000167 secs]86.875: [CMS87.090: [CMS-concurrent-mark: 0.265/0.265 secs]
(concurrent mode failure): 101397K->107775K(107776K), 0.7588176 secs] 120531K->108870K(126912K), [CMS Perm : 15590K->15589K(28412K)], 0.7589328 secs]
更糟糕的是,ParNew试图分配内存,于是CMS回收只能失败了(concurrent mode failure),为了不让程序知道发生了什么,以便让这个游戏继续下去,GC决定使用它的杀手锏,Full GC。不过尽管用了这个大招,结果也并不妙,因为Full GC回收完后老生代还有107775K在使用而总的大小才只有107776K!内存几乎是100%用完了。当然现在还能继续运行,因为新生代占用的1095K(108870K-107775k)已经全塞到survivor区里了。这已经是千钧一发的时刻了,GC为了维持这个庞氏骗局,只能继续垂死挣扎。
再来一次Full GC
为了解决内存不足的问题,第二个Full GC现在上场了。这次发生在JVM启动后的87.734秒。前面一次暂停的时间是0.7589328秒。加上上次Full GC开始的时间86.875结果是87.634秒,也就是说应用程序只执行了100ms又开始被中断了。
这个英勇的行为为GC又赢取到了一次宝贵的时间,在下一次CMS开始之前,ParNew的一次失败直接唤起了 Full GC,它还一直欺骗应用程序说现在一切都很好,其实不然。
87.734: [Full GC 87.734: [CMS: 107775K->107775K(107776K), 0.5109214 secs] 111054K->109938K(126912K), [CMS Perm : 15589K->15589K(28412K)], 0.5110117 secs]
悲剧仍在继续
一轮又一轮的CMS以及伴随着的concurrent mode failures都表明了,虽然垃圾回收器还在力图维持局面,但说实话你得考虑下这个代价是不是有点太大了,这个时候是不是抛一个什么警告或者错误更好一些。
88.246: [GC [1 CMS-initial-mark: 107775K(107776K)] 109938K(126912K), 0.0040875 secs]
88.250: [CMS-concurrent-mark-start]
88.640: [CMS-concurrent-mark: 0.390/0.390 secs]
88.640: [CMS-concurrent-preclean-start]
88.844: [CMS-concurrent-preclean: 0.204/0.204 secs]
88.844: [CMS-concurrent-abortable-preclean-start]
88.844: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs]
88.844: [GC[YG occupancy: 11380 K (19136 K)]88.844: [Rescan (parallel) , 0.0109385 secs]88.855: [weak refs processing, 0.0002293 secs] [1 CMS-remark: 107775K(107776K)] 119156K(126912K), 0.0112696 secs]
88.855: [CMS-concurrent-sweep-start]
88.914: [CMS-concurrent-sweep: 0.059/0.059 secs]
88.914: [CMS-concurrent-reset-start]
88.915: [CMS-concurrent-reset: 0.001/0.001 secs]
89.260: [GC 89.260: [ParNew: 19135K->19135K(19136K), 0.0000156 secs]89.260: [CMS: 105875K->107775K(107776K), 0.5703972 secs] 125011K->116886K(126912K), [CMS Perm : 15589K->15584K(28412K)], 0.5705219 secs]
89.831: [GC [1 CMS-initial-mark: 107775K(107776K)] 117010K(126912K), 0.0090772 secs]
89.840: [CMS-concurrent-mark-start]
90.192: [CMS-concurrent-mark: 0.351/0.352 secs]
90.192: [CMS-concurrent-preclean-start]
90.343: [Full GC 90.343: [CMS90.379: [CMS-concurrent-preclean: 0.187/0.187 secs]
(concurrent mode failure): 107775K->104076K(107776K), 0.5815666 secs] 126911K->104076K(126912K), [CMS Perm : 15586K->15585K(28412K)], 0.5816572 secs]
90.973: [GC [1 CMS-initial-mark: 104076K(107776K)] 104883K(126912K), 0.0025093 secs]
90.976: [CMS-concurrent-mark-start]
91.335: [CMS-concurrent-mark: 0.359/0.359 secs]
91.335: [CMS-concurrent-preclean-start]
91.367: [CMS-concurrent-preclean: 0.031/0.032 secs]
91.367: [CMS-concurrent-abortable-preclean-start]
92.136: [GC 92.136: [ParNew: 17024K->17024K(19136K), 0.0000167 secs]92.136: [CMS92.136: [CMS-concurrent-abortable-preclean: 0.054/0.769 secs]
(concurrent mode failure): 104076K->107775K(107776K), 0.5377208 secs] 121100K->110436K(126912K), [CMS Perm : 15588K->15586K(28412K)], 0.5378416 secs]
92.838: [GC [1 CMS-initial-mark: 107775K(107776K)] 112679K(126912K), 0.0050877 secs]
92.843: [CMS-concurrent-mark-start]
93.209: [CMS-concurrent-mark: 0.366/0.366 secs]
93.209: [CMS-concurrent-preclean-start]
93.425: [CMS-concurrent-preclean: 0.215/0.215 secs]
93.425: [CMS-concurrent-abortable-preclean-start]
93.425: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs]
93.425: [GC[YG occupancy: 13921 K (19136 K)]93.425: [Rescan (parallel) , 0.0130859 secs]93.438: [weak refs processing, 0.0002302 secs] [1 CMS-remark: 107775K(107776K)] 121697K(126912K), 0.0134232 secs]
93.439: [CMS-concurrent-sweep-start]
93.505: [CMS-concurrent-sweep: 0.067/0.067 secs]
93.506: [CMS-concurrent-reset-start]
93.506: [CMS-concurrent-reset: 0.001/0.001 secs]
那么对JVM来说到底什么才是内存不足?
定义内存不足
显而易见,Java堆的内存太小了,不足以维持应用程序的运行。大点的堆能让GC把这个庞氏骗局一直持续下去。不过应用程序并没有意味到问题的出现,但终端用户肯定是知道的。我们非常希望应用程序能在用户发觉之前发现这个问题。不幸的是我们没有一个AlmostOutOfMemoryError的异常,不过我们可以通过调整GCTimeLimit和GCHeapFreeLimit参数来重新定义何时抛出OutOfMemoryError错误。
GCTimeLimit 的默认值是98%,也就是说如果98%时间都用花在GC上,则会抛出OutOfMemoryError。GCHeapFreeLimit 是回收后可用堆的大小。默认值是2%。
如果我们分析下GC日志里面的数据可以发现,GC刚刚好没有超出这两个参数的阈值。因此GC会一直维持这个庞氏骗局。但是这两个值又设置的有点太武断了,你可以重新定义下它们,来告诉GC,如果你这么努力工作就是为了维持这个错觉的话,或者你还是认输好一点,让应用程序能够知道它的内存已经用得差不多了。在这里把GCHeapFreeLimit设置成5%,GCTimeLimit设置成90%,来触发一个OutOfMemoryError。这就能解释为什么应用程序这么久没有响应,也让这个庞氏骗局的受害者们知道,他们现在到底是什么情况。
http://it.deepinmind.com/gc/2014/04/01/hotspot-jvm-ponzi-scheme.html
http://bluedavy.me/?p=300
相关推荐
本文主要围绕HotSpot JVM的优化展开,旨在防止出现内存溢出错误和StackOverflowError异常,以及减少频繁的垃圾回收对程序响应的影响。 首先,我们要理解JVM内存结构中的几个关键区域:虚拟机栈、本地方法栈、永久代...
JVM调优是指通过调整JVM的各种参数来优化应用程序的性能,主要包括以下方面: - **内存设置**:合理设置堆内存大小,避免频繁的垃圾回收。 - **垃圾回收器选择**:根据应用特性选择合适的垃圾回收器。 - **监控工具...
因此,解决`OutOfMemoryError`的关键在于理解内存的分配机制,合理设置JVM参数,监测内存使用情况,检查是否存在内存泄漏,以及在必要时调整系统的物理内存分配。对于大型Java应用,了解JVM内存模型和操作系统内存...
- Sun HotSpot JVM采用了分代管理策略,即将堆划分为年轻代和老年代。 - 年轻代进一步分为Eden和两个Survivor空间(S0/S1)。 - 新创建的对象首先在Eden空间分配,经过一次或多次GC后仍然存活的对象会被转移到...
5. **JVM内部错误**:可能是由于JVM自身的bug或者不兼容性问题,比如HotSpot JVM中的已知问题。 解决JVM崩溃的问题通常涉及以下几个步骤: 1. **分析错误日志**:`hs_err_pid*.log`文件会包含错误发生的详细信息,...
了解HotSpot JVM中的Client和Server模式,以及如何触发和优化JIT编译,能显著提升性能。 6. **异常处理与线程模型**:JVM提供了丰富的异常处理机制,确保程序的健壮性。同时,JVM支持多线程,理解线程的创建、同步...
当一个方法被频繁调用或是包含了大量的循环时,HotSpot VM会分别触发标准编译和OSR(On-Stack Replacement)编译。这种方式使得HotSpot VM能够在最短的时间内提供最佳的性能,同时也减少了即时编译的压力,从而允许...
总之,从永久代到元空间的转变是HotSpot JVM内存管理的一大进步,它提升了JVM在处理大量类加载场景下的性能,并降低了内存管理的复杂性。这一改变反映了JVM在不断演进,以更好地适应现代应用程序的需求。
现代JVM,如Hotspot,通常采用这些算法的组合,例如,新生代使用复制算法,老年代采用标记-整理或标记-压缩算法,以提高效率并避免内存碎片。 理解JVM内存管理和垃圾回收机制对于诊断和解决OOM问题至关重要。通过...
**常用JVM调优参数:** - `-Xms`: 设置JVM的初始堆内存大小。 - `-Xmx`: 设置JVM的最大堆内存大小。 - `-XX:NewSize`: 设置新生代初始大小。 - `-XX:MaxNewSize`: 设置新生代最大大小。 - `-XX:+UseParallelGC`: ...
永生区空间不足 JVM 规范中运行时数据区域中的方法区,在 HotSpot 虚拟机中又被习惯称为永生代或者永生区,Permanet Generation 中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类...
讲解了虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果;第五部分探讨了Java实现高效并发的原理,包括JVM内存模型的结构和操作;原子性、可见性和...
与Java堆相比,直接内存的读写速度更快,但直接内存的分配并不受JVM管理,而是由操作系统管理,因此可能会引发`OutOfMemoryError`异常。 #### 二、HotSpot虚拟机对象探秘 在深入了解Java内存区域的同时,我们还...
2. **JVM阵营**:Sun HotSpot VM、BEA JRockit VM、IBM J9 VM、Azul VM、Apache Harmony、Google Dalvik VM、Microsoft JVM等是不同的JVM实现,它们各有特点和优化方向。 3. **JVM启动流程**:Java程序编译成字节码...