|  首页  |  资讯  |  评测  |  活动  |  学院  |  访谈  |  专题  |  杂志  |  产服  |  
您现在的位置:硅谷网> 资讯> 软件>

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

2017-09-18 12:17 作者:佚名 来源:硅谷网 HV: 编辑:何睿 【搜索试试

作者:东方龙马 · 邓钊星

在JVM内部实现缓存容器,东方龙马认为最麻烦的事情是要对缓存大小进行控制。为何这样说?当我们缓存的是一些值对象(ValueObject)时,一个难点是计算这一些对象(及对象引用的大小)。JVM的API并没有赋予我们通过简单的调用即可获得对象(及其引用)大小的能力。当然,你可以通过ObjectOutputStream又或者自定义的方式将对象转换成二进制数据[bytes],从而做到精确控制缓存占用的内存,但是带来的一个问题是对象的序列化与反序列化带来的开销。

JVM的Reference(java.lang.ref.Reference:Since JDK1.2)的出现似乎给开发者带来了美好的前景。关于Java编程中的引用,粗略介绍如下:

1.强引用

这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

强引用的例子:方法局部变量、JNI变量、类变量,概括起来,就是所有GC Root引用可达的都是强引用;

2.软引用(SoftReference)

如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

3.弱引用(WeakReference)

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

4.虚引用(PhantomReference)

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

东方龙马LOGO贴图15

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

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

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

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

OS:Linux

中间件:Gerrit2

JDK:Sun JDK1.8_0_x

JVM Heap分配:16G/32G

接到这个问题,遵循既定的思路,让用户做一定的准备,调整JVM的参数捕获故障时的现场信息进行问题分析。最后定位为JVM Heap频繁的Full GC问题导致应用出现性能故障,参考如下:

JVM GC日志显示,每一次GC以后,JVM Heap空闲的空间仍然有1GB以上的空间可用;

但是有Overhead为100%的GC情况;

分析GC Completed以及Overhead情况,在接近故障点时,有明显的GC频繁及GC时间上升(峰值5923ms);

原始的JVM GC日志显示,在故障时间点附近,有非常频繁的Full GC,触发的原因为JVM Old区满,并且每次Full GC后,Old区能释放出来的空闲空间相当少;但是整个JVM总计的空闲Heap仍然有1GB以上的空间。

性能问题原因:JVM Old区满,频繁的Full GC导致应用性能下降非常严重;

附注:

GC Completed or GC :Time(millisecond) spent during garbage collection.

Overhead: Ratio(%) time spent in allocation failure vs. time between AF

继续深入分析问题,我们发现了内存中存在的大对象:

Class Name | Shallow Heap | Retained Heap

------------------------------------------------------------------------------------

org.eclipse.jgit.internal.storage.file.WindowCache @ 0x7ff59077b508| 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.ReentrantLock @ 0x7ff590c04510

ref |locks |org.eclipse.jgit.internal.storage.file.WindowCache$Lock[16384] @ 0x7ff590e9c7c0

ref |table |java.util.concurrent.atomic.AtomicReferenceArray @ 0x7ff59077b5c0

ref |clock |95846830

int |tableSize |3200

ref |queue |java.lang.ref.ReferenceQueue @ 0x7ff59077b570

-------------------------------------------------------------------------------------

Class Name | Shallow Heap | Retained Heap

-------------------------------------------------------------------------------------

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf48e46a0| 48 | 8,264

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf47ba558| 48 | 48

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf478bff0| 48 | 8,264

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf478bf40| 48 | 8,264

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf478be90| 48 | 8,264

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf473ef90| 48 | 8,264

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf473eee0| 48 | 8,264

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf473ee30| 48 | 8,264

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf473b980| 48 | 8,264

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf4736210| 48 | 8,264

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf47344e0| 48 | 8,264

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf47343d0| 48 | 8,264

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf4727498| 48 | 8,264

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf46640d0| 48 | 8,264

org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf4664020| 48 | 8,264

