秉承一向的学习风格,技术性文章,先来一张脑图java
技术没什么大不了的,再深刻不也就那些东西嘛,只不过看你在学习的过程当中是否真的用心了,我我的以为这也是学习和涉猎的区别算法
而后咱们来看具体的技术讲解sql
1、JVM内存模型及垃圾收集算法apache
1.根据Java虚拟机规范,JVM将内存划分为:缓存
其中New和Tenured属于堆内存,堆内存会从JVM启动参数(-Xmx:3G)指定的内存中分配,Perm不属于堆内存,有虚拟机直接分配,但能够经过-XX:PermSize -XX:MaxPermSize 等参数调整其大小。服务器
New又分为几个部分:多线程
2.垃圾回收算法架构
垃圾回收算法能够分为三类,都基于标记-清除(复制)算法:并发
JVM会根据机器的硬件配置对每一个内存代选择适合的回收算法,好比,若是机器多于1个核,会对年轻代选择并行算法,关于选择细节请参考JVM调优文档。app
稍微解释下的是,并行算法是用多线程进行垃圾回收,回收期间会暂停程序的执行,而并发算法,也是多线程回收,但期间不中止应用执行。因此,并发算法适用于交互性高的一些程序。通过观察,并发算法会减小年轻代的大小,其实就是使用了一个大的年老代,这反过来跟并行算法相比吞吐量相对较低。
还有一个问题是,垃圾回收动做什么时候执行?
另外一个问题是,什么时候会抛出OutOfMemoryException,并非内存被耗空的时候才抛出
知足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以作一些Down以前的操做,好比手动打印Heap Dump。
2、内存泄漏及解决方法
1.系统崩溃前的一些现象:
以后系统会没法响应新的请求,逐渐到达OutOfMemoryError的临界值。
2.生成堆的dump文件
经过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,若是没有启动JMX能够经过Java的jmap命令来生成该文件。
3.分析dump文件
下面要考虑的是如何打开这个3G的堆信息文件,显然通常的Window系统没有这么大的内存,必须借助高配置的Linux。固然咱们能够借助X-Window把Linux上的图形导入到Window。咱们考虑用下面几种工具打开该文件:
使用这些工具时为了确保加载速度,建议设置最大内存为6G。使用后发现,这些工具都没法直观地观察到内存泄漏,Visual VM虽能观察到对象大小,但看不到调用堆栈;HeapAnalyzer虽然能看到调用堆栈,却没法正确打开一个3G的文件。所以,咱们又选用了Eclipse专门的静态内存分析工具:Mat。
4.分析内存泄漏
经过Mat咱们能清楚地看到,哪些对象被怀疑为内存泄漏,哪些对象占的空间最大及对象的调用关系。针对本案,在ThreadLocal中有不少的JbpmContext实例,通过调查是JBPM的Context没有关闭所致。
另,经过Mat或JMX咱们还能够分析线程状态,能够观察到线程被阻塞在哪一个对象上,从而判断系统的瓶颈。
5.回归问题
Q:为何崩溃前垃圾回收的时间愈来愈长?
A:根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记、清除(复制),标记部分只要内存大小固定时间是不变的,变的是复制部分,由于每次垃圾回收都有一些回收不掉的内存,因此增长了复制量,致使时间延长。因此,垃圾回收的时间也能够做为判断内存泄漏的依据
Q:为何Full GC的次数愈来愈多?
A:所以内存的积累,逐渐耗尽了年老代的内存,致使新对象分配没有更多的空间,从而致使频繁的垃圾回收
Q:为何年老代占用的内存愈来愈大?
A:由于年轻代的内存没法被回收,愈来愈多地被Copy到年老代
3、性能调优
除了上述内存泄漏外,咱们还发现CPU长期不足3%,系统吞吐量不够,针对8core×16G、64bit的Linux服务器来讲,是严重的资源浪费。
在CPU负载不足的同时,偶尔会有用户反映请求的时间过长,咱们意识到必须对程序及JVM进行调优。从如下几个方面进行:
1.Java线程池(
java.util.concurrent.ThreadPoolExecutor)
大多数JVM6上的应用采用的线程池都是JDK自带的线程池,之因此把成熟的Java线程池进行罗嗦说明,是由于该线程池的行为与咱们想象的有点出入。Java线程池有几个重要的配置参数:
Java线程池须要传入一个Queue参数(workQueue)用来存放执行的任务,而对Queue的不一样选择,线程池有彻底不一样的行为:
其实咱们的要求很简单,但愿线程池能跟链接池同样,能设置最小线程数、最大线程数,当最小数<任务<最大数时,应该分配新的线程处理;当任务>最大数时,应该等待有空闲线程再处理该任务。
但线程池的设计思路是,任务应该放到Queue中,当Queue放不下时再考虑用新线程处理,若是Queue满且没法派生新线程,就拒绝该任务。设计致使“先放等执行”、“放不下再执行”、“拒毫不等待”。因此,根据不一样的Queue参数,要提升吞吐量不能一味地增大maximumPoolSize。
固然,要达到咱们的目标,必须对线程池进行必定的封装,幸运的是ThreadPoolExecutor中留了足够的自定义接口以帮助咱们达到目标。咱们封装的方式是:
2.链接池(
org.apache.commons.dbcp.BasicDataSource)
在使用
org.apache.commons.dbcp.BasicDataSource的时候,由于以前采用了默认配置,因此当访问量大时,经过JMX观察到不少Tomcat线程都阻塞在BasicDataSource使用的Apache ObjectPool的锁上,直接缘由当时是由于BasicDataSource链接池的最大链接数设置的过小,默认的BasicDataSource配置,仅使用8个最大链接。
我还观察到一个问题,当较长的时间不访问系统,好比2天,DB上的Mysql会断掉因此的链接,致使链接池中缓存的链接不能用。为了解决这些问题,咱们充分研究了BasicDataSource,发现了一些优化的点:
3.JVM参数
在JVM启动参数中,能够设置跟内存、垃圾回收相关的一些参数设置,默认状况不作任何设置JVM会工做的很好,但对一些配置很好的Server和具体的应用必须仔细调优才能得到最佳性能。经过设置咱们但愿达到一些目标:
前两个目前是相悖的,要想GC时间小必需要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,咱们只能取其平衡。
(1)针对JVM堆的设置,通常能够经过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,咱们一般把最大、最小设置为相同的值 (2)年轻代和年老代将根据默认的比例(1:2)分配堆内存,能够经过调整两者之间的比率NewRadio来调整两者之间的大小,也能够针对回收代,好比年轻代,经过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。一样,为了防止年轻代的堆收缩,咱们一般会把-XX:newSize -XX:MaxNewSize设置为一样大小
(3)年轻代和年老代设置多大才算合理?这个我问题毫无疑问是没有答案的,不然也就不会有调优。咱们观察一下两者大小变化有哪些影响
(4)在配置较好的机器上(好比多核、大内存),能够为年老代选择并行收集算法: -XX:+UseParallelOldGC ,默认为Serial收集
(5)线程堆栈的设置:每一个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,通常256K就足用。理论上,在内存不变的状况下,减小每一个线程的堆栈,能够产生更多的线程,但这实际上还受限于操做系统。
(4)能够经过下面的参数打Heap Dump信息
经过下面参数能够控制OutOfMemoryError时打印堆的信息
请看一下一个时间的Java参数配置:(服务器:Linux 64Bit,8Core×16G)
JAVA_OPTS="$JAVA_OPTS -server -Xms3G -Xmx3G -Xss256k -XX:PermSize=128m -XX:MaxPermSize=128m -XX:+UseParallelOldGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/aaa/dump -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/usr/aaa/dump/heap_trace.txt -XX:NewSize=1G -XX:MaxNewSize=1G"
通过观察该配置很是稳定,每次普通GC的时间在10ms左右,Full GC基本不发生,或隔很长很长的时间才发生一次
经过分析dump文件能够发现,每一个1小时都会发生一次Full GC,通过多方求证,只要在JVM中开启了JMX服务,JMX将会1小时执行一次Full GC以清除引用,关于这点请参考附件文档。
4.程序算法调优:本次不做为重点
================================================================================
调优方法
一切都是为了这一步,调优,在调优以前,咱们须要记住下面的原则:
一、多数的Java应用不须要在服务器上进行GC优化;
二、多数致使GC问题的Java应用,都不是由于咱们参数设置错误,而是代码问题;
三、在应用上线以前,先考虑将机器的JVM参数设置到最优(最适合);
四、减小建立对象的数量;
五、减小使用全局变量和大对象;
六、GC优化是到最后不得已才采用的手段;
七、在实际使用中,分析GC状况优化代码比优化GC参数要多得多;
GC优化的目的有两个(
一、将转移到老年代的对象数量下降到最小;
二、减小full GC的执行时间;
为了达到上面的目的,通常地,你须要作的事情有:
一、减小使用全局变量和大对象;
二、调整新生代的大小到最合适;
三、设置老年代的大小为最合适;
四、选择合适的GC收集器;
在上面的4条方法中,用了几个“合适”,那究竟什么才算合适,通常的,请参考上面“收集器搭配”和“启动内存分配”两节中的建议。但这些建议不是万能的,须要根据您的机器和应用状况进行发展和变化,实际操做中,能够将两台机器分别设置成不一样的GC参数,而且进行对比,选用那些确实提升了性能或减小了GC时间的参数。
真正熟练的使用GC调优,是创建在屡次进行GC监控和调优的实战经验上的,进行监控和调优的通常步骤为:
1,监控GC的状态
使用各类JVM工具,查看当前日志,分析当前JVM参数设置,而且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,以为是否进行优化;
2,分析结果,判断是否须要优化
若是各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化;若是GC时间超过1-3秒,或者频繁GC,则必须优化;
注:若是知足下面的指标,则通常不须要进行GC:
Minor GC执行时间不到50ms;
Minor GC执行不频繁,约10秒一次;
Full GC执行时间不到1s;
Full GC执行频率不算频繁,不低于10分钟1次;
3,调整GC类型和内存分配
若是内存分配过大或太小,或者采用的GC收集器比较慢,则应该优先调整这些参数,而且先找1台或几台机器进行beta,而后比较优化过的机器和没有优化的机器的性能对比,并有针对性的作出最后选择;
4,不断的分析和调整
经过不断的试验和试错,分析并找到最合适的参数
5,全面应用参数
若是找到了最合适的参数,则将这些参数应用到全部服务器,并进行后续跟踪。
调优实例
上面的内容都是纸上谈兵,下面咱们以一些真实例子来进行说明:
实例1:
笔者昨日发现部分开发测试机器出现异常:
java.lang.OutOfMemoryError: GC overhead limit exceeded,这个异常表明:
GC为了释放很小的空间却耗费了太多的时间,其缘由通常有两个:1,堆过小,2,有死循环或大对象;
笔者首先排除了第2个缘由,由于这个应用同时是在线上运行的,若是有问题,早就挂了。因此怀疑是这台机器中堆设置过小;
使用ps -ef |grep "java"查看,发现:
该应用的堆区设置只有768m,而机器内存有2g,机器上只跑这一个java应用,没有其余须要占用内存的地方。另外,这个应用比较大,须要占用的内存也比较多;
笔者经过上面的状况判断,只须要改变堆中各区域的大小设置便可,因而改为下面的状况:
跟踪运行状况发现,相关异常没有再出现;
实例2:(
一个服务系统,常常出现卡顿,分析缘由,发现Full GC时间太长:
jstat -gcutil:
S0 S1 E O P YGC YGCT FGC FGCT GCT
12.16 0.00 5.18 63.78 20.32 54 2.047 5 6.946 8.993
分析上面的数据,发现Young GC执行了54次,耗时2.047秒,每次Young GC耗时37ms,在正常范围,而Full GC执行了5次,耗时6.946秒,每次平均1.389s,数据显示出来的问题是:Full GC耗时较长,分析该系统的是指发现,NewRatio=9,也就是说,新生代和老生代大小之比为1:9,这就是问题的缘由:
1,新生代过小,致使对象提早进入老年代,触发老年代发生Full GC;
2,老年代较大,进行Full GC时耗时较大;
优化的方法是调整NewRatio的值,调整到4,发现Full GC没有再发生,只有Young GC在执行。这就是把对象控制在新生代就清理掉,没有进入老年代(这种作法对一些应用是颇有用的,但并非对全部应用都要这么作)
实例3:
一应用在性能测试过程当中,发现内存占用率很高,Full GC频繁,使用sudo -u admin -H jmap -dump:format=b,file=文件名.hprof pid 来dump内存,生成dump文件,并使用Eclipse下的mat差距进行分析,发现:
从图中能够看出,这个线程存在问题,队列LinkedBlockingQueue所引用的大量对象并未释放,致使整个线程占用内存高达378m,此时通知开发人员进行代码优化,将相关对象释放掉便可。
关于JVM,就这么一点拙见,有不对的地方,但愿你们指出,谢谢
关注公众号:Java架构师联盟,每日更新技术好文,须要架构图,关注公众号后回复“架构图”便可