jvm原理
Java虚拟机是整个java平台的基石,是java技术实现硬件无关和操作系统无关的关键环节,是java语言生成极小体积的编译代码的运行平台,是保护用户机器免受恶意代码侵袭的保护屏障。JVM是虚拟机,也是一种规范,他遵循着冯·诺依曼体系结构的设计原理。
冯·诺依曼体系结构中,指出计算机处理的数据和指令都是二进制数,采用存储程序方式不加区分的存储在同一个存储器里,并且顺序执行,指令由操作码和地址码组成,操作码决定了操作类型和所操作的数的数字类型,地址码则指出地址码和操作数。从dos到window8,从unix到ubuntu和CentOS,还有MACOS等等,不同的操作系统指令集以及数据结构都有着差异,而JVM通过在操作系统上建立虚拟机,自己定义出来的一套统一的数据结构和操作指令,把同一套语言翻译给各大主流的操作系统,实现了跨平台运行,可以说JVM是java的核心,是java可以一次编译到处运行的本质所在。
一、JVM的组成和运行原理
JVM的毕竟是个虚拟机,是一种规范,虽说符合冯诺依曼的计算机设计理念,但是他并不是实体计算机,所以他的组成也不是什么存储器,控制器,运算器,输入输出设备。在我看来,JVM放在运行在真实的操作系统中表现的更像应用或者说是进程,他的组成可以理解为JVM这个进程有哪些功能模块,而这些功能模块的运作可以看做是JVM的运行原理。JVM有多种实现,例如Oracle的JVM,HP的JVM和IBM的JVM等,而在本文中研究学习的则是使用最广泛的Oracle的HotSpotJVM。
1.JVM在JDK中的位置。
JDK是java开发的必备工具箱,JDK其中有一部分是JRE,JRE是JAVA运行环境,JVM则是JRE最核心的部分。
从最底层的位置可以看出来JVM有多重要,而实际项目中JAVA应用的性能优化,OOM等异常的处理最终都得从JVM这儿来解决。HotSpot是Oracle关于JVM的商标,区别于IBM,HP等厂商开发的JVM。JavaHotSpotClientVM和JavaHotSpotServerVM是JDK关于JVM的两种不同的实现,前者可以减少启动时间和内存占用,而后者则提供更加优秀的程序运行速度。
2.JVM的组成
JVM由4大部分组成:ClassLoader,RuntimeDataArea,ExecutionEngine,NativeInterface。
2.1.ClassLoader是负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由ExecutionEngine决定。
2.2.NativeInterface是负责调用本地接口的。他的作用是调用不同语言的接口给JAVA用,他会在NativeMethodStack中记录对应的本地方法,然后调用该方法时就通过ExecutionEngine加载对应的本地lib。原本多于用一些专业领域,如JAVA驱动,地图制作引擎等,现在关于这种本地方法接口的调用已经被类似于Socket通信,WebService等方式取代。
2.3.ExecutionEngine是执行引擎,也叫Interpreter。Class文件被加载后,会把指令和数据信息放入内存中,ExecutionEngine则负责把这些命令解释给操作系统。
2.4.RuntimeDataArea则是存放数据的,分为五部分:Stack,Heap,MethodArea,PCRegister,NativeMethodStack。几乎所有的关于java内存方面的问题,都是集中在这块。
可以看出它把MethodArea化为了Heap的一部分,基于网上的资料大部分认为MethodArea是Heap的逻辑区域,但这取决于JVM的实现者,而HotSpotJVM中把MethodArea划分为非堆内存,显然是不包含在Heap中的。下图是javacodegeeks. 常量池后面两个字节是访问标志access_flags:
值为0x,在javap中我们看到这个类的标志是
其中ACC_PUBLIC的值为0x,ACC_SUPER的值为0x,与字节码是相匹配的。
至于ClassFile的其他结构,包括this_class、super_class、接口计数器、接口等等都可以通过同样的方法进行分析,这里就不再多说了。
五、关于jvm优化
不管是YGC还是FullGC,GC过程中都会对导致程序运行中中断,正确的选择不同的GC策略,调整JVM、GC的参数,可以极大的减少由于GC工作,而导致的程序运行中断方面的问题,进而适当的提高Java程序的工作效率。但是调整GC是以个极为复杂的过程,由于各个程序具备不同的特点,如:web和GUI程序就有很大区别(Web可以适当的停顿,但GUI停顿是客户无法接受的),而且由于跑在各个机器上的配置不同(主要cup个数,内存不同),所以使用的GC种类也会不同。
1.gc策略
现在比较常用的是分代收集(generationalcollection,也是SUNVM使用的,J2SE1.2之后引入),即将内存分为几个区域,将不同生命周期的对象放在不同区域里:younggeneration,tenuredgeneration和permanetgeneration。绝大部分的objec被分配在younggeneration(生命周期短),并且大部分的object在这里die。当younggeneration满了之后,将引发minorcollection(YGC)。在minorcollection后存活的object会被移动到tenuredgeneration(生命周期比较长)。最后,tenuredgeneration满之后触发majorcollection。majorcollection(Fullgc)会触发整个heap的回收,包括回收younggeneration。permanetgeneration区域比较稳定,主要存放classloader信息。
younggeneration有eden、2个survivor区域组成。其中一个survivor区域一直是空的,是eden区域和另一个survivor区域在下一次copycollection后活着的objecy的目的地。object在survivo区域被复制直到转移到tenured区。
我们要尽量减少Fullgc的次数(tenuredgeneration一般比较大,收集的时间较长,频繁的Fullgc会导致应用的性能收到严重的影响)。
JVM(采用分代回收的策略),用较高的频率对年轻的对象(younggeneration)进行YGC,而对老对象(tenuredgeneration)较少(tenuredgeneration满了后才进行)进行FullGC。这样就不需要每次GC都将内存中所有对象都检查一遍。
GC不会在主程序运行期对PermGenSpace进行清理,所以如果你的应用中有很多CLASS(特别是动态生成类,当然permgenspace存放的内容不仅限于类)的话,就很可能出现PermGenSpace错误。
2.内存申请过程
1.JVM会为相关Java对象在Eden中初始化一块内存区域;
2.当Eden空间足够时,内存申请结束。否则到下一步;
3.JVM试图释放在Eden中所有不活跃的对象(minorcollection),释放后若Eden空间4.仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
5.Survivor区被用来作为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
6.当old区空间不够时,JVM会在old区进行majorcollection;
7.完全垃圾收集后,若Survivor及old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现"Outofmemory错误";
3.性能考虑
对于GC的性能主要有2个方面的指标:吞吐量throughput(工作时间不算gc的时间占总的时间比)和暂停pause(gc发生时app对外显示的无法响应)。
1.TotalHeap
默认情况下,vm会增加/减少heap大小以维持freespace在整个vm中占的比例,这个比例由MinHeapFreeRatio和MaxHeapFreeRatio指定。
一般而言,server端的app会有以下规则:
对vm分配尽可能多的memory;
将Xms和Xmx设为一样的值。如果虚拟机启动时设置使用的内存比较小,这个时候又需要初始化很多对象,虚拟机就必须重复地增加内存。
处理器核数增加,内存也跟着增大。
2.TheYoungGeneration
另外一个对于app流畅性运行影响的因素是younggeneration的大小。younggeneration越大,minorcollection越少;但是在固定heapsize情况下,更大的younggeneration就意味着小的tenuredgeneration,就意味着更多的majorcollection(majorcollection会引发minorcollection)。
NewRatio反映的是young和tenuredgeneration的大小比例。NewSize和MaxNewSize反映的是younggeneration大小的下限和上限,将这两个值设为一样就固定了younggeneration的大小(同Xms和Xmx设为一样)。
如果希望,SurvivorRatio也可以优化survivor的大小,不过这对于性能的影响不是很大。SurvivorRatio是eden和survior大小比例。
首先决定能分配给vm的最大的heapsize,然后设定最佳的younggeneration的大小;
如果heapsize固定后,增加younggeneration的大小意味着减小tenuredgeneration大小。让tenuredgeneration在任何时候够大,能够容纳所有live的data(留10%-20%的空余)。
4.经验总结
1.年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择).在此种情况下,年轻代收集发生的频率也是最小的.同时,减少到达年老代的对象.
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度.因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用.
避免设置过小.当新生代设置过小时会导致:1.YGC次数更加频繁2.可能导致YGC对象直接进入旧生代,如果此时旧生代满了,会触发FGC.
2.年老代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得:
a.并发垃圾收集信息、持久代并发收集次数、传统GC信息、花在年轻代和年老代回收上的时间比例。
b.吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象.
3.较小堆引起的碎片问题
因为年老代的并发收集器使用标记,清除算法,所以不会对堆进行压缩.当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象.但是,当堆空间较小时,运行一段时间以后,就会出现"碎片",如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记,清除方式进行回收.如果出现"碎片",可能需要进行如下配置:
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩.
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次FullGC后,对年老代进行压缩
4.用64位操作系统,Linux下64位的jdk比32位jdk要慢一些,但是吃得内存更多,吞吐量更大
5.XMX和XMS设置一样大,MaxPermSize和MinPermSize设置一样大,这样可以减轻伸缩堆大小带来的压力
6.使用CMS的好处是用尽量少的新生代,经验值是M-M,然后老生代利用CMS并行收集,这样能保证系统低延迟的吞吐效率。实际上cms的收集停顿时间非常的短,2G的内存,大约20-80ms的应用程序停顿时间
7.系统停顿的时候可能是GC的问题也可能是程序的问题,多用jmap和jstack查看,或者killall-3java,然后查看java控制台日志,能看出很多问题。(相关工具的使用方法将在后面的blog中介绍)
8.仔细了解自己的应用,如果用了缓存,那么年老代应该大一些,缓存的HashMap不应该无限制长,建议采用LRU算法的Map做缓存,LRUMap的最大长度也要根据实际情况设定。
9.采用并发回收时,年轻代小一点,年老代要大,因为年老大用的是并发回收,即使时间长点也不会影响其他程序继续运行,网站不会停顿
10.-Xnoclassgc禁用类垃圾回收,性能会高一点;
11.-XX:+DisableExplicitGC禁止System.gc(),免得程序员误调用gc方法影响性能
12.JVM参数的设置(特别是–Xmx–Xms–Xmn-XX:SurvivorRatio-XX:MaxTenuringThreshold等参数的设置没有一个固定的公式,需要根据PVold区实际数据YGC次数等多方面来衡量。为了避免promotionfaild可能会导致xmn设置偏小,也意味着YGC的次数会增多,处理并发访问的能力下降等问题。每个参数的调整都需要经过详细的性能测试,才能找到特定应用的最佳配置。
5.promotionfailed:
垃圾回收时promotionfailed是个很头痛的问题,一般可能是两种原因产生,第一个原因是救助空间不够,救助空间里的对象还不应该被移动到年老代,但年轻代又有很多对象需要放入救助空间;第二个原因是年老代没有足够的空间接纳来自年轻代的对象;这两种情况都会转向FullGC,网站停顿时间较长。
解决方方案一:
第一个原因我的最终解决办法是去掉救助空间,设置-XX:SurvivorRatio=-XX:MaxTenuringThreshold=0即可,第二个原因我的解决办法是设置CMSInitiatingOccupancyFraction为某个值(假设70),这样年老代空间到70%时就开始执行CMS,年老代有足够的空间接纳来自年轻代的对象。
解决方案一的改进方案:
又有改进了,上面方法不太好,因为没有用到救助空间,所以年老代容易满,CMS执行会比较频繁。我改善了一下,还是用救助空间,但是把救助空间加大,这样也不会有promotionfailed。具体操作上,32位Linux和64位Linux好像不一样,64位系统似乎只要配置MaxTenuringThreshold参数,CMS还是有暂停。为了解决暂停问题和promotionfailed问题,最后我设置-XX:SurvivorRatio=1,并把MaxTenuringThreshold去掉,这样即没有暂停又不会有promotoinfailed,而且更重要的是,年老代和永久代上升非常慢(因为好多对象到不了年老代就被回收了),所以CMS执行频率非常低,好几个小时才执行一次,这样,服务器都不用重启了。
6.CMSInitiatingOccupancyFraction值与Xmn的关系公式
上面介绍了promontionfaild产生的原因是EDEN空间不足的情况下将EDEN与Fromsurvivor中的存活对象存入Tosurvivor区时,Tosurvivor区的空间不足,再次晋升到oldgen区,而oldgen区内存也不够的情况下产生了promontionfaild从而导致fullgc.那可以推断出:eden+fromsurvivoroldgen区剩余内存时,不会出现promontionfaild的情况,即:
(Xmx-Xmn)*(1-CMSInitiatingOccupancyFraction/)=(Xmn-Xmn/(SurvivorRatior+2))进而推断出:
CMSInitiatingOccupancyFraction=((Xmx-Xmn)-(Xmn-Xmn/(SurvivorRatior+2)))/(Xmx-Xmn)*
例如:
当xmx=xmn=36SurvivorRatior=1时CMSInitiatingOccupancyFraction=((.0-36)-(36-36/(1+2)))/(-36)*=73.
当xmx=xmn=24SurvivorRatior=1时CMSInitiatingOccupancyFraction=((.0-24)-(24-24/(1+2)))/(-24)*=84.…
当xmx=xmn=SurvivorRatior=1时CMSInitiatingOccupancyFraction=((.0-)-(-/(1+2)))/(-)*=83.33
CMSInitiatingOccupancyFraction低于70%需要调整xmn或SurvivorRatior值。
对此,网上牛人们得出的公式是是:(Xmx-Xmn)*(-CMSInitiatingOccupancyFraction)/=Xmn。
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元信息,其大小跟项目的规模、类、方法的量有关,一般设置为M就足够,设置原则是预留30%的空间。
New又分为几个部分:
Eden:Eden用来存放JVM刚分配的对象
Survivor1
Survivro2:两个Survivor空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当满足某个条件,比如Copy次数,就会被Copy到Tenured。显然,Survivor只是增加了对象在年轻代中的逗留时间,增加了被垃圾回收的可能性。
2.垃圾回收算法
垃圾回收算法可以分为三类,都基于标记-清除(复制)算法:
Serial算法(单线程)
并行算法
并发算法
JVM会根据机器的硬件配置对每个内存代选择适合的回收算法,比如,如果机器多于1个核,会对年轻代选择并行算法,关于选择细节请参考JVM调优文档。
稍微解释下的是,并行算法是用多线程进行垃圾回收,回收期间会暂停程序的执行,而并发算法,也是多线程回收,但期间不停止应用执行。所以,并发算法适用于交互性高的一些程序。经过观察,并发算法会减少年轻代的大小,其实就是使用了一个大的年老代,这反过来跟并行算法相比吞吐量相对较低。
还有一个问题是,垃圾回收动作何时执行?
当年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代。需要强调的时,年轻代满是指Eden代满,Survivor满不会引发GC
当年老代满时会引发FullGC,FullGC将会同时回收年轻代、年老代
当永久代满时也会引发FullGC,会导致Class、Method元信息的卸载
另一个问题是,何时会抛出OutOfMemoryException,并不是内存被耗空的时候才抛出
JVM98%的时间都花费在内存回收
每次回收的内存小于2%
满足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以做一些Down之前的操作,比如手动打印HeapDump。
二、内存泄漏及解决方法
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。我们考虑用下面几种工具打开该文件:
VisualVM
IBMHeapAnalyzer
JDK自带的Hprof工具
使用这些工具时为了确保加载速度,建议设置最大内存为6G。使用后发现,这些工具都无法直观地观察到内存泄漏,VisualVM虽能观察到对象大小,但看不到调用堆栈;HeapAnalyzer虽然能看到调用堆栈,却无法正确打开一个3G的文件。因此,我们又选用了Eclipse专门的静态内存分析工具:Mat。
4.分析内存泄漏
通过Mat我们能清楚地看到,哪些对象被怀疑为内存泄漏,哪些对象占的空间最大及对象的调用关系。针对本案,在ThreadLocal中有很多的JbpmContext实例,经过调查是JBPM的Context没有关闭所致。
另,通过Mat或JMX我们还可以分析线程状态,可以观察到线程被阻塞在哪个对象上,从而判断系统的瓶颈。
5.回归问题
Q:为什么崩溃前垃圾回收的时间越来越长?
A:根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记、清除(复制),标记部分只要内存大小固定时间是不变的,变的是复制部分,因为每次垃圾回收都有一些回收不掉的内存,所以增加了复制量,导致时间延长。所以,垃圾回收的时间也可以作为判断内存泄漏的依据
Q:为什么FullGC的次数越来越多?
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.