Total: 15 of 2,488,602 entries; 2,488,587 more | |

---------------------------------------------------------------------------------------

评析:

Class Name | Shallow Heap | Retained Heap

---------------------------------------------------------------------------------------

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbf42d39e0| 112 | 6,312

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbf3999e48| 112 | 5,752

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbf385dd28| 112 | 264

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbf27e1c20| 112 | 12,504

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbf148de08| 112 | 10,048

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbf0b97010| 112 | 12,240

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbef2869e0| 112 | 9,352

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbeee8bc50| 112 | 41,408

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbeee26698| 112 | 10,000

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbec1c1318| 112 | 9,888

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbec1ba1a0| 112 | 9,920

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbeb619898| 112 | 47,144

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbe94a62a0| 112 | 11,696

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbe90dd688| 112 | 9,080

org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbe56b3f88| 112 | 12,344

Total: 15 of 3,379 entries; 3,364 more | |

----------------------------------------------------------------------------------------

评析:

Class Name | Shallow Heap | Retained Heap

----------------------------------------------------------------------------------------

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff593248670| 128 | 168,684,904

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5ca5e57e0| 128 | 163,743,112

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff65d2797c8| 128 | 130,335,888

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff67ed5a5a0| 128 | 116,092,248

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5d36b1350| 128 | 111,606,864

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff741d9c980| 128 | 92,786,784

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5c56577d0| 128 | 55,945,608

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5d4cb7ed0| 128 | 31,806,712

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5e3ec9c60| 128 | 26,108,840

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff593a07f80| 128 | 21,771,144

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5923c0150| 128 | 20,065,688

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5b7dd8768| 128 | 17,462,328

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5d74ec5c0| 128 | 16,689,600

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff65327b220| 128 | 15,634,496

org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff677da56e0| 128 | 13,699,608

Total: 15 of 6,459 entries; 6,444 more | |

----------------------------------------------------------------------------------------

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.1 org.eclipse.jgit.lib.RepositoryCache以及org.eclipse.jgit.internal.storage.file.WindowCache缓存的PackFile以及ByteArrayWindow占用了大片的内存空间。缓存占用了大片Old区的内存,并且触发了频繁的Full GC导致性能问题的发生。开始的时侯,笔者也犯了一个同样肤浅的错误,建议客户通过增大JVM Heap对问题进行缓解,但最终的结果是:服务器发生问题的频率比设置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 为回收:

interval<=free_heap*ms_per_mb

说明:

interval:最后一次GC时间和SoftReference对象实例timestamp的属性的差。简单理解就是这个SoftReference引用对象的生存的时长;

free_heap:JVM Heap中空闲空间大小,单位为MB

ms_per_mb:每1M空闲空间可保持的SoftReference对象生存的时长(单位毫秒)。简单地将这个参数理解为一个常量就好,默认值是1000;Sun JVM可以通过参数:-XX:SoftRefLRUPolicyMSPerMB进行设置;

东方龙马上述的判断简单地理解就是:如果SoftReference引用对象的生存时长<=空闲内存可保持软引用的最大时间范围,则不清除SoftReference所引用的对象;否则,则将其清除;

举例:有一个SoftReference,其属性timestamp值为2000,最后一次GC clock值为5000,ms_per_mb值为1000,并且空闲空间为1MB,那么表达式:

5000-2000<=1000*1

上述表达式返回值为false(3000>1000),因此,这个SoftReference所引用的对象,会被GC所回收;

如果此时我们有4MB的空闲内存,那么这个表达式:

5000-2000<=1000*4

上述表达式返回值为true(3000<4000),因此,这个SoftReference所引用的对象,不会被GC所回收;

需要注意的是,JVM总是保留GC以后访问过的SoftReference引用的对象。为何?因为GC以后访问过的对象,clock-timestamp总是等于0,即使你通过参数-XX:SoftRefLRUPolicyMSPerMB设置ms_per_mb=0,表达式interval<=free_heap*ms_per_mb总是返回true,所以得出上述的结论;

