jvm很难吗?我不这么以为,不吹牛,这份图谱都能学明白

秉承一向的学习风格,技术性文章,先来一张脑图java

技术没什么大不了的,再深刻不也就那些东西嘛,只不过看你在学习的过程当中是否真的用心了,我我的以为这也是学习和涉猎的区别算法

而后咱们来看具体的技术讲解sql


1、JVM内存模型及垃圾收集算法apache

1.根据Java虚拟机规范,JVM将内存划分为:缓存

  • New(年轻代)
  • Tenured(年老代)
  • 永久代(Perm)

其中New和Tenured属于堆内存,堆内存会从JVM启动参数(-Xmx:3G)指定的内存中分配,Perm不属于堆内存,有虚拟机直接分配,但能够经过-XX:PermSize -XX:MaxPermSize 等参数调整其大小。服务器

 

  • 年轻代(New):年轻代用来存放JVM刚分配的Java对象
  • 年老代(Tenured):年轻代中通过垃圾回收没有回收掉的对象将被Copy到年老代
  • 永久代(Perm):永久代存放Class、Method元信息,其大小跟项目的规模、类、方法的量有关,通常设置为128M就足够,设置原则是预留30%的空间。

New又分为几个部分:多线程

  • Eden:Eden用来存放JVM刚分配的对象
  • Survivor1
  • Survivro2:两个Survivor空间同样大,当Eden中的对象通过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当知足某个条件,好比Copy次数,就会被Copy到Tenured。显然,Survivor只是增长了对象在年轻代中的逗留时间,增长了被垃圾回收的可能性。

2.垃圾回收算法架构

垃圾回收算法能够分为三类,都基于标记-清除(复制)算法:并发

  • Serial算法(单线程)
  • 并行算法
  • 并发算法

JVM会根据机器的硬件配置对每一个内存代选择适合的回收算法,好比,若是机器多于1个核,会对年轻代选择并行算法,关于选择细节请参考JVM调优文档。app

稍微解释下的是,并行算法是用多线程进行垃圾回收,回收期间会暂停程序的执行,而并发算法,也是多线程回收,但期间不中止应用执行。因此,并发算法适用于交互性高的一些程序。通过观察,并发算法会减小年轻代的大小,其实就是使用了一个大的年老代,这反过来跟并行算法相比吞吐量相对较低。

 

还有一个问题是,垃圾回收动做什么时候执行?

  • 当年轻代内存满时,会引起一次普通GC,该GC仅回收年轻代。须要强调的,年轻代尽是指Eden代满,Survivor满不会引起GC
  • 当年老代满时会引起Full GC,Full GC将会同时回收年轻代、年老代
  • 当永久代满时也会引起Full GC,会致使Class、Method元信息的卸载

另外一个问题是,什么时候会抛出OutOfMemoryException,并非内存被耗空的时候才抛出

  • JVM98%的时间都花费在内存回收
  • 每次回收的内存小于2%

知足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以作一些Down以前的操做,好比手动打印Heap Dump。


2、内存泄漏及解决方法

1.系统崩溃前的一些现象:

  • 每次垃圾回收的时间愈来愈长,由以前的10ms延长到50ms左右,FullGC的时间也有以前的0.5s延长到四、5s
  • FullGC的次数愈来愈多,最频繁时隔不到1分钟就进行一次FullGC
  • 年老代的内存愈来愈大而且每次FullGC后年老代没有内存被释放

以后系统会没法响应新的请求,逐渐到达OutOfMemoryError的临界值。

 

2.生成堆的dump文件

经过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,若是没有启动JMX能够经过Java的jmap命令来生成该文件。

 

3.分析dump文件

下面要考虑的是如何打开这个3G的堆信息文件,显然通常的Window系统没有这么大的内存,必须借助高配置的Linux。固然咱们能够借助X-Window把Linux上的图形导入到Window。咱们考虑用下面几种工具打开该文件:

  1. Visual VM
  2. IBM HeapAnalyzer
  3. JDK 自带的Hprof工具

使用这些工具时为了确保加载速度,建议设置最大内存为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进行调优。从如下几个方面进行:

  • 线程池:解决用户响应时间长的问题
  • 链接池
  • JVM启动参数:调整各代的内存比例和垃圾回收算法,提升吞吐量
  • 程序算法:改进程序逻辑算法提升性能

