郑州资讯网|郑州信息港-郑州最专业的便民信息网站
您的位置:郑州资讯 > 财经新闻

东方龙马慎用java.lang.ref.SoftReference实现缓

时间:2017-09-24 11:47   来源: 慧聪    作者:宋元明清  

在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)

"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

东方龙马慎用java.lang.ref.SoftReference实现缓

虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

实际上,虚引用的get,总是返回null。

java.lang.ref这个包(特别是java.lang.ref.SoftReference)似乎把开发者从繁琐的以及容易出问题的内存管理中解放了出来:既不担心在内存消耗过多时如何快速地释放内存,也不担心缓存管理不当带来的内存泄漏,事实真是如此么?让我们来看一个实际的案例。

某用户使用Gerrit2作为其代码管理的工具。系统运维工程师反映,近期系统在运行过程中频繁出现性能问题,最终用户使用系统时经常出现挂起(无响应)。运行环境如下:

OS:Linux

中间件:Gerrit2

JDK:SunJDK1.8_0_x

JVMHeap分配:16G/32G

东方龙马慎用java.lang.ref.SoftReference实现缓
东方龙马慎用java.lang.ref.SoftReference实现缓
东方龙马慎用java.lang.ref.SoftReference实现缓
东方龙马慎用java.lang.ref.SoftReference实现缓
东方龙马慎用java.lang.ref.SoftReference实现缓

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实现缓

对于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的时间戳;

东方龙马慎用java.lang.ref.SoftReference实现缓
东方龙马慎用java.lang.ref.SoftReference实现缓

当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余年,希望我们的宝贵经验和独到见解可以帮助到你。

东方龙马慎用java.lang.ref.SoftReference实现缓

参考:

类似案例:

郑重声明:此文内容为本网站转载企业宣传资讯,目的在于传播更多信息,与本站立场无关。仅供读者参考,并请自行核实相关内容。

精选导读