参考上述的理论,我们大概可以估算一下当一个对象仅有SoftReference引用可达时,其最大生命的周期情况:

SoftRefLRUPolicyMSPerMB:1000ms(默认值)

空闲空间 清理间隔(生存周期上限)

1M: 1S

10M: 10S

100M: 100S

1000M 1000S

SoftRefLRUPolicyMSPerMB:100ms

空闲空间 清理间隔(生存周期上限)

1M 0.1S

10M 1S

100M 10S

1000M 100S

SoftRefLRUPolicyMSPerMB:10ms

空闲空间 清理间隔(生存周期上限)

1M 0.01S

10M 0.1S

100M 1S

1000M 10S

10000M 100S

SoftRefLRUPolicyMSPerMB:5ms

空闲空间 清理间隔(生存周期上限)

2M 0.01S

20M 0.1S

200M 1S

2000M 10S

20000M 100S

SoftRefLRUPolicyMSPerMB:1ms

空闲空间 清理间隔(生存周期上限)

1M 0.001S

10M 0.01S

100M 0.1S

1000M 1S

10000M 10S

至此,对于上述案例的故障成因,东方龙马有了一个更深层次的认识:

设置较大的JVM Heap时,因为Sun的New Generation与Old Generation比例关系,每一次GC以后,New Generation释放出来的空闲空间的数量,总是使SoftReference引用的对象的生存周期保持在一个较大的值,换言而之,其淘汰的速度较慢。而Old Generation满频繁触发的Full GC以及内存碎片整理,使得整个JVM非常卡顿;

而设置更大的JVM Heap后,使得每一次GC以后,New Generation释放出来的空闲空间的数量更多,从而加剧了这种故障的情况;

当然,故障的根本成因,是应用程序代码并未对缓存进行控制;

上述案例,在未改动代码及结构的情况下,通过增大大JVM Heap,以及通过设置参数:-XX:SoftRefLRUPolicyMSPerMB=0解决;

其它:IBM的JVM针对SoftReference的回收控制,同样有类似参数:-Xsoftrefthreshold进行控制。以下是关于-Xsoftrefthreshold的描述:

Sets the number of GCs after which a soft reference will be cleared if its referent has not been marked. The default is 32, meaning that on the 32nd GC where the referent is not marked the soft reference will be cleared.

结束语:

JVM的Reference(java.lang.ref.Reference:Since JDK1.2)并未像其描述的那样美好,特别是java.lang.ref.SoftReference的使用。同样地,即使是使用Reference实现In-Box的缓存,也需要充分考虑其对内存的消耗。这样才使我们的应用运行得更稳定。

东方龙马凭借在数据库,中间件领域耕耘20余年,希望我们的宝贵经验和独到见解可以帮助到你。

东方龙马-商务素材2

参考:

https://jeremymanson.blogspot.jp/2009/07/how-hotspot-decides-to-clear_07.html

http://www.oracle.com/technetwork/java/hotspotfaq-138619.html

https://xmlandmore.blogspot.com/2014/12/jdk-8-when-softly-referenced-objects.html

类似案例:

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6912889

https://www.ibm.com/developerworks/community/blogs/kevgrig/entry/be_careful_when_
diagnosing_java_memory_leaks17?lang=en

【对“东方龙马慎用java.lang.ref.SoftReference实现缓存”发布评论】

