管中窥豹——从对象的生命周期梳理JVM内存结构、GC、类加载、AOP编程及性能监控

如题,本文的宗旨既是透过对象的生命周期,来梳理JVM内存结构及GC相关知识,并辅以AOP及双亲委派机制原理,学习不只仅是海绵式的吸取学习,还须要本身去分析why,加深对技术的理解和认知,祝你们早日走上本身的“成金之路”。html


Java对象的建立

本部分,从攻城狮编写.java文件入手,详解了编译、载入、AOP原理。
读过《程序员的自我修养》的朋友,对程序的编译及执行会有一个很清晰的认识:编译其实就是将人类能理解的代码文件转译为机器/CPU能执行的文件(包括数据段、代码段),而执行的过程,则是根据文件头部字节的标识(简称魔数),映射为对应的文件结构体,找到程序入口,当获取到CPU执行权限时,将方法压栈,执行对应的指令码,完成相应的逻辑操做。
而对应.java文件,则先须要使用javac进行编译,编译后的.class文件,此文件将java程序能读懂的数据段和代码段,以后用java执行文件,既是载入.class文件,找到程序入口,并根据要执行的方法,不停的压栈、出栈,进行逻辑处理。java

class文件载入过程

加载

在加载阶段,虚拟机须要完成如下三件事情:linux

  • 经过一个类的全限定名来获取其定义的二进制字节流。
  • 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
  • 在Java堆中生成一个表明这个类的java.lang.Class对象,做为对方法区中这些数据的访问入口。

即至关于在内存中将代码段和数据段关联起来,组织好Class对象的内存空间,做为对象成员和方法的引入入口,并将.class及方法载入Perm内存区。相对于类加载的其余阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动做)是可控性最强的阶段,由于开发人员既可使用系统提供的类加载器来完成加载,也能够自定义本身的类加载器来完成加载。程序员

链接

分为四个阶段:算法

  • 第一阶段是验证,确保被加载的类的正确性。

这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。验证阶段大体会完成4个阶段的检验动做:
文件格式验证验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围以内、常量池中的常量是否有不被支持的类型。
元数据验证对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object以外。
字节码验证经过数据流和控制流分析,肯定程序语义是合法的、符合逻辑的。
符号引用验证确保解析动做能正确执行。spring

验证阶段是很是重要的,但不是必须的,它对程序运行期没有影响,若是所引用的类通过反复验证,那么能够考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。数据库

  • 第二阶段是准备:为类的静态变量分配内存,并将其初始化为默认值

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有如下几点须要注意:
一、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。编程

二、这里所设置的初始值一般状况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。如public static int value = 3;那么变量value在准备阶段事后的初始值为0,而不是3,由于这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器 ()方法之中的,因此把value赋值为3的动做将在 初始化阶段才会执行。这里还须要注意以下几点:
i.对基本数据类型来讲,对于类变量(static)和全局变量,若是不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于 局部变量来讲,在使用前必须显式地为其赋值,不然编译时不经过。
ii.对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,不然编译时不经过;而只被final修饰的常量则既能够在声明时显式地为其赋值,也能够在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值, 系统不会为final修饰的常量赋予默认零值
iii.对于 引用数据类型 reference来讲,如数组引用、对象引用等,若是没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
iv.若是在 数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
数组

三、若是类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。如public static final int value = 3;编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。浏览器

  • 第三阶段是解析:把类中的符号引用转换为直接引用

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动做主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,能够是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

这个阶段的主要目的将编译后的虚拟地址(相似动态库,库数据段都是0x00开始,载入内存后须要与实际分配的地址关联起来)与实际运行的地址关联起来。

  • 第四阶段是初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化,包括给声明类变量指定初始值,和为类static变量指定静态代码块地址。

一、假如这个类尚未被加载和链接,则程序先加载并链接该类
二、假如该类的直接父类尚未被初始化,则先初始化其直接父类
三、假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会致使类的初始化,类的主动使用包括如下六种:
– 建立类的实例,也就是new的方式
– 访问某个类或接口的静态变量,或者对该静态变量赋值
– 调用类的静态方法
– 反射(如Class.forName(“com.xxx.Test”))
– 初始化某个类的子类,则其父类也会被初始化
– Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类。

