MyException - 我的异常网
当前位置:我的异常网» 综合 » 【垃圾回收】Java内存回收实践经验 防止内存储器报警

【垃圾回收】Java内存回收实践经验 防止内存储器报警

www.MyException.Cn  网友分享于:2013-10-11  浏览:0次
【垃圾回收】Java内存回收实践经验 防止内存报警

jdk6和7服务器端(-server) 默认的新生代的垃圾回收器为:PS Scavenge,老年代默认的垃圾回收器为:PS MarkSweep

目前项目使用了jdk7,tomcat7,经常出现内存堆使用量200s持续超过堆总内存80%,触发报警。

由于项目最近的更新为jdk和tomcat升级,从6升级到7,而之前使用tomcat6时并未报警,是因为tomcat的一个监听器行为模式变更造成的

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />

 在tomcat6中,它会每隔一个小时3600s(1小时)调用一次System.gc()方法,手动执行Full GC,使老年代中的无用对象每隔一小时清理一次;

    在tomcat7中,事情发生了变化,它会每隔Long.Max_Value -1  毫秒秒执行一次System.gc()

                /*
                 * Several components end up calling
                 * sun.misc.GC.requestLatency(long) which creates a daemon
                 * thread without setting the TCCL.
                 *
                 * Those libraries / components known to trigger memory leaks
                 * due to eventual calls to requestLatency(long) are:
                 * - javax.management.remote.rmi.RMIConnectorServer.start()
                 *
                 * Note: Long.MAX_VALUE is a special case that causes the thread
                 *       to terminate
                 *
                 */
                if (gcDaemonProtection) {
                    try {
                        Class<?> clazz = Class.forName("sun.misc.GC");
                        Method method = clazz.getDeclaredMethod(
                                "requestLatency",
                                new Class[] {long.class});
                        method.invoke(null, Long.valueOf(Long.MAX_VALUE - 1));
                    } catch (ClassNotFoundException e) {
                        if (JreVendor.IS_ORACLE_JVM) {
                            log.error(sm.getString(
                                    "jreLeakListener.gcDaemonFail"), e);
                        } else {
                            log.debug(sm.getString(
                                    "jreLeakListener.gcDaemonFail"), e);
                        }
                    } catch (SecurityException e) {
                        log.error(sm.getString("jreLeakListener.gcDaemonFail"),
                                e);
                    } catch (...) {
                        ...
                    } 
                }

  

调用GC类的方法,最终开启一个优先级为2的Deamon线程,在一个无限循环中调用System.gc()

        public void run() {
            while(true) {
                synchronized(GC.lock) {
                    long var1 = GC.latencyTarget;
                    if(var1 == 9223372036854775807L) {
                        GC.daemon = null;
                        return;
                    }

                    long var4 = GC.maxObjectInspectionAge();
                    if(var4 >= var1) {
                        System.gc();
                        var4 = 0L;
                    }

                    try {
                        GC.lock.wait(var1 - var4);
                    } catch (InterruptedException var8) {
                        ;
                    }
                }
            }
        }

  

Long.Max_Value -1  毫秒换算成时间,是62亿年,这个时间太长了,因此我们指望不了这个功能帮你什么忙,并且最好的方式应该是应用自己在适当的时候触发Full GC

 

回到实际项目中,老年代持续增加,平均每天会触发1~2次Full GC,这个次数倒是不多;由于老年代内存使用量长时间居高不下,而新生代的内存使用量随着业务频率的变化,有时候迅速撑满,触发Young GC;有时候又增长缓慢,如果(Young Memory + Turned Memory / Total Memeory) > 80%,就会报警。

总的来说,老年代内存占用量长期偏高,增长较快,是导致这个问题的主要原因。

先来了解垃圾回收算法的一些术语:

PS Scavenge:新生代垃圾回收器,使用复制算法(指针碰撞,内存分为Eden,S1,S2),并行(不能和App线程同时运行,GC会stw)的多线程收集器,它关注的系统的吞吐量,有效的利用cpu,尽快完成任务,不太关注线程停顿时间。它有一个自适应调整策略(可以通过-XX:-UseAdaptiveSizePolicy关闭,默认开启),导致虚拟机参数配置-XX:SurvivorRatio参数没有实际意义(S1,S2的大小会不定的变化,实际观察来看,S1和S2的大小是每次回收后剩余的对象所占用空间的大小),适用于高吞吐量的场景,而不适合时间敏感的场景

 

