jvm组成
1、类加载器,class loader subsystem,加载类型,赋予唯一名字。
2、执行引擎,execution engine,执行被加载类中包含的指令。
3、数据区,data area,保存字节码、加载类的其他信息、对象、方法、参数、返回值、变量等。
4、本地方法接口,其他编程语言交互的接口。
5、垃圾回收,GC。
jvm结构
两个子系统:类加载子系统、执行引擎子系统
两个组件:运行时数据区域、本地接口
数据区
1、分程序共享和线程单独控制。
2、方法区和堆,属于程序共享。类加载解析出来的信息保存在方法区,程序执行时创建的对象则保存在堆中。
3、栈保存线程调用方法时的状态,本地变量、参数、中间变量、返回值等。
4、PC寄存器保存线程执行的下一条指令的地址,指针或偏移量,线程启动时创建,。
5、线程创建时,会被分配只属于它自己的PC寄存器和栈。
6、栈有栈帧,当线程调用一个方法时,会产生压入一个新的栈帧,当方法调用结束时,将栈帧弹出并抛弃。只有压栈和出栈两个操作。
7、栈帧有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。
8、调用本地方法时的状态保存在本地方法栈中,或PC寄存器,或其他非平台独立的内存中。
9、直接内存,就是jvm外的内存。jdk有一种基于通道(channel)和缓冲区(buffer)的内存分配方式,将由c语言实现的native函数库分配在直接内存中,用存储在jvm堆中的DirectByteBuffer来引用。由于受到机器内存的限制,也可能出现outofmemory。
方法区
1、程序中所有线程共享一个方法区,所以访问方法区的方法必须是线程安全的,阻塞式。
2、程序运行时,方法区的大小是可变的,可扩展,可以初始化方法区的初始值、最小值、最大值。
3、方法区可以GC,当类变成没有被引用的状态的时候。
4、方法区保存的类型信息有:类全名、父类型全名、是类或接口、修饰符、父接口列表,常量池、字段、方法、静态变量、类加载器引用、Class引用,及方法列表。
5、方法区即持久化代。
6、除class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时池中。符号引用就是编码是用字符串表示某个变量、接口的地址。直接引用就是根据符号引用翻译过来的地址,将类链接阶段完成翻译。
引用
Jdk1.2后对象引用分为四类:强引用、软引用、弱引用和虚引用
强引用,平时我们编程的时候例如:Object object=new Object();那object就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用对象来解决内存不足问题。
软引用(SoftReference),如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联 合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
弱引用(WeakReference),如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回 收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
虚引用(PhantomReference),顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在 任何时候都可能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队 列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
堆
1、程序创建对象时,在堆中分配内存。所有线程共享堆。
2、当堆中对象变为未引用状态时,GC。
3、堆存储的内容主要是:变量和引用。
4、异常:如果堆中没有内存完成实例分配,堆无法扩展,抛outfomemory异常。
栈
1、jvm是基于栈的体系结构来执行class字节码的。线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。
2、jvm的多线程是通过线程轮流切换并分配cpu时间片来实现的,在任何一个确定的时刻,只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的pc寄存器来计数,各条线程之间的计数互不影响,独立存储,是线程私有内存。
3、异常:如果请求的栈深度大于jvm所允许的深度,抛stackoverflow异常。如果jvm可以动态扩展,但扩展时无法申请足够的内存抛outofmemory异常。
存储实例
1、一般来说,一个java的引用访问涉及到三个内存区域:堆、栈、方法区。
2、以最简单的本地变量引用:Object obj = new Object(); 为例如下:
1)Object obj表示一个本地引用,存在栈的本地变量表中,表示一个reference类型数据。
2)new Object()作为实例对象数据存储在堆中。
3)堆中还记录了Object类的类型信息的地址,存储在方法区中。
4)对于通过reference类型应用访问具体对象的方式有两种:通过句柄访问方式、通过直接指针方式。
5)sun hotspot采用指针方式,如下:
类加载
1、类加载器分两种:原始类加载器(primordial class loader)和类加载器对象(class loader objects)。
2、原始类加载器是jvm实现的一部分,而类加载器对象是运行中的程序的一部分。不同类加载器加载的类被不同的命名空间所分割。
3、类加载器对象的子类可以访问类加载机制,该对象和其他对象一样保存在堆中,被加载的信息保存在方法区中。
类加载过程:加载、连接、初始化
1、加载:寻找并导入指定类型的二进制信息。
2、连接:验证(确保导入类型的正确性)、准备(为类型分配内存并初始化默认值)、解析(将字符引用改为直接引用)。
3、初始化:调用代码,初始化类变量为合适的值。
原始类加载器
1、加载遵守类文件格式且被信任的类。
类加载器对象
1、三个方法可以访问类加载子系统。
2、defineClass(),输入一个字节数组,定义一个新类型。
3、findSystemClass(),加载指定类。
4、resoleClass(),defineClass只是加载一个类,resoleClass负责后续的连接、初始化。
5、多个类加载器加载了同一个类时,需要在类名前加上类加载器的标识。
类加载过程
加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。JVM在加载类时默认采用的是双亲委派机制,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
1、隐式加载:程序在运行过程中碰到new等方式生成对象的,通过类加载器加载类到jvm中。
2、显式加载:通过class.forName()等方法,显式加载类。
类加载的动态性:程序总是由很多类组成的,java程序启动时,并不是一次把所有的类全部加载后再运行,而是先把保证程序运行的基础类一次性加载到jvm中,其他类等到jvm用到时再加载,这样的好处是节省了内存的开销。因为java最早就是为嵌入式系统开发的,内存宝贵。
1)Bootstrap ClassLoader /启动类加载器
$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
2)Extension ClassLoader/扩展类加载器
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
3)App ClassLoader/ 系统类加载器
负责记载classpath中指定的jar包及目录中class
4)Custom ClassLoader/用户自定义类加载器(java.lang.ClassLoader的子类)
属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader
热部署
1、一个实例是通过本身的类名+加载它的ClassLoader识别的,也就是说不同的ClassLoader加载同一个类在JVM是不同的。
2、同一个ClassLoader是不允许多次加载一个类的,否则会报java.lang.LinkageError: attempted duplicate class definition for
name XXX。
3、
既然JVM不支持热部署,那么要实现热部署,就必须自定义ClassLoader,当类被修改过后,重新实例ClassLoader后加载该类。
类执行
1、编译过程
2、类加载过程
3、执行过程
类编译
1、分析和输入到符号表
2、注解处理
3、语义分析和生成Class文件
class文件组成:
1、结构信息,class文件格式版本号、各部分数量与大小。
2、元数据,对应源码中声明与常量信息,类/父类/接口的声明信息、字段/方法的声明信息、常量池。
3、方法信息,对应源码中语句和表达式对应的信息,字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息等。
class文件结构
1、java语言跨平台,原因在于定义了一套与操作硬件无关、jvm识别的字节码格式,unicode编码,用class文件表示。同时,jvm定义了一套指令,用于解析执行class文件,翻译成机器语言。
2、class文件由jvm规范规定。组成如下:
ClassFile { //U4 代表由无符号四个字节组成
u4 magic; //是一个固定的数值,java虚拟机里面称为魔数 ,主要是用来标识是否为java虚拟机所支持的文件结构,目前是0xCAFEBABE
u2 minor_version; //代表次版本号和主版本号
u2 major_version;
u2 constant_pool_count; //这里面代表常量池个数以及常量池信息
cp_info constant_pool[constant_pool_count-1];
u2 access_flags; //代表class访问标记,例如:public protected
u2 this_class; //代表这个类的名称 例如 java.lang.Object
u2 super_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[attributes_count]; //java class文件内部属性信息,和java语言定义的属性没有关系,纯粹就是给java虚拟机用的
}
3、直接看class文件可对应出上述结构,也可以用javap反编译看到更清楚些。举例如下:
.java文件如下
public class TestClass {
private int m;
public int inc(){
return m+1;
}
}
.class文件如下:
00000000 ca fe ba be 00 00 00 32 00 13 0a 00 04 00 0f 09 |.......2........|
00000010 00 03 00 10 07 00 11 07 00 12 01 00 01 6d 01 00 |.............m..|
00000020 01 49 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 |.I...<init>...()|
00000030 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e |V...Code...LineN|
00000040 75 6d 62 65 72 54 61 62 6c 65 01 00 03 69 6e 63 |umberTable...inc|
00000050 01 00 03 28 29 49 01 00 0a 53 6f 75 72 63 65 46 |...()I...SourceF|
00000060 69 6c 65 01 00 0e 54 65 73 74 43 6c 61 73 73 2e |ile...TestClass.|
00000070 6a 61 76 61 0c 00 07 00 08 0c 00 05 00 06 01 00 |java............|
00000080 18 6f 72 67 2f 6b 61 6b 61 2f 63 6c 61 7a 7a 2f |.org/kaka/clazz/|
00000090 54 65 73 74 43 6c 61 73 73 01 00 10 6a 61 76 61 |TestClass...java|
000000a0 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 00 21 00 03 |/lang/Object.!..|
000000b0 00 04 00 00 00 01 00 02 00 05 00 06 00 00 00 02 |................|
000000c0 00 01 00 07 00 08 00 01 00 09 00 00 00 1d 00 01 |................|
000000d0 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00 01 00 |......*.........|
000000e0 0a 00 00 00 06 00 01 00 00 00 03 00 01 00 0b 00 |................|
000000f0 0c 00 01 00 09 00 00 00 1f 00 02 00 01 00 00 00 |................|
00000100 07 2a b4 00 02 04 60 ac 00 00 00 01 00 0a 00 00 |.*....`.........|
00000110 00 06 00 01 00 00 00 07 00 01 00 0d 00 00 00 02 |................|
00000120 00 0e |..|
00000122
人眼对应如下:
step 1) java magic number
首先的四个字节0x ca fe ba be 即class文件的magic number
step2) java version
接下来的四个字节0x 00 00 00 32 即class文件的版本号(可参看class文件的版本号列表)
step3) 常量池
接下来是描述常量表的长度0x 00 13,一共是(19-1)项,人肉分析如下
第1项
tag: 0x 0a ,CONSTANT_Methodref_info即方法声明
index: 0x 00 04,指向常量池中CONSTANT_Class_info,见常量池第4项
index: 0x 00 0f,指向常量池中CONSTANT_NameAndType_info见常量池第15项
...
step4) 访问标志
接下来的两个字节0x 00 21是由下标中的0x 00 01 | 0x 0020计算出来的,表类是public 的,且是jdk1.2以后的编译器编译出来的访问标志 标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可以设置,接口不能设置该标志
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令(查了一下该命令的作用为"调用超类的构造方法,实例的构造方法,私有方法"),JDK1.2以后编译器编译出来的class文件
该标志都为真
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否被声明为abstract类型,对于接口和抽象类来说此标志为真,其他类为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码生成
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举
step5) 类信息
头两个字节0x 00 03,指向常量池中的第3项,即类名
接下来的两个字节 0x 00 04 指向常量池中的第4项,即父类名
接下来的两个字节 0x 00 00 表示类实现的接口的个数,本例子中为0,如果有的n个话,后面还会有2n个字节的常量池索引
step 6)字段信息
头两个字节 0x 00 01 字段的个数,本例中为1
接下来就是1个field_info结构
头两个字节 0x 00 02 表示accss_flags,即private
接下来0x 00 05表示字段名,指向常量池的第5项,即本例中"m"
接下来的0x 00 06 表示字段的描述信息,指向常量池的第6项,即本例中"I" ,表示是int类型
接下来的0x 00 00 表示字段的属性信息(用于扩展或者补充说明字段信息)的个数,本例中为0,如果有n个的话,后面还会有n个attribute_info结构
step 7) 方法信息
头两个字节为0x 00 02,表示方法的个数,本例中为2
接下来是两个method_info结构
第1个method_info结构
头两个字节0x 00 01 表示accss_flags,即public 方法
接下来0x 00 07表示方法名,指向常量池的第7项,即本例中"<init>"
接下来的0x 00 08 表示方法的描述信息,指向常量池的第8项,即本例中"()V" ,表示是一个没有参数,返回类型为void的方法
接下来的0x 00 01表示方法的属性信息的个数,本例中为1
接下是1个attribute_info结构
0x 00 09表示属性名,指向常量池的第9项,即Code,表示方法的代码
0x 00 00 00 1d 表示该属性的长度,即29个字节
0x 00 01 表示max_stack
0x 00 01 表示max_locals
0x 00 00 00 05 表示code_length, 即该方法的代码为编译后为5个字节
0x 2a b7 00 01 b1 即代码
0x 00 00 表示 没有异常信息
0x 00 01 表示有一个属性信息
step 8) 属性信息
头两个字节为0x 00 01,表示有一个属性
接下是1个attribute_info结构
0x 00 0d表示属性名,指向常量池的第13项,即SourceFile,表示类的源文件
0x 00 00 00 02,属性长度,即接下来的字节个数
0x 00 0e 表示源文件名,指向常量池的第14项,即"TestClass.java"
javap对应如下:
public class org.kaka.clazz.TestClass extends java.lang.Object
SourceFile: "TestClass.java"
minor version: 0
major version: 50
Constant pool:
const #1 = Method #4.#15; // java/lang/Object."<init>":()V
const #2 = Field #3.#16; // org/kaka/clazz/TestClass.m:I
const #3 = class #17; // org/kaka/clazz/TestClass
const #4 = class #18; // java/lang/Object
const #5 = Asciz m;
const #6 = Asciz I;
const #7 = Asciz <init>;
const #8 = Asciz ()V;
const #9 = Asciz Code;
const #10 = Asciz LineNumberTable;
const #11 = Asciz inc;
const #12 = Asciz ()I;
const #13 = Asciz SourceFile;
const #14 = Asciz TestClass.java;
const #15 = NameAndType #7:#8;// "<init>":()V
const #16 = NameAndType #5:#6;// m:I
const #17 = Asciz org/kaka/clazz/TestClass;
const #18 = Asciz java/lang/Object;
{
public org.kaka.clazz.TestClass();
Code:
Stack=1, Locals=1, Args_size=1
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public int inc();
Code:
Stack=2, Locals=1, Args_size=1
0: aload_0
1: getfield #2; //Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 7: 0
}
.class
文件瘦身
1、javac A.java,大小291字节,默认编译方式,包含代码、源文件信息、代码行序号表。
2、javac -g A.java,大小422字节,调试编译方式,包含代码、源文件信息、代码行序号表、本地变量表。
3、javac -g:none A.java,大小207字节,代码编译方式,包含代码。
jvm指令
1、装载和存储指令。这类指令在局部变量和操作数栈之间传递值,iload、istore、bipush、iconst、lconst等。
2、运算指令。这类指令计算一个通常是操作数栈上的两个值的函数的结果,并把结果压回到操作数栈,iadd、isub等。
3、类型转换指令。这类指令用于JVM数值类型之间的转换,i2l、i2f、f2d等。
4、对象创建和操纵指令。这类指令用于创建类实例、创建数组、访问类域(static)和类实例的域(非static)、数组与操作数栈传递值、数组长度、类型检查等,new、newarray、anewarray、multianewarray、getfield、putfield、getstatic、putstatic、iaload、iastore、arraylength、instanceof、checkcast等。
5、操作数栈管理指令。这类指令直接操作操作数栈的值,pop、dup、swap等。
6、控制转移指令。这类指令有条件或无条件的使JVM执行一条不是控制转移指令后面的指令,ifeq、ifltif_icmpeq、lcmp、tableswitch、lookupswitch、goto、jsr、ret等。
7、方法调用和返回指令。invokevirtual调用对象的实现方法,invokeinterface调用接口的方法,invokespecial调用特殊处理的实例方法,invokestatic调用类方法;返回指令有ireturn、lreturn、freturn、dreturn、areturn和return。
解读举例如下:
35 0: new #1; //class com/asiainfo/dryr/test1/Test1Class1
36 3: dup
37 4: invokespecial #31; //Method "<init>":()V
38 7: astore_1
39 8: aload_1
第35行。new指令新创建一个对象并将对象引用压入操作数栈,#1是常数池索引,表示这个对象的类是常数池中的第1个表项中的类常量(class com/asiainfo/dryr/test1/Test1Class1)。这个指令占3个字节。
第36行。dup指令复制操作数栈顶一个字(JVM自定义存储单元,一般在32位机器上是32 bit),并将复制结果压入栈顶。这里为什么要dup一下是有理由的,因为JVM新创建一个对象后,首先需要调用<init>实例初始化方法,初始化完成后再会做保存到局部变量或调用方法的处理(类似代码:Object obj = new Object();),这样就需要在操作数栈上保留两个相同的对象引用,所以需要dup。这个指令占1个字节。
第37行。从操作数栈顶弹出对象引用,调用这个对象的实例初始化方法<init>以初始化com/asiainfo/dryr/test1/Test1Class1类的这个对象,这个初始化方法必须使用invokespecial指令调用,#31表示这个方法是常数池中的第31个表项中的方法常量。帧2被创建,并成为当前帧。这个指令占3个字节。
第39行。aload_1将第1个局部变量this(对象引用)装载到操作数栈。这个指令占1个字节。
gc收集器1
Serial收集器:Serial收集器是在client模式下默认的新生代收集器,其收集效率大约是100M左右的内存需要几十到100多毫秒;在client模式下,收集桌面应用的内存垃圾,基本上不影响用户体验。所以,一般的Java桌面应用中,直接使用Serial收集器(不需要配置参数,用默认即可)。
ParNew收集器:Serial收集器的多线程版本,这种收集器默认开通的线程数与CPU数量相同,-XX:ParallelGCThreads可以用来设置开通的线程数。可以与CMS收集器配合使用,事实上用-XX:+UseConcMarkSweepGC选择使用CMS收集器时,默认使用的就是ParNew收集器,所以不需要额外设置-XX:+UseParNewGC,设置了也不会冲突,因为会将ParNew+Serial Old作为一个备选方案;如果单独使用-XX:+UseParNewGC参数,则选择的是ParNew+Serial Old收集器组合收集器。一般情况下,在server模式下,如果选择CMS收集器,则优先选择ParNew收集器。
Parallel Scavenge收集器:关注的是吞吐量,可以这么理解,关注吞吐量,意味着强调任务更快的完成,而如CMS等关注停顿时间短的收集器,强调的是用户交互体验。在需要关注吞吐量的场合,比如数据运算服务器等,就可以使用Parallel Scavenge收集器。
Serial Old收集器:在1.5版本及以前可以与 Parallel Scavenge结合使用(事实上,也是当时Parallel Scavenge唯一能用的版本),另外就是在使用CMS收集器时的备用方案,发生 Concurrent Mode Failure时使用。如果是单独使用,Serial Old一般用在client模式中。
Parallel Old收集器:在1.6版本之后,与 Parallel Scavenge结合使用,以更好的贯彻吞吐量优先的思想,如果是关注吞吐量的服务器,建议使用Parallel Scavenge + Parallel Old 收集器。
CMS收集器:这是当前阶段使用很广的一种收集器,国内很多大的互联网公司线上服务器都使用这种垃圾收集器,笔者公司的收集器也是这种,CMS收集器以获取最短回收停顿时间为目标,非常适合对用户响应比较高的B/S架构服务器。CMSIncrementalMode: CMS收集器变种,属增量式垃圾收集器,在并发标记和并发清理时交替运行垃圾收集器和用户线程。
G1收集器:面向服务器端应用的垃圾收集器,计划未来替代CMS收集器。
1)一般来说,如果是Java桌面应用,建议采用Serial+Serial Old收集器组合,即:-XX:+UseSerialGC(-client下的默认参数)
2)在开发/测试环境,可以采用默认参数,即采用Parallel Scavenge+Serial Old收集器组合,即:-XX:+UseParallelGC(-server下的默认参数)
3)在线上运算优先的环境,建议采用Parallel Scavenge+Serial Old收集器组合,即:-XX:+UseParallelGC
4)在线上服务响应优先的环境,建议采用ParNew+CMS+Serial Old收集器组合,即:-XX:+UseConcMarkSweepGC
另外在选择了垃圾收集器组合之后,还要配置一些辅助参数,以保证收集器可以更好的工作。
1)选用了ParNew收集器,你可能需要配置4个参数: -XX:SurvivorRatio, -XX:PretenureSizeThreshold, -XX:+HandlePromotionFailure,-XX:MaxTenuringThreshold;
2)选用了 Parallel Scavenge收集器,你可能需要配置3个参数: -XX:MaxGCPauseMillis,-XX:GCTimeRatio, -XX:+UseAdaptiveSizePolicy ;
3)选用了CMS收集器,你可能需要配置3个参数: -XX:CMSInitiatingOccupancyFraction, -XX:+UseCMSCompactAtFullCollection, -XX:CMSFullGCsBeforeCompaction;
gc收集器2
Serial收集器:新生代收集器,使用停止复制算法,使用一个线程进行GC,其它工作线程暂停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式运行进行内存回收(这也是虚拟机在Client模式下运行的默认值)。
Serial收集算法:(标记)算法的第一步是标记老年代中依然存活对象。(清理)从头开始检查堆内存空间,并且只留下依然幸存的对象。(压缩)从头开始,顺序地填满堆内存空间,并且将对内存空间分成两部分:一个保存着对象,另一个空着,压缩。
ParNew收集器:新生代收集器,使用停止复制算法,Serial收集器的多线程版,用多个线程进行GC,其它工作线程暂停,关注缩短垃圾收集时间。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。
Parallel Scavenge 收集器:新生代收集器,使用停止复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃 圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适 合用户交互,提高用户体验)。使用-XX:+UseParallelGC开关控制使用 Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即 1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效)
Serial Old收集器:老年代收集器,单线程收集器,使用标记整理(整理的方法是Sweep(清理)和Compact(压缩),清理是将废弃的对象干掉,只留幸存 的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行GC,其它工作线程暂停(注意,在老年代中进行标 记整理算法清理,也需要暂停其它线程),在JDK1.5之前,Serial Old收集器与ParallelScavenge搭配使用。
Parallel Old收集器:老年代收集器,多线程,多线程机制与Parallel Scavenge差不错,使用标记整理(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清 理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。Parallel Old出现后(JDK 1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。使用-XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。
CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于获取最短回收停顿时间,使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS(原因见后面),当用户线程内存不足时,采用备用方案Serial Old收集。
CMS收集的方法:先3次标记,再1次清除,3次标记中前两次是初始标记和重新标记(此时仍然需要停止(stop the world)), 初始标记(Initial Remark)是标记GC Roots能关联到的对象(即有引用的对象),停顿时间很短;并发标记(Concurrent remark)是执行GC Roots查找引用的过程,不需要用户线程停顿;重新标记(Remark)是在初始标记和并发标记期间,有标记变动的那部分仍需要标记,所以加上这一部分 标记的过程,停顿时间比并发标记小得多,但比初始标记稍长。在完成标记之后,就开始并发清除,不需要用户线程停顿。所以在CMS清理过程中,只有初始标记和重新标记需要短暂停顿,并发标记和并发清除都不需要暂停用户线程,因此效率很高,很适合高交互的场合。CMS也有缺点,它需要消耗额外的CPU和内存资源,在CPU和内存资源紧张,CPU较少时,会加重系统负担(CMS默认启动线程数为(CPU数量+3)/4)。另外,在并发收集过程中,用户线程仍然在运行,仍然产生内存垃圾,所以可能产生“浮动垃圾”,本次无法清理,只能下一次Full GC才清理,因此在GC期间,需要预留足够的内存给用户线程使用。所以使用CMS的收集器并不是老年代满了才触发Full GC,而是在使用了一大半(默认68%,即2/3,使用-XX:CMSInitiatingOccupancyFraction来设置)的时候就要进行Full GC,如果用户线程消耗内存不是特别大,可以适当调高-XX:CMSInitiatingOccupancyFraction以降低GC次数,提高性能,如果预留的用户线程内存不够,则会触发Concurrent Mode Failure,此时,将触发备用方案:使用Serial Old 收集器进行收集,但这样停顿时间就长了,因此-XX:CMSInitiatingOccupancyFraction不宜设的过大。还有,CMS采用的是标记清除算法,会导致内存碎片的产生,可以使用-XX:+UseCMSCompactAtFullCollection来设置是否在Full GC之后进行碎片整理,用-XX:CMSFullGCsBeforeCompaction来设置在执行多少次不压缩的Full GC之后,来一次带压缩的Full GC。
G1收集器:在JDK1.7中正式发布,与现状的新生代、老年代概念有很大不同,目前使用较少,不做介绍。
说明:在新生代采用的停止复制算法中,“停 止(Stop-the-world)”的意义是在回收内存时,需要暂停其他所 有线程的执行。这个是很低效的,现在的各种新生代收集器越来越优化这一点,但仍然只是将停止的时间变短,并未彻底取消停止。
gc策略对比
类别
|
serial collector
|
parallel collector ( throughput collector ) |
concurrent collector (concurrent low pause collector) |
介绍
|
单线程收集器 使用单线程去完成所有的gc工作,没有线程间的通信,这种方式会相对高效 |
并行收集器 使用多线程的方式,利用多CUP来提高GC的效率。主要以到达一定的吞吐量为目标 |
并发收集器 使用多线程的方式,利用多CUP来提高GC的效率。并发完成大部分工作,使得gc pause短 |
试用场景
|
单处理器机器且没有pause time的要求
|
适用于科学技术和后台处理,有中规模/大规模数据集大小的应用且运行在多处理器上,关注吞吐量(throughput)
|
适合中规模/大规模数据集大小的应用,应用服务器,电信领域。关注response time,而不是throughput
|
使用
|
Client
模式下默认
可用 -XX:+UseSerialGC 强制使用 优点 : 对 server 应用没什么优点 缺点 : 慢 , 不能充分发挥硬件资源 |
Server
模式下默认
--YGC:PS FGC:Parallel MSC ,可用 -XX:+UseParallelGC 或 -XX:+UseParallelOldGC 强制指定, --ParallelGC 代表 FGC 为 Parallel MSC , --ParallelOldGC 代表 FGC 为 Parallel Compacting 。优点 : 高效。缺点 : 当 heap 变大后 , 造成的暂停时间会变得比较长 |
可用-XX:+UseConcMarkSweepGC强制指定 优点: 对old进行回收时,对应用造成的暂停时间非常短,适合对latency要求比较高的应用 缺点: 1.内存碎片和浮动垃圾 2.old去的内存分配效率低 3.回收的整个耗时比较长 4.和应用争抢CPU |
内存回收触发
|
YGC eden 空间不足 FGC old 空间不足 perm 空间不足 显示调用 System.gc() , 包括 RMI 等的定时触发 YGC时的悲观策略 dump live 的内存信息时 (jmap –dump:live) |
YGC eden 空间不足 FGC old 空间不足 perm 空间不足 显示调用 System.gc() , 包括 RMI 等的定时触发 YGC时的悲观策略 --YGC 前 &YGC 后 dump live 的内存信息时 (jmap –dump:live) |
YGC eden 空间不足 CMS GC 1.old Gen 的使用率大的一定的比率 默认为 92% 2. 配置了 CMSClassUnloadingEnabled, 且 Perm Gen 的使用达到一定的比率 默认为 92% 3.Hotspot 自己根据估计决定是否要触法 4. 在配置了 ExplictGCInvokesConcurrent 的情况下显示调用了 System.gc. Full GC(Serial MSC) promotion failed 或 concurrent Mode Failure 时 ; |
内存回收触发时发生了什么
|
YGC 清空 eden+from 中所有 no ref 的对象占用的内存。将 eden+from 中的所有存活的对象 copy 到 to 中。在这个过程中一些对象将晋升到 old 中 :--to 放不下的, -- 存活次数超过 tenuring threshold 的,重新计算 Tenuring Threshold 。单线程做以上动作。全程暂停应用。 FGC 如果配置了 CollectGen0First, 则先触发 YGC 。清空 heap 中 no ref 的对象 ,permgen 中已经被卸载的 classloader 中加载的 class 的信息。单线程做以上动作。全程暂停应用 |
YGC 同 serial 动作基本相同 , 不同点 : 1. 多线程处理。 2.YGC 的最后不仅重新计算 Tenuring Threshold, 还会重新调整 Eden 和 From 的大小 FGC 1. 如配置了 ScavengeBeforeFullGC( 默认 ), 则先触发 YGC 。 2.MSC: 清空 heap 中的 no ref 对象 ,permgen 中已经被卸载的 classloader 中加载的 class 信息 , 并进行压缩。 3.Compacting: 清空 heap 中部分 no ref 的对象 ,permgen 中已经被卸载的 classloader 中加载的 class 信息 , 并进行部分压缩。多线程做以上动作 . |
YGC 同 serial 动作基本相同 , 不同点 : 1. 多线程处理 CMSGC: 1.old gen 到达比率时只清除 old gen 中 no ref 的对象所占用的空间 2.perm gen 到达比率时只清除已被清除的 classloader 加载的 class 信息 FGC 同 serial |
细节参数
| 可用-XX:+UseSerialGC强制使用
-XX:SurvivorRatio=x,控制eden/s0/s1大小。-XX:MaxTenuringThreshold,用于控制对象在新生代存活的最大次数 -XX:PretenureSizeThreshold=x,控制超过多大的字节的对象就在old分配. |
-XX:SurvivorRatio=x,
控制
eden/s0/s1
的大小
-XX:MaxTenuringThreshold, 用于控制对象在新生代存活的最大次数 -XX:UseAdaptiveSizePolicy 去掉 YGC 后动态调整 eden from 已经 tenuringthreshold 的动作 -XX:ParallelGCThreads 设置并行的线程数 |
-XX:CMSInitiatingOccupancyFraction
设置
old gen
使用到达多少比率时触发
-XX:CMSInitiatingPermOccupancyFraction, 设置 Perm Gen 使用到达多少比率时触发 -XX:+UseCMSInitiatingOccupancyOnly 禁止 hostspot 自行触发 CMS GC |
注:
1、throughput collector与concurrent low pause collector区别是throughput collector只在young area使用使用多线程,而concurrent low pause collector则在tenured generation也使用多线程。
2、根据官方文档,他们俩个需要在多CPU的情况下,才能发挥作用。在一个CPU的情况下,会不如默认的serial collector,因为线程管理需要耗费CPU资源。而在两个CPU的情况下,也提高不大。只是在更多CPU的情况下,才会有所提高。当然 concurrent low pause collector有一种模式可以在CPU较少的机器上,提供尽可能少的停顿的模式,见CMS GC Incremental mode。
3、当要使用throughput collector时,在java opt里加上-XX:+UseParallelGC,启动throughput collector收集。也可加上-XX:ParallelGCThreads=<desired number>来改变线程数。还有两个参数 -XX:MaxGCPauseMillis=<nnn>和 -XX:GCTimeRatio=<nnn>,MaxGCPauseMillis=<nnn>用来控制最大暂停时间,而-XX: GCTimeRatio可以提高GC说占CPU的比,以最大话的减小heap。
4、gc选择标准:
1)重点考虑peak application performance(高性能),没有pause time太严格要求,让vm选择或者UseParallelGC+UseParallelOldGC(optionally)。
2)重点考虑response time,pause time要小,UseConcMarkSweepGC。
gc机制
1、方法区,在sun的hotspot中其实就是持久化代,也有gc,主要对常量池gc和已加载类的卸载,但很困难,一般不太做过多考虑。
2、年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接 被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉,这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在Eden区上的GC。
1)绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;
2)当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);
3)此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0;
4)当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。
5)当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。
6)从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活 着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方 式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下高效,如果在老年代采用停止复制,则挺悲剧的。
7)在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread- Local Allocation Buffers),这两种技术的做法分别是:由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对 象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的,将Eden区分为若干 段,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内 存。
3、年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次 Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC,也叫 Full GC。
1)可以使用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄。
2)如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。
3)可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。
gc机制
年轻代:
在新生代中,使用“停止-复制”算法进行清理,将新生代内存分为2部分,1部分 Eden区较大,1部分Survivor比较小,并被划分为两个等量的部分。每次进行清理时,将Eden区和一个Survivor中仍然存活的对象拷贝到 另一个Survivor中,然后清理掉Eden和刚才的Survivor。
这里也可以发现,停止复制算法中,用来复制的两部分并不总是相等的(传统的停止复制算法两部分内存相等,但新生代中使用1个大的Eden区和2个小的Survivor区来避免这个问题)
由于绝大部分的对象都是短命的,甚至存活不到Survivor中,所以,Eden区与Survivor的比例较大,HotSpot默认是 8:1,即分别占新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下来的内存超过了10%,则需要将一部分对象分配到 老年代。用-XX:SurvivorRatio参数来配置Eden区域Survivor区的容量比值,默认是8,代表Eden:Survivor1:Survivor2=8:1:1.
老年代:
老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-整理算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。
在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次Full GC,否则,就查看是否设 置了-XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行MinorGC,此时可以容忍内存分配失败;如果不 允许,则仍然进行Full GC(这代表着如果设置-XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)。
方法区(永久代):
永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:
1)类的所有实例都已经被回收
2)加载类的ClassLoader已经被回收
3)类对象的Class对象没有被引用(即没有通过反射引用该类的地方)
永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收。HotSpot提供-Xnoclassgc进行控制
使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看类加载和卸载信息
-verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;
-XX:+TraceClassUnLoading需要fastdebug版HotSpot支持
gc原理
1、原理:把对象分为年青代(young)、年老代(tenured)、持久代(perm),对不同生命周期的对象使用不同的回收算法。(基于对对象生命周期的分析)。概括地说,对jvm内存进行标记,确定哪些需要gc,根据gc策略,确定什么时候执行gc,以及如何gc。
2、年青代,分为三个区。一个eden区,两个survivor区。大部分对象都是在eden区生成的。当eden区满时,还存活的对象将被复制到两个survivor区中的一个,当这个survivor区满时,此区存活的对象将被复制到另外一个survivor区,当第二个suervivor区满时,从第一个survivor区复制过来并且存活的对象,将被复制到年老区。需要注意,sruvivor区是对称的,没有先后关系,所以同一个区中可能同时存在从eden复制过来的对象,和从前一个survivor区复制过来的对象,而复制到年老区的只有从第一个survivor区过来的对象。而且survivor区总有一个是空的。
3、年老代,存放从年青代过来的对象,生命周期较长。
4、持久代,存放静态文件,如类、方法等。持久代对垃圾回收没有显著影响,但有些应用可能动态生成或者调用一些class,例如hibernate,在这种时候需要设置一个比较大的持久代来存放这些运行过程中新增的类。
5、举例子:当程序中生成对象时,正常会在年青代中分配空间,如果是过大的对象也可能直接在年老代中生成(某程序运行时每次回生成一个十兆的空间用于收发消息,这部分内存就会直接在年老代中分配)。年青代在空间分配完的时候就会发起垃圾回收,大部分内存会被回收,一部分存活下来的内存会被拷贝到survivor区的from区,经过多次回收后,如果from区的内存也分配完毕,就会发生垃圾回收,然后将剩余的存活对象拷贝到to区,等到to区也满的时候,就会再次发生垃圾回收,然后将存活的对象拷贝到年老区。
6、通常的垃圾回收指堆内回收,确实只有堆内的内容是动态申请分配的,所以上述对象的年青代和年老代都是指的jvm的heap空间,而持久代则是在方法区,不属于heap。
7、建议:手动将无用对象置null,加快回收。采用对象池减少对象数量。配置jvm参数提高垃圾回收速度,但要经过实体机的长期测试来看实际效果。
8、垃圾回收算法:停止-执行、标记-清除(复制)算法。
9、垃圾回收分为三类:单线程的Serial算法、多线程的并行算法、非停止-执行的并发算法。
10、jvm会根据机器的硬件配置对每个内存带选择适合的回收算法,比如cpu内核多于一个,则会对年青代选择多线程的并行算法。
11、并行算法使用多线程进行回收,并暂停程序执行。而并发算法也是多线程回收,但不停止程序执行。所以并发算法适用于交互性比较高的程序,经过观察,并发算法会减少年青代的大小,其实就是使用了一个大的年老代,这反过来跟并行算法比的话,吞吐量要低,而并行算法吞吐量高。
12、垃圾回收的时机:
1)当年青代满时,会引发一次普通的gc,只回收年青代。这里指eden满,survivor满不会引发gc。
2)当年老代满时,会引发full gc,同时回收年青代、年老代。
3)当持久化代满时,会引发full gc,将导致class、method元信息的卸载。
17、为什么崩溃前gc的时间越来越长?
答:gc分为两部分:内存标记、清除(复制)。标记部分只要是内存大小固定则时间是不变的,变的是复制的部分,因为每次gc都有一些回收不掉的内存,所以增加了复制量,导致时间延长,这是判断泄漏的依据。
18、为什么full gc的次数越来越多?
答:因为内存的积累,逐渐耗尽了年老代的内存,导致新对象分配没有更多的空间,从而导致频繁的垃圾回收。
19、为什么年老代占用的内存越来越大?
答:因为年老代的内存无法被回收,越来越多的被拷贝的年老代。
13、outfomemory出现时机:
1)jvm的98%的时间都花费在内存回收上。
2)每次回收的内存小于2%。
3)满足1)和2)将触发outofmemory,留给系统微少的时间来做down之前的动作,如打印heap dump。
14、outofmemory前的现象:
1)每次gc的时间越来越长,由之前的10ms延长到50ms,full gc的时间也由之前的0.5s延长到5s。
2)full gc的次数越老越多,最频繁的时候不到1分钟就进行一次full gc。
3)年老代的内存越来越大,并且每次full gc后年老代没有内存释放。
4)最后系统无法响应新的请求,逐渐到达outofmemory的临界值。
15、outofmemory处理办法:
1)生成dump文件:通过jmx的mbean生成当前heap信息,一般是整个堆大小的hprof文件,如果没有启动jmx则可以通过java的jmap命令来生成该文件。
2)分析dump文件:可以通过visual vm、imb heapanalyzer、jdk自带hprof工具来打开查看dump文件。最好是使用eclipse专门的静态内存分析工具Mat。
3)通过Mat可以看到,哪些对象被怀疑泄漏,哪些对象占用的空间最大,及对象的调用关系。
4)通过Mat或Jmx还可以分析线程状态,可以观察到线程被阻塞在哪个对象上,从而判断系统的瓶颈。
16、崩溃分类:
1)java.lang.OutOfMemoryError: PermGen space:像spring、hibernate等框架使用动态生成类,而这些类不能被gc自动释放,导致崩溃。加大PermSize即可。
2)java.lang.OutOfMemoryError: heap space:堆内存不足。
3)java.lang.OutOfMemoryError: GC overhead limit exceeded:jdk6后新增,发生在gc时间过长但只释放很小空间的时候,是一种保护机制。可通过-XX:-UseGCOverheadLimit来关闭该功能。
4)java.lang.OutOfMemoryError: runtime constant pool:方法区内存不足。
5)java.lang.StackOverflowError:栈内线程超过允许量。native method要求更大内存,可通过-XX:ThreadStackSize来提高
gc日志格式
-XX:+PrintGCTimeStamps输出格式:
289.556: [GC [PSYoungGen: 314113K->15937K(300928K)] 405513K->107901K(407680K), 0.0178568 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
293.271: [GC [PSYoungGen: 300865K->6577K(310720K)] 392829K->108873K(417472K), 0.0176464 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
293.271是从jvm启动直到垃圾收集发生所经历的时间,GC表示这是一次Minor GC(新生代垃圾收集);
[PSYoungGen: 300865K->6577K(310720K)] 提供了新生代空间的信息,PSYoungGen,表示新生代使用的是多线程垃圾收集器Parallel Scavenge。300865K表示垃圾收集之前新生代占用空间,6577K表示垃圾收集之后新生代的空间。新生代又细分为一个Eden区和两个Survivor区,Minor GC之后Eden区为空,6577K就是Survivor占用的空间。括号里的310720K表示整个年轻代的大小。392829K->108873K(417472K),表示垃圾收集之前(392829K)与之后(108873K)Java堆的大小(总堆417472K,堆大小包括新生代和年老代)
由新生代和Java堆占用大小可以算出年老代占用空间,如,Java堆大小417472K,新生代大小310720K那么年老代占用空间是417472K-310720K=106752k;垃圾收集之前老年代占用的空间为392829K-300865K=91964k 垃圾收集之后老年代占用空间108873K-6577K=102296k.0.0176464 secs表示垃圾收集过程所消耗的时间。
[Times: user=0.06 sys=0.00, real=0.01 secs] 提供cpu使用及时间消耗,user是用户模式垃圾收集消耗的cpu时间,实例中垃圾收集器消耗了0.06秒用户态cpu时间,sys是消耗系统态cpu时间,real是指垃圾收集器消耗的实际时间。
-XX:+PrintGCDetails输出格式:
293.289: [Full GC [PSYoungGen: 6577K->0K(310720K)]
[PSOldGen: 102295K->102198K(134208K)] 108873K->102198K(444928K)
[PSPermGen: 59082K->58479K(104192K)], 0.3332354 secs]
[Times: user=0.33 sys=0.00, real=0.33 secs]
Full GC表示执行全局垃圾回收
[PSYoungGen: 6577K->0K(310720K)] 提供新生代空间信息,解释同上
[PSOldGen: 102295K->102198K(134208K)]提供了年老代空间信息;
108873K->102198K(444928K)整个堆空间信息;
[PSPermGen: 59082K->58479K(104192K)]提供了持久代空间信息;
jvm参数表达
在jvm的参数中,有3种表示方法
1、标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
2、非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;
3、非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用(但是,这些参数往往是非常有用的);分3类:
1
)性能参数(
Performance Options
):用于
JVM
的性能调优和内存分配控制,如初始化内存大小的设置;
2
)行为参数(
Behavioral Options
):用于改变
JVM
的基础行为,如
GC
的方式和算法的选择;
3
)调试参数(
Debugging Options
):用于监控、打印、输出等
jvm
参数,用于显示
jvm
更加详细的信息;
对于非
Stable
参数,使用方法有
4
种:
1
、
-XX:+<option>
启用选项
2
、
-XX:-<option>
不启用选项
3
、
-XX:<option>=<number>
给选项设置一个数字类型值,可跟单位,例如
32k, 1024m, 2g
4
、
-XX:<option>=<string>
给选项设置一个字符串值,例如
-XX:HeapDumpPath=./dump.core
jvm参数列表
jvm标准参数:
1、-client,设置jvm使用client模式,这是一般在pc机器上使用的模式,启动很快,但性能和内存管理效率并不高;多用于桌面应用。
2、-server,使用server模式,启动速度虽然慢(比client模式慢10%左右),但是性能和内存管理效率很高,适用于服务器,用于生成环境、开发环境或测试环境的服务端。
3、-classpath / -cp,JVM加载和搜索文件的目录路径,多个路径用;分隔。注意,如果使用了-classpath,JVM就不会再搜索环境变量中定义的CLASSPATH路径。
4、-DpropertyName=value,定义系统的全局属性值,如配置文件地址等,如果value有空格,可以用-Dname="space string"这样的形式来定义,用System.getProperty("propertyName")可以获得这些定义的属性值,在代码中也可以用System.setProperty("propertyName","value")的形式来定义属性。
5、-verbose:class,输出jvm载入类的相关信息,当jvm报告说找不到类或者类冲突时可此进行诊断。
6、-verbose:gc,输出每次GC的相关情况。
7、-verbose:jni,输出native方法调用的相关情况,一般用于诊断jni调用错误信息。
jvm非标准参数
1、-Xms:堆内存初始由-Xms指定,默认为物理内存的1/64。
2、-Xmx:堆内存最大由-Xmx指定,默认为物理内存的1/4,最大不要超过物理内存的80%。默认空余堆内存小于40%时,会增大堆直到-Xmx的最大限制,可由XX:MinHeapFreeRatio指定。默认空余堆内存大约70%时,会减小堆直到-Xms的最小限制,可由XX:MaxHeapFreeRatio指定。所以,一般设置-Xms、-Xmx相等,以避免每次GC后调整堆的大小。
3、-Xmn:年青代大小,如果程序需要创建大量临时对象,可调大。反之调小。通常-Xmn为1/4-Xmx大小。只能使用在JDK1.4或之后的版本中。
4、-Xss:线程栈大小,web应用一般256就足够。如果程序有大规模递归,经测试后可调大。
5、-Xnoclassgc,取消方法区的类回收,如弱引用、软引用、虚引用等情况。因为其阻止内存回收,所以可能会导致OutOfMemoryError错误,慎用;
6、-Xrs,减少JVM对操作系统信号(OS Signals)的使用(JDK1.3.1之后才有效),当此参数被设置之后,jvm将不接收控制台的控制handler,以防止与在后台以服务形式运行的JVM冲突
7、-Xprof,跟踪正运行的程序,并将跟踪数据在标准输出输出;适合于开发环境调试。
8、-Xincgc,开启增量gc(默认为关闭);这有助于减少长时间GC时应用程序出现的停顿;但由于可能和应用程序并发执行,所以会降低CPU对应用的处理能力。
9、-Xloggc,与-verbose:gc功能类似,只是将每次GC事件的相关情况记录到一个文件中,文件的位置最好在本地,以避免网络的潜在问题。若与verbose命令同时出现在命令行中,则以-Xloggc为准。
jvm非Stable参数
常用性能参数,性能参数往往用来定义内存分配的大小和比例,相比于行为参数和调试参数,一个比较明显的区别是性能参数后面往往跟的有数值。
1、-XX:NewSize,年青代内存初始值,即方法区,默认为物理内存的1/64。
2、-XX:MaxNewSize,年青代内存的最大值,默认为物理内存的1/4。JDK1.3/1.4版本中。
3、-XX:PermSize,持久代初始值,即方法区,默认为物理内存的1/64。
4、-XX:MaxPermSize,持久代最大值,默认为物理内存1/4。一般将PermSisze和MaxPermSize都设置为128m,不必过大。但像spring、hibernate等框架使用动态生成类,需调大。
5、-XX:SurvivorRatio,年青代中,eden和一个survivor的比例。如果8,在年青代10m情况下,eden为8m,每个survivor各1m。
6、-XX:NewRatio,年青代与年老代比例,默认1:2。在使用CMS gc时,此参数失效。
7、-XX:MinHeapFreeRatio,GC后堆中空闲量占的最小比例,小于该值,则堆内存会增加。Xms==Xmx情况下,该设置无效。
8、-XX:MaxHeapFreeRatio,GC后堆中空闲量占的最大比例,大于该值,则堆内存会减少。Xms==Xmx情况下,该设置无效。
9、-XX:ThreadStackSize,提高native method内存。也有的说与-Xss相同,但是jdk1.6以前的,1.6之后主线程以-Xss为准,其他线程以-XX:ThreadStackSize为准。
10、-XX:MaxTenuringThreshold。 在年青代最大存活次数。坚持过MinorGC的次数,每坚持过一次,该值就增加1,大于该值会进入老年代。
11、-XX:LargePageSizeInBytes,指定堆分页页面大小,如4m。
12、-XX:PretenureSizeThreshold,控制超过多大的字节的对象就直接在年老代中分配.
13、-XX:UseAdaptiveSizePolicy 去掉YGC后动态调整eden from已经tenuringthreshold的动作
14、-XX:ReservedCodeCacheSize,保留代码占用的内存容量。
15、-XX:MaxDirectMemorySize,nio的buffer分配内存比较特殊,读写流可共享内存,如果有大量数据交互,可能导致outofmemory。
常用行为参数,主要用来选择使用什么样的垃圾收集器组合,以及控制运行过程中的GC策略等。
1、-XX:+UseSerialGC,串行GC,即采用Serial+Serial Old模式。
2、-XX:-UseParallelGC,并行GC,即采用Parallel Scavenge+Serial Old收集器组合(-Server模式下的默认组合)。
3、-XX:+UseConcMarkSweepGC,使用ParNew+CMS+Serial Old组合并发收集,优先使用ParNew+CMS,当用户线程内存不足时,采用备用方案Serial Old收集。
4、-XX:+UseParNewGC,建议年轻代使用并行回收,同时按照操作系统processer的个数设置并行线程数,理论上<=操作系统processer个数
5、-XX:+UseParalleOldGC,建议年老代使用并发回收,并发回收以降低中断次数,减少中断时间为目标,适用于对系统响应时间有较高要求的服务
6、-XX:ParallelGCThreads,设置并行gc的线程数,在+UseParNewGC的情况下使用
7、-XX:GCTimeRatio=99,设置用户执行时间占总时间的比例(默认值99,即1%的时间用于GC)
8、-XX:MaxGCPauseMillis=time,设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效)
9、-XX:-DisableExplicitGC,禁止调用System.gc();但jvm的gc仍然有效
10、-XX:+ScavengeBeforeFullGC,新生代GC优先于Full GC执行
11、-XX:CMSInitiatingOccupancyFraction,设置年老代使用到达多少比率时触发,预留足够的空间给年青代gc,防止从年青代gc过来一个较大的内存块,而年老代无足够预留空间,无法提供内存快用于回收后的分配,引起强制的full gc,造成较长时间的线程中断。
12、-XX:CMSInitiatingPermOccupancyFraction,设置持久代使用到达多少比率时触发
13、-XX:CMSFullGCsBeforeCompaction,设置多少次full gc后进行内存压缩,由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理。
14、-XX:+UseCMSCompactAtFullCollection,设置在FULL GC的时候,对年老代的压缩;CMS是不会移动内存的,因此这个非常容易产生碎片,导致内存不够用,因此内存的压缩这个时候就会被启用。增加这个参数是个好习惯。可能会影响性能,但是可以消除碎片。
15、-XX:+UseCMSInitiatingOccupancyOnly,禁止hostspot自行触发CMS GC
常用调试参数,主要用于监控和打印GC的信息。
1、-XX:ErrorFile=./hs_err_pid<pid>.log,保存错误日志或者数据到文件中
2、-XX:-PrintGC 每次GC时打印相关信息
3、-XX:-PrintGCDetails 每次GC时打印详细信息
4、-XX:-PrintGCTimeStamps 打印每次GC的时间戳
4、-XX:+PrintGCDateStamps
5、-XX:+PrintTenuringDistribution
6、-XX:+PrintGCApplicationStoppedTime
5、-XX:HeapDumpPath=./java_pid<pid>.hprof,指定导出堆信息时的路径或文件名
6、-XX:-HeapDumpOnOutOfMemoryError,当首次遭遇OOM时导出此时堆中相关信息
7、-XX:-TraceClassLoading 跟踪类的加载信息
8、-XX:-TraceClassLoadingPreorder 跟踪被引用到的所有类的加载信息
9、-XX:-TraceClassResolution 跟踪常量池
10、-XX:-TraceClassUnloading 跟踪类的卸载信息
11、-XX:-TraceLoaderConstraints 跟踪类加载器约束的相关信息
12、-XX:-CITime,打印消耗在JIT编译的时间
13、-XX:-ExtendedDTraceProbes,开启solaris特有的dtrace探针
14、-XX:OnError="<cmd args>;<cmd args>",出现致命ERROR之后运行自定义命令
15、-XX:OnOutOfMemoryError="<cmd args>;<cmd args>",当首次遭遇OOM时执行自定义命令
16、-XX:-PrintClassHistogram,遇到Ctrl-Break后打印类实例的柱状信息,与jmap -histo功能相同
17、-XX:-PrintConcurrentLocks 遇到Ctrl-Break后打印并发锁的相关信息,与jstack -l功能相同
18、-XX:-PrintCommandLineFlags 打印在命令行中出现过的标记
19、-XX:-PrintCompilation 当一个方法被编译时打印相关信息
jvm调优原则
1、调优目标:
1)gc时间足够小
2)gc次数足够少
3)发生full gc的周期足够长
2、目标1)和2)是相悖的,要想gc时间小则堆要小。要想gc少则堆要大。
3、jvm堆通过-Xms和-Xmx设置,为防止堆在最大值、最小值之间收缩,通常设置两值相等。
4、年青代和年老代将根据默认比例1:2分配堆内存,可以通过NewRadio来调整。
5、年青代可以通过-XX:newSize和-XX:MaxNewSize来设置,通过为防止在最大值、最小值之间收缩,通常设置两值相等。
6、年青代和年老代设置多大才算合理呢?这个问题没有答案,否则也就不会有调优了。观察下二者大小变化有哪些影响。
1)更大的年青代必然导致更小的年老代,大的年青代会延长普通gc周期,但会增加每次gc的时间;小的年老代会导致更频繁的full gc。
2)更小的年青代必然导致更大的年老代,小的年青代会导致普通gc频繁,但每次gc的时间会更短;大的年老代会减少full gc的频率。
3)如何选择应该依赖对象生命周期的分布情况:
(1)如果程序存在大量的临时对象,应该选择更大的年青代。
(2)如果程序存在相对较多的持久对象,年老代应该适当增加。
4)但是,大多数程序都没有上述的这样明显的特征,在抉择时应该根据以下两点:
(1)本着full gc尽量少的原则,让年老代尽量缓存常用对象,jvm默认比例1:2也是这个道理。
(2)通过观察程序一段时间,看在峰值时年老代会占用多少内存。在不影响full gc的前提下,根据实际情况加大年青代,比如比例改为1:1。但应该给年老代至少保留1/3的增长空间。
7、在配置较好的机器上,比如多核、大内存,可以为年老代选择并行收集算法:-XX:+UseParalleOldGC,默认为Serial。
8、线程栈的设置:每个线程默认会开启1M的栈,用于存放栈帧、参数、变量等,对大多数程序而言这个设置太大了,一般256k就足够。理论上在内存不变的情况下,减少每个线程的栈,可以产生更多的线程,但这实际上还受限于操作系统。
9、可以通过下面的参数打印heap dump信息:
-XX:HeapDumpPath
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/usr/aaa/dump/heap_trace.txt
可以通过下面的参数打印outofmemory信息:
-XX:+HeapDumpOnOutOfMemoryError
jvm调优考量
1)-XX:PermSize尽量比-XX:MaxPermSize小,-XX:MaxPermSize>= 2 * -XX:PermSize, -XX:PermSize> 64m,一般对于4G内存,-XX:MaxPermSize不会超过256m;
2)-Xms = -Xmx(线上Server模式),以防止抖动,大小受操作系统和内存大小限制,如果是32位系统,则一般-Xms设置为1g-2g(假设有4g内存),在64位系统上,没有限制,不过一般为机器最大内存的一半左右;
3)-Xmn,在开发环境下,可以用-XX:NewSize和-XX:MaxNewSize来设置新生代的大小(-XX:NewSize<=-XX:MaxNewSize),在生产环境,建议只设置-Xmn,一般-Xmn的大小是-Xms的1/2左右,不要设置的过大或过小,过大导致老年代变小,频繁Full GC,过小导致minor GC频繁。如果不设置-Xmn,可以采用-XX:NewRatio=2来设置,也是一样的效果;
4)-Xss一般是不需要改的,默认值即可。
5)-XX:SurvivorRatio一般设置8-10左右,推荐设置为10,也即:Survivor区的大小是Eden区的1/10,一般来说,普通的Java程序应用,一次minorGC后,至少98%-99%的对象,都会消亡,所以,survivor区设置为Eden区的1/10左右,能使Survivor区容纳下10-20次的minor GC才满,然后再进入老年代,这个与 -XX:MaxTenuringThreshold的默认值15次也相匹配的。如果XX:SurvivorRatio设置的太小,会导致本来能通过minor回收掉的对象提前进入老年代,产生不必要的full gc;如果XX:SurvivorRatio设置的太大,会导致Eden区相应的被压缩。
6)-XX:MaxTenuringThreshold默认为15,也就是说,经过15次Survivor轮换(即15次minor GC),就进入老年代, 如果设置的小的话,则年轻代对象在survivor中存活的时间减小,提前进入年老代,对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概率。需要注意的是,设置了 -XX:MaxTenuringThreshold,并不代表着,对象一定在年轻代存活15次才被晋升进入老年代,它只是一个最大值,事实上,存在一个动态计算机制,计算每次晋入老年代的阈值,取阈值和MaxTenuringThreshold中较小的一个为准。
7)-XX:PretenureSizeThreshold一般采用默认值即可。
jvm调优方法
一切都是为了这一步,调优,在调优之前,我们需要记住下面的原则:
1)多数的Java应用不需要在服务器上进行GC优化;
2)多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;
3)在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合);
4)减少创建对象的数量;
5)减少使用全局变量和大对象;
6)GC优化是到最后不得已才采用的手段;
7)在实际使用中,分析GC情况优化代码比优化GC参数要多得多;
GC优化的目的有两个(http://www.360doc.com/content/13/0305/10/15643_269388816.shtml):
1)将转移到老年代的对象数量降低到最小;
2)减少full GC的执行时间;
为了达到上面的目的,一般地,你需要做的事情有:
1)减少使用全局变量和大对象;
2)调整新生代的大小到最合适;
3)设置老年代的大小为最合适;
4)选择合适的GC收集器;
在上面的4条方法中,用了几个“合适”,那究竟什么才算合适,一般的,请参考上面“收集器搭配”和“启动内存分配”两节中的建议。但这些建议不是万能的,需要根据您的机器和应用情况进行发展和变化,实际操作中,可以将两台机器分别设置成不同GC参数,并且进行对比,选用那些确实提高了性能或减少GC时间的参数。
真正熟练的使用GC调优,是建立在多次进行GC监控和调优的实战经验上的,进行监控和调优的一般步骤为:
1,监控GC的状态
使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化;
2,分析结果,判断是否需要优化
如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化;如果GC时间超过1-3秒,或者频繁GC,则必须优化;
注:如果满足下面的指标,则一般不需要进行GC:
1)Minor GC执行时间不到50ms;
2)Minor GC执行不频繁,约10秒一次;
3)Full GC执行时间不到1s;
4)Full GC执行频率不算频繁,不低于10分钟1次;
3,调整GC类型和内存分配
如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择;
4,不断的分析和调整
通过不断的试验和试错,分析并找到最合适的参数
5,全面应用参数
如果找到了最合适的参数,则将这些参数应用到所有服务器,并进行后续跟踪。
jvm调优监控
通过监控GC,我们可以搞清楚很多问题,如:
1,minor GC和full GC的频率;
2,执行一次GC所消耗的时间;
3,新生代的对象何时被移到老生代以及花费了多少时间;
4,每次GC中,其它线程暂停(Stop the world)的时间;
5,每次GC的效果如何,是否不理想;
jvm调优监控-jps
jps命令用于查询正在运行的JVM进程,常用的参数为:
-q:只输出LVMID,省略主类的名称
-m:输出虚拟机进程启动时传给主类main()函数的参数
-l:输出主类的全类名,如果进程执行的是Jar包,输出Jar路径
-v:输出虚拟机进程启动时JVM参数
命令格式:jps [option] [hostid]
一个简单的例子:
在上图中,有一个vid为309的apache进程在提供web服务。
jvm调优监控-jstat
jstat可以实时显示本地或远程JVM进程中类装载、内存、垃圾收集、JIT编译等数据(如果要显示远程JVM信息,需要远程主机开启RMI支持)。如果在服务启动时没有指定启动参数-verbose:gc,则可以用jstat实时查看gc情况。
jstat有如下选项:
-class:监视类装载、卸载数量、总空间及类装载所耗费的时间
-gc:监听Java堆状况,包括Eden区、两个Survivor区、老年代、永久代等的容量,以用空间、GC时间合计等信息
-gccapacity:监视内容与-gc基本相同,但输出主要关注java堆各个区域使用到的最大和最小空间
-gcutil:监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause:与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因
-gcnew:监视新生代GC状况
-gcnewcapacity:监视内同与-gcnew基本相同,输出主要关注使用到的最大和最小空间
-gcold:监视老年代GC情况
-gcoldcapacity:监视内同与-gcold基本相同,输出主要关注使用到的最大和最小空间
-gcpermcapacity:输出永久代使用到最大和最小空间
-compiler:输出JIT编译器编译过的方法、耗时等信息
-printcompilation:输出已经被JIT编译的方法
命令格式:jstat [option vmid [interval[s|ms] [count]]]
jstat可以监控远程机器,命令格式中VMID和LVMID特别说明:如果是本地虚拟机进程,VMID和LVMID是一致的,如果是远程虚拟机进程,那么VMID格式是: [protocol:][//]lvmid[@hostname[:port]/servername],如果省略interval和count,则只查询一次
查看gc情况的例子:
在图中,命令sudo jstat -gc 309 1000 5代表着:搜集vid为309的java进程的整体gc状态, 每1000ms收集一次,共收集5次;XXXC表示该区容量,XXXU表示该区使用量,各列解释如下:
S0C:S0区容量(S1区相同,略)
S0U:S0区已使用
EC:E区容量
EU:E区已使用
OC:老年代容量
OU:老年代已使用
PC:Perm容量
PU:Perm区已使用
YGC:Young GC(Minor GC)次数
YGCT:Young GC总耗时
FGC:Full GC次数
FGCT:Full GC总耗时
GCT:GC总耗时
用gcutil查看内存的例子:
图中的各列与用gc参数时基本一致,不同的是这里显示的是已占用的百分比,如S0为86.53,代表着S0区已使用了86.53%
jvm调优监控-jinfo
用于查询当前运行这的JVM属性和参数的值。
jinfo可以使用如下选项:
-flag:显示未被显示指定的参数的系统默认值
-flag [+|-]name或-flag name=value: 修改部分参数
-sysprops:打印虚拟机进程的System.getProperties()
命令格式:jinfo [option] pid
jvm调优监控-jmap
用于显示当前Java堆和永久代的详细信息(如当前使用的收集器,当前的空间使用率等)
-dump:生成java堆转储快照
-heap:显示java堆详细信息(只在Linux/Solaris下有效)
-F:当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照(只在Linux/Solaris下有效)
-finalizerinfo:显示在F-Queue中等待Finalizer线程执行finalize方法的对象(只在Linux/Solaris下有效)
-histo:显示堆中对象统计信息
-permstat:以ClassLoader为统计口径显示永久代内存状态(只在Linux/Solaris下有效)
命令格式:jmap [option] vmid
其中前面3个参数最重要,如:
查看对详细信息:sudo jmap -heap 309
生成dump文件: sudo jmap -dump:file=./test.prof 309
部分用户没有权限时,采用admin用户:sudo -u admin -H jmap -dump:format=b,file=文件名.hprof pid
查看当前堆中对象统计信息:sudo jmap -histo 309:该命令显示3列,分别为对象数量,对象大小,对象名称,通过该命令可以查看是否内存中有大对象;
有的用户可能没有jmap权限:sudo -u admin -H jmap -histo 309 | less
jvm调优监控-jhat
用于分析使用jmap生成的dump文件,是JDK自带的工具,使用方法为: jhat -J -Xmx512m [file]
不过jhat没有mat好用,推荐使用mat(Eclipse插件: http://www.eclipse.org/mat ),mat速度更快,而且是图形界面。
jvm调优监控-jstack
用于生成当前JVM的所有线程快照,线程快照是虚拟机每一条线程正在执行的方法,目的是定位线程出现长时间停顿的原因。
-F:当正常输出的请求不被响应时,强制输出线程堆栈
-l:除堆栈外,显示关于锁的附加信息
-m:如果调用到本地方法的话,可以显示C/C++的堆栈
命令格式:jstack [option] vmid
jvm调优监控- -verbosgc
-verbosegc是一个比较重要的启动参数,记录每次gc的日志,下面的表格对比了jstat和-verbosegc:
jstat
|
-verbosegc
| |
监控对象
|
运行在本机的Java应用可以把日志输出到终端上,或者借助jstatd命令通过网络连接远程的Java应用。
|
只有那些把-verbogc作为启动参数的JVM。
|
输出信息
|
堆状态(已用空间,最大限制,GC执行次数/时间,等等)
|
执行GC前后新生代和老年代空间大小,GC执行时间。
|
输出时间
| Every designated time
每次设定好的时间。
|
每次GC发生的时候。
|
用途
|
观察堆空间变化情况
|
了解单次GC产生的效果。
|
与-verbosegc配合使用的一些常用参数为:
-XX:+PrintGCDetails,打印GC信息,这是-verbosegc默认开启的选项
-XX:+PrintGCTimeStamps,打印每次GC的时间戳
-XX:+PrintHeapAtGC:每次GC时,打印堆信息
-XX:+PrintGCDateStamps (from JDK 6 update 4) :打印GC日期,适合于长期运行的服务器
-Xloggc:/home/admin/logs/gc.log:制定打印信息的记录的日志位置
每条verbosegc打印出的gc日志,都类似于下面的格式:
time [GC [<collector>: <starting occupancy1> -> <ending occupancy1>(total occupancy1), <pause time1> secs] <starting occupancy3> -> <ending occupancy3>(total occupancy3), <pause time3> secs]
time:执行GC的时间,需要添加-XX:+PrintGCDateStamps参数才有;
collector:minor gc使用的收集器的名字。
starting occupancy1:GC执行前新生代空间大小。
ending occupancy1:GC执行后新生代空间大小。
total occupancy1:新生代总大小
pause time1:因为执行minor GC,Java应用暂停的时间。
starting occupancy3:GC执行前堆区域总大小
ending occupancy3:GC执行后堆区域总大小
total occupancy3:堆区总大小
pause time3:Java应用由于执行堆空间GC(包括full GC)而停止的时间。
jvm调优监控- -可视化工具
监控和分析GC也有一些可视化工具,比较常见的有JConsole和VisualVM。
http://blog.csdn.net/java2000_wl/article/details/8049707
jvm调优实践1
1、JAVA_OPTS="$JAVA_OPTS -server -Xms3G -Xmx3G -Xss256k -XX:PermSize=128m -XX:MaxPermSize=128m -XX:NewSize=1G -XX:MaxNewSize=1G -XX:UseParallelOldGC - XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/aaa/dump -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/usr/aaa/dump/heap_trace.xtxt "
2、上述配置经观察非常稳定,每次普通gc时间10ms左右,full gc基本不发生,或隔很长时间发生一次。
3、通过dump文件发现,每1个小时发生一次full gc,经多方求证,只要在jvm中开启jmx服务,jmx会1小时执行一次full gc以清除引用。
jvm调优实践2
类型 参数
运行模式 -sever
整个堆内存大小 为-Xms和-Xmx设置相同的值。
新生代空间大小 -XX:NewRatio: 2到4. -XX:NewSize=? –XX:MaxNewSize=?. 使用NewSize代替NewRatio也是可以的。
持久代空间大小 -XX:PermSize=256m -XX:MaxPermSize=256m. 设置一个在运行中不会出现问题的值即可,这个参数不影响性能。
GC日志 -Xloggc:$CATALINA_BASE/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps. 记录GC日志并不会特别地影响Java程序性能,尽可能。
GC算法 -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75. 一般来说推荐使用这些配置,但是根据程序不同的特性,其他的也有可能更好。
发生OOM时创建堆内存转储文件 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$CATALINA_BASE/logs
发生OOM后的操作 -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/stop.sh 或 -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/restart.sh. 记录内存转储文件后,为了管理的需要执行一个合适的操作。
为了得到程序的性能表现,需要以下这些信息:
1)系统吞吐量(TPS、OPS):从整体概念上理解程序的性能。
2)每秒请求数(Request Per Second – RPS):严格来说,RPS和单纯的响应能力是不同的,但是你可以把它理解为响应能力。通过这个指标,你能够了解到用户需要多长时间才能得到请求的结果。
3)RPS的标准差:如果可能的话,还有必要包括事件的RPS。一旦出现了偏差,你应该检查GC或者网络系统。
stop-the-world耗时过长
stop-the-world耗时过长可能是由于GC参数不合理或者代码实现不正确。你可以通过分析工具或堆内存转储文件(Heap dump)来定位问题,比如检查堆内存中对象的类型和数量。如果在其中找到了很多不必要的对象,那么最好去改进代码。如果没有发现创建对象的过程中有特别的问题,那么最好单纯地修改GC参数。
为了适当地调整GC参数,你需要获取一段足够长时间的GC日志,还必须知道哪些情况会导致长时间的stop-the-world。想了解更多关于如何选择合适的GC参数,可以阅读我同事的一篇博文:How to Monitor Java Garbage Collection。
CPU使用率过低
当系统发生阻塞,吞吐量和CPU使用率都会降低。这可能是由于网络系统或者并发的问题。为了解决这个问题,你可以分析线程转储信息(Thread dump)或者使用分析工具。阅读这篇文章可以获得更多关于线程转储分析的知识:How to Analyze Java Thread Dumps。
你可以使用商业的分析工具对线程锁进行精确的分析,不过大部分时候,只需使用JVisualVM中的CPU分析器,就能获得足够的信息。
CPU使用率过高
如果吞吐量很低但是CPU使用率却很高,很可能是低效率代码导致的。这种情况下,你应该使用分析工具定位代码中性能的瓶颈。可使用的工具有:JVisualVM、Eclipse TPTP或者JProbe。
jvm调优实践-实例1
笔者昨日发现部分开发测试机器出现异常:
java.lang.OutOfMemoryError: GC overhead limit exceeded
,这个异常代表:
GC
为了释放很小的空间却耗费了太多的时间,其原因一般有两个:
1
,堆太小,
2
,有死循环或大对象;
笔者首先排除了第
2
个原因,因为这个应用同时是在线上运行的,如果有问题,早就挂了。所以怀疑是这台机器中堆设置太小;
使用
ps -ef |grep "java"
查看,发现:
该应用的堆区设置只有
768m
,而机器内存有
2g
,机器上只跑这一个
java
应用,没有其他需要占用内存的地方。另外,这个应用比较大,需要占用的内存也比较多;
笔者通过上面的情况判断,只需要改变堆中各区域的大小设置即可,于是改成下面的情况:
跟踪运行情况发现,相关异常没有再出现;
jvm调优实践-实例2
一个服务系统,经常出现卡顿,分析原因,发现Full GC时间太长:
jstat -gcutil:
S0 S1 E O P YGC YGCT FGC FGCT GCT
12.16 0.00 5.18 63.78 20.32 54 2.047 5 6.946 8.993
分析上面的数据,发现Young GC执行了54次,耗时2.047秒,每次Young GC耗时37ms,在正常范围,而Full GC执行了5次,耗时6.946秒,每次平均1.389s,数据显示出来的问题是:Full GC耗时较长,分析该系统的是指发现,NewRatio=9,也就是说,新生代和老生代大小之比为1:9,这就是问题的原因:
1,新生代太小,导致对象提前进入老年代,触发老年代发生Full GC;
2,老年代较大,进行Full GC时耗时较大;
优化的方法是调整NewRatio的值,调整到4,发现Full GC没有再发生,只有Young GC在执行。这就是把对象控制在新生代就清理掉,没有进入老年代(这种做法对一些应用是很有用的,但并不是对所有应用都要这么做)
jvm调优实践-实例3
一应用在性能测试过程中,发现内存占用率很高,Full GC频繁,使用sudo -u admin -H jmap -dump:format=b,file=文件名.hprof pid 来dump内存,生成dump文件,并使用Eclipse下的mat差距进行分析,发现:
从图中可以看出,这个线程存在问题,队列LinkedBlockingQueue所引用的大量对象并未释放,导致整个线程占用内存高达378m,此时通知开发人员进行代码优化,将相关对象释放掉即可。
jvm调优实践-实例4
代码:
//imports skipped for brevity
public class Producer implements Runnable {
private static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
private Deque<byte[]> deque;
private int objectSize;
private int queueSize;
public Producer(int objectSize, int ttl) {
this.deque = new ArrayDeque<byte[]>();
this.objectSize = objectSize;
this.queueSize = ttl * 1000;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
deque.add(new byte[objectSize]);
if (deque.size() > queueSize) {
deque.poll();
}
}
}
public static void main(String[] args) throws InterruptedException {
executorService.scheduleAtFixedRate(new Producer(200 * 1024 * 1024 / 1000, 5), 0, 100, TimeUnit.MILLISECONDS);
executorService.scheduleAtFixedRate(new Producer(50 * 1024 * 1024 / 1000, 120), 0, 100, TimeUnit.MILLISECONDS);
TimeUnit.MINUTES.sleep(10);
executorService.shutdownNow();
}
}
参数:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps
日志:
2015-06-04T13:34:16.119-0200: 1.723: [GC (Allocation Failure) [PSYoungGen: 114016K->73191K(234496K)] 421540K->421269K(745984K), 0.0858176 secs] [Times: user=0.04 sys=0.06, real=0.09 secs]
2015-06-04T13:34:16.738-0200: 2.342: [GC (Allocation Failure) [PSYoungGen: 234462K->93677K(254976K)] 582540K->593275K(766464K), 0.2357086 secs] [Times: user=0.11 sys=0.14, real=0.24 secs]
2015-06-04T13:34:16.974-0200: 2.578: [Full GC (Ergonomics) [PSYoungGen: 93677K->70109K(254976K)] [ParOldGen: 499597K->511230K(761856K)] 593275K->581339K(1016832K), [Metaspace: 2936K->2936K(1056768K)], 0.0713174 secs] [Times: user=0.21 sys=0.02, real=0.07 secs]
以三个不同的配置各运行了10分钟,在下表中总结了三个差距较大的结果:
堆 GC算法 有效工作 长暂停
-Xmx12g -XX:+UseConcMarkSweepGC 89.8% 560 ms
-Xmx12g -XX:+UseParallelGC 91.5% 1,104 ms
-Xmx8g -XX:+UseConcMarkSweepGC 66.3% 1,610 ms
jvm调优实践-实例5
建议1:预测收集能力
让我们看一下下面这个简单的例子:
public static List reverse(List<?extends T> list) {
List result = new ArrayList();
for (int i = list.size() - 1; i >= 0; i--) {
result.add(list.get(i));
}
return result;
}
以上方法分配了一个新的数组,再将另一个列表的项目填充其中,但只能按倒序填充。
但是,难就难在如何优化增加项目到新列表这一步骤。每次添加后,该列表还需确保其底层数组有足够的空槽能装下新项目。如果能装下,它就会直接在下一个空槽中存储新项目;但如果空间不够,它就会重新分配一个底层数组,将旧数组的内容复制到新数组中,然后再添加新项目。这一过程会导致分配的多个数组都会占据内存,直到GC最后来回收。
所以,我们可以在构建时告知数组需容纳多少个项目,重构后的代码如下:
public static List reverse(List<?extends T> list) {
List result = new ArrayList(list.size());
for (int i = list.size() - 1; i >= 0; i--) {
result.add(list.get(i));
}
return result;
}
这样一来,可以保证ArrayList构造函数在最初配置时就能容纳下list.size()个项目,这意味着它不需要再在迭代中重新分配内存。
Guava的集合类则更加先进,允许我们用一个确切数量或估计值来初始化集合。
List result =Lists.newArrayListWithCapacity(list.size());
List result =Lists.newArrayListWithExpectedSize(list.size());
第一行代码是我们知道有多少项目需要存储的情况,第二行会分配一些多余填充以适应预估误差。
建议2:直接用处理流
当处理数据流时,如从文件中读取数据或从网上下载数据,例如,我们通常可以从数据流中有所发现:
byte[] fileData = readFileToByteArray(newFile(“myfile.txt”));
由此产生的字节数组可以被解析为XML文档、JSON对象或协议缓冲消息,来命名一些常用选项。
当处理大型或未知大小的文件时,这个想法则不适用了,因为当JVM无法分配文件大小的缓冲区时,则会出现OutOfMemoryErrors错误。
但是,即使数据大小看似能管理,当涉及到垃圾回收时,上述模式仍会造成大量开销,因为它在堆上分配了相当大的blob来容纳文件数据。
更好的处理方式是使用合适的InputStream(本例中是FileInputStream),并直接将其送到分析器,而不是提前将整个文件读到字节数组中。所有主要库会将API直接暴露给解析流,例如:
FileInputStream fis = newFileInputStream(fileName);
MyProtoBufMessage msg = MyProtoBufMessage.parseFrom(fis);
建议3:使用不可变对象
不变性有诸多优势,但有一个优势却极少被重视,那就是不变性对垃圾回收的影响。
不可变对象是指对象一旦创建后,其字段(本例中指非原始字段)将无法被修改。例如:
public class ObjectPair {
private final Object first;
private final Object second;
public ObjectPair(Object first, Object second) {
this.first = first;
this.second = second;
}
publicObject getFirst() {
return first;
}
public Object getSecond() {
return second;
}
}
实例化上面类的结果为不可变对象——所有的字段一旦标记后则不能再被修改。
不变性意味着在构造容器完成之前,由不可变容器引用的所有对象都已经创建。在GC看来:容器会和其最新的新生代保持一致。这意味着当对新生代(young generations)执行垃圾回收周期时,GC可以跳过老年代(older generations)中的不可变对象,因为它知道不可变对象不能引用新生代的任何内容。
越少对象扫描意味着需扫描的内存页越少,而越少的内存页扫描意味着GC周期越短,同时也预示着更短的GC停顿和更好的整体吞吐量。
建议4:慎用字符串连接
字符串可能是任何基于JVM的应用中最普遍的非原始数据结构。但是,其隐含重量和使用便利性使得它们成为应用内存变大的罪魁祸首。
很明显,问题不在于被内联和拘留的文字字符串,而在于字符串在运行时被分配和构建。接下来看看构建动态字符串的简单示例:
public static String toString(T[] array) {
String result = "[";
for (int i = 0; i < array.length; i++) {
result += (array[i] == array ? "this" : array[i]);
if (i < array.length - 1) {
result += ", ";
}
}
result += "]";
return result;
}
获取数组并返回它的字符串表示是一个很不错的方法,但这也正是对象分配的问题所在。
要看到其背后所有的语法糖并不容易,但真正的幕后场景应该是这样:
public static String toString(T[] array) {
String result = "[";
for (int i = 0; i < array.length; i++) {
StringBuilder sb1 = new StringBuilder(result);
sb1.append(array[i] == array ? "this" : array[i]);
result = sb1.toString();
if (i < array.length - 1) {
StringBuilder sb2 = new StringBuilder(result);
sb2.append(", ");
result = sb2.toString();
}
}
StringBuilder sb3 = new StringBuilder(result);
sb3.append("]");
result = sb3.toString();
return result;
}
字符串是不可变的,所以在其连接时并没有被修改,而是依次分配新的字符串。此外,编译器利用标准StringBuilder类来执行的这些链接。这就导致了双重麻烦,在每次循环迭代时,我们得到(1)隐式分配临时字符串,(2)隐式分配临时的StringBuilder对象来帮助我们构建最终结果。
避免上述问题的最佳方法是明确使用StringBuilder并直接附加给它,而不是使用略幼稚的串联运算符(“+”)。所以应该是这样:
public static String toString(T[] array) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < array.length; i++) {
sb.append(array[i] == array ? "this" : array[i]);
if (i < array.length - 1) {
sb.append(", ");
}
}
sb.append("]");
return sb.toString();
}
此时,在方法开始时我们只分配了StringBuilder。从这一点来看,所有的字符串和列表项都会被添加到唯一的StringBuilder中,最终只调用一次toString方法转换成字符串,然后返回结果。
建议5:使用专门的原始集合
Java的标准库非常方便且通用,支持使用集合绑定半静态类型。例如,如果要用一组字符串(Set<String>),或一对字符串映射到字符串列表(Map<Pair, List<String>>),直接利用标准库会非常方便。
事实上,问题之所以出现是因为我们想把double类型的值放在 int 类型的list集合或map映射中。由于泛型不能调用原始集合,则可以用包装类型代替,所以放弃List<int>而使用List<Integer>更好。
但其实这非常浪费,Integer本身就是一个完备对象,由12字节的对象头和内部4字节的整数字段组合而成,加起来每个Integer对象占16个字节,这是同样大小的基类int类型长度的4倍!然而,更大的问题是所有这些Integer实际上都是垃圾回收过程中的对象实例。
为了解决这个问题,我们在Takipi 中使用优秀Trove 集合库。Trove放弃了一些(但不是全部)支持专业高效内存的原始集合的泛型。例如,不用浪费的Map
TIntDoubleMap map = newTIntDoubleHashMap();
map.put(5, 7.0);
map.put(-1, 9.999);
...
Trove底层实现了原始数组的使用,所以在操作集合时没有装箱(int -> Integer)或拆箱(Integer -> int)发生,因此也不会将对象存储在基类中。
jvm调优实践-实例6
开始之前
Java 虚拟机有自己完善的硬件架构, 如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 Java 虚拟机上运行的目标代码 (字节码), 就可以在多种平台上不加修改地运行。Java 虚拟机在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
注意:本文仅针对 JDK7、HotSPOT Java 虚拟机,对于 JDK8 引入的 JVM 新特性及其他 Java 虚拟机,本文不予关注。
我们以一个例子开始这篇文章。假设你是一个普通的 Java 对象,你出生在 Eden 区,在 Eden 区有许多和你差不多的小兄弟、小姐妹,可以把 Eden 区当成幼儿园,在这个幼儿园里大家玩了很长时间。Eden 区不能无休止地放你们在里面,所以当年纪稍大,你就要被送到学校去上学,这里假设从小学到高中都称为 Survivor 区。开始的时候你在 Survivor 区里面划分出来的的“From”区,读到高年级了,就进了 Survivor 区的“To”区,中间由于学习成绩不稳定,还经常来回折腾。直到你 18 岁的时候,高中毕业了,该去社会上闯闯了。于是你就去了年老代,年老代里面人也很多。在年老代里,你生活了 20 年 (每次 GC 加一岁),最后寿终正寝,被 GC 回收。有一点没有提,你在年老代遇到了一个同学,他的名字叫爱德华 (慕光之城里的帅哥吸血鬼),他以及他的家族永远不会死,那么他们就生活在永生代。
之前的文章《JVM 垃圾回收器工作原理及使用实例介绍》中已经介绍过年轻代、年老代、永生代,本文主要讲讲如何运用这些区域,为系统性能提供更好的帮助。本文不再重复这些概念,直接进入主题。
如何将新对象预留在年轻代
众所周知,由于 Full GC 的成本远远高于 Minor GC,因此某些情况下需要尽可能将对象分配在年轻代,这在很多情况下是一个明智的选择。虽然在大部分情况下,JVM 会尝试在 Eden 区分配对象,但是由于空间紧张等问题,很可能不得不将部分年轻对象提前向年老代压缩。因此,在 JVM 参数调优时可以为应用程序分配一个合理的年轻代空间,以最大限度避免新对象直接进入年老代的情况发生。清单 1 所示代码尝试分配 4MB 内存空间,观察一下它的内存使用情况。
清单 1. 相同大小内存分配
public class PutInEden {
public static void main(String[] args){
byte[] b1,b2,b3,b4;//定义变量
b1=new byte[1024*1024];//分配 1MB 堆空间,考察堆空间的使用情况
b2=new byte[1024*1024];
b3=new byte[1024*1024];
b4=new byte[1024*1024];
}
}
使用 JVM 参数-XX:+PrintGCDetails -Xmx20M -Xms20M 运行清单 1 所示代码,输出如清单 2 所示。
清单 2. 清单 1 运行输出
[GC [DefNew: 5504K->640K(6144K), 0.0114236 secs] 5504K->5352K(19840K),
0.0114595 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
[GC [DefNew: 6144K->640K(6144K), 0.0131261 secs] 10856K->10782K(19840K),
0.0131612 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
[GC [DefNew: 6144K->6144K(6144K), 0.0000170 secs][Tenured: 10142K->13695K(13696K),
0.1069249 secs] 16286K->15966K(19840K), [Perm : 376K->376K(12288K)],
0.1070058 secs] [Times: user=0.03 sys=0.00, real=0.11 secs]
[Full GC [Tenured: 13695K->13695K(13696K), 0.0302067 secs] 19839K->19595K(19840K),
[Perm : 376K->376K(12288K)], 0.0302635 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
[Full GC [Tenured: 13695K->13695K(13696K), 0.0311986 secs] 19839K->19839K(19840K),
[Perm : 376K->376K(12288K)], 0.0312515 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
[Full GC [Tenured: 13695K->13695K(13696K), 0.0358821 secs] 19839K->19825K(19840K),
[Perm : 376K->371K(12288K)], 0.0359315 secs] [Times: user=0.05 sys=0.00, real=0.05 secs]
[Full GC [Tenured: 13695K->13695K(13696K), 0.0283080 secs] 19839K->19839K(19840K),
[Perm : 371K->371K(12288K)], 0.0283723 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC [Tenured: 13695K->13695K(13696K), 0.0284469 secs] 19839K->19839K(19840K),
[Perm : 371K->371K(12288K)], 0.0284990 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
[Full GC [Tenured: 13695K->13695K(13696K), 0.0283005 secs] 19839K->19839K(19840K),
[Perm : 371K->371K(12288K)], 0.0283475 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
[Full GC [Tenured: 13695K->13695K(13696K), 0.0287757 secs] 19839K->19839K(19840K),
[Perm : 371K->371K(12288K)], 0.0288294 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
[Full GC [Tenured: 13695K->13695K(13696K), 0.0288219 secs] 19839K->19839K(19840K),
[Perm : 371K->371K(12288K)], 0.0288709 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
[Full GC [Tenured: 13695K->13695K(13696K), 0.0293071 secs] 19839K->19839K(19840K),
[Perm : 371K->371K(12288K)], 0.0293607 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
[Full GC [Tenured: 13695K->13695K(13696K), 0.0356141 secs] 19839K->19838K(19840K),
[Perm : 371K->371K(12288K)], 0.0356654 secs] [Times: user=0.01 sys=0.00, real=0.03 secs]
Heap
def new generation total 6144K, used 6143K [0x35c10000, 0x362b0000, 0x362b0000)
eden space 5504K, 100% used [0x35c10000, 0x36170000, 0x36170000)
from space 640K, 99% used [0x36170000, 0x3620fc80, 0x36210000)
to space 640K, 0% used [0x36210000, 0x36210000, 0x362b0000)
tenured generation total 13696K, used 13695K [0x362b0000, 0x37010000, 0x37010000)
the space 13696K, 99% used [0x362b0000, 0x3700fff8, 0x37010000, 0x37010000)
compacting perm gen total 12288K, used 371K [0x37010000, 0x37c10000, 0x3b010000)
the space 12288K, 3% used [0x37010000, 0x3706cd20, 0x3706ce00, 0x37c10000)
ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)
清单 2 所示的日志输出显示年轻代 Eden 的大小有 5MB 左右。分配足够大的年轻代空间,使用 JVM 参数-XX:+PrintGCDetails -Xmx20M -Xms20M-Xmn6M 运行清单 1 所示代码,输出如清单 3 所示。
清单 3. 增大 Eden 大小后清单 1 运行输出
[GC [DefNew: 4992K->576K(5568K), 0.0116036 secs] 4992K->4829K(19904K),
0.0116439 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
[GC [DefNew: 5568K->576K(5568K), 0.0130929 secs] 9821K->9653K(19904K),
0.0131336 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
[GC [DefNew: 5568K->575K(5568K), 0.0154148 secs] 14645K->14500K(19904K),
0.0154531 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
[GC [DefNew: 5567K->5567K(5568K), 0.0000197 secs][Tenured: 13924K->14335K(14336K),
0.0330724 secs] 19492K->19265K(19904K), [Perm : 376K->376K(12288K)],
0.0331624 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
[Full GC [Tenured: 14335K->14335K(14336K), 0.0292459 secs] 19903K->19902K(19904K),
[Perm : 376K->376K(12288K)], 0.0293000 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
[Full GC [Tenured: 14335K->14335K(14336K), 0.0278675 secs] 19903K->19903K(19904K),
[Perm : 376K->376K(12288K)], 0.0279215 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
[Full GC [Tenured: 14335K->14335K(14336K), 0.0348408 secs] 19903K->19889K(19904K),
[Perm : 376K->371K(12288K)], 0.0348945 secs] [Times: user=0.05 sys=0.00, real=0.05 secs]
[Full GC [Tenured: 14335K->14335K(14336K), 0.0299813 secs] 19903K->19903K(19904K),
[Perm : 371K->371K(12288K)], 0.0300349 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
[Full GC [Tenured: 14335K->14335K(14336K), 0.0298178 secs] 19903K->19903K(19904K),
[Perm : 371K->371K(12288K)], 0.0298688 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space[Full GC [Tenured:
14335K->14335K(14336K), 0.0294953 secs] 19903K->19903K(19904K),
[Perm : 371K->371K(12288K)], 0.0295474 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
[Full GC [Tenured
: 14335K->14335K(14336K), 0.0287742 secs] 19903K->19903K(19904K),
[Perm : 371K->371K(12288K)], 0.0288239 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
[Full GC [Tenuredat GCTimeTest.main(GCTimeTest.java:16)
: 14335K->14335K(14336K), 0.0287102 secs] 19903K->19903K(19904K),
[Perm : 371K->371K(12288K)], 0.0287627 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
Heap
def new generation total 5568K, used 5567K [0x35c10000, 0x36210000, 0x36210000)
eden space 4992K, 100% used [0x35c10000, 0x360f0000, 0x360f0000)
from space 576K, 99% used [0x36180000, 0x3620ffe8, 0x36210000)
to space 576K, 0% used [0x360f0000, 0x360f0000, 0x36180000)
tenured generation total 14336K, used 14335K [0x36210000, 0x37010000, 0x37010000)
the space 14336K, 99% used [0x36210000, 0x3700ffd8, 0x37010000, 0x37010000)
compacting perm gen total 12288K, used 371K [0x37010000, 0x37c10000, 0x3b010000)
the space 12288K, 3% used [0x37010000, 0x3706ce28, 0x3706d000, 0x37c10000)
ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)
通过清单 2 和清单 3 对比,可以发现通过设置一个较大的年轻代预留新对象,设置合理的 Survivor 区并且提供 Survivor 区的使用率,可以将年轻对象保存在年轻代。一般来说,Survivor 区的空间不够,或者占用量达到 50%时,就会使对象进入年老代 (不管它的年龄有多大)。清单 4 创建了 3 个对象,分别分配一定的内存空间。
清单 4. 不同大小内存分配
blic class PutInEden2 {
public static void main(String[] args){
byte[] b1,b2,b3;
b1=new byte[1024*512];//分配 0.5MB 堆空间
b2=new byte[1024*1024*4];//分配 4MB 堆空间
b3=new byte[1024*1024*4];
b3=null; //使 b3 可以被回收
b3=new byte[1024*1024*4];//分配 4MB 堆空间
}
}
使用参数-XX:+PrintGCDetails -Xmx1000M -Xms500M -Xmn100M -XX:SurvivorRatio=8 运行清单 4 所示代码,输出如清单 5 所示。
清单 5. 清单 4 运行输出
Heap
def new generation total 92160K, used 11878K [0x0f010000, 0x15410000, 0x15410000)
eden space 81920K, 2% used [0x0f010000, 0x0f1a9a20, 0x14010000)
from space 10240K, 99% used [0x14a10000, 0x1540fff8, 0x15410000)
to space 10240K, 0% used [0x14010000, 0x14010000, 0x14a10000)
tenured generation total 409600K, used 86434K [0x15410000, 0x2e410000, 0x4d810000)
the space 409600K, 21% used [0x15410000, 0x1a878b18, 0x1a878c00, 0x2e410000)
compacting perm gen total 12288K, used 2062K [0x4d810000, 0x4e410000, 0x51810000)
the space 12288K, 16% used [0x4d810000, 0x4da13b18, 0x4da13c00, 0x4e410000)
No shared spaces configured.
清单 5 输出的日志显示,年轻代分配了 8M,年老代也分配了 8M。我们可以尝试加上-XX:TargetSurvivorRatio=90 参数,这样可以提高 from 区的利用率,使 from 区使用到 90%时,再将对象送入年老代,运行清单 4 代码,输出如清单 6 所示。
清单 6. 修改运行参数后清单 4 输出
Heap
def new generation total 9216K, used 9215K [0x35c10000, 0x36610000, 0x36610000)
eden space 8192K, 100% used [0x35c10000, 0x36410000, 0x36410000)
from space 1024K, 99% used [0x36510000, 0x3660fc50, 0x36610000)
to space 1024K, 0% used [0x36410000, 0x36410000, 0x36510000)
tenured generation total 10240K, used 10239K [0x36610000, 0x37010000, 0x37010000)
the space 10240K, 99% used [0x36610000, 0x3700ff70, 0x37010000, 0x37010000)
compacting perm gen total 12288K, used 371K [0x37010000, 0x37c10000, 0x3b010000)
the space 12288K, 3% used [0x37010000, 0x3706cd90, 0x3706ce00, 0x37c10000)
ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)
如果将 SurvivorRatio 设置为 2,将 b1 对象预存在年轻代。输出如清单 7 所示。
清单 7. 再次修改运行参数后清单 4 输出
Heap
def new generation total 7680K, used 7679K [0x35c10000, 0x36610000, 0x36610000)
eden space 5120K, 100% used [0x35c10000, 0x36110000, 0x36110000)
from space 2560K, 99% used [0x36110000, 0x3638fff0, 0x36390000)
to space 2560K, 0% used [0x36390000, 0x36390000, 0x36610000)
tenured generation total 10240K, used 10239K [0x36610000, 0x37010000, 0x37010000)
the space 10240K, 99% used [0x36610000, 0x3700fff0, 0x37010000, 0x37010000)
compacting perm gen total 12288K, used 371K [0x37010000, 0x37c10000, 0x3b010000)
the space 12288K, 3% used [0x37010000, 0x3706ce28, 0x3706d000, 0x37c10000)
ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)
如何让大对象进入年老代
我们在大部分情况下都会选择将对象分配在年轻代。但是,对于占用内存较多的大对象而言,它的选择可能就不是这样的。因为大对象出现在年轻代很可能扰乱年轻代 GC,并破坏年轻代原有的对象结构。因为尝试在年轻代分配大对象,很可能导致空间不足,为了有足够的空间容纳大对象,JVM 不得不将年轻代中的年轻对象挪到年老代。因为大对象占用空间多,所以可能需要移动大量小的年轻对象进入年老代,这对 GC 相当不利。基于以上原因,可以将大对象直接分配到年老代,保持年轻代对象结构的完整性,这样可以提高 GC 的效率。如果一个大对象同时又是一个短命的对象,假设这种情况出现很频繁,那对于 GC 来说会是一场灾难。原本应该用于存放永久对象的年老代,被短命的对象塞满,这也意味着对堆空间进行了洗牌,扰乱了分代内存回收的基本思路。因此,在软件开发过程中,应该尽可能避免使用短命的大对象。可以使用参数-XX:PetenureSizeThreshold 设置大对象直接进入年老代的阈值。当对象的大小超过这个值时,将直接在年老代分配。参数-XX:PetenureSizeThreshold 只对串行收集器和年轻代并行收集器有效,并行回收收集器不识别这个参数。
清单 8. 创建一个大对象
public class BigObj2Old {
public static void main(String[] args){
byte[] b;
b = new byte[1024*1024];//分配一个 1MB 的对象
}
}
使用 JVM 参数-XX:+PrintGCDetails –Xmx20M –Xms20MB 运行,可以得到清单 9 所示日志输出。
清单 9. 清单 8 运行输出
Heap
def new generation total 6144K, used 1378K [0x35c10000, 0x362b0000, 0x362b0000)
eden space 5504K, 25% used [0x35c10000, 0x35d689e8, 0x36170000)
from space 640K, 0% used [0x36170000, 0x36170000, 0x36210000)
to space 640K, 0% used [0x36210000, 0x36210000, 0x362b0000)
tenured generation total 13696K, used 0K [0x362b0000, 0x37010000, 0x37010000)
the space 13696K, 0% used [0x362b0000, 0x362b0000, 0x362b0200, 0x37010000)
compacting perm gen total 12288K, used 374K [0x37010000, 0x37c10000, 0x3b010000)
the space 12288K, 3% used [0x37010000, 0x3706dac8, 0x3706dc00, 0x37c10000)
ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)
可以看到该对象被分配在了年轻代,占用了 25%的空间。如果需要将 1MB 以上的对象直接在年老代分配,设置-XX:PetenureSizeThreshold=1000000,程序运行后输出如清单 10 所示。
清单 10. 修改运行参数后清单 8 输出
Heap
def new generation total 6144K, used 354K [0x35c10000, 0x362b0000, 0x362b0000)
eden space 5504K, 6% used [0x35c10000, 0x35c689d8, 0x36170000)
from space 640K, 0% used [0x36170000, 0x36170000, 0x36210000)
to space 640K, 0% used [0x36210000, 0x36210000, 0x362b0000)
tenured generation total 13696K, used 1024K [0x362b0000, 0x37010000, 0x37010000)
the space 13696K, 7% used [0x362b0000, 0x363b0010, 0x363b0200, 0x37010000)
compacting perm gen total 12288K, used 374K [0x37010000, 0x37c10000, 0x3b010000)
the space 12288K, 3% used [0x37010000, 0x3706dac8, 0x3706dc00, 0x37c10000)
ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)
清单 10 里面可以看到当满 1MB 时进入到了年老代。
如何设置对象进入年老代的年龄
堆中的每一个对象都有自己的年龄。一般情况下,年轻对象存放在年轻代,年老对象存放在年老代。为了做到这点,虚拟机为每个对象都维护一个年龄。如果对象在 Eden 区,经过一次 GC 后依然存活,则被移动到 Survivor 区中,对象年龄加 1。以后,如果对象每经过一次 GC 依然存活,则年龄再加 1。当对象年龄达到阈值时,就移入年老代,成为老年对象。这个阈值的最大值可以通过参数-XX:MaxTenuringThreshold 来设置,默认值是 15。虽然-XX:MaxTenuringThreshold 的值可能是 15 或者更大,但这不意味着新对象非要达到这个年龄才能进入年老代。事实上,对象实际进入年老代的年龄是虚拟机在运行时根据内存使用情况动态计算的,这个参数指定的是阈值年龄的最大值。即,实际晋升年老代年龄等于动态计算所得的年龄与-XX:MaxTenuringThreshold 中较小的那个。清单 11 所示代码为 3 个对象申请了若干内存。
清单 11. 申请内存
public class MaxTenuringThreshold {
public static void main(String args[]){
byte[] b1,b2,b3;
b1 = new byte[1024*512];
b2 = new byte[1024*1024*2];
b3 = new byte[1024*1024*4];
b3 = null;
b3 = new byte[1024*1024*4];
}
}
参数设置为:-XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn10M -XX:SurvivorRatio=2
运行清单 11 所示代码,输出如清单 12 所示。
清单 12. 清单 11 运行输出
[GC [DefNew: 2986K->690K(7680K), 0.0246816 secs] 2986K->2738K(17920K),
0.0247226 secs] [Times: user=0.00 sys=0.02, real=0.03 secs]
[GC [DefNew: 4786K->690K(7680K), 0.0016073 secs] 6834K->2738K(17920K),
0.0016436 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 7680K, used 4888K [0x35c10000, 0x36610000, 0x36610000)
eden space 5120K, 82% used [0x35c10000, 0x36029a18, 0x36110000)
from space 2560K, 26% used [0x36110000, 0x361bc950, 0x36390000)
to space 2560K, 0% used [0x36390000, 0x36390000, 0x36610000)
tenured generation total 10240K, used 2048K [0x36610000, 0x37010000, 0x37010000)
the space 10240K, 20% used [0x36610000, 0x36810010, 0x36810200, 0x37010000)
compacting perm gen total 12288K, used 374K [0x37010000, 0x37c10000, 0x3b010000)
the space 12288K, 3% used [0x37010000, 0x3706db50, 0x3706dc00, 0x37c10000)
ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)
更改参数为-XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn10M -XX:SurvivorRatio=2 -XX:MaxTenuringThreshold=1,运行清单 11 所示代码,输出如清单 13 所示。
清单 13. 修改运行参数后清单 11 输出
[GC [DefNew: 2986K->690K(7680K), 0.0047778 secs] 2986K->2738K(17920K),
0.0048161 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew: 4888K->0K(7680K), 0.0016271 secs] 6936K->2738K(17920K),
0.0016630 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 7680K, used 4198K [0x35c10000, 0x36610000, 0x36610000)
eden space 5120K, 82% used [0x35c10000, 0x36029a18, 0x36110000)
from space 2560K, 0% used [0x36110000, 0x36110088, 0x36390000)
to space 2560K, 0% used [0x36390000, 0x36390000, 0x36610000)
tenured generation total 10240K, used 2738K [0x36610000, 0x37010000, 0x37010000)
the space 10240K, 26% used [0x36610000, 0x368bc890, 0x368bca00, 0x37010000)
compacting perm gen total 12288K, used 374K [0x37010000, 0x37c10000, 0x3b010000)
the space 12288K, 3% used [0x37010000, 0x3706db50, 0x3706dc00, 0x37c10000)
ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)
清单 13 所示,第一次运行时 b1 对象在程序结束后依然保存在年轻代。第二次运行前,我们减小了对象晋升年老代的年龄,设置为 1。即,所有经过一次 GC 的对象都可以直接进入年老代。程序运行后,可以发现 b1 对象已经被分配到年老代。如果希望对象尽可能长时间地停留在年轻代,可以设置一个较大的阈值。
稳定的 Java 堆 VS 动荡的 Java 堆
一般来说,稳定的堆大小对垃圾回收是有利的。获得一个稳定的堆大小的方法是使-Xms 和-Xmx 的大小一致,即最大堆和最小堆 (初始堆) 一样。如果这样设置,系统在运行时堆大小理论上是恒定的,稳定的堆空间可以减少 GC 的次数。因此,很多服务端应用都会将最大堆和最小堆设置为相同的数值。但是,一个不稳定的堆并非毫无用处。稳定的堆大小虽然可以减少 GC 次数,但同时也增加了每次 GC 的时间。让堆大小在一个区间中震荡,在系统不需要使用大内存时,压缩堆空间,使 GC 应对一个较小的堆,可以加快单次 GC 的速度。基于这样的考虑,JVM 还提供了两个参数用于压缩和扩展堆空间。
-XX:MinHeapFreeRatio 参数用来设置堆空间最小空闲比例,默认值是 40。当堆空间的空闲内存小于这个数值时,JVM 便会扩展堆空间。
-XX:MaxHeapFreeRatio 参数用来设置堆空间最大空闲比例,默认值是 70。当堆空间的空闲内存大于这个数值时,便会压缩堆空间,得到一个较小的堆。
当-Xmx 和-Xms 相等时,-XX:MinHeapFreeRatio 和-XX:MaxHeapFreeRatio 两个参数无效。
清单 14. 堆大小设置
import java.util.Vector;
public class HeapSize {
public static void main(String args[]) throws InterruptedException{
Vector v = new Vector();
while(true){
byte[] b = new byte[1024*1024];
v.add(b);
if(v.size() == 10){
v = new Vector();
}
Thread.sleep(1);
}
}
}
清单 14 所示代码是测试-XX:MinHeapFreeRatio 和-XX:MaxHeapFreeRatio 的作用,设置运行参数为-XX:+PrintGCDetails -Xms10M -Xmx40M -XX:MinHeapFreeRatio=40 -XX:MaxHeapFreeRatio=50 时,输出如清单 15 所示。
清单 15. 修改运行参数后清单 14 输出
[GC [DefNew: 2418K->178K(3072K), 0.0034827 secs] 2418K->2226K(9920K),
0.0035249 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]
[GC [DefNew: 2312K->0K(3072K), 0.0028263 secs] 4360K->4274K(9920K),
0.0029905 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]
[GC [DefNew: 2068K->0K(3072K), 0.0024363 secs] 6342K->6322K(9920K),
0.0024836 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]
[GC [DefNew: 2061K->0K(3072K), 0.0017376 secs][Tenured: 8370K->8370K(8904K),
0.1392692 secs] 8384K->8370K(11976K), [Perm : 374K->374K(12288K)],
0.1411363 secs] [Times: user=0.00 sys=0.02, real=0.16 secs]
[GC [DefNew: 5138K->0K(6336K), 0.0038237 secs] 13508K->13490K(20288K),
0.0038632 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]
改用参数:-XX:+PrintGCDetails -Xms40M -Xmx40M -XX:MinHeapFreeRatio=40 -XX:MaxHeapFreeRatio=50,运行输出如清单 16 所示。
清单 16. 再次修改运行参数后清单 14 输出
[GC [DefNew: 10678K->178K(12288K), 0.0019448 secs] 10678K->178K(39616K),
0.0019851 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]
[GC [DefNew: 10751K->178K(12288K), 0.0010295 secs] 10751K->178K(39616K),
0.0010697 secs] [Times: user=0.00 sys=0.00, real=0.02 secs]
[GC [DefNew: 10493K->178K(12288K), 0.0008301 secs] 10493K->178K(39616K),
0.0008672 secs] [Times: user=0.00 sys=0.00, real=0.02 secs]
[GC [DefNew: 10467K->178K(12288K), 0.0008522 secs] 10467K->178K(39616K),
0.0008905 secs] [Times: user=0.00 sys=0.00, real=0.02 secs]
[GC [DefNew: 10450K->178K(12288K), 0.0008964 secs] 10450K->178K(39616K),
0.0009339 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC [DefNew: 10439K->178K(12288K), 0.0009876 secs] 10439K->178K(39616K),
0.0010279 secs] [Times: user=0.00 sys=0.00, real=0.02 secs]
从清单 16 可以看出,此时堆空间的垃圾回收稳定在一个固定的范围。在一个稳定的堆中,堆空间大小始终不变,每次 GC 时,都要应对一个 40MB 的空间。因此,虽然 GC 次数减小了,但是单次 GC 速度不如一个震荡的堆。
增大吞吐量提升系统性能
吞吐量优先的方案将会尽可能减少系统执行垃圾回收的总时间,故可以考虑关注系统吞吐量的并行回收收集器。在拥有高性能的计算机上,进行吞吐量优先优化,可以使用参数:
java –Xmx3800m –Xms3800m –Xmn2G –Xss128k –XX:+UseParallelGC
–XX:ParallelGC-Threads=20 –XX:+UseParallelOldGC
–Xmx380m –Xms3800m:设置 Java 堆的最大值和初始值。一般情况下,为了避免堆内存的频繁震荡,导致系统性能下降,我们的做法是设置最大堆等于最小堆。假设这里把最小堆减少为最大堆的一半,即 1900m,那么 JVM 会尽可能在 1900MB 堆空间中运行,如果这样,发生 GC 的可能性就会比较高;
-Xss128k:减少线程栈的大小,这样可以使剩余的系统内存支持更多的线程;
-Xmn2g:设置年轻代区域大小为 2GB;
–XX:+UseParallelGC:年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器,可以尽可能地减少 GC 时间。
–XX:ParallelGC-Threads:设置用于垃圾回收的线程数,通常情况下,可以设置和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的;
–XX:+UseParallelOldGC:设置年老代使用并行回收收集器。
尝试使用大的内存分页
CPU 是通过寻址来访问内存的。32 位 CPU 的寻址宽度是 0~0xFFFFFFFF ,计算后得到的大小是 4G,也就是说可支持的物理内存最大是 4G。但在实践过程中,碰到了这样的问题,程序需要使用 4G 内存,而可用物理内存小于 4G,导致程序不得不降低内存占用。为了解决此类问题,现代 CPU 引入了 MMU(Memory Management Unit 内存管理单元)。MMU 的核心思想是利用虚拟地址替代物理地址,即 CPU 寻址时使用虚址,由 MMU 负责将虚址映射为物理地址。MMU 的引入,解决了对物理内存的限制,对程序来说,就像自己在使用 4G 内存一样。内存分页 (Paging) 是在使用 MMU 的基础上,提出的一种内存管理机制。它将虚拟地址和物理地址按固定大小(4K)分割成页 (page) 和页帧 (page frame),并保证页与页帧的大小相同。这种机制,从数据结构上,保证了访问内存的高效,并使 OS 能支持非连续性的内存分配。在程序内存不够用时,还可以将不常用的物理内存页转移到其他存储设备上,比如磁盘,这就是大家耳熟能详的虚拟内存。
在 Solaris 系统中,JVM 可以支持 Large Page Size 的使用。使用大的内存分页可以增强 CPU 的内存寻址能力,从而提升系统的性能。
java –Xmx2506m –Xms2506m –Xmn1536m –Xss128k –XX:++UseParallelGC
–XX:ParallelGCThreads=20 –XX:+UseParallelOldGC –XX:+LargePageSizeInBytes=256m
–XX:+LargePageSizeInBytes:设置大页的大小。
过大的内存分页会导致 JVM 在计算 Heap 内部分区(perm, new, old)内存占用比例时,会出现超出正常值的划分,最坏情况下某个区会多占用一个页的大小。
使用非占有的垃圾回收器
为降低应用软件的垃圾回收时的停顿,首先考虑的是使用关注系统停顿的 CMS 回收器,其次,为了减少 Full GC 次数,应尽可能将对象预留在年轻代,因为年轻代 Minor GC 的成本远远小于年老代的 Full GC。
java –Xmx3550m –Xms3550m –Xmn2g –Xss128k –XX:ParallelGCThreads=20
–XX:+UseConcMarkSweepGC –XX:+UseParNewGC –XX:+SurvivorRatio=8 –XX:TargetSurvivorRatio=90
–XX:MaxTenuringThreshold=31
–XX:ParallelGCThreads=20:设置 20 个线程进行垃圾回收;
–XX:+UseParNewGC:年轻代使用并行回收器;
–XX:+UseConcMarkSweepGC:年老代使用 CMS 收集器降低停顿;
–XX:+SurvivorRatio:设置 Eden 区和 Survivor 区的比例为 8:1。稍大的 Survivor 空间可以提高在年轻代回收生命周期较短的对象的可能性,如果 Survivor 不够大,一些短命的对象可能直接进入年老代,这对系统来说是不利的。
–XX:TargetSurvivorRatio=90:设置 Survivor 区的可使用率。这里设置为 90%,则允许 90%的 Survivor 空间被使用。默认值是 50%。故该设置提高了 Survivor 区的使用率。当存放的对象超过这个百分比,则对象会向年老代压缩。因此,这个选项更有助于将对象留在年轻代。
–XX:MaxTenuringThreshold:设置年轻对象晋升到年老代的年龄。默认值是 15 次,即对象经过 15 次 Minor GC 依然存活,则进入年老代。这里设置为 31,目的是让对象尽可能地保存在年轻代区域。
结束语
通过本文的学习,读者了解了如何将新对象预留在年轻代、如何让大对象进入年老代、如何设置对象进入年老代的年龄、稳定的 Java 堆 VS 动荡的 Java 堆、增大吞吐量提升系统性能、尝试使用大的内存分页、使用非占有的垃圾回收器等主题,通过实例及对应输出解释的形式让读者对于 JVM 优化有一个初步认识。如其他文章相同的观点,没有哪一条优化是固定不变的,读者需要自己判断、实践后才能找到正确的道路。
线程池调优
1、java线程池有几个重要的配置参数:
1)corePoolSize:核心线程数
2)maximumPoolSize:最大线程数,超过这个数量的任务会被拒绝,可以通过RejectedExecutionHandler接口自定处理方式。
3)keepAliveTime:线程保持活动的时间。
4)workQueue:工作队列,存放执行的任务。
2、Queue不同选择,线程池有完全不同的行为:
1)SynchronousQueue:没有容量的等待队列,insert必须等待remove后,每个任务分配一个线程。
2)LinkedBlockingQueue:无界队列,线程池将会忽略maximumPoolSize参数,而用corePoolSize数目的线程来处理队列中的任务,未处理的则在队列中排队。
3)ArrayBlockingQueue:有界队列,很难调优。大的Queue和小的maximumPoolSize将导致cpu低负载,小的Queue和大的maximumPoolSize将导致queue没作用。
3、java线程池的思想:任务放到queue中,当queue中放不下时,才考虑创建新的线程,当然最大线程数为maximumPoolSize。如果queue满了并且无法创建新线程,就决绝该任务。该设计导致“先放等执行”、“放不下再执行”、“拒绝不等待”。所以根据不同的queue,要提高吞吐量不能一味的增大maximumPoolSize。
4、需要的线程池的思想:能设置线程数的最小值、最大值,当最小值<任务数<最大值,创建新线程处理。当任务数>最大值,排队等待有空闲线程再处理。
5、重新实现java线程池:
1)选用SynchronousQueue,是线程池的maximumPoolSize发挥作用,防止线程被无限创建,适当提高maximumPoolSize来提高程序吞吐量。
2)自定义RejectExecutionHandler,当线程数超过maximumPoolSize时,隔一段时间自动检查线程池是否有空闲线程,如果有则执行新任务。检查时间以来于keepAliveTime大小。
Servlet安全
servlet体系结构是建立在java多线程机制上的,它的生命周期是由web容器负责的。当客户端第一次请求某servlet时,容器根据web.xml中配置实例化servlet类。当有新的客户端请求该servlet时,一般不会再次实例化该servlet类,也就是多个线程在使用同一个实例。这样,当多线程同时访问一个servlet时,可能会发生多线程同时访问同一资源的情况,数据会变的不一致。所以说servlet是线程不安的。所以servlet内,一般不声明全局共享变量,而只是在方法中声明局部变量,而这些局部变量是存放在栈帧中的,栈帧属于线程私有的,不存在一致性问题。
A. JVM会试图为相关Java对象在Eden中初始化一块内存区域
B. 当Eden空间足够时,内存申请结束。否则到下一步
C. JVM试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收), 释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区
D. Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区
E. 当OLD区空间不够时,JVM会在OLD区进行完全的垃圾收集(0级)
F. 完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”
young generation的内存,由一块Eden(伊甸园,有意思)和两块Survivor Space(1.4文档中称为semi-space)构成。新创建的对象的内存都分配自eden。两块Survivor Space总有会一块是空闲的,用作copying collection的目标空间。Minor collection的过程就是将eden和在用survivor space中的活对象copy到空闲survivor space中。所谓survivor,也就是大部分对象在伊甸园出生后,根本活不过一次GC。对象在young generation里经历了一定次数的minor collection后,年纪大了,就会被移到old generation中,称为tenuring。
剩余内存空间不足会触发GC,如eden空间不够了就要进行minor collection,old generation空间不够要进行major collection,permanent generation空间不足会引发full GC。