前言
JVM 是一个虚拟化的操作系统,相似于 Linux 和 Window,只是他被架构在了操作系统上进行接管 class 文件并把 class 翻译成零碎辨认的机器码进行执行,即 JVM 为咱们屏蔽了不同操作系统在底层硬件和操作指令的不同。
因而,JVM 最重要的作用浮出水面,即跨平台性。因为 JVM 为 java 程序屏蔽了操作系统底层的细节,Java 只须要关怀如何编译,如何让加载进 JVM 即可。
因为 JVM 接管的是 Class 文件,而不是接管特定的语言,因而只有某种语言能够编译成 Class 文件,就能够在 JVM 上运行,这些语言有 Groovy、Kotlin、Scala 等等。因而 JVM 的另一个重要个性就是语言无关性,即跨语言。
一、JVM内存模型及垃圾收集算法
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只是减少了对象在年老代中的勾留工夫,减少了被垃圾回收的可能性。
《2020最新Java根底精讲视频教程和学习路线!》
2.垃圾回收算法
垃圾回收算法能够分为三类,都基于标记-革除(复制)算法:
Serial算法(单线程)
并行算法
并发算法
JVM会依据机器的硬件配置对每个内存代抉择适宜的回收算法,比方,如果机器多于1个核,会对年老代抉择并行算法,对于抉择细节请参考JVM调优文档。
略微解释下的是,并行算法是用多线程进行垃圾回收,回收期间会暂停程序的执行,而并发算法,也是多线程回收,但期间不进行利用执行。所以,并发算法实用于交互性高的一些程序。通过察看,并发算法会缩小年老代的大小,其实就是应用了一个大的年轻代,这反过来跟并行算法相比吞吐量绝对较低。
垃圾回收动作何时执行?
还有一个问题是,垃圾回收动作何时执行?
当年老代内存满时,会引发一次一般GC,该GC仅回收年老代。须要强调的时,年老代满是指Eden代满,Survivor满不会引发GC
当年老代满时会引发Full GC,Full GC将会同时回收年老代、年轻代
当永恒代满时也会引发Full GC,会导致Class、Method元信息的卸载
另一个问题是,何时会抛出OutOfMemoryException,并不是内存被耗空的时候才抛出
JVM98%的工夫都破费在内存回收
每次回收的内存小于2%
满足这两个条件将触发OutOfMemoryException,这将会留给零碎一个渺小的间隙以做一些Down之前的操作,比方手动打印Heap Dump。
二、内存透露及解决办法
1.零碎解体前的一些景象:
每次垃圾回收的工夫越来越长,由之前的10ms缩短到50ms左右,FullGC的工夫也有之前的0.5s缩短到4、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。咱们思考用上面几种工具关上该文件:
Visual VM
IBM HeapAnalyzer
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到年轻代
三、性能调优
除了上述内存透露外,咱们还发现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是如何放弃的?
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根本不产生,或隔很长很长的工夫才产生一次
链接:https://juejin.cn/post/690443...