本文开始只是简单介绍jvm的结构,后面把工作中的jvm调优也一起集成在一起,开始引用了两篇博文,如有越权请与我联系
1、jvm介绍与类型
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。这个就不详细描述;
现在jvm有多个版本,每个版本内部实习技术可能不相同,大家比较熟知的是 Oracle HotSpot,IBM J9、Oricale JRockit和Microsoft JVM,其中hotSpot是我们经常使用安装使用的。
2、jvm 体系结构
这儿简单介绍一下我们使用最多的jvm实现方式 (hotSpot)
下面解释来自:http://orange5458.iteye.com/blog/1913937
1)执行引擎:解析JVM字节码指令,得到执行结果。在《Java虚拟机规范》中详细地定义了执行引擎遇到每条字节码指令应该处理什么,并且应该得到什么,但是没有规定执行引擎应该如何或者采取什么方式处理而得到这个结果,而是由JVM的实现厂家去决定。
执行引擎也就是执行一条条代码的一个流程,而代码是包含在方法体内的,所以执行引擎本质就是执行一个个方法所串起来的流程,对应到OS中一个执行流程就是一个Java线程,即每个Java线程就是一个执行引擎的实例。
2)方法区:在类装载器加载class文件到内存的过程中,虚拟机会提取其中的类型信息,并将这些信息存储到方法区。方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。由于所有线程都共享方法区,因此它们对方法区数据的访问必须被设计为是线程安全的。在hotSpot里面,方法区就是相当于我们常说的永久代,但是在其他实现方法就不一定。
4)堆:存储Java程序创建的类实例。所有线程共享,因此设计程序时也要考虑到多线程访问对象(堆数据)的同步问题(堆表示所有线程共享)。
5)Java栈:Java栈是线程私有的。每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈(表明每个线程有自己的栈)。Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈或出栈。当线程调用java方法时,虚拟机压入一个新的栈帧到该线程的java栈中。当方法返回时,这个栈帧被从java栈中弹出并抛弃。一个栈帧包含一个java方法的调用状态,它存储有局部变量表、操作栈、动态链接、方法出口等信息。
6)程序寄存器:一个运行中的Java程序,每当启动一个新线程时,都会为这个新线程创建一个自己的PC(程序计数器)寄存器(表明每个线程有自己的程序寄存器)。程序计数器的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。
7)本地方法栈:本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态链接并直接调用指定的本地方法。如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。本地方法表示java虚拟机直接操作计算机硬件,执行主要依靠JVM的设计者;
3、jvm OOM可能出现的地方
由于jvm内存固定,通过上面,介绍我们可以大致分出jvmOOM出现的地方
1、堆溢出 java.lang.OutOfMemoryError: Java heap space,由于所以实现方法,对象实例都是存放在堆里面,如果对象太多,而jvm清理又不及时,那么有可能造成堆溢出;
2、永久带(方法区)溢出 java.lang.OutOfMemoryError: PermGen space,方法区主要存储这里classes和method,如果设置太小就会造成溢出。
3、栈溢出 java.lang.StackOverflowError,由于每个线程独立拥有一个栈,而一个栈帧包含一个java方法的调用状态,它存储有局部变量表、操作栈、动态链接、方法出口等信息,如果线程不断压栈可能造成栈越来越大,直至内存耗尽。可能原因可能是:
- 是否有递归调用
- 是否有大量循环或死循环
- 全局变量是否过多
- 数组、List、map数据是否过大
4、 jvm 垃圾清理方法
一、垃圾清理计算方法简介
标记-清除算法(Mark-Sweep)
从根节点开始标记所有可达对象,其余没标记的即为垃圾对象,执行清除。但回收后的空间是不连续的。
复制算法(copying)
上图中,Eden+Survivor1+Survivor2组成了新生代,然后 新生代+老年代(Tenured)组成了我们的堆内存;这里的复制算法只是适用于新生代
- 图1:jvm新增实例化对象时,将对象房贷Eden内,如果Eden满了以后,执行minor GC,将Eden数据中仍然存在调用的对象存放到S1中,不存在调用的直接清理掉;
- 图2:jvm新增对象都放在eden区中,如果到需要执行minorGC,将Eden与S1中的仍然被调用的对象放到S2中,清理掉不被调用的对象
- 图3:jvm经过多次minorGC以后,如果对象生命周期大于某个阀值(默认为15),就将对象放入老年代中,生命周期表示每经过一次minorGC以后,对象仍然存活,那么对象生命周期+1;
标记-压缩算法(Mark-compact)
适合用于老年代的算法(存活对象多于垃圾对象)。
标记后不复制,而是将存活对象压缩到内存的一端,然后清理边界外的所有对象。
二、jvm普遍的垃圾收集器
以下内容来自博文:http://blog.youkuaiyun.com/java2000_wl/article/details/8030172
下图表示jvm一般的垃圾收集器与新生代与老年代一般的搭配
Serial(串行GC)收集器
Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
ParNew(并行GC)收集器
ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。
Parallel Scavenge(并行回收GC)收集器
Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。
Serial Old(串行GC)收集器
Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。
Parallel Old(并行GC)收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
CMS(并发GC)收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器
三、CMS的详细介绍
因为CMS在使用度与高效性上面普遍很高,原博主也详细介绍了,我也就照搬过来了:
CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:
- ①.初始标记(CMS initial mark)
- ②.并发标记(CMS concurrenr mark)
- ③.重新标记(CMS remark)
④.并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,器主要有三个显著缺点:
CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。
CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%(JDK6之前默认为68%,现为92%)的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。
最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。
5、jvm 一般发生GC的条件
1. Eden区空间不足时虚拟机发起Minor GC
- 大对象直接进入老年代
-XX:PretenureSizeThreshold 大于此值的对象在老年代分配 - 长期存活对象进入老年代
–XX:MaxTenuringThreshold 晋升老年代的年龄阈值,默认15
-XX:TargetSurvivorRatio 计算期望存活大小以动态调整阈值,默认50% - Survivor区无法放置的对象进入老年代
2.以下时刻虚拟机发起Full GC
- 老年代空间不足、达到阈值、空间担保失败
- 永久代空间不足
空间担保定义:在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的,就是空间担保成立;
6、jvm一般调优的阀值
了解jvm的大概原理与过程以后 我们以一个项目来作为调优目标:
1、在jvm后面添加打印GC日志命令,-XX:+PrintGCDetails -XX:+PrintGCDateStamps –Xloggc:,我们截取一次GC后的数据打印:
2015-05-21T20:01:00.911+0800: 6845.299:
[GC 6845.300:
[ParNew :1497773K->31926K(1523712K), 0.0219710 secs]
3615834K->2149987K(4145152K), 0.0229380 secs]
[Times: user=0.12 sys=0.00, real=0.02 secs]
ParNew表示是执行年轻代的GC我们看到它GC以后由1.5G变为32M 新生代所以空间大小为1.5G
年老代由3.6G变为了2.1G,年老代最大内存为4G左右;
2、我们通过手动触发Full GC jmap –histo:live
等程序运行一定时间以后,我们的新生代,年老代都已经比较平稳,执行fullGC比较有代表性
2015-05-21T20:02:23.233+0800: 6927.621:
[Full GC 6927.621:
[2121506K->1724611K(2621440K), 4.7489140 secs]
2146572K->1724611K(4145152K),
[CMS Perm :145073K->138076K(262144K)], 4.7496820 secs]
3、通过我们的通用法则,设置我们系统一般的新生代,老年代:
这个通用法则可能会根据不同应用特性变动,一般的项目是可以根据通用来进行调优
- 老年代不应小于FullGC后老年代空间占用量的1.5倍
- 将-XX:PermSize及-XX:MaxPermSize设置为1.2-1.5倍FullGC后的永久代空间占用量
- 将-Xmn设置为1-1.5倍FullGC后的老年代空间占用量
- 视应用情况设置-Xss及预留直接内存等空间
所以我们将系统配置为:
-Xms4096m -Xmx4096m -XX:PermSize=256m -XX:MaxPermSize=256m
-XX:NewSize=1536m -XX:MaxNewSize=1536m(或写为-Xmn1536m)
其中老年代的大小为: -Xmx4096m - -XX:NewSize=1536m = 2560M
4、后续调优流程
- 迭代优化新生代及老年代大小以降低minor GC频率
- 只调整自身大小,其它大小不变
- Survivor区调优使临时对象不提升至老年代
- Survivor区调优使临时对象不提升至老年代
- XX:+UseConcMarkSweepGC(需要显示使用CMS垃圾收集器)
- 避免碎片化,根据应用特点调整CMS参数
5、其他jvm介绍
Parallel Scavenge收集器参数
- -XX:-UseAdaptiveSizePolicy 关闭动态调整
- -XX:MaxGCPauseMillis与–XX:GCTimeRatio控制吞吐量
常用命令
- jstat 监视虚拟机统计信息:–gc –class -compiler
- jmap 内存映像工具:-dump –heap -histo
- jstack 堆栈跟踪工具