在JVM内部实现缓存容器,东方龙马认为最麻烦的事情是要对缓存大小进行控制。为何这样说?当我们缓存的是一些值对象(ValueObject)时,一个难点是计算这一些对象(及对象引用的大小)。JVM的API并没有赋予我们通过简单的调用即可获得对象(及其引用)大小的能力。当然,你可以通过ObjectOutputStream又或者自定义的方式将对象转换成二进制数据[bytes],从而做到精确控制缓存占用的内存,但是带来的一个问题是对象的序列化与反序列化带来的开销。
JVM的Reference(java.lang.ref.Reference:SinceJDK1.2)的出现似乎给开发者带来了美好的前景。关于Java编程中的引用,粗略介绍如下:
1.强引用
这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
强引用的例子:方法局部变量、JNI变量、类变量,概括起来,就是所有GCRoot引用可达的都是强引用;
2.软引用(SoftReference)
如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
3.弱引用(WeakReference)
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
4.虚引用(PhantomReference)
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
实际上,虚引用的get,总是返回null。
java.lang.ref这个包(特别是java.lang.ref.SoftReference)似乎把开发者从繁琐的以及容易出问题的内存管理中解放了出来:既不担心在内存消耗过多时如何快速地释放内存,也不担心缓存管理不当带来的内存泄漏,事实真是如此么?让我们来看一个实际的案例。
某用户使用Gerrit2作为其代码管理的工具。系统运维工程师反映,近期系统在运行过程中频繁出现性能问题,最终用户使用系统时经常出现挂起(无响应)。运行环境如下:
OS:Linux
中间件:Gerrit2
JDK:SunJDK1.8_0_x
JVMHeap分配:16G/32G
JVMGC日志显示,每一次GC以后,JVMHeap空闲的空间仍然有1GB以上的空间可用;
但是有Overhead为100%的GC情况;
分析GCCompleted以及Overhead情况,在接近故障点时,有明显的GC频繁及GC时间上升(峰值5923ms);
原始的JVMGC日志显示,在故障时间点附近,有非常频繁的FullGC,触发的原因为JVMOld区满,并且每次FullGC后,Old区能释放出来的空闲空间相当少;但是整个JVM总计的空闲Heap仍然有1GB以上的空间。
性能问题原因:JVMOld区满,频繁的FullGC导致应用性能下降非常严重;
附注:
GCCompletedorGC:Time(millisecond)spentduringgarbagecollection.
Overhead:Ratio(%)timespentinallocationfailurevs.timebetweenAF
继续深入分析问题,我们发现了内存中存在的大对象:
ClassName|ShallowHeap|RetainedHeap
org.eclipse.jgit.internal.storage.file.WindowCache0x7ff59077b508|104|20,638,034,208
Type|Name|Value
-
ref|openBytes|20382985278
ref|openFiles|1859
int|windowSize|8192
int|windowSizeShift|13
boolean|mmap|false
long|maxBytes|10485760
int|maxFiles|16384
int|evictBatch|64
ref|evictLock|java.util.concurrent.locks.ReentrantLock0x7ff590c04510
ref|locks|org.eclipse.jgit.internal.storage.file.WindowCache$Lock[16384]0x7ff590e9c7c0
ref|table|java.util.concurrent.atomic.AtomicReferenceArray0x7ff59077b5c0
ref|clock|95846830
int|tableSize|3200
ref|queue|java.lang.ref.ReferenceQueue0x7ff59077b570
-
ClassName|ShallowHeap|RetainedHeap
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf48e46a0|48|8,264
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf47ba558|48|48
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf478bff0|48|8,264
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf478bf40|48|8,264
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf478be90|48|8,264
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf473ef90|48|8,264
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf473eee0|48|8,264
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf473ee30|48|8,264
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf473b980|48|8,264
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf4736210|48|8,264
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf47344e0|48|8,264
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf47343d0|48|8,264
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf4727498|48|8,264
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf46640d0|48|8,264
org.eclipse.jgit.internal.storage.file.ByteArrayWindow0x7ffbf4664020|48|8,264
Total:15of2,488,602entries;2,488,587more||
评析:
ClassName|ShallowHeap|RetainedHeap
--
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbf42d39e0|112|6,312
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbf3999e48|112|5,752
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbf385dd28|112|264
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbf27e1c20|112|12,504
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbf148de08|112|10,048
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbf0b97010|112|12,240
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbef2869e0|112|9,352
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbeee8bc50|112|41,408
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbeee26698|112|10,000
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbec1c1318|112|9,888
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbec1ba1a0|112|9,920
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbeb619898|112|47,144
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbe94a62a0|112|11,696
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbe90dd688|112|9,080
org.eclipse.jgit.internal.storage.file.FileRepository0x7ffbe56b3f88|112|12,344
Total:15of3,379entries;3,364more||
--
评析:
。
ClassName|ShallowHeap|RetainedHeap
--
org.eclipse.jgit.internal.storage.file.PackFile0x7ff593248670|128|168,684,904
org.eclipse.jgit.internal.storage.file.PackFile0x7ff5ca5e57e0|128|163,743,112
org.eclipse.jgit.internal.storage.file.PackFile0x7ff65d2797c8|128|130,335,888
org.eclipse.jgit.internal.storage.file.PackFile0x7ff67ed5a5a0|128|116,092,248
org.eclipse.jgit.internal.storage.file.PackFile0x7ff5d36b1350|128|111,606,864
org.eclipse.jgit.internal.storage.file.PackFile0x7ff741d9c980|128|92,786,784
org.eclipse.jgit.internal.storage.file.PackFile0x7ff5c56577d0|128|55,945,608
org.eclipse.jgit.internal.storage.file.PackFile0x7ff5d4cb7ed0|128|31,806,712
org.eclipse.jgit.internal.storage.file.PackFile0x7ff5e3ec9c60|128|26,108,840
org.eclipse.jgit.internal.storage.file.PackFile0x7ff593a07f80|128|21,771,144
org.eclipse.jgit.internal.storage.file.PackFile0x7ff5923c0150|128|20,065,688
org.eclipse.jgit.internal.storage.file.PackFile0x7ff5b7dd8768|128|17,462,328
org.eclipse.jgit.internal.storage.file.PackFile0x7ff5d74ec5c0|128|16,689,600
org.eclipse.jgit.internal.storage.file.PackFile0x7ff65327b220|128|15,634,496
org.eclipse.jgit.internal.storage.file.PackFile0x7ff677da56e0|128|13,699,608
Total:15of6,459entries;6,444more||
--
org.eclipse.jgit.internal.storage.file.WindowCache.openBytes接近20G,org.eclipse.jgit.internal.storage.file.ByteArrayWindow对象实例达2,488,602个,每个8K,总计19,908,816KB(20,386,627,584Byte)。org.eclipse.jgit.internal.storage.file.FileRepository对象实例3,379个,org.eclipse.jgit.internal.storage.file.PackFile对象实例6,459个。
问题来到这里基本上就清晰了:JGit4.1org.eclipse.jgit.lib.RepositoryCache以及org.eclipse.jgit.internal.storage.file.WindowCache缓存的PackFile以及ByteArrayWindow占用了大片的内存空间。缓存占用了大片Old区的内存,并且触发了频繁的FullGC导致性能问题的发生。开始的时侯,笔者也犯了一个同样肤浅的错误,建议客户通过增大JVMHeap对问题进行缓解,但最终的结果是:服务器发生问题的频率比设置32G的时侯更频繁;
笔者尝试分析一下缓存的机制,容器组件RepositoryCache以及WindowCache其使用的是正是java.lang.ref.SoftReference对缓存对象进行引用。并且,RepositoryCache组件没有缓存消耗机制(例如缓存的对象的数量或者缓存总计大小),而WindowCache组件虽然有控制缓存文件数量及总计内存大小,但是最终的结果与实际想要控制的差距太大,并未如设想那样有效地控制内存消耗。
既然程序是使用java.lang.ref.SoftReference保持对缓存对象的引用,参考原来Sun的说法,如果一个对象只有软引用可达,在内存不足时,是可以被回收的,那关键的问题是JVM的GC如何判定这个SoftReference引用的对象何时被回收?
通过Google大神,东方龙马终于找到相关参考的文章,以下为原文参考:
对于java.lang.ref.SoftReference对象,有一个全局的变量clock(实际上就是java.lang.ref.SoftReference的类变量clock,如下图代码所示):其保持了最后一次GC的时间点(以毫秒为单位),即每一次GC发生时,该值均会被重新设置。同时,java.lang.ref.SoftReference对象实例均有一个timestamp的属性,其被设置为最后一次成功通过SoftReference对象获取其引用对象时的clock的值(最后一次GC)。所以,java.lang.ref.SoftReference对象实例的timestamp属性,保持的是这个对象被访问时的最后一次GC的时间戳;
当GC发生时,以下两个因素影响SoftReference引用的对象是否被回收:
1、SoftReference对象实例的timestamp有多旧;
2、内存空闲空间的大小;
是否保留SoftReference引用对象的判断参考表达式,true为不回收,false为回收:
intervallt;=free_heap*ms_per_mb
说明:
interval:最后一次GC时间和SoftReference对象实例timestamp的属性的差。简单理解就是这个SoftReference引用对象的生存的时长;
free_heap:JVMHeap中空闲空间大小,单位为MB
ms_per_mb:每1M空闲空间可保持的SoftReference对象生存的时长(单位毫秒)。简单地将这个参数理解为一个常量就好,默认值是1000;SunJVM可以通过参数:-XX:SoftRefLRUPolicyMSPerMB进行设置;
东方龙马上述的判断简单地理解就是:如果SoftReference引用对象的生存时长lt;=空闲内存可保持软引用的最大时间范围,则不清除SoftReference所引用的对象;否则,则将其清除;
举例:有一个SoftReference,其属性timestamp值为2000,最后一次GCclock值为5000,ms_per_mb值为1000,并且空闲空间为1MB,那么表达式:
5000-2000lt;=1000*1
上述表达式返回值为false(3000gt;1000),因此,这个SoftReference所引用的对象,会被GC所回收;
如果此时我们有4MB的空闲内存,那么这个表达式:
5000-2000lt;=1000*4
上述表达式返回值为true(3000lt;4000),因此,这个SoftReference所引用的对象,不会被GC所回收;
需要注意的是,JVM总是保留GC以后访问过的SoftReference引用的对象。为何?因为GC以后访问过的对象,clock-timestamp总是等于0,即使你通过参数-XX:SoftRefLRUPolicyMSPerMB设置ms_per_mb=0,表达式intervallt;=free_heap*ms_per_mb总是返回true,所以得出上述的结论;
参考上述的理论,我们大概可以估算一下当一个对象仅有SoftReference引用可达时,其最大生命的周期情况:
SoftRefLRUPolicyMSPerMB:1000ms(默认值)
空闲空间清理间隔(生存周期上限)
1M:1S
10M:10S
100M:100S
1000M1000S
SoftRefLRUPolicyMSPerMB:100ms
空闲空间清理间隔(生存周期上限)
1M0.1S
10M1S
100M10S
1000M100S
SoftRefLRUPolicyMSPerMB:10ms
空闲空间清理间隔(生存周期上限)
1M0.01S
10M0.1S
100M1S
1000M10S
10000M100S
SoftRefLRUPolicyMSPerMB:5ms
空闲空间清理间隔(生存周期上限)
2M0.01S
20M0.1S
200M1S
2000M10S
20000M100S
SoftRefLRUPolicyMSPerMB:1ms
空闲空间清理间隔(生存周期上限)
1M0.001S
10M0.01S
100M0.1S
1000M1S
10000M10S
至此,对于上述案例的故障成因,东方龙马有了一个更深层次的认识:
设置较大的JVMHeap时,因为Sun的NewGeneration与OldGeneration比例关系,每一次GC以后,NewGeneration释放出来的空闲空间的数量,总是使SoftReference引用的对象的生存周期保持在一个较大的值,换言而之,其淘汰的速度较慢。而OldGeneration满频繁触发的FullGC以及内存碎片整理,使得整个JVM非常卡顿;
而设置更大的JVMHeap后,使得每一次GC以后,NewGeneration释放出来的空闲空间的数量更多,从而加剧了这种故障的情况;
当然,故障的根本成因,是应用程序代码并未对缓存进行控制;
上述案例,在未改动代码及结构的情况下,通过增大大JVMHeap,以及通过设置参数:-XX:SoftRefLRUPolicyMSPerMB=0解决;
其它:IBM的JVM针对SoftReference的回收控制,同样有类似参数:-Xsoftrefthreshold进行控制。以下是关于-Xsoftrefthreshold的描述:
SetsthenumberofGCsafterwhichasoftreferencewillbeclearedifitsreferenthasnotbeenmarked.Thedefaultis32,meaningthatonthe32ndGCwherethereferentisnotmarkedthesoftreferencewillbecleared.
结束语:
JVM的Reference(java.lang.ref.Reference:SinceJDK1.2)并未像其描述的那样美好,特别是java.lang.ref.SoftReference的使用。同样地,即使是使用Reference实现In-Box的缓存,也需要充分考虑其对内存的消耗。这样才使我们的应用运行得更稳定。
东方龙马凭借在数据库,中间件领域耕耘20余年,希望我们的宝贵经验和独到见解可以帮助到你。
参考:
类似案例:
郑重声明:此文内容为本网站转载企业宣传资讯,目的在于传播更多信息,与本站立场无关。仅供读者参考,并请自行核实相关内容。