1.Java线程池(
java.util.concurrent.ThreadPoolExecutor)

大多数JVM6上的应用采用的线程池都是JDK自带的线程池,之因此把成熟的Java线程池进行罗嗦说明,是由于该线程池的行为与咱们想象的有点出入。Java线程池有几个重要的配置参数:

  • corePoolSize:核心线程数(最新线程数)
  • maximumPoolSize:最大线程数,超过这个数量的任务会被拒绝,用户能够经过RejectedExecutionHandler接口自定义处理方式
  • keepAliveTime:线程保持活动的时间
  • workQueue:工做队列,存放执行的任务

Java线程池须要传入一个Queue参数(workQueue)用来存放执行的任务,而对Queue的不一样选择,线程池有彻底不一样的行为:

  • SynchronousQueue: 一个无容量的等待队列,一个线程的insert操做必须等待另外一线程的remove操做,采用这个Queue线程池将会为每一个任务分配一个新线程
  • LinkedBlockingQueue : 无界队列,采用该Queue,线程池将忽略 maximumPoolSize参数,仅用corePoolSize的线程处理全部的任务,未处理的任务便在LinkedBlockingQueue中排队
  • ArrayBlockingQueue: 有界队列,在有界队列和 maximumPoolSize的做用下,程序将很难被调优:更大的Queue和小的maximumPoolSize将致使CPU的低负载;小的Queue和大的池,Queue就没起动应有的做用。

其实咱们的要求很简单,但愿线程池能跟链接池同样,能设置最小线程数、最大线程数,当最小数<任务<最大数时,应该分配新的线程处理;当任务>最大数时,应该等待有空闲线程再处理该任务。

但线程池的设计思路是,任务应该放到Queue中,当Queue放不下时再考虑用新线程处理,若是Queue满且没法派生新线程,就拒绝该任务。设计致使“先放等执行”、“放不下再执行”、“拒毫不等待”。因此,根据不一样的Queue参数,要提升吞吐量不能一味地增大maximumPoolSize。

固然,要达到咱们的目标,必须对线程池进行必定的封装,幸运的是ThreadPoolExecutor中留了足够的自定义接口以帮助咱们达到目标。咱们封装的方式是:

  • 以SynchronousQueue做为参数,使maximumPoolSize发挥做用,以防止线程被无限制的分配,同时能够经过提升maximumPoolSize来提升系统吞吐量
  • 自定义一个RejectedExecutionHandler,当线程数超过maximumPoolSize时进行处理,处理方式为隔一段时间检查线程池是否能够执行新Task,若是能够把拒绝的Task从新放入到线程池,检查的时间依赖keepAliveTime的大小。

2.链接池(
org.apache.commons.dbcp.BasicDataSource)

在使用
org.apache.commons.dbcp.BasicDataSource的时候,由于以前采用了默认配置,因此当访问量大时,经过JMX观察到不少Tomcat线程都阻塞在BasicDataSource使用的Apache ObjectPool的锁上,直接缘由当时是由于BasicDataSource链接池的最大链接数设置的过小,默认的BasicDataSource配置,仅使用8个最大链接。

我还观察到一个问题,当较长的时间不访问系统,好比2天,DB上的Mysql会断掉因此的链接,致使链接池中缓存的链接不能用。为了解决这些问题,咱们充分研究了BasicDataSource,发现了一些优化的点:

  • Mysql默认支持100个连接,因此每一个链接池的配置要根据集群中的机器数进行,若有2台服务器,可每一个设置为60
  • initialSize:参数是一直打开的链接数
  • minEvictableIdleTimeMillis:该参数设置每一个链接的空闲时间,超过这个时间链接将被关闭
  • timeBetweenEvictionRunsMillis:后台线程的运行周期,用来检测过时链接
  • maxActive:最大能分配的链接数
  • maxIdle:最大空闲数,当链接使用完毕后发现链接数大于maxIdle,链接将被直接关闭。只有initialSize < x < maxIdle的链接将被按期检测是否超期。这个参数主要用来在峰值访问时提升吞吐量。
  • initialSize是如何保持的?通过研究代码发现,BasicDataSource会关闭全部超期的链接,而后再打开initialSize数量的链接,这个特性与minEvictableIdleTimeMillis、timeBetweenEvictionRunsMillis一块儿保证了全部超期的initialSize链接都会被从新链接,从而避免了Mysql长时间无动做会断掉链接的问题。