我们的系统是一个后端任务系统,单纯从系统属性来分析,他是很适合使用PS Scavenge的,是什么导致它变得不适合呢?

分析原因:

1.系统大量引入了MQ、ServiceFramework,调用三方系统,使用的是nio封装的长连接,在每次调用后,连接依然还在;
2.由于1产生了很多长连接,在某次minor GC回收后s1空间大小为n,下次minor GC回收时,剩余存活对象占用空间 > n,此时如果系统没有重新设置S空间大小,对象更易进入老年代
3.在2的基础上,一些长连接进入老年代后,由于网络异常、连接断开或其他因素,已不可用,伴生对象仍然在老年代中,但它需要等到下次full GC时才会回收

  

此时我们已经想到一个解决办法,那就是如何让老年代在空间用完前就执行Full GC,这个问题就可完美解决,而这是PS MarkSweep收集器不具备的功能。

 

标记-清除MarkSweep:把内存分成很多块,对象没有引用了就标记出来,标记完后统一回收对象,清除后空间不再连续,分配大对象时可能无法找到足够空间将触发一次垃圾回收,如果在老年代,将触发Full GC

(《深入理解Java虚拟机第二版》图)

 

 

PS MarkSweep:老年代收集器,是一个可以并行标记和清理垃圾的回收器,无整理,使用的是空闲列表的方式,就像一个多线程版本的Serial Old收集器

 

能做到老年代提前GC的垃圾回收器有CMS收集器,但它的搭配伙伴是ParNew,由ParNew来执行新生代垃圾回收。

 

ParNew:多个线程并行(app线程需等待,stop the world)收集,新生代收集器,最大的优点是可以配合CMS回收器,默认gc线程数和cpu数量相同,当cpu太多,如32个时,可以使用-XX:ParallelGCThreads来限制gc线程个数

CMS:(-XX:+UseConcMarkSweepGC)分为初始标记(并行,app线程挂起 stop the world,仅标记GC Roots关联的对象,速度很快)、并发标记(GC Roots对象向下追踪)、重新标记(并行,app线程挂起 stop the world,矫正并发标记时的修改,时间稍长)、并发清除,垃圾收集过程中最耗时的并发标记、并发清除都可以和用户线程一起工作,所以,整体来说,它的内存回收过程与app线程是一起并发执行的,stop the world的时间较短;

 

但并发执行也意味着此时会与app线程竞争cpu资源,app会变慢,系统吞吐量降低,gc线程数为(cpu+3)/4个;无法处理浮动垃圾:即stop the world后的并发阶段,生成的对象。jdk1.6会在老年代占比92%时触发full gc,以预留空间给浮动对象,参数为(-XX:CMSInitiatingOccupancyFraction=80 手动设定),设置太高时可能会出现Concurrent Mode Failure(老年代剩余空间大于 新生代 Eden + S1的和),不建议设置太高

CMSInitiatingOccupancyFraction = (100 - MinHeapFreeRatio) + (CMSTriggerRatio * MinHeapFreeRatio / 100)
MinHeapFreeRatio默认40、 指定 jvm heap 在使用率小于 n 的情况下 ,heap 进行收缩 ,Xmx==Xms 的情况下无效
CMSTriggerRatio默认80, 设置由-XX:MinHeapFreeRatio指定值的百分比的值。默认是80%
计算结果
CMSInitiatingOccupancyFraction=92

查看方式:jinfo -flag CMSInitiatingOccupancyFraction pid

CMS基于标记-清除算法,收集结束后会有大量碎片空间,不利于大对象(当前连续的碎片空间已不能满足某个对象分配所需内存时,这里的大对象相对而言的)分配,大对象分配失败时,会触发一次FullGC,CMS为此提供了-XX:+UseCMSCompactAtFullCollection开关(默认开启),在没有足够的连续空间时,启动碎片整理合并,但此时stop the world时间相应增加。默认情况下,每次Full GC都会对老年代进行碎片整理

 

