前言JVM是一个虚拟化的操作系统,类似于Linux和Window,只是建立在操作系统之上,接收classes文件并翻译class转化为系统识别的机器码执行,也就是JVM屏蔽了我们不同操作系统底层硬件和操作指令的差异。于是,JVM最重要的作用就浮出水面,那就是跨平台。由于JVM为java程序屏蔽了操作系统的底层细节,Java只需要关心如何编译,如何加载到JVM中即可。由于JVM接收的是Class文件,而不是特定的语言,所以只要某种语言能够编译成Class文件,就可以在JVM上运行。这些语言包括Groovy、Kotlin、Scala等。因此,JVM的另一个重要特点是语言无关性,即跨语言。一、JVM内存模型和垃圾回收算法1、JVM根据Java虚拟机规范,将内存分为:New(年轻代)Tenured(老年代)永久代(Perm)其中New和Tenured属于堆内存,而堆内存会从JVM启动参数指定的内存(-Xmx:3G)中分配,Perm不属于堆内存,由虚拟机直接分配,但其大小可以通过参数调整,如-XX:PermSize-XX:MaxPermSize。年轻代(New):年轻代用于存放JVM刚刚分配的Java对象Tenured:年轻代中垃圾回收后还没有回收的对象会被复制到老年代Perm:永久代存储的大小类和方法元信息与项目规模、类和方法的数量有关。一般设置为128M即可。设置原则是预留30%的空间。new分为几个部分:Eden:Eden用于存放JVM刚刚分配的对象Survivor1Survivor2:两个Survivor空间大小相同,当Eden中的对象经过垃圾回收没有被回收时,会来回两个Survivors之间的Copy,当满足一定的条件,比如副本的数量,就会被复制到Tenured中。显然,Survivor只是增加了对象在新生代的停留时间,增加了被垃圾回收的可能性。2.垃圾收集算法垃圾收集算法可以分为三类,都是基于mark-sweep(复制)算法:串行算法(单线程)并行算法并发算法JVM会根据硬件为每一代内存选择合适的回收机器算法的配置,比如机器有1个以上的核,新生代会选择并行算法,具体选择请参考JVM调优文档。稍微解释一下,并行算法使用的是多线程垃圾收集,收集期间会暂停程序的执行,而并发算法也是多线程的,只是期间应用程序执行不会停止。因此,并发算法适用于一些交互性高的程序。经过观察,并发算法会减小新生代的大小,实际上是使用了较大的老年代,相对于并行算法吞吐量相对较低。什么时候执行垃圾收集?另一个问题是,什么时候进行垃圾收集?当年轻代内存满时,会触发普通GC,GC只会回收年轻代。需要强调的是,当年轻一代满了,就意味着伊甸园满了。Survivorfull不会触发GC。当老年代满了,就会触发FullGC。FullGC会同时回收新生代和老年代。当永久代满时,也会触发FullGC,导致Class和Method元数据的卸载。另一个问题是在抛出OutOfMemoryException时,而不是在内存耗尽时。JVM98%的时间花在了内存回收上。2%满足这两个条件就会触发OutOfMemoryException,这会给系统留下一个小空隙,让系统在Down之前做一些操作,比如手动打印HeapDump。二、内存泄漏及解决方案1、系统崩溃前的一些现象:每次垃圾回收的时间越来越长,从之前的10ms到50ms左右,FullGC的时间也从之前的0.5s延长到4、5sFullGC次数越来越多,最频繁的FullGC不到1分钟就执行一次。老年代的内存越来越大,每次FullGC之后,老年代就没有内存了。释放后,系统将无法响应新的请求。OutOfMemoryError的阈值。2.生成堆的dump文件通过JMX的MBean生成当前Heap的信息,大小为3G(整个堆的大小)的hprof文件。如果没有启动JMX,可以通过Java的jmap命令生成该文件。3.分析转储文件接下来要考虑的是如何打开3G堆信息文件。很显然,一般的Windows系统是没有这么大内存的,所以必须依赖高配置的Linux。当然,我们可以借助X-Window将Linux上的图形导入到Window中。我们考虑使用以下工具打开文件:VisualVMIBMHeapAnalyzerJDK自带的Hprof工具,使用这些工具时保证加载速度,建议设置最大内存为6G。使用之后发现这些工具都不能直观的观察到内存泄漏。VisualVM虽然可以观察对象大小,但是看不到调用栈;虽然HeapAnalyzer可以看到调用堆栈,但它无法正确打开3G文件。因此,我们选择了Eclipse的专用静态内存分析工具:Mat。4、分析内存泄漏通过Mat,我们可以清楚的看到哪些对象疑似内存泄漏,哪些对象占用的空间最大,以及对象的调用关系。对于这种情况,ThreadLocal中有很多JbpmContext实例。经查,是因为JBPMContext没有关闭。另外,通过Mat或者JMX,我们还可以分析线程状态,观察线程阻塞在哪个对象上,从而判断系统瓶颈。5.回归问题Q:为什么崩溃前的垃圾回收时间越来越长?A:根据内存模型和垃圾回收算法,垃圾回收分为内存标记和清除(复制)两部分。只要内存大小固定一段时间,标记部分就保持不变,复制部分发生变化,因为每次垃圾回收都有一些无法回收的内存,所以增加了复制量,导致时间延长.所以垃圾回收的时间也可以作为判断内存泄漏的依据Q:为什么FullGC的次数越来越多?A:因此,内存的积累逐渐耗尽了老年代的内存,导致没有更多的空间分配新的对象,从而导致频繁的垃圾回收Q:为什么老年代占用的内存越来越大?A:因为年轻代的内存无法回收,越来越多的复制到老年代。对于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负载低;smallQueue和largepool,Queue不会启动它应有的效果。其实我们的要求很简单。我们希望线程池可以和连接池一样,可以设置最小线程数和最大线程数。当最小数<任务<最大数时,分配新的线程进行处理;当任务>最大数量时,应该等待一个空闲线程来处理任务。线程池的设计思想但是线程池的设计思想是任务应该放在Queue中。当Queue放不下的时候,可以考虑使用新的线程来处理。如果Queue已满,无法派生出新的线程,任务将被拒绝。设计导致“先等执行”,“放不下就执行”,“不等就拒绝”。因此,根据不同的Queue参数,不能盲目增加maximumPoolSize来提高吞吐量。当然要达到我们的目的,我们必须对线程池进行一定程度的封装。幸运的是,ThreadPoolExecutor中留下了足够的自定义接口来帮助我们实现我们的目标。我们封装的方式是:使用SynchronousQueue作为参数,让maximumPoolSize起作用,防止线程被无限分配。同时,我们可以通过增加maximumPoolSize来提高系统吞吐量。自定义一个RejectedExecutionHandler,在线程数超过maximumPoolSize时进行处理。方法是每隔一段时间检查线程池是否可以执行新的任务。如果被拒绝的任务可以放回线程池,检查时间取决于keepAliveTime的大小。2.连接池(org.apache.commons.dbcp.BasicDataSource)在使用org.apache.commons.dbcp.BasicDataSource的时候,因为之前使用的是默认配置,在流量大的时候,通过JMX观察到很多Tomcat线程都被阻塞了在BasicDataSource使用的ApacheObjectPool锁上。直接原因是BasicDataSource连接池的最大连接数设置的太小了。默认的BasicDataSource配置最多只使用8个连接。我也观察到一个问题。当系统长时间不被访问时,比如2天,DB上的Mysql会断开所有连接,导致连接池中缓存不可用的连接。为了解决这些问题,我们充分研究了BasicDataSource,发现了一些优化点:Mysql默认支持100个连接,所以每个连接池的配置要根据集群的机器数来配置。如果有2台服务器,每台设置为60initialSize:该参数为一直打开的连接数minEvictableIdleTimeMillis:该参数设置每个连接的空闲时间,超过该连接数将被关闭maxIdle:最大空闲数。当连接用完后发现连接数大于maxIdle,则直接关闭连接。只有initialSize