3.JVM参数

在JVM启动参数中,能够设置跟内存、垃圾回收相关的一些参数设置,默认状况不作任何设置JVM会工做的很好,但对一些配置很好的Server和具体的应用必须仔细调优才能得到最佳性能。经过设置咱们但愿达到一些目标:

  • GC的时间足够的小
  • GC的次数足够的少
  • 发生Full GC的周期足够的长

前两个目前是相悖的,要想GC时间小必需要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,咱们只能取其平衡。

(1)针对JVM堆的设置,通常能够经过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,咱们一般把最大、最小设置为相同的值 (2)年轻代和年老代将根据默认的比例(1:2)分配堆内存,能够经过调整两者之间的比率NewRadio来调整两者之间的大小,也能够针对回收代,好比年轻代,经过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。一样,为了防止年轻代的堆收缩,咱们一般会把-XX:newSize -XX:MaxNewSize设置为一样大小

(3)年轻代和年老代设置多大才算合理?这个我问题毫无疑问是没有答案的,不然也就不会有调优。咱们观察一下两者大小变化有哪些影响

  • 更大的年轻代必然致使更小的年老代,大的年轻代会延长普通GC的周期,但会增长每次GC的时间;小的年老代会致使更频繁的Full GC
  • 更小的年轻代必然致使更大年老代,小的年轻代会致使普通GC很频繁,但每次的GC时间会更短;大的年老代会减小Full GC的频率
  • 如何选择应该依赖应用程序对象生命周期的分布状况:若是应用存在大量的临时对象,应该选择更大的年轻代;若是存在相对较多的持久对象,年老代应该适当增大。但不少应用都没有这样明显的特性,在抉择时应该根据如下两点:(A)本着Full GC尽可能少的原则,让年老代尽可能缓存经常使用对象,JVM的默认比例1:2也是这个道理 (B)经过观察应用一段时间,看其余在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际状况加大年轻代,好比能够把比例控制在1:1。但应该给年老代至少预留1/3的增加空间

(4)在配置较好的机器上(好比多核、大内存),能够为年老代选择并行收集算法: -XX:+UseParallelOldGC ,默认为Serial收集

(5)线程堆栈的设置:每一个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,通常256K就足用。理论上,在内存不变的状况下,减小每一个线程的堆栈,能够产生更多的线程,但这实际上还受限于操做系统。

(4)能够经过下面的参数打Heap Dump信息

  • -XX:HeapDumpPath
  • -XX:+PrintGCDetails
  • -XX:+PrintGCTimeStamps
  • -Xloggc:/usr/aaa/dump/heap_trace.txt

经过下面参数能够控制OutOfMemoryError时打印堆的信息

  • -XX:+HeapDumpOnOutOfMemoryError

请看一下一个时间的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"查看,发现:

JVM性能调优总结:JVM内存模型,内存泄漏及解决方法,调优方法~

 

该应用的堆区设置只有768m,而机器内存有2g,机器上只跑这一个java应用,没有其余须要占用内存的地方。另外,这个应用比较大,须要占用的内存也比较多;

笔者经过上面的状况判断,只须要改变堆中各区域的大小设置便可,因而改为下面的状况:

 

JVM性能调优总结:JVM内存模型,内存泄漏及解决方法,调优方法~

 

 

跟踪运行状况发现,相关异常没有再出现;

 

实例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差距进行分析,发现:

JVM性能调优总结:JVM内存模型,内存泄漏及解决方法,调优方法~

从图中能够看出,这个线程存在问题,队列LinkedBlockingQueue所引用的大量对象并未释放,致使整个线程占用内存高达378m,此时通知开发人员进行代码优化,将相关对象释放掉便可。

 

关于JVM,就这么一点拙见,有不对的地方,但愿你们指出,谢谢

关注公众号:Java架构师联盟,每日更新技术好文,须要架构图,关注公众号后回复“架构图”便可

相关文章
相关标签/搜索