版权及免责声明:
① 本网站部分投稿来源于“网友”,涉及投资、理财、消费等内容,请亲们反复甄别,切勿轻信。本网站部分由赞助商提供的内容属于【广告】性质,仅供阅读,不构成具体实施建议,请谨慎对待。据此操作,风险自担。
② 内容来源注明“硅谷网”及其相关称谓的文字、图片和音视频,版权均属本网站所有,任何媒体、网站或个人需经本网站许可方可复制或转载,并在使用时必须注明来源【硅谷网】或对应来源,违者本网站将依法追究责任。
③ 注明来源为各大报纸、杂志、网站及其他媒体的文章,文章原作者享有著作权,本网站转载其他媒体稿件是为传播更多的信息,并不代表赞同其观点和对其真实性负责,本网站不承担此类稿件侵权行为的连带责任。
④ 本网站不对非自身发布内容的真实性、合法性、准确性作担保。若硅谷网因为自身和转载内容,涉及到侵权、违法等问题,请有关单位或个人速与本网站取得联系(联系电话:01057255600),我们将第一时间核实处理。
广告
相关
·千名中老年游横店 糖豆达人年会尽显东方美
·盛大游戏新文创APP“文物加” 追寻东方审美潮流
头条
“魔搜”软件开发者张某被判刑1年2个月缓刑1年10个月 “魔搜”软件开发者张某被判刑1年2个月缓刑1
一个取名魔搜的软件篡改消费者在电商平台的购物浏览记录,企图通过数据造假制造爆款。……
·“魔搜”软件开发者张某被判刑1年2个月缓刑1
·45款应用程序遭谷歌下架 猎豹移动股价惨跌16.
·腾讯会议一星好评,如何占得线上视频会议市场
·高仿APP捞钱套路:蹭官方、发广告、索取隐私
·报告称APP的支出和使用率在2019年达到创纪录
图文
“远程办公”战线拉长,如何确保“私有化”办公?
“远程办公”战线拉长,如何确保“私有化”
友盟+智能认证:用户增长始于头,体验提升只需1.3秒
友盟+智能认证:用户增长始于头,体验提升
知米背单词APP那些不为人知的小细节(图)
知米背单词APP那些不为人知的小细节(图)
Realme引入广告什么情况 Realme广告怎么设置关闭?
Realme引入广告什么情况 Realme广告怎么设
最新
·“远程办公”战线拉长,如何确保“私有化”办公?
·友盟+智能认证:用户增长始于头,体验提升只需1.3
·“魔搜”软件开发者张某被判刑1年2个月缓刑1年10
·字节跳动称旗下办公产品飞书被微信全面封禁
·疫情下的危机,如何通过“信源豆豆”实现企业安全
热点
·群控、云控时代即将终结,智控时代已到来
·106短信群发平台APP,致力于成为领域内佼佼者
·DT小听App:防偷拍,还是用这款国产app(图)
·软件技术行业发展变化非常快,软件人才要按需
·嗨学网一级消防可靠吗?新手妈妈亲生经历告诉
旧闻
·全国多地上线电子社保卡 微信10秒即可申领
·社会“抢票软件”存隐患随时有可能被屏蔽
·喜推人工智能名片:看懂客户心,销售才能更欢
·微软发布2013年首批补丁 暂未修复IE漏洞
·漂亮有创意的思维导图怎么画?办公也有小技巧
广告
硅谷影像
“远程办公”战线拉长,如何确保“私有化”办公?
“远程办公”战线拉长,如何确保“私有化”办公?
友盟+智能认证:用户增长始于头,体验提升只需1.3秒
友盟+智能认证:用户增长始于头,体验提升只需1.3
“魔搜”软件开发者张某被判刑1年2个月缓刑1年10个月
“魔搜”软件开发者张某被判刑1年2个月缓刑1年10
打响“科技防疫战” 中软国际解放号在行动
打响“科技防疫战” 中软国际解放号在行动
钉钉5.0新增在线办公室、圈子,满足用户个性化需求
钉钉5.0新增在线办公室、圈子,满足用户个性化需
钉钉5.0产品见面会 彩蛋Real如我曝光号召无压力分享
钉钉5.0产品见面会 彩蛋Real如我曝光号召无压力分
关于我们·About | 联系我们·contact | 加入我们·Join | 关注我们·Invest | Site Map | Tags | RSS Map
电脑版·PC版 移动版·MD版 网站热线:(+86)010-57255600
Copyright © 2007-2020 硅谷网. 版权所有. All Rights Reserved. <京ICP备12003855号-2>