我们看中的并非Java语言,而是JVM。
——Java之父James Gosling接受著名IT网站eWEEK高级编辑的采访时如是说
Java之所以能够崛起,JVM功不可没。Java虚拟机最初服务于让Java语言凌驾于平台之上,实现“一次编写,到处运行”;而随着时间的推移,JVM经过不同公司和团体以不同方式的实现(如IBM的J9、 BEA公司的JRockit,本文档主要介绍SUN官方的虚拟机HotSpot),逐渐有更多Java以外的语言登上了JVM这条船,如Groovy、JRuby、Scala、Jython等,而且本人经常会去中关村图书大厦“溜达”,发现被人们一致看好的Scala相关的书越来越多,也许Scala真的会有不错的明天。这里不对以上编程语言加以置评,旨在于对JVM的介绍。
初识JVM体系结构
Java虚拟机之所以被称之为是“虚拟的”,就是因为它仅仅是由一个规范来定义的抽象计算机,因此要运行某个Java程序,必须需要一个符合该规范的具体实现。
我们经常听说Java虚拟机,其实这只是侠义上的理解。JVM可能指的是以下三种不同的概念:
1、 虚拟机规范
2、 一个具体实现
3、 一个运行中的虚拟机实例
如图是Sun HotSpot的虚拟机实现的体系结构,它分为类装载子系统、运行时数据区、执行引擎以及本地方法接口,接下来一一介绍。
类装载子系统
装载器把一个类装入JVM中要要经过三个步骤来完成
1. 装载:查找和装入类或接口的二进制数据。
2. 连接:执行以下三步,其中解析是可选的
1) 验证:检验装入类或接口的二进制数据的正确性。
2) 准备:为静态变量分配存储空间。
3) 解析:将常量池内的符号引用替换为直接引用。
3. 初始化:激活类的静态变量和静态Java代码块。
JVM内置了三个默认的装载器:
BootstrapLoader是由C/C++实现,我们无法在程序中获取它的实例,这个装载器负责装载lib目录下的dt.jar、tools.jar等Java核心核心类库。
ExtClassLoader
这个装载器负责装载jdk/lib/ext目录下的jar包。
AppClassLoader
这个装载器主要负责装载classpath目录下的类。
这三个装载器存在层级关系:ExtClassloader为AppClassLoader的父装载器, BootstrapLoader为ExtClassLoader的父装载器。类的装载遵循“双亲委派”模式,如果AppClassLoader被请求装载一个类,它首先会去询问ExtClassLoader是否已经装载,如果已经装载,则返回其对象;如果尚未装载,会继续询问BootstrapLoader,也就是说BootstrapLoader拥有最高的优先级。
为了进一步探索JVM的classloader机制,写了如下两个Demo:
e.g 1
/***
*
*@authorJoher
*
*/
publicclass ClassLoaderTest {
/**
*@paramargs
*/
publicstaticvoid main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}
结果输出:
示例说明我们无法直接获取BootstrapLoader实例。
e.g 2
package java.lang;
/**
*
*@authorJoher
*
*/
publicclass String {
publicstaticvoid main(String[] args) {
int a = 1;
int b = 2;
System.out.println(a + b);
}
}
这个小程序会输出什么?
答案是这个小程序根本不能运行!
运行其会出现如图:
提示说:发生了致命异常,程序将退出。WHY?
如果你够敏感,肯定会注意到次类名比较特殊,这是因为次类名与JVM核心类库的java.lang.String类发生了冲突,JVM会认为此同名的类是“恶意”的,所以导致程序直接退出。
好吧,为了能使其运行,把类名改成StringTest,这个肯定没有重复的,再次运行会是什么结果?
运行抛出一个安全异常,如图:
为寻其因,于是溯源而上,根据错误信息定位到java.lang.ClassLoader类,发现如下代码:
/* Determine protection domain, and check that:
- not define java.* class,
- signer of this class matches signers for the rest of the classes in package.
*/
private ProtectionDomain preDefineClass(String name,
ProtectionDomain protectionDomain)
{
if (!checkName(name))
thrownew NoClassDefFoundError("IllegalName: " + name);
if ((name != null) && name.startsWith("java.")) {
thrownew SecurityException("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (protectionDomain == null) {
protectionDomain = getDefaultDomain();
}
if (name != null)
checkCerts(name, protectionDomain.getCodeSource());
return protectionDomain;
}
不难看出,ClassLoader子系统在进行类装载的时候对其全限定名进行了检验,如果以java.开头的话将抛出:禁止的包名安全异常。这也是ClassLoader子系统为了保护jvm安全对恶意代码采取的安全措施。
运行时数据区
方法区
方法区是被各个线程所共享的内存区域,它用来存储已经被虚拟机加载的类信息、常量、静态变量等数据。
堆
对于大多数应用来说,堆是JVM所管理的内存区域中最大的一块,堆是一个线程共享的内存区。在JVM启动时创建。此内存区域的唯一目的就是用来存放对象实例,几乎所有的对象实例都在这里分配内存。
Java栈
Java栈是线程私有的, java栈由许多栈帧组成的,如图,当一个线程调用java方法时,虚拟机压入一个新的栈帧到java栈中,当方法返回的时候,这个栈帧被从java栈弹出并被抛弃。
在JVM规范中对Java栈这个区域规定了两种异常情况
一) 如果线程请求的栈深度大于JVM所允许的栈深度,将抛出StackOverflowError异常。
二) JVM允许自动扩展,如果无法向系统申请到足够的内存的话则会抛出OOM异常。
为了测试本机的“栈深度”写了个小Demo,如下:
/**
*
*@authorJoher
*
*/
publicclass StackLengthTest {
publicstaticintstackLen = 0;
publicstaticvoid invokeStack(){
++stackLen;
invokeStack();
}
/**
*@paramargs
*/
publicstaticvoid main(String[] args) {
try {
invokeStack();
} catch (StackOverflowError e) {
System.out.println("StackLen : " + stackLen);
e.printStackTrace();
}
}
}
运行输出如下结果:
通过捕获StackOverflowError异常我们可以得知本机的栈深度为16640.
PC寄存器
每个新线程产生都将得到自己的pc寄存器以及一个java栈帧。
本地方法栈
本地方法栈与Java栈的功能非常相似,其区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为VM用到的本地方法服务,虚拟机规范中对本地方法栈中的方法是用的语言、是用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
执行引擎
Java虚拟机的主要任务是装在class文件并且执行其中的字节码。Java虚拟机包含一个类装载器,它可以从程序和API中装载class文件。Java API中只有程序执行时需要的那些类才会被装载。字节码由执行引擎来执行。不同的Java虚拟机中,执行引擎可能实现得非常不同。在由软件实现的虚拟机中,最简单的执行引擎就是一次性解释字节码。
另一种执行引擎更快,但是也更消耗内存,叫做"即时编译器(just-in-time compiler)"。在这种情况下,第一次被执行的字节码会被编译成本地机器代码。编译出的本地机器代码会被缓存,当方法以后被调用的时候可以重用。
第三种执行引擎是自适应优化器。在这种方法里,虚拟机开始的时候解释字节码,但是会监视运行中程序的活动,并且记录下使用最频繁的代码段。程序运行的时候,虚拟机只把那些活动最频繁的代码编译成本地代码,其他的代码由于使用得不是很频繁,继续保留为字节码-由虚拟机继续解释它们。
一个自适应的优化器可以使得Java虚拟机在80%~90%的时间里执行被优化过的本地代码,而只需要编译10%~20%的对性能有影响的代码。
本地方法接口
CLASS文件结构
The ClassFile Structure
ClassFile{
u4 magic; //魔数
u2 minor_version; //class 次版本号
u2 major_version; //class 主版本号
u2 constant_pool_count; //常量池计数
cp_info constant_pool[constant_pool_count-1]; //常量池
u2 access_flags; //修饰符
u2 this_class; /常量池索引
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attrributes_count];
}
Magic Number
每个class文件的前四个字节被称为它的“魔数”(0xCAFEBABE),通过这个属性,如果jvm可以轻松鉴别出要装载的文件是否是一个class文件。
Version of Class File
Class文件版本号分为主版本号和次版本号,在Sun的JDK1.0.2发布版中,Java虚拟经济实现支 持从45.0(主版本号为45,次版本号为0)到45.3的class文件格式。在所有JDK1.1发布版本中虚拟机都能够支持版本从450.到 45.65535的class文件格式。在Sun的1.2版本的SDK中,虚拟机能够支持从版本45.0到46.0的class文件格式。JDK 1.3支持到47.0,JDK 1.4支持到48.0 依次类推,所以根据class文件版本号可以轻松判断出是由哪个版本的JDK编译的。只有高版本执行低版本的class文件,这也就是jdk5不能执行jdk6编译的代码的原因。
Constant Pool
常量池中包含了与文件中类和接口相关的常量。常量池中存储了诸如static常量、final变量值、类名和方法名的常量。Java虚拟机把常量池作为为入口列表的形式。在实际列表constant_pool之前,是入口在列表中的计数。
Access Flags
access_flags标识了文件中定义的类或接口的信息。例如,访问标志指明文件中丁定义的是类还是接口;访问表示还定义了在类或接口的声明中使用了哪种修饰符;类和接口是抽 象的,还是公共的;类的类型可以为final,而final类不可能是抽象的;接口不能为final类型。
This Class
this_class是一个对常量池的索引。在this_class位置的常量池入口必须为CONSTANT_Class_info表。
Super Class
紧接在this_class之后的是super_class项,他是一个两个字节的常量池索引。在super_class位置的常量池入口是一个指向该类超类全限定名CONSTANT_Class_info.
Interfaces
interface_count此项的含义为:在文件中由该类直接实现或者由接口扩展的父接口的数量。在这个计数的后面,是名为interface的数组,他包含了对每个由该类或者接口直接实现的父接口的常量池索引。
Fields
Fields是对在该类或接口中所声明的字段的描述。首先是名为fields_count的计数,他是类变量和实例变量的字段的数量总和。
Methods
Methods是对在该类或接口中所声明的方法的描述。是对在该类或接口中所声明的方法的描述。
Attributes
Attributes给出了在该文件中类或接口锁定义的属性的基本信息。
JVM的沙箱机制
我们平时说Java是安全的,可以使用户免受而已程序的侵犯,这是因为Java提供了一个“沙箱”机制,这个“沙箱”基本组件包括如下4部分:
1、类装载器
在Java沙箱中,类装载体系结构是第一道防线,可以防止而已代码去干扰正常程序代码,这是通过由不同的类装载器装入的类提供不同的命名空间来实现的。命名空间由一系列唯一的名称组成,每一个被装载的类都有不同的命名空间是由Java虚拟机为每一个类装载器维护的。
类装载器体系结构在三个方面对java的沙箱起作用
1)它防止恶意代码区干涉善意的代码
2)它守护了被信任的类库边界
3)它将代码归入保护域,该域确定了代码可以进行哪些操作。
2、 Class文件检验器
Class文件检验器保证装载的class文件内容的内部结构的正确,并且这些class文件相互协调一致。
Class文件检验器实现的安全目标之一就是程序的健壮性,JAVA虚拟机的class文件检验器要进行四趟扫描来完成它的操作:
第一趟:此次扫描是在类被装载的时候进行的,在这次扫描中,class文件检验器检查这个class文件的内部结构,以确保它能够被安全的编译。
第二趟、第三趟:这两次扫描是在连接过程中进行的,这次扫描中,class文件检验器确认类型数据遵行java语言的语法规则和语义,包括检验它所包含字节码的完整性。
第四趟:这次扫描是在进行动态连接的过程中解析符号引用时进行的,在这次扫描中,class文件检验器确认被引用的类、字段以及方法确实存在。
3、 内置于Java虚拟机的安全特性
Java虚拟机装载一个类,并且对它进行了第一到第三趟的class文件检验,这些字节码就可以运行了。除了对符号引用的检验,Java虚拟机在执行字节码时还进行其他一些内置的安全机制的操作。这些机制大多数是java的类型安全的基础,它们作为java语言程序健壮性的特性。
1、类型安全的引用转换
2、结构化的内存访问
3、 自动垃圾收集
4、 数组边界检查
5、空引用检查
4、 安全管理及Java API
Java安全模型的前三个组成部分——类装载器体系结构,class文件检验器及java中的内置安全特性一起达到一个共同的目的:保持java虚拟机的实例和它正在运行的应用程序的内部完整性,使得它们不被恶意代码侵犯。相反,这个安全模型的第四个组成部分是安全管理器,它主要用于保护虚拟机的外部资源不被虚拟机内运行的恶意代码侵犯。这个安全管理器是一个单独的对象,在运行的java虚拟机中,它在访问控制对于外部资源的访问中起中枢的作用。
JVM垃圾回收机制
标记算法
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
标记-清理算法
标记-清除:此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
优点:不会存在类似标记算法对象互相引用导致无法回收的问题。
缺点:因为被回收的堆空间不能保证一定是连续的,所以会产生许多空间碎片,可能导致大对象无法创建。
复制算法
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每 次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不过出现“碎片”问题。
优点:因为大多数对象遵循“朝生夕灭”的原则,使用这种回收算法进行回收的时候每次只需要将存活的对象复制到另一块内存区域即可,所以回收效率绝对OK。
缺点:很明显,需要双倍内存,或者说,在内存一定情况下,只能使用一半内存。
标记-整理算法
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除 未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
优点:这种算法解决了标记-清除算法里的内存碎片问题和复制算法里的空间问题。
缺点:这种算法虽然不会产生空间碎片也不会浪费内存,但是,在回收完之后对堆空间的整理过程是非常耗时的。
分代算法
基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年轻代、年老代、持久代,对不同生命周期的对象使用不同的算法进行回收。
从J2SE 1.2开始均采用此种垃圾回收算法,也是到目前为止相对快速且稳定的垃圾回收算法(JDK 7采用了所谓G-First的回收算法,有待研究)这里详细介绍分代算法:
为什么要分代
在Java程序中会产生大量的对象,其中有些对象是与业务信息相关的,比如Session对象,Socket连接,这些对象生命周期相对较长;但是有些对象生命周期很短,比如String对象,由于它的final特性,甚至有的只用一次就被回收。
如果在不进行分代的情况下,每次进行垃圾回收都要对整个堆空间进行扫描,已死对象即可被回收,但是对于依然存活的对象,这种遍历是没有任何意义的。因此采用分代的回收理念,按照生命周期进行划分,把不同生命周期的对象放在不同的代上,不同的代采用不同的垃圾回收算法进行回收,以便提高回收效率。
如何分代
如图所示:
JVM中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息以及JVM自身所需的空间,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
年轻代
每当对象被创建之后会首先进入年轻代,年轻代的目标就是尽可能回收那些短生命周期的对象。年轻代分为三个区:一个Eden Space,两个相同大小的Survivor Space(也称为From Space和To Space)。大部分对象在Eden Space中生成(大对象会直接进入年老代),当Eden Space达到出发GC的时候被回收的对象会被放到一个Survivor Space里,当一个Survivor Space快满的时候会将依然存活的对象复制到两一个Survivor Space里,对象每“逃过”一次GC,对象的年龄就会长一岁,达到默认的年龄之后就会被放入年老代。
这两个Survivor Space总有一个是空的,可以看出,此空间采用了以上介绍的复制算法,不过,根据用户需要Survivor Space可以配置多余两个区域,这样可以增加对象在年轻代中存在的时间,降低被存放在年老代的可能,减少full gc的发生(为什么要减少full gc的发生会在后面介绍)。
年老代
主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured。一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。
持久代
主要保存class、method、filed等对象,这部分的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到应用服务器的热部署时,有时候 会遇到java.lang.OutOfMemoryError: PermGen space 的错误,造成这个错误的很大原因就有可能是每次热部署后,旧的class没有被卸载掉,这样就造成了大量的class对象保存在了Perm中,这种情况下 一般重新启动应用服务器可以解决问题,或者通过-XX:MaxPermSize=<N> 将持久代大小设大点。
如何回收
JVM的GC类型分为两种:
<!--[if !supportLists]-->1) <!--[endif]-->Scavenge GC (也称Minor GC)
对年轻代进行回收,包括Eden Space和两个Survivor Space。
<!--[if !supportLists]-->2) <!--[endif]-->Full GC
对整个堆空间(包括Young Gen、 Old Gen、 Perm Gen)进行扫描回收,比较耗时。
一般情况下,当新对象生成并在Eden申请空间失败时就会触发Minor GC,对Eden区域进行GC,清除已死对象,并把尚且存活的对象移到Survivor Space里,然后整理Surivor的两个区域,这种对年轻代的GC不会影响年老代,因为大部分对象是从Eden space开始的,同时 Eden Space不会分配太大,默认情况下,年轻代空间大小与年老代空间大小的比值在30%左右,所以EdenSpace的GC会频繁进行,因而这里需要速度快、效率高的算法尽可能把年轻代空间空闲出来供其他对象申请以复用。
JVM在进行Full GC的时候会,所有线程会跑到最近的安全点并挂起,等待GC完成之后再恢复运行,Sun官方将这件事称为“Stop The World”,Full GC发生过程中所有线程处于等待状态,程序无响应,所以JVM调优应运而生,其关键在于通过对程序代码、硬件、网络等软硬件环境综合分析运用JVM提供的诸多垃圾收集器进行共同协作来完成GC这件看似平常却又极为关键的事儿。
JVM调优实战
优化eclipse运行速度
做开发每天肯定离不开eclipse,也许你的eclipse在写代码过程中会时不时“ba gong”出现卡的现象,这是一件很郁闷的事儿。如果明白JVM的GC原理并通过简单配置也许这个问题会迎刃而解。下面是本人用我的PC机做的优化测试:
硬件环境
CPU:AMD X2 240 2.81GHz
RAM:2G
软件环境
OS:Win 7 x86
IDE:MyEclipse 6.5
JDK:JKD 1.6
调优评估
首先将workspace里的所有项目close掉,关闭eclipse。
优化用例就是open一个项目,eclipse会自动build该项目。
优化前eclipse.ini的配置信息如下:
-clean
-showsplash
com.genuitec.myeclipse.product.ide
--launcher.XXMaxPermSize
256m
-vmargs
-Xms128m
-Xmx512m
-XX:PermSize=128M
-XX:MaxPermSize=256M
先解释一下这些参数分别是什么意思:
-Xms 用来设置JVM所能使用的初始空间大小。
-Xmx 用来设置JVM所能使用的最大空间大小(为了避免运行时JVM不断自动扩展内存空间,通常把-Xms和-Xmx设为相同的值)。
-XX:PermSize 设置JVM初始持久代大小。
-XX:MaxPermSize 设置JVM持久代所能使用的最大空间
调优流程
在eclipse.ini中加入如下参数
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-verbose:gc
-Xloggc:gc.log
打开MyEclipse,然后打开TCH项目,查看gc日志:
1.554: [GC 1.554: [DefNew: 8128K->960K(9088K), 0.0076318 secs] 8128K->1004K(130112K), 0.0078368 secs]
2.897: [GC 2.898: [DefNew: 9088K->951K(9088K), 0.0101030 secs] 9132K->1898K(130112K), 0.0103400 secs]
3.633: [GC 3.633: [DefNew: 9079K->27K(9088K), 0.0025283 secs] 10026K->1479K(130112K), 0.0027274 secs]
4.252: [GC 4.252: [DefNew: 8155K->54K(9088K), 0.0008784 secs] 9607K->1506K(130112K), 0.0009702 secs]
4.437: [GC 4.437: [DefNew: 8182K->211K(9088K), 0.0009378 secs] 9634K->1663K(130112K), 0.0010506 secs]
14.377: [GC 14.377: [DefNew: 8646K->670K(9088K), 0.0025745 secs] 13247K->5467K(130112K), 0.0026663 secs]
15.690: [GC 15.690: [DefNew: 8798K->670K(9088K), 0.0025225 secs] 13595K->5724K(130112K), 0.0026109 secs]
15.935: [GC 15.935: [DefNew: 8798K->800K(9088K), 0.0039703 secs] 13852K->6186K(130112K), 0.0040638 secs]
/**********************************此处省略N行******************************/
38121K->38062K(139652K), [Perm : 47029K->47029K(131072K)], 0.3506692 secs]
300.263: [Full GC 300.263: [Tenured: 38062K->38063K(129564K), 0.3543569 secs] 38122K->38063K(139356K), [Perm : 47029K->47029K(131072K)], 0.3545029 secs]
360.274: [Full GC 360.274: [Tenured: 38063K->38063K(128492K), 0.3477797 secs] 38277K->38063K(138220K), [Perm : 47029K->47029K(131072K)], 0.3479213 secs]
420.290: [Full GC 420.290: [Tenured: 38063K->35960K(126880K), 0.4125601 secs] 39399K->35960K(136480K), [Perm : 47056K->46999K(131072K)], 0.4128833 secs]
480.304: [Full GC 480.304: [Tenured: 35960K->35961K(121028K), 0.3393506 secs] 36049K->35961K(130180K), [Perm : 46999K->46999K(131072K)], 0.3394406 secs]
540.315: [Full GC 540.315: [Tenured: 35961K->35962K(121028K), 0.3428345 secs] 36050K->35962K(130180K), [Perm : 46999K->46999K(131072K)], 0.3429336 secs]
600.326: [Full GC 600.326: [Tenured: 35962K->35963K(121028K), 0.3362861 secs] 36484K->35963K(130180K), [Perm : 46999K->46999K(131072K)], 0.3363906 secs]
660.342: [Full GC 660.342: [Tenured: 35963K->35901K(121028K), 0.3389731 secs] 36677K->35901K(130180K), [Perm : 46999K->46999K(131072K)], 0.3390565 secs]
720.353: [Full GC 720.353: [Tenured: 35901K->35902K(121028K), 0.3397715 secs] 35990K->35902K(130180K), [Perm : 46999K->46999K(131072K)], 0.3398570 secs]
780.366: [Full GC 780.366: [Tenured: 35902K->35902K(121028K), 0.3343494 secs] 35991K->35902K(130180K), [Perm : 46999K->46999K(131072K)], 0.3344462 secs]
840.378: [Full GC 840.378: [Tenured: 35902K->35903K(121028K), 0.3418487 secs] 35991K->35903K(130180K), [Perm : 46999K->46999K(131072K)], 0.3419390 secs]
注:鉴于篇幅,GC日志有删节
就如此一步操作,就产生了如此多次GC,其中包括244次Minor GC和17次Full GC,
Minor GC耗时未统计,Full GC共耗时 12.78s 也就是说在这12.78秒之内程序是处于停顿的状态。
第一次优化
首先增大了JVM可用的空间大小,监狱本机只有2G内存,分给JVM1024M使用, 在eclipse.ini中修改参数-Xms和-Xmx值为1024m,并重新启动MyEclipse,打开TCH项目,查看gc日志:
1.467: [GC 1.468: [DefNew: 64512K->2496K(72576K), 0.0179864 secs] 64512K->2496K(1040512K), 0.0182066 secs]
1.892: [GC 1.892: [DefNew: 67008K->1931K(72576K), 0.0064207 secs] 67008K->1931K(1040512K), 0.0066300 secs]
2.315: [GC 2.315: [DefNew: 66443K->3770K(72576K), 0.0108631 secs] 66443K->3770K(1040512K), 0.0110735 secs]
3.257: [GC 3.257: [DefNew: 68282K->7313K(72576K), 0.0208940 secs] 68282K->7313K(1040512K), 0.0210957 secs]
3.858: [GC 3.858: [DefNew: 71825K->8064K(72576K), 0.0336250 secs] 71825K->12992K(1040512K), 0.0338172 secs]
5.145: [GC 5.145: [DefNew: 72576K->6706K(72576K), 0.0445646 secs] 77504K->18948K(1040512K), 0.0447662 secs]
6.714: [GC 6.715: [DefNew: 71218K->7709K(72576K), 0.0536708 secs] 83460K->26429K(1040512K), 0.0538837 secs]
8.898: [GC 8.898: [DefNew: 72221K->7022K(72576K), 0.0601340 secs] 90941K->33322K(1040512K), 0.0602305 secs]
10.861: [GC 10.861: [DefNew: 71534K->5049K(72576K), 0.0572166 secs] 97834K->38272K(1040512K), 0.0572996 secs]
20.100: [GC 20.100: [DefNew: 68644K->4630K(72576K), 0.0398203 secs] 117977K->57655K(1040512K), 0.0399219 secs]
20.497: [GC 20.497: [DefNew: 69142K->5041K(72576K), 0.0428251 secs] 122167K->62626K(1040512K), 0.0429227 secs]
20.861: [GC 20.861: [DefNew: 69553K->5401K(72576K), 0.0470543 secs] 127138K->67950K(1040512K), 0.0471475 secs]
21.361: [GC 21.361: [DefNew: 69913K->6576K(72576K), 0.0522008 secs] 132462K->74520K(1040512K), 0.0523052 secs]
27.887: [Full GC 27.887: [Tenured: 43812K->43942K(967936K), 0.3216027 secs] 68465K->43942K(1040512K), [Perm : 40847K->40847K(131072K)], 0.3216904 secs]
29.543: [GC 29.543: [DefNew: 64512K->342K(72576K), 0.0029950 secs] 108454K->44284K(1040512K), 0.0030791 secs]
30.953: [GC 30.953: [DefNew: 64854K->837K(72576K), 0.0046343 secs] 108796K->44779K(1040512K), 0.0047257 secs]
31.730: [Full GC 31.730: [Tenured: 43942K->37766K(967936K), 0.4068460 secs] 106913K->37766K(1040512K), [Perm : 40850K->40805K(131072K)], 0.4069367 secs]
32.958: [Full GC 32.958: [Tenured: 37766K->37788K(967936K), 0.3240356 secs] 93940K->37788K(1040512K), [Perm : 40805K->40805K(131072K)], 0.3241215 secs]
55.767: [Full GC 55.767: [Tenured: 37788K->40857K(967936K), 0.3601019 secs] 101895K->40857K(1040512K), [Perm : 42503K->42503K(131072K)], 0.3602064 secs]
66.794: [Full GC 66.794: [Tenured: 40857K->43282K(967936K), 0.3902751 secs] 89966K->43282K(1040512K), [Perm : 48778K->48778K(131072K)], 0.3903642 secs]
126.803: [Full GC 126.803: [Tenured: 43282K->42677K(967936K), 0.4507163 secs] 45732K->42677K(1040512K), [Perm : 48784K->48777K(131072K)], 0.4508092 secs]
186.817: [Full GC 186.818: [Tenured: 42677K->42677K(967936K), 0.3734635 secs] 44235K->42677K(1040512K), [Perm : 48777K->48777K(131072K)], 0.3735523 secs]
注:鉴于篇幅,GC日志有删节
第一次优化后,gc次数明显减少,Minor gc由原来的244次减少到现在的29次,降低了8倍多,Full gc由原来的17次减少到现在的9次,降低了近两倍。仅仅是启动并无任何操作就发生了9次gc,有点说不过去,于是继续优化。
第二次优化
通过分析第一次优化后的gc日志,发现发生full gc的时候Perm Gen在不断扩大,也就是说由于持久代引起的full gc。于是修改-XX:PermSize增大持久代大小。重新运行MyEclipse并打开TCH项目,gc日志如下:
1.152: [GC 1.153: [DefNew: 64512K->2497K(72576K), 0.0177614 secs] 64512K->2497K(1040512K), 0.0179867 secs]
1.559: [GC 1.559: [DefNew: 67009K->1935K(72576K), 0.0062536 secs] 67009K->1935K(1040512K), 0.0064829 secs]
1.984: [GC 1.984: [DefNew: 66447K->3769K(72576K), 0.0104448 secs] 66447K->3769K(1040512K), 0.0106461 secs]
2.894: [GC 2.894: [DefNew: 68281K->7313K(72576K), 0.0208969 secs] 68281K->7313K(1040512K), 0.0211000 secs]
3.488: [GC 3.488: [DefNew: 71825K->8064K(72576K), 0.0331456 secs] 71825K->12992K(1040512K), 0.0333469 secs]
4.418: [GC 4.418: [DefNew: 72576K->6706K(72576K), 0.0441106 secs] 77504K->18948K(1040512K), 0.0443181 secs]
5.591: [GC 5.591: [DefNew: 71218K->7707K(72576K), 0.0520267 secs] 83460K->26427K(1040512K), 0.0522281 secs]
8.300: [GC 8.300: [DefNew: 72219K->7187K(72576K), 0.0604406 secs] 90939K->33449K(1040512K), 0.0605297 secs]
9.427: [GC 9.427: [DefNew: 71699K->6059K(72576K), 0.0582956 secs] 97961K->39434K(1040512K), 0.0583921 secs]
9.841: [GC 9.841: [DefNew: 70571K->6969K(72576K), 0.0440330 secs] 103946K->44173K(1040512K), 0.0441324 secs]
29.922: [Full GC 29.922: [Tenured: 43950K->37753K(967936K), 0.3943425 secs] 106742K->37753K(1040512K), [Perm : 40768K->40723K(262144K)], 0.3944415 secs]
31.125: [Full GC 31.125: [Tenured: 37753K->37776K(967936K), 0.3217680 secs] 93900K->37776K(1040512K), [Perm : 40723K->40723K(262144K)], 0.3218586 secs]
注:鉴于篇幅,GC日志有删节
经过此次优化后,发现minor gc并无减少,full gc倒是由原来的9次减少到5次,分析第二次优化后的gc日志,发现频繁发生gc的根源在于Tenured Gen。
第三次优化:
由于无法直接对年老代进行大小控制,所以只能间接地对年轻代设置来控制年老代,加入-Xmn参数,一般年轻代设置为整个堆空间的30%左右,我这里设置了256m,重新运行MyEclipse并打开TCH项目,gc日志如下:
2.259: [GC 2.259: [DefNew: 209792K->4854K(235968K), 0.0233411 secs] 209792K->4854K(1022400K), 0.0235741 secs]
4.779: [GC 4.779: [DefNew: 214646K->19721K(235968K), 0.0781295 secs] 214646K->19721K(1022400K), 0.0783363 secs]
9.887: [GC 9.887: [DefNew: 229513K->20457K(235968K), 0.1698051 secs] 229513K->38730K(1022400K), 0.1700046 secs]
11.083: [GC 11.083: [DefNew: 230249K->18426K(235968K), 0.1369559 secs] 248522K->53630K(1022400K), 0.1371572 secs]
12.475: [GC 12.476: [DefNew: 228218K->17499K(235968K), 0.0895838 secs] 263422K->53803K(1022400K), 0.0897640 secs]
38.289: [GC 38.289: [DefNew: 227291K->6876K(235968K), 0.0457259 secs] 263595K->43403K(1022400K), 0.0459436 secs]
38.792: [Full GC 38.792: [Tenured: 36527K->39491K(786432K), 0.3451502 secs] 98865K->39491K(1022400K), [Perm : 41879K->41879K(131072K)], 0.3453869 secs]
经过此次优化,minor gc由原来的29次降低到现在的6次,full gc只发生了一次,停顿了约0.34秒,还算比较满意。
调优总结如下:
调优总结
统计项 |
Minor GC次数 |
Full GC次数 |
Minor GC耗时/s |
Full GC耗时/s |
调优前 |
244 |
17 |
未统计 |
12.78 |
调优后 |
6 |
1 |
未统计 |
0.34 |
提升差值 |
238 |
16 |
未统计 |
12.44 |
提升百分比 |
97.54% |
94.11% |
— |
97.33% |
注:此表值仅为GC时间值
Web系统调优
探索中,待添加。。。
JVM学习参考资料
深入Java虚拟机(原书第2版)
http://book.douban.com/subject/1138768/
深入理解Java虚拟- JVM高级特性与最佳实践
http://book.douban.com/subject/6522893/