使用ParNew + CMS后

1.Eden、S1、S2空间大小固定,相对于PS Scavenge收集器,S1、S2空间更大,能容纳更大的对象,这样对象在S1、S2呆的时间更容易长久,在对象被回收前进入老年代的概率大大降低

2.老年代对象增加的速度明显变慢,并且在jdk6、jdk7环境默认参数下,在 占用空间 / 总空间  > 92%时,及时回收掉无用对象,老年代内存使用量大幅降低

备注:须知老年代在执行Full GC后对象应该是大大降低的,如果GC后对象并未下降太多,可能有两种情况:老年代内存分配较少\程序有内存泄露的风险

如果使用CMS后还会有堆内存长时间使用率 > 80%的异常情况,我们可根据New空间和Turned空间的空间总占用量极限值来推导参数
CMSInitiatingOccupancyFraction的合适值
如新生代 1000M、老年代1000M -Xmx\-Xmn
则Eden(800m) S1(100M) S2(100M)
Tenured(1000M)
Eden + S1 = 900M
Tenured* 92% = 920M
则最大推内存使用率为 900M + 920M = 1820M
最大堆内存使用率 1820 / 2000 = 91%
当然这是极端情况,如果你的内存占用率要求不能长时间高于80%,则可以通过配置新生代、老年代的比例,以及-XX:CMSInitiatingOccupancyFraction=n参数来保证

内存分配了,如果一直不使用也是一种浪费,我们的目标不是完全杜绝堆利用 > 80%,而是长时间大于80%
目前来看,默认的老年代92%时进行Full GC已经满足了我们的需求

 

HotSpot为什么perm和heap要一起回收:因为两块区域可能有相互引用的关系,分开回收比较困难
由于新生代采用复制算法进行垃圾收集,所以可能会由于分配担保导致一次full gc,空间分配担保机制可使对象提前进入老年代
也就是Eden + S1 > Turned的剩余连续空间 或者 turned剩余连续空间 > 历次晋升到老年代对象空间大小的平均值, 并且 -XX:HandlePromotionFailure(JDK6 Update 24之后这个参数就没有意义了,也就是强制使用空间分配担保策略了)为是否开启担保,如果担保,就进行Minor GC,如果无担保则会Full GC,也就是

大对象直接进入老年代,避免在Eden和Survivor之间进行大量内存复制

 

G1:充分利用多核环境缩短stop-the-world,通过并发执行减少app线程停顿时间;拥有分代收集,将整个堆划分为多个相等的独立区域,这样,老年代和新生代不再是物理隔离的了,它们有可能是挨着的,且不只是两块区域,但可以独立管理整个堆,整体上是标记-整理算法;可预测的停顿时间,可以知道在长度多少毫秒时间片段内,垃圾收集时间不超过n毫秒

 

GC日志:

jvm参数:-XX:MaxPermSize=400M -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=80 -XX:+PrintGCDetails -Xloggc:D:\gc.log
3324K->152K(11904K)表示GC前堆已使用量 -> GC后堆已使用容量(堆总容量)

  

有书上(《深入理解java虚拟机》)说GC、FULL GC说明的是停顿类型,若是Full GC则在GC时产生了stop-the-world,也就是用它来区分是否发生了stop-the-world,而不是用来说明是对哪个代进行回收的(Young代还是turned代、Perm),这个说法应该是有误的

根据GC算法原理,每个代的回收都会有GC Roots分析可达性,Young代也是如此,因此他们都会有stop-the-world(虚拟机的某些机制可加速,如在安全点的基础上,OopMap快速找出栈上有普通对象引用的区域,避免挨个遍历栈的内存区域)。

84.884: [GC 84.884: [ParNew: 18175K->1358K(19136K), 0.0099206 secs] 74132K->57459K(109692K), 0.0100078 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]  
85.104: [GC 85.184: [ParNew: 18382K->1160K(19136K), 0.0042949 secs] 74483K->58053K(109692K), 0.0043920 secs] [Times: user=0.00 sys=0.00, real=0.08 secs] 

两次普通GC      新生代回收空间    堆总的回收空间       (新生代回收-堆总空间回收)
第一条GC日志:    16817             16673                   144
第二条GC日志:    17222             16430                   792

可以看到每次MinorGC后,新生代回收的空间都是大于了堆总的回收空间,这说明有新生代的对象进入了老年代,而这两个GC日志间并没有老年代GC日志,ParNew是新生代回收器

  

Full GC会回收老年代内的对象,Hotspot中还会回收Perm空间,同时,触发Full GC一般是因为需要往老年代放入对象,而这个操作一般都伴随了先前一次的Young GC

-Xmx150M -Xms150M -XX:+PrintGCDetails 不断插入1M的大对象,且不释放引用时的GC日志
[GC [PSYoungGen: 43187K->5680K(44800K)] 103604K->101937K(147200K), 0.0601768 secs] [Times: user=0.03 sys=0.06, real=0.06 secs] [Full GC [PSYoungGen: 5680K->0K(44800K)] [PSOldGen: 96257K->101848K(102400K)] 101937K->101848K(147200K) [PSPermGen: 3718K->3718K(21248K)], 0.1401878 secs] [Times: user=0.01 sys=0.00, real=0.14 secs]

可以看到这种极端情况下Full GC会同时触发Minor GC、Turned GC、PermGen GC,同时Minor GC依然用的青年代的收集器,所以这次Full GC,其实是有多个阶段的,因为他们不是同时进行的;

 

虚拟机性能监控指令:

 

jps 
jps -m 显示main函数的参数
jps -v 显示虚拟机启动参数

jstat -gcutil pid 查看垃圾回收各个代的内存情况,百分比,gc时间

  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT

  8.90   0.00  40.80  60.12   -      -    238    1.165    16    0.080    1.245  

SO\S1\E\O分别是两个Survior空间的使用率,Eden空间、老年代使用率,在java8中引入了MetaSpace jinfo 查看虚拟机配置参数 jmap -dump 生成java堆转储快照到文件 jmap -heap 显示堆详细信息,如回收器、参数、分代情况 jmap -histo 堆中对象统计 jhat xxx.bin 解析jmap -dump出来的文件,会生成一个http server,展示页面中包括了各种信息 jstack pid查看线程的堆栈信息 jstack -l pid额外查询锁的附加信息

 

 

 

再谈引用:强引用、软引用、弱引用、虚引用

强引用:比如new,只要强引用还存在,垃圾收集永远不会回收掉对象

软引用:对于被软引用关联的对象,在系统发布内存溢出之前,会把这些对象列入可回收返回,进行二次垃圾回收

弱引用:只被弱引用关联的对象,只能生存到下次垃圾回收

虚引用:一个对象是否有虚引用,对其生命周期毫不影响

文章评论

Google伦敦新总部 犹如星级庄园
Google伦敦新总部 犹如星级庄园
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
程序员的一天:一寸光阴一寸金
程序员的一天:一寸光阴一寸金
程序员的鄙视链
程序员的鄙视链
总结2014中国互联网十大段子
总结2014中国互联网十大段子
那些性感的让人尖叫的程序员
那些性感的让人尖叫的程序员
我的丈夫是个程序员
我的丈夫是个程序员
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
鲜为人知的编程真相
鲜为人知的编程真相
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
 程序员的样子
程序员的样子
10个调试和排错的小建议
10个调试和排错的小建议
中美印日四国程序员比较
中美印日四国程序员比较
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
那些争议最大的编程观点
那些争议最大的编程观点
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
旅行,写作,编程
旅行,写作,编程
程序员必看的十大电影
程序员必看的十大电影
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
编程语言是女人
编程语言是女人
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
代码女神横空出世
代码女神横空出世
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
我是如何打败拖延症的
我是如何打败拖延症的
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
程序员应该关注的一些事儿
程序员应该关注的一些事儿
2013年美国开发者薪资调查报告
2013年美国开发者薪资调查报告
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
每天工作4小时的程序员
每天工作4小时的程序员
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
程序员和编码员之间的区别
程序员和编码员之间的区别
为什么程序员都是夜猫子
为什么程序员都是夜猫子
程序员都该阅读的书
程序员都该阅读的书
Java程序员必看电影
Java程序员必看电影
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有