拓展:双亲委派机制与AOP面向切面编程原理

JVM最底层的载入是BootstrapClassLoader,为C语言编写,从Java中引用不到,其上是ExtClassLoader,而后是AppClassLoader,普通类中getContextClassLoader()获得的是AppClassLoader,getContextClassLoader().getParent()获得的是ExtClassLoader,而再往上getParent()为null,即引用不到BootstrapLoader。

启动类加载器:BootstrapClassLoader,负责加载存放在JDK\jre\lib(JDK表明JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,而且能被虚拟机识别的类库(如rt.jar,全部的java.*开头的类均被BootstrapClassLoader加载)。启动类加载器是没法被Java程序直接引用的。

扩展类加载器:ExtensionClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的全部类库(如javax.*开头的类),开发者能够直接使用扩展类加载器。

应用程序类加载器:ApplicationClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者能够直接使用该类加载器,若是应用程序中没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,若是有必要,咱们还能够加入自定义的类加载器。由于JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,所以若是编写了本身的ClassLoader,即可以作到以下几点:
1)在执行非置信代码以前,自动验证数字签名。
2)动态地建立符合用户特定须要的定制化构建类。
3)从特定的场所取得java class,例如数据库中和网络中。

双亲委派机制
简单来讲既是先拿父类加载器加载class,父类加载失败后再使用本类加载器加载。当AppClassLoader加载一个class时,它首先不会本身去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。当ExtClassLoader加载一个class时,它首先也不会本身去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。若是BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,若是AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

其它相关点
1.全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其余Class也将由该类加载器负责载入,除非显示使用另一个类加载器来载入。
2.父类委托,先让父类加载器试图加载该类,只有在父类加载器没法加载该类时,才使用本类加载器从本身的类路径中加载该类。
3.缓存机制,缓存机制将会保证全部加载过的Class都会被缓存,当程序中须要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为何修改了Class后,必须重启JVM,程序的修改才会生效。

--

AOP面向切面编程
AOP 专门用于处理系统中分布于各个模块(不一样方法)中的交叉关注点的问题,在 Java EE 应用中,经常经过AOP来处理一些具备横切性质的系统级服务,如事务管理、安全检查、缓存、对象池管理等,在不改变已有代码的状况下,静态/动态的插入代码。

将AOP放到这里的主要缘由是由于AOP改变的class文件,达到嵌入方法的目的,静态模式使用AspectJ进行由.java到.class文件编译。而动态模式时使用CGLIB载入使用javac编译的.class文件后,使用动态代理的方式,将要执行的方法嵌入到原有class方法中,完成在内存中对class对象的构造,这也就是所谓动态代理技术的内在原理。同时静态方式在载入前已经修好完.class文件,而动态方式在.class载入时须要作额外的处理,致使性能受到必定影响,但其优点是无须使用额外的编译器。整体的技术的切入点在于在修改机器执行码,达到增长执行方法的目的。参考我。

对象的内存分配和回收

对象建立及方法调用

数据分类:基本类型与引用类型

基本类型包括:byte,short,int,long,char,float,double,boolean,returnAddress
引用类型包括:类类型,接口类型和数组。

数据存储:使用堆存储对象信息

方法调用:使用栈来解决方法嵌套调用,而栈内部由一个个栈帧构成,调用一个方法时,在当前栈上压入一个栈帧,此栈帧包含局部变量表,操做栈等子项,每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。表面上代码在运行时,是经过程序计数器不断执行下一条指令;而实际指令运算等操做是经过控制操做栈的操做数入栈和出栈,将操做数在局部变量表和操做栈之间转移。参见我

这里对内存的分配作个深刻的扩展,解释下基础类型的自动装箱 boxing
以"Object obj = new Object();"为例,一个空Object对象占用8byte空间,而obj引用占用4byte,此条语句执行后共占用12byte;而Java中对象大小是8的整数倍,则Boolean b = new Boolean(true)至少须要20byte(16byte+4byte),而若是直接使用基本数据类型boolean b = true则仅仅须要1byte,在栈帧中存储;为优化此问题,JVM提出了基本类型的自动装载技术,来自动化进行基本类型与基本类型对象间的转换,来下降内存的使用量。

内存集中管理(模型及GC机制)

  • JVM内存模型图以下

内存结构主要有三大块:堆内存、方法区和栈。
1.堆内存是JVM中最大的一块由Young Generation(年轻代、新生代)和Old Generation(年老代)组成,而Young Generation内存又被分红三部分,Eden空间、From Survivor空间、To Survivor空间,默认状况下年轻代按照8:1:1的比例来分配。
2.方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆)。
3.又分为java虚拟机栈(方法执行的内存区,每一个方法执行时会在虚拟机栈中建立栈帧)和本地方法栈(虚拟机的Native方法执行的内存区)主要用于方法的执行。
4.内存设置参数:
-Xms设置堆的最小空间大小。
-Xmx设置堆的最大空间大小。
-XX:NewSize设置新生代最小空间大小。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:PermSize设置永久代最小空间大小。
-XX:MaxPermSize设置永久代最大空间大小。
-Xss设置每一个线程的堆栈大小。
-XX:SurvivorRatio=x #Eden区与Survivor区的大小比值,默认为8(Eden:From-Survivor=8:1)
-XX:NewRatio=x #年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)默认值为2,即年轻代:年老代=1:2(这里与数学的比值有差别)

  • Minor GC

对象分配先从Eden空间与From Survivor空间获取内存,当二者中空间不足时,进行Minor GC,将Eden与From Survivor空间存活的对象,Copy到To Survivor空间,而后清空Eden与From Survivor空间,以后将To Survivor空间对象年龄加1,并将To Survivor空间设置为From Survivor空间,保证Minor GC时To Survivor空间始终为空;而当对象年龄为15后(默认是 15,能够经过参数-XX:MaxTenuringThreshold 来设定),将存活对象放入老年代。

例外状况
1.对于一些较大的对象(即须要分配一块较大的连续内存空间)则是直接进入到老年代。虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。避免在新生代采用复制算法收集内存时,在Eden区及两个Survivor区之间发生大量的内存复制。
2.为了更好的适应不一样的程序,虚拟机并非永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,若是在Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象能够直接进入Old Generation,无需等到MaxTenuringThreshold中要求的年龄。

这里的疑问,按照默认的8:1:1设置,一个To Survivor空间占10%空间,每次Minor GC能保证To Survivor空间够用吗?IBM的研究代表,98%的对象都是很快消亡的,大部分的对象在建立后很快就再也不使用。这里能够根据GC detail来查看和分析比例设置是否合理。

  • Full GC

工做:同时回收年轻代、年老代,按照配置的不一样算法进行回收。
时机:在Minor GC触发时,会检测以前每次晋升到老年代的平均大小是否大于老年代的剩余空间,若是大于,改成直接进行一次Full GC;若是小于则查看HandlePromotionFailure设置(是否容许担保,使用Old Gerneration空间担保),若是容许,那仍然进行Minor GC,若是不容许,则也要改成进行一次Full GC。
取平均值进行比较其实仍然是一种动态几率的手段,也就是说若是某次Minor GC存活后的对象突增,大大高于平均值的话,依然会致使担保失败,这样就只好在失败后从新进行一次Full GC。

回收算法

首先判断对象是否存活,通常有两种方式:

  • 引用计数:每一个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时能够回收。此方法简单,没法解决对象相互循环引用的问题。
  • 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证实此对象是不可用的、不可达对象。

在Java语言中,GC Roots包括:
--虚拟机栈中引用的对象。
--方法区中类静态属性实体引用的对象。
--方法区中常量引用的对象。
--本地方法栈中JNI引用的对象。

标记-清除算法:首先标记出全部须要回收的对象,在标记完成后统一回收掉全部被标记的对象。之因此说它是最基础的收集算法,是由于后续的收集算法都是基于这种思路并对其缺点进行改进而获得的。
缺点:一个是效率问题,标记和清除过程的效率都不高;另一个是空间问题,标记清除以后会产生大量不连续的内存碎片,空间碎片太多可能会致使,当程序在之后的运行过程当中须要分配较大对象时没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。

复制算法:对空间问题的改进,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。
缺点是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则致使效率下降。若是不想浪费50%的空间,就须要有额外的空间进行分配担保(HandlePromotionFailure设置为true),以应对被使用的内存中全部对象都100%存活的极端状况,因此在老年代通常不能直接选用这种算法。

标记-压缩/整理算法:对复制算法在老年代上的改进,标记过程仍然与“标记-清除”算法同样,但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存。

分代收集算法:把Java堆分为新生代和老年代,这样就能够根据各个年代的特色采用最适当的收集算法。
新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集。
老年代中,由于对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

回收器

新生代收集器

  • Serial收集器

串行收集器是最古老,最稳定以及效率高的收集器,使用中止复制方法,只使用一个线程去串行回收;垃圾收集的过程当中会Stop The World(服务暂停);
参数控制:使用-XX:+UseSerialGC可使用Serial+Serial Old模式运行进行内存回收(这也是虚拟机在Client模式下运行的默认值)
缺点是串行效率较低

  • ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,使用中止复制方法。新生代并行,其它工做线程暂停。
参数控制:使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。

  • Parallel收集器

Parallel Scavenge收集器相似ParNew收集器,Parallel收集器更关注CPU吞吐量,即运行用户代码的时间/总时间,使用中止复制算法。能够经过参数来打开自适应调节策略,虚拟机会根据当前系统的运行状况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也能够经过参数控制GC的时间不大于多少毫秒或者比例。

参数控制:使用-XX:+UseParallelGC开关控制使用Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效),用开关参数-XX:+UseAdaptiveSizePolicy能够进行动态控制,如自动调整Eden/Survivor比例,老年代对象年龄,新生代大小等,这个参数在ParNew下没有。

老年代收集器

  • Serial Old收集器:老年代收集器,单线程收集器,串行,使用"标记-整理"算法(整理的方法是Sweep(清理)和Compact(压缩),

  • Parallel Old 收集器

多线程机制与Parallel Scavenge差不错,使用标记整理(与Serial Old不一样,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然须要暂停其它线程。Parallel Old在多核计算中颇有用。这个收集器是在JDK 1.6中,与Parallel Scavenge配合有很好的效果。

参数控制: 使用-XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。

  • CMS收集器:老年代收集器(新生代使用ParNew)

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤为重视服务的响应速度,但愿系统停顿时间最短,以给用户带来较好的体验。
从名字(包含“Mark Sweep”)上就能够看出CMS收集器是基于“标记-清除”算法实现的,它的运做过程相对于前面几种收集器来讲要更复杂一些,整个过程分为6个步骤,其中初始标记从新标记这两个步骤仍然须要“Stop The World”,包括:

  1. 初始标记(CMS-initial-mark):为了收集应用程序的对象引用须要暂停应用程序线程,该阶段完成后,应用程序线程再次启动。
  2. 并发标记(CMS-concurrent-mark):从第一阶段收集到的对象引用开始,遍历全部其余的对象引用,此阶段会打印2条日志:CMS-concurrent-mark-start,CMS-concurrent-mark。
  3. 并发预清理(CMS-concurrent-preclean):改变当运行第二阶段时,由应用程序线程产生的对象引用,以更新第二阶段的结果。 此阶段会打印2条日志:CMS-concurrent-preclean-start,CMS-concurrent-preclean。
  4. 下一阶段是CMS-concurrent-abortable-preclean阶段,加入此阶段的目的是使cms gc更加可控一些,做用也是执行一些预清理,以减小Rescan阶段形成应用暂停的时间.
    经过两个参数来来控制是否进行下一阶段:
    -XX:CMSScheduleRemarkEdenSizeThreshold(默认2M):即当eden使用小于此值时;
    -XX:CMSScheduleRemarkEdenPenetratio(默认50%):当Eden区占用比例此比例时
    在concurrent preclean阶段以后,若是Eden占用率高于CMSScheduleRemarkEdenSizeThreshold,开启'concurrent abortable preclean',而且持续的precleanig直到Eden占比超过CMSScheduleRemarkEdenPenetratio,以后,开启remark阶段,参考1参考2
    另外,-XX:CMSMaxAbortablePrecleanTime:当abortable-preclean阶段执行达到这个时间时会结束进入下一阶段。
  5. 重标记CMS-concurrent-remark:因为上面三阶段是并发的,对象引用可能会发生进一步改变。所以,应用程序线程会再一次被暂停以更新这些变化,而且在进行实际的清理以前确保一个正确的对象引用视图。这一阶段十分重要,由于必须避免收集到仍被引用的对象。
  6. 并发清理CMS-concurrent-sweep:全部再也不被应用的对象将从堆里清除掉。
  7. 并发重置CMS-concurrent-reset:收集器作一些收尾的工做,以便下一次GC周期能有一个干净的状态。
    尽管CMS收集器为老年代垃圾回收提供了几乎彻底并发的解决方案,然而年轻代仍然经过“stop-the-world”方法来进行收集。对于交互式应用,停顿也是可接受的,背后的原理是年轻带的垃圾回收时间一般是至关短的。

优势:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会下降吞吐量

  • 堆碎片:CMS收集器并无任何碎片整理的机制,可能出现总的堆大小远没有耗尽,但由于没有足够连续的空间却不能分配对象,只能触发Full GC来解决,形成应用停顿。
  • 对象分配率高:若是获取对象实例的频率高于收集器清除堆里死对象的频率,并发算法将再次失败,从某种程度上说,老年代将没有足够的可用空间来容纳一个从年轻代提高过来的对象。常常被证明是老年代有大量没必要要的对象。一个可行的办法就是增长年轻代的堆大小,以防止年轻代短生命的对象提早进入老年代。另外一个办法就彷佛利用分析器,快照运行系统的堆转储,而且分析过分的对象分配,找出这些对象,最终减小这些对象的申请。

参数控制

-XX:+UseConcMarkSweepGC 使用CMS收集器
当使用-XX:+UseConcMarkSweepGC时,-XX:UseParNewGC会自动开启。所以,若是年轻代的并行GC不想开启,能够经过设置-XX:-UseParNewGC来关掉
-XX:+CMSClassUnloadingEnabled相对于并行收集器,CMS收集器默认不会对永久代进行垃圾回收。
-XX:+CMSConcurrentMTEnabled当该标志被启用时,并发的CMS阶段将以多线程执行(所以,多个GC线程会与全部的应用程序线程并行工做)。该标志已经默认开启,若是顺序执行更好,这取决于所使用的硬件,多线程执行能够经过-XX:-CMSConcurremntMTEnabled禁用(注意是-号)。
-XX:+UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引发停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几回Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量(通常状况约等于可用CPU数量)
-XX:CMSMaxAbortablePrecleanTime:当abortable-preclean阶段执行达到这个时间时才会结束

-XX:CMSInitiatingOccupancyFraction,-XX:+UseCMSInitiatingOccupancyOnly来决定什么时间开始垃圾收集;若是设置了-XX:+UseCMSInitiatingOccupancyOnly,那么只有当old代占用确实达到了-XX:CMSInitiatingOccupancyFraction参数所设定的比例时才会触发cms gc;若是没有设置-XX:+UseCMSInitiatingOccupancyOnly,那么系统会根据统计数据自行决定何时触发cms gc;所以有时会遇到设置了80%比例才cms gc,可是50%时就已经触发了,就是由于这个参数没有设置的缘由.

G1收集器

G1 GC是Jdk7的新特性之1、Jdk7+版本均可以自主配置G1做为JVM GC选项;做为JVM GC算法的一次重大升级、DK7u后G1已相对稳定、且将来计划替代CMS。
不一样于其余的分代回收算法、G1将堆空间划分红了互相独立的区块。每块区域既有可能属于O区、也有多是Y区,且每类区域空间能够是不连续的(对比CMS的O区和Y区都必须是连续的)。区别以下:

  1. G1在压缩空间方面有优点
  1. G1经过将内存空间分红区域(Region)的方式避免内存碎片问题
  2. Eden, Survivor, Old区再也不固定、在内存使用效率上来讲更灵活
  3. G1能够经过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
  4. G1在回收内存后会立刻同时作合并空闲内存的工做、而CMS默认是在STW(stop the world)的时候作
  5. G1会在Young GC中使用、而CMS只能在O区使用

就目前而言、CMS仍是默认首选的GC策略、可能在如下场景下G1更适合:

  1. 大量内存的系统提供一个保证GC低延迟的解决方案,也就是说堆内存在6GB及以上,稳定和可预测的暂停时间小于0.5秒。
  1. Full GC 次数太频繁或者消耗时间太长
  2. 应用在运行过程当中会产生大量内存碎片、须要常常压缩空间
  3. 想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象
  4. 对象分配的频率或代数提高(promotion)显著变化
  5. 受够了太长的垃圾回收或内存整理时间(超过0.5~1秒)
    注意: 若是正在使用CMS或ParallelOldGC,而应用程序的垃圾收集停顿时间并不长,那么建议继续使用如今的垃圾收集器。使用最新的JDK时,并不要求切换到G1收集器。
  • 内存模型:

G1堆由多个区(region)组成,每一个区大小1M~32M,逻辑上区有3种类型,包括(Eden、Survivor、Old),按分代划分包括:年轻代(Young Generation)和老年代(Old Generation)。

若是从 ParallelOldGC 或者 CMS收集器迁移到 G1,可能会看到JVM进程占用更多的内存(a larger JVM process size)。 这在很大程度上与 “accounting” 数据结构有关,如 Remembered Sets 和 Collection Sets。
Remembered Sets 简称 RSets。跟踪指向某个heap区内的对象引用。 堆内存中的每一个区都有一个 RSet。 RSet 使heap区能并行独立地进行垃圾集合。 RSets的整体影响小于5%。
Collection Sets 简称 CSets。收集集合, 在一次GC中将执行垃圾回收的heap区。GC时在CSet中的全部存活数据(live data)都会被转移(复制/移动)。集合中的heap区能够是 Eden, survivor, 和/或 old generation。CSets所占用的JVM内存小于1%。

  • Young GC

Young GC是stop-the-world活动,会致使整个应用线程的中止。其过程以下:

  1. 新对象进入Eden区
  2. 存活对象拷贝到Survivor区;
  3. 存活时间达到年龄阈值时,对象晋升到Old区。
    young gc是多线程并行执行的。
  • Old GC阶段描述为:
  1. 初始化标记(stop_the_world事件):这是一个stop_the_world的过程,是随着年轻代GC作的,标记survivor区域(根区域),这些区域可能含有对老年代对象的引用。
  1. 根区域扫描:扫描survivor区域中对老年代的引用,这个过程和应用程序一块儿执行的,这个阶段必须在年轻代GC发生以前完成。
  2. 并发标记:查找整个堆中存活的对象,这也是和应用程序一块儿执行的。这个阶段能够被年轻代的垃圾收集打断。
  3. 从新标记(stop-the-world事件):完成堆内存活对象的标记。使用了一个叫开始前快照snapshot-at-the-beginning (SATB)的算法,这个会比CMS collector使用的算法快。
  4. 清理(stop-the-world事件,而且是并发的):对存活的对象和彻底空的区域进行统计(stop-the-world)、刷新Remembered Sets(stop-the-world)、重置空的区域,把他们放到free列表(并发)(译者注:大致意思就是统计下哪些区域又空了,能够拿去从新分配了)
  5. 复制(stop-the-world事件):这个stop-the-world的阶段是来移动和复制存活对象到一个未被使用的区域,这个能够是年轻代区域,打日志的话就标记为 [GC pause (young)]。或者老年代和年轻代都用到了,打日志就会标记为[GC Pause (mixed)]。

参考1参考2

  • 参数分析

-XX:+UseG1GC使用G1 GC
-XX:MaxGCPauseMillis=n设置一个暂停时间指望目标,这是一个软目标,JVM会近可能的保证这个目标
-XX:InitiatingHeapOccupancyPercent=n内存占用达到整个堆百分之多少的时候开启一个GC周期,G1 GC会根据整个栈的占用,而不是某个代的占用状况去触发一个并发GC周期,0表示一直在GC,默认值是45
-XX:NewRatio=n年轻代和老年代大小的比例,默认是2
-XX:SurvivorRatio=n eden和survivor区域空间大小的比例,默认是8
-XX:MaxTenuringThreshold=n晋升的阈值,默认是15(译者注:一个存活对象经历多少次GC周期以后晋升到老年代)
-XX:ParallelGCThreads=n GC在并行处理阶段试验多少个线程,默认值和平台有关。(译者注:和程序一块儿跑的时候,使用多少个线程)
-XX:ConcGCThreads=n并发收集的时候使用多少个线程,默认值和平台有关。(译者注:stop-the-world的时候,并发处理的时候使用多少个线程)
-XX:G1ReservePercent=n预留多少内存,防止晋升失败的状况,默认值是10
-XX:G1HeapRegionSize=n G1 GC的堆内存会分割成均匀大小的区域,这个值设置每一个划分区域的大小,这个值的默认值是根据堆的大小决定的。最小值是1Mb,最大值是32Mb

转载请注明出处,百度搜“成金之路”。

调优

命令行工具(以Linux场景进行详解)

  • GC监控:

./jstat -gc pid 3s对pid GC每隔3s进行监控

  • 谁动了个人CPU
  1. top查看CPU使用状况,或经过CPU使用率收集,找到CPU占用率高Java进程,假设其进程编号为pid;
  2. 使用top -Hp pid(或ps -Lfp pid或者ps -mp pid -o THREAD,tid,time)查看pid进程中占用CPU较高的线程,假设其编号为tid;
  3. 使用Linux命令,将tid转换为16进制数,printf '%0x\n' tid,假设获得值txid;
  4. 使用jstack pid | grep txid查看线程CPU占用代码,而后根据获得的对象信息,去追踪代码,定位问题。
  • 死锁

另外,jstack -l long listings,会打印出额外的锁信息,在发生死锁时能够用jstack -l pid来观察锁持有状况。

  • Memory
  1. 查看java 堆(heap)使用状况jmap -J-d64 -heap pid,其中-J-d64为64位机器标识,启动
  2. 查看堆内存(histogram)中的对象数量及大小jmap -J-d64 -histo pid;而查看存活对象-histo:live,这个命令执行,JVM会先触发gc,而后再统计信息。
    若是有大量对象在持续被引用,并无被释放掉,那就产生了内存泄露,就要结合代码,把不用的对象释放掉。
    其中class name中B 表明 byte,C 表明 char,D 表明 double,F 表明 float,I 表明 int,J 表明 long,Z 表明 boolean,前边有 [ 表明数组, [I 就至关于 int[],对象用 [L+ 类名表示
  3. 程序内存不足或者频繁GC,极可能存在内存泄露将内存使用的详细状况输出到文件,执行命令:jmap -J-d64 -dump:format=b,file=heapDump pid;而后用jhat命令能够参看 jhat -port 5000 heapDump 在浏览器中访问:http://localhost:5000/ 查看详细信息

--

内存泄漏OOM,一般作法:
方法1. 首先配置JVM启动参数,让JVM在遇到OutOfMemoryError时自动生成Dump文件-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path
方法2. 使用上面第3步进行进行分析;
方法3. 使用eclipse的MAT分析工具对dump文件进行分析

  1. 发现问题
  • 使用uptime命令查看CPU的Load状况,Load越高说明问题越严重;
  • 使用jstat查看FGC发生的频率及FGC所花费的时间,FGC发生的频率越快、花费的时间越高,问题越严重;
  1. 使用jmap -head 观察老年代大小,当快达到配置的阀值CMSInitiatingOccupancyFraction时,将对象导出进行分析;
  2. 对head中对象进行命令行排序分析:./jmap -J-d64 -histo pid | grep java | sort -k 3 -n -r | more按照bytes大小进行排序;数量最多排序将-k 3换成-k 2便可

图形化工具jvisualvm.exe

通常结合JMX分析,分析远程tomcat状态

  • tomcat配置

修改远程tomcat的catalina.sh配置文件,在其中增长(不走权限校验。只是打开jmx端口):
JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=192.168.122.128 -Dcom.sun.management.jmxremote.port=18999 -Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false"

--

若是链接的是公网上的Tomcat,那么就要注意安全性了,接下来看看使用用户名和密码链接
JAVA_OPTS='-Xms128m -Xmx256m -XX:MaxPermSize=128m -Djava.rmi.server.hostname=10.10.23.10
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=true
-Dcom.sun.management.jmxremote.password.file=/path/to/passwd/jmxremote.password
-Dcom.sun.management.jmxremote.access.file=/path/to/passwd/jmxremote.access'

如下分别编辑jmxremote.password与jmxremote.access两个文件
jmxremote.password

monitorRole 123456
controlRole 123456789

jmxremote.access

monitorRole readonly
controlRole readwrite

完成这两个文件后修改jmxremote.password的权限chmod 600 jmxremote.password

  • 工具使用
    配置后重启tomcat,而后从本机上使用jvisualvm.exe,按照配置输入远程地址、用户名密码,便可进行监控,包括CPU、Head、Thread、Objects等分析

优化策略使用

本质上是减小Full GC的次数(Minor GC很快基本不会有影响)

  • 第一个问题既是目前年轻代、年老代分别使用什么收集器;
  • 针对不一样的收集器,进行优化:串行改并行、针对目前的Head对象状况,根据实际场景设置SurvivorRatio与NewRatio;
  • 根据对象回收状况,判断是否须要压缩,减少碎片化。

若是是频繁建立对象的应用,能够适当增长新生代大小。常量较多能够增长持久代大小。对于单例较多的对象能够增长老生代大小。好比spring应用中。

GC选择,在JDK5.0之后,JVM会根据当前系统配置进行判断。通常执行-Server命令即可以。gc包括三种策略:串行,并行,并发(使用CMS收集器老年代)。

吞吐量大的应用,通常采用并行收集,开启多个线程,加快gc的速率。
响应速度高的应用,通常采用并发收集,好比应用服务器。
年老代建议配置为并发收集器,因为并发收集器不会压缩和整理磁盘碎片,所以建议配置:
-XX:+UseConcMarkSweepGC #并发收集年老代
-XX:CMSInitiatingOccupancyFraction=80 # 表示年老代空间到80%时就开始执行CMS
-XX:+UseCMSInitiatingOccupancyOnly #是阀值生效
-XX:+UseCMSCompactAtFullCollection#打开对年老代的压缩。可能会影响性能,可是能够消除内存碎片。
-XX:CMSFullGCsBeforeCompaction=10 # 因为并发收集器不对内存空间进行压缩、整理,因此运行一段时间之后会产生“碎片”,使得运行效率下降。此参数设置运行次FullGC之后对内存空间进行压缩、整理。

若是以为不错,还请不吝推荐,您的承认是我分享的动力,同时也但愿帮助更多的人走上本身“成金之路”。谢谢!转载请注明出处,百度搜“成金之路”。

相关文章
相关标签/搜索