JVM宝典
JVM概述
- JDK(Java Development Kit)是Java语言的软件开发工具包,也是整个java开发的核心,包含了JRE和开发工具包
- JRE(Java Runtime Environment),Java运行环境,包含了JVM和Java的核心类库(Java API)
- JVM(Java Virtual Machine),Java虚拟机,它是运行在操作系统之上的,与硬件没有直接的交互
所谓“一次编码,随处运行”正是基于不同系统下的jvm掩盖了系统间的接口差异。
java运行过程
- 源码编译:通过Java编译器将Java代码编译成JVM字节码(.class文件)
- 类加载:通过类加载器ClassLoader及其子类完成类加载
- 类执行:字节码被装入内存,进入JVM虚拟机,被解释器解释执行
JVM虚拟机中主要是由三部分构成,分别是类加载子系统、运行时数据区、执行引擎。
类加载子系统
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、
转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
运行时数据区
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同
的数据区域。
这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进
程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销
毁。
执行引擎
执行引擎用于执行JVM字节码指令,主要有两种方式,分别是解释执行和编
译执行,区别在于,解释执行是在执行时翻译成虚拟机指令执行,而编译执行
是在执行之前先进行编译再执行。
解释执行启动快,执行效率低。编译执行,启动慢,执行效率高。
垃圾回收器就是自动管理运行数据区的内存,将无用的内存占用进行清
除,释放内存资源。
本地方法库、本地库接口
在jdk的底层中,有一些实现是需要调用本地方法完成的(使用c或c++写的
方法),就是通过本地库接口调用完成的。比如:System.currentTimeMillis()
方法。
类文件结构
测试案例
源代码:
/*
* 基本类结构
* */
public class ClassStruct {private static String name = "JVM";private static final int age = 18;public static void main(String[] args) {System.out.println("Hello " + name);}
}
编译:
1)maven定义编译的版本
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-
plugin</artifactId><configuration><source>1.8</source><target>1.8</target></configuration></plugin></plugins></build>
2)编译
mvn clean compile
字节码结构
二进制概览
vscode 安装 hexdump 插件,然后选择文件以hexdump展示
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: CA FE BA BE 00 00 00 34 00 38 0A 00 02 00 03 07 J~:>…4.8…
00000010: 00 04 0C 00 05 00 06 01 00 10 6A 61 76 61 2F 6C …java/l
00000020: 61 6E 67 2F 4F 62 6A 65 63 74 01 00 06 3C 69 6E ang/Object…<in
00000030: 69 74 3E 01 00 03 28 29 56 09 00 08 00 09 07 00 it>…()V…
00000040: 0A 0C 00 0B 00 0C 01 00 10 6A 61 76 61 2F 6C 61 …java/la
00000050: 6E 67 2F 53 79 73 74 65 6D 01 00 03 6F 75 74 01 ng/System…out.
00000060: 00 15 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 …Ljava/io/Print
00000070: 53 74 72 65 61 6D 3B 07 00 0E 01 00 17 6A 61 76 Stream;…jav
00000080: 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 42 75 69 a/lang/StringBui
00000090: 6C 64 65 72 0A 00 0D 00 03 08 00 11 01 00 06 48 lder…H
000000a0: 65 6C 6C 6F 20 0A 00 0D 00 13 0C 00 14 00 15 01 ello…
000000b0: 00 06 61 70 70 65 6E 64 01 00 2D 28 4C 6A 61 76 …append…-(Ljav
000000c0: 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 4C a/lang/String;)L
000000d0: 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 java/lang/String
000000e0: 42 75 69 6C 64 65 72 3B 09 00 17 00 18 07 00 19 Builder;…
000000f0: 0C 00 1A 00 1B 01 00 1B 63 6F 6D 2F 66 75 66 65 …com/fufe
00000100: 6E 67 2F 74 65 73 74 2F 43 6C 61 73 73 53 74 72 ng/test/ClassStr
00000110: 75 63 74 01 00 04 6E 61 6D 65 01 00 12 4C 6A 61 uct…name…Lja
00000120: 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 0A va/lang/String;.
00000130: 00 0D 00 1D 0C 00 1E 00 1F 01 00 08 74 6F 53 74 …toSt
00000140: 72 69 6E 67 01 00 14 28 29 4C 6A 61 76 61 2F 6C ring…()Ljava/l
00000150: 61 6E 67 2F 53 74 72 69 6E 67 3B 0A 00 21 00 22 ang/String;…!."
00000160: 07 00 23 0C 00 24 00 25 01 00 13 6A 61 76 61 2F …#…$.%…java/
00000170: 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 01 00 io/PrintStream…
00000180: 07 70 72 69 6E 74 6C 6E 01 00 15 28 4C 6A 61 76 .println…(Ljav
00000190: 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 a/lang/String;)V
000001a0: 08 00 27 01 00 03 4A 56 4D 01 00 03 61 67 65 01 …'…JVM…age.
000001b0: 00 01 49 01 00 0D 43 6F 6E 73 74 61 6E 74 56 61 …I…ConstantVa
000001c0: 6C 75 65 03 00 00 00 12 01 00 04 43 6F 64 65 01 lue…Code.
000001d0: 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C …LineNumberTabl
000001e0: 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C e…LocalVariabl
000001f0: 65 54 61 62 6C 65 01 00 04 74 68 69 73 01 00 1D eTable…this…
00000200: 4C 63 6F 6D 2F 66 75 66 65 6E 67 2F 74 65 73 74 Lcom/fufeng/test
00000210: 2F 43 6C 61 73 73 53 74 72 75 63 74 3B 01 00 04 /ClassStruct;…
00000220: 6D 61 69 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C main…([Ljava/l
00000230: 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 01 00 04 ang/String;)V…
00000240: 61 72 67 73 01 00 13 5B 4C 6A 61 76 61 2F 6C 61 args…[Ljava/la
00000250: 6E 67 2F 53 74 72 69 6E 67 3B 01 00 08 3C 63 6C ng/String;…<cl
00000260: 69 6E 69 74 3E 01 00 0A 53 6F 75 72 63 65 46 69 init>…SourceFi
00000270: 6C 65 01 00 10 43 6C 61 73 73 53 74 72 75 63 74 le…ClassStruct
00000280: 2E 6A 61 76 61 00 21 00 17 00 02 00 00 00 02 00 .java.!..
00000290: 0A 00 1A 00 1B 00 00 00 1A 00 28 00 29 00 01 00 …(.)…
000002a0: 2A 00 00 00 02 00 2B 00 03 00 01 00 05 00 06 00 *…+…
000002b0: 01 00 2C 00 00 00 2F 00 01 00 01 00 00 00 05 2A …,…/…*
000002c0: B7 00 01 B1 00 00 00 02 00 2D 00 00 00 06 00 01 7…1…-…
000002d0: 00 00 00 03 00 2E 00 00 00 0C 00 01 00 00 00 05 …
000002e0: 00 2F 00 30 00 00 00 09 00 31 00 32 00 01 00 2C ./.0…1.2…,
000002f0: 00 00 00 4A 00 03 00 01 00 00 00 1C B2 00 07 BB …J…2…;
00000300: 00 0D 59 B7 00 0F 12 10 B6 00 12 B2 00 16 B6 00 …Y7…6…2…6.
00000310: 12 B6 00 1C B6 00 20 B1 00 00 00 02 00 2D 00 00 .6…6…1…-…
00000320: 00 0A 00 02 00 00 00 08 00 1B 00 09 00 2E 00 00 …
00000330: 00 0C 00 01 00 00 00 1C 00 33 00 34 00 00 00 08 …3.4…
00000340: 00 35 00 06 00 01 00 2C 00 00 00 1E 00 01 00 00 .5…,…
00000350: 00 00 00 06 12 26 B3 00 16 B1 00 00 00 01 00 2D …&3…1…-
00000360: 00 00 00 06 00 01 00 00 00 04 00 01 00 36 00 00 …6…
00000370: 00 02 00 37 …7
class 文件是一个二进制文件,转化后是16进制展示,实际上class文件
就是一张表,它由以下数据项构成,这些数据项从头到尾严格按照以下顺序排
列:
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u4 | magic | 1 | 模数 |
u2 | minor_version | 1 | 次版本号 |
u2 | major_version | 1 | 主版本号 |
u2 | constant_pool_count | 1 | 常量计数 |
cp_info | constant_pool | constant_pool_count- 1 | 具体常量 |
u2 | access_flags | 1 | 访问标志 |
u2 | this_class | 1 | 类索引 |
u2 | super_class | 1 | 父类索引 |
u2 | interfaces_count | 1 | 接口索引 |
u2 | interfaces | interfaces_count | 具体接口 |
u2 | fields_count | 1 | 字段个数 |
field_info | fields | fields_count | 具体字段 |
u2 | methods_count | 1 | 方法个数 |
method_info | methods | methods_count | 具体方法 |
u2 | attributes_count | 1 | 属性个数 |
attribute_info | attributes | attributes_count | 具体属性 |
运行时数据区
程序计数器:
- 线程私有,每一个线程都有对应的程序计数器,是一块较小的内存空间,用于记录当前线程执行的下一个字节码指令的地址。
- 不会溢出
虚拟机栈:
- 线程私有,生命周期与线程相同。它描述的是Java方法执行的当前线程的内存模型,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从被调用直至执行完毕就对应者栈帧在虚拟机栈中从入栈到出栈的过程。
- 如果栈的深度大于虚拟机允许的深度,抛出栈溢出异常 java.lang.StackOverflowError,内存申请不足 抛出
OutOfMemoryError
本地方法栈:
- 线程私有,本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点。不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法
- 如果是创建的栈的深度大于虚拟机允许的深度,抛出 StackOverFlowError
内存申请不够的时候,抛出 OutOfMemoryError
堆:
由上图可以看出,jdk1.8的内存模型是由2部分组成,年轻代 + 年老代。永
久代被干掉,换成了Metaspace(元数据空间)
年轻代:Eden + 2*Survivor (不变)
年老代:OldGen (不变)
元空间:原来的perm区 (重点!)
需要特别说明的是:Metaspace所占用的内存空间不是在虚拟机内部,而
是在本地内存空间中,这也是与1.7的永久代最大的区别所在。
- 在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例
- 内存不足时,抛出 java.lang.OutOfMemoryError: Java heap space
方法区:
线程共享,是个逻辑概念,没有具体的归属,具体方法区主要存的内容可以划分为两类:
- 类信息(元空间):主要指类相关的版本、字段、方法、接口描述、引用等
- 运行时常量池(堆):编译阶段生成的常量与符号引用、运行时加入的动态变
量 - OutOfMemoryError: Metaspace
类加载
加载
类的加载就是将class文件中的二进制数据读取到内存中,然后将该字节流
所代表的静态数据结构转化为方法区中运行的数据结构,并且在堆内存中生成
一个java.lang.Class对象作为访问方法区数据结构的入口。
类加载器
jvm提供了3个系统加载器,分别是Bootstrp loader、ExtClassLoader、AppClassLoader
1)Bootstrp loader
Bootstrp加载器是用C++语言写的,它在Java虚拟机启动后初始化,它主要负责加载以下路径的文件:
%JAVA_HOME%/jre/lib/.jar
%JAVA_HOME%/jre/classes/
-Xbootclasspath参数指定的路径
System.out.println(System.getProperty("sun.boot.class.path"));
2)ExtClassLoader
ExtClassLoader是用Java写的,具体来说就是sun.misc.Launcher$ExtClassLoader
ExtClassLoader主要加载:
- %JAVA_HOME%/jre/lib/ext/*
- ext下的所有classes目录
- java.ext.dirs系统变量指定的路径中类库
System.getProperty("java.ext.dirs");
3)AppClassLoader
AppClassLoader也是用Java写成的,它的实现类是
sun.misc.Launcher$AppClassLoader,另外我们知道ClassLoader中有个
getSystemClassLoader方法,此方法返回的就是它。
- 负责加载 -classpath 所指定的位置的类或者是jar文档
- 也是Java程序默认的类加载器
System.getProperty("java.class.path");
除了上面的系统提供的3种loader,jvm允许自己定义类加载器,继承ClassLoader这个抽象类,并覆盖对应的findClass方法即可典型的在tomcat上。
双亲委派
ClassLoader.loadClass()方法:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 首先,检测是否已经加载Class<?> c = findLoadedClass(name);if (c == null) {//如果没有加载,开始按如下规则执行:long t0 = System.nanoTime();try {if (parent != null) {//重点!父加载器不为空则调用父加载器的
loadClassc = parent.loadClass(name, false);} else {//父加载器为空则调用Bootstrap
Classloaderc = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {}if (c == null) {long t1 = System.nanoTime();//父加载器没有找到,则调用findclass,自己查找
并加载c = findClass(name);}}if (resolve) {resolveClass(c);}return c;}}
设计目的:避免重复加载、 避免核心类篡改
验证
加载完成后,class里定义的类结构就进入了内存的方法区,验证时连接阶段的第一步,以下多个验证:
文件格式验证:验证加载的字节码是不是符合规范
元数据验证:主要验证属性、字段、类关系、方法等是否合规
字节码验证:主要验证class里定义的方法,看方法内部的code是否合法。
符号引用验证符号引用验证:字节码里有的是直接引用,有的是指向了其他的字节码地址,这里验证对应的内容是否合法
准备
这个阶段为class中定义的各种类变量分配内存,并赋初始值。
-
类变量是静态变量
-
存储位置:1.8以后,静态类变量如果是一个对象,其实它在堆里
-
初始值:
//普通类变量:在准备阶段为它开了内存空间,但是它的value是int的初始 值,也就是 0! //而真正的123赋值,是在类构造器,也就是下面的初始化阶段 public static int a = 123; //final修饰的类变量,编译成字节码后,是一个ConstantValue类型 //这种类型,在准备阶段,直接给定值123,后期也没有二次初始化一说 public static final int b = 123;
解析
解析阶段开始解析类之间的关系,需要关联的类被加载。
这涉及到:
- 类或接口的解析:类相关的父子继承,实现的接口都有哪些类型?
- 字段的解析:字段对应的类型?
- 方法的解析:方法的参数、返回值、关联了哪些类型
- 接口方法的解析:接口上的类型?
经过解析后,当前class里的方法字段父子继承等对象级别的关系解析完成。
这些操作上相关的类信息也被加载。
初始化
这里所说的初始化是一个class类加载到内存的过程,所谓的初始化指的是
类里定义的类变量。也就是静态变量。
这个初始化要和new一个类区分开来。new的是实例变量,是在执行阶段
才创建的。
实例变量创建的过程
当我们在方法里写了一段代码,执行过程中,要new一个类的时候,会发
生以下事情:
- 在方法区中找到对应类型的类信息
- 在当前方法栈帧的本地变量表中放置一个reference指针
- 在堆中开辟一块空间,放这个对象的实例
- 将指针指向堆里对象的地址,完工!
对象创建
内存分配方式
-
指针碰撞
这种分配前提是内存中有整片连续的空间,用的在一边,空闲的在另一
边,一个指针指向分界线。需要多少指针往空闲那边移动多少,直接划分出来一段,给当前对象,完工。 -
空闲列表
那如果jvm堆不那么规整呢?用的和没用的交叉在一起,也就是我们所说的
内存碎片。 这种情况就需要我们单独有一张表来记录,哪些内存块是空的。
分配的时候查表,找到大小够用的一块,分配给对象,同时更新列表。 具体哪种方式,和我们的垃圾回收器有关系。有的垃圾回收器会对内存做
整理压缩,那就指针碰撞简单高效。如果没有压缩功能,那只能是采用空闲列
表 -
并发性
指针碰撞和空闲列表,在并发分配的情况下都会存在并发问题,解决方式:
方式一:cas原子操作 + 失败重试
方式二:本地线程分配缓冲(TLAB)
全称是 Thread Local Allocation Buffer,需要 -XX:+/-UseTLAB
让线程在创建时,先独享划走一部分堆。
那么线程创建对象需要内存时,可以在自己划走的堆上先操作。相当于每
个线程批发了一批内存先用着。当前线程空间不够时,再去公共堆上申请,这样就减少了并发冲突的机
会。当然也多少有点浪费
内存布局
对象在堆上的布局,可以分为三个部分:对象头、实例数据、对齐填充。
对象头
对象头一般分为两部分,Mark Word 和 类型指针
- Mark Word,存储对象自己运行时的数据,如哈希码、GC分代年龄、锁状态标记、线程持有的锁、偏向的线程id……
- 类型指针,指向当前对象的类型,也就是方法去里类信息的地址。
实例数据
对象里各个字段的值
对齐填充
内存管理系统要求对象的大小必须是8字节的整数倍。实例数据部分不一定。如果没有对齐的话,通过这里的对齐填充补满它。
对象的访问
对象创建了,就要用,它在堆里。
我们的程序运行时,大家知道,每一个方法相关的变量信息都在栈里。那么怎么找到这个对象呢?
一般来讲,两种方案
句柄访问
句柄方式:
栈指针指向堆里的一个句柄的地址,这个句柄再定义俩指针分别指向类型
和实例
很显然,垃圾回收移动对象的话只需要改句柄即可,不会波及到栈,但是
多了一次寻址操作
直接地址
直接地址:
栈指针指向的就是实例本身的地址,在实例里封装一个指针指向它自己的
类型
很显然,垃圾回收移动对象要改栈里的地址值,但是它减少了一次寻址操
作。
备注:hostspot使用的是直接地址方式
对象的销毁
jvm参数
jvm的参数类型看上去杂乱,其实一共就三类,分别是:
-
标准参数(-):所有的JVM实现都必须实现这些参数的功能,而且向后兼容
-
-help
-
-version
-
-
非标准参数(-X):默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容
- -Xint
- -Xcomp
-
非Stable参数(-XX):各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用
- -XX:newSize
- -XX:+UseSerialGC
标准参数
jvm的标准参数,一般都是很稳定的,在未来的JVM版本中不会改变。
使用java -help列出来的就是标准参数。
实例一:java -version
实例二:通过-D设置系统属性参数
public class TestJVM {public static void main(String[] args) {//获取命令行 -D设置的参数String str = System.getProperty("str");System.out.println(str);}
}
将会输出abc
-X参数
jvm的-X参数是非标准参数,也就意味着,在不同版本的jvm中,参数可能
会有所不同,可以通过java -X查看非标准参数
[root@node01 test]# java -X
-Xmixed 混合模式执行 (默认) #了解!
-Xint 仅解释模式执行 #了解!
-Xbootclasspath:<用 : 分隔的目录和 zip/jar 文件>
设置搜索路径以引导类和资源
-Xbootclasspath/a:<用 : 分隔的目录和 zip/jar 文件>
附加在引导类路径末尾
-Xbootclasspath/p:<用 : 分隔的目录和 zip/jar 文件>
置于引导类路径之前
-Xdiag 显示附加诊断消息
-Xnoclassgc 禁用类垃圾收集
2)案例一:-Xint、-Xcomp、-Xmixed
在解释模式(interpreted mode)下,-Xint标记会强制JVM执行所有的字
节码,当然这会降低运行速度,通常低10倍或更多。
-Xcomp参数与它(-Xint)正好相反,JVM在第一次使用时会把所有的
字节码编译成本地代码,从而带来最大程度的优化。
然而,很多应用在使用-Xcomp也会有一些性能损失,当然这
比使用-Xint损失的少,原因是-xcomp没有让JVM启用JIT编译
器的全部功能。JIT编译器可以对是否需要编译做判断,如果所
有代码都进行编译的话,对于一些只执行一次的代码就没有意
义了。
-Xmixed是混合模式,将解释模式与编译模式进行混合使用,由jvm自
己决定,这是jvm默认的模式,也是推荐使用的模式。
-Xincgc 启用增量垃圾收集
-Xloggc:<file> 将 GC 状态记录在文件中 (带时间戳)
-Xbatch 禁用后台编译
-Xms<size> 设置初始 Java 堆大小 #掌握!
-Xmx<size> 设置最大 Java 堆大小 #掌握!
-Xss<size> 设置 Java 线程堆栈大小 #掌握!
-Xprof 输出 cpu 配置文件数据
-Xfuture 启用最严格的检查, 预期将来的默认值
-Xrs 减少 Java/VM 对操作系统信号的使用 (请参阅文
档)
-Xcheck:jni 对 JNI 函数执行其他检查
-Xshare:off 不尝试使用共享类数据
-Xshare:auto 在可能的情况下使用共享类数据 (默认)
-Xshare:on 要求使用共享类数据, 否则将失败。
-XshowSettings 显示所有设置并继续
-XshowSettings:all
显示所有设置并继续
-XshowSettings:vm 显示所有与 vm 相关的设置并继续
-XshowSettings:properties
显示所有属性设置并继续
-XshowSettings:locale
显示所有与区域设置相关的设置并继续
-X 选项是非标准选项, 如有更改, 恕不另行通知。
例如:-Xms与-Xmx参数
-Xms与-Xmx分别是设置jvm的堆内存的初始大小和最大大小。
-Xmx2048m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为
2048M。
-Xms512m:等价于-XX:InitialHeapSize,设置JVM初始堆内存为
512M。
-XX参数
-XX参数也是非标准参数,主要用于改变jvm的一些基础行为,比如垃圾回
收行为、jvm的调优、输出debug调试信息等。
可以通过以下方式查看(非常多!):
java -XX:+PrintFlagsFinal : 被修改过,最终生效的配置
java -XX:+PrintFlagsInitial : jvm初始化的配置
-XX参数的使用有2种方式,一种是boolean类型,一种是非boolean类型:
boolean类型
-
格式:-XX:[±] 表示启用或禁用属性
-
如:-XX:+DisableExplicitGC 表示禁用手动调用gc操作,也就是说调用System.gc()无效
非boolean类型
- 格式:-XX:= 表示属性的值为
- 如:-XX:NewRatio=4 表示新生代和老年代的比值为1:4
#行为参数(功能开关)
-XX:-DisableExplicitGC 禁止调用System.gc();但jvm的gc仍然有效
-XX:+MaxFDLimit 最大化文件描述符的数量限制
-XX:+ScavengeBeforeFullGC 新生代GC优先于Full GC执行
-XX:+UseGCOverheadLimit 在抛出OOM之前限制jvm耗费在GC上的时间比例
-XX:-UseConcMarkSweepGC 对老生代采用并发标记交换算法进行GC
-XX:-UseParallelGC 启用并行GC
-XX:-UseParallelOldGC 对Full GC启用并行,当-XX:-UseParallelGC
启用时该项自动启用
-XX:-UseSerialGC 启用串行GC
-XX:+UseThreadPriorities 启用本地线程优先级
#性能调优
-XX:LargePageSizeInBytes=4m 设置用于Java堆的大页面尺寸
-XX:MaxHeapFreeRatio=70 GC后java堆中空闲量占的最大比例
-XX:MaxNewSize=size 新生成对象能占用内存的最大值
6.1.5 参数查询
如果想要查看正在运行的jvm就需要借助于jinfo命令查看。
1)起一个进程,让它处于运行中
-XX:MaxPermSize=64m 老生代对象能占用内存的最大值
-XX:MinHeapFreeRatio=40 GC后java堆中空闲量占的最小比例
-XX:NewRatio=2 新生代内存容量与老生代内存容量的比例
-XX:NewSize=2.125m 新生代对象生成时占用内存的默认值
-XX:ReservedCodeCacheSize=32m 保留代码占用的内存容量
-XX:ThreadStackSize=512 设置线程栈大小,若为0则使用系统默认值
-XX:+UseLargePages 使用大页面内存
#调试参数
-XX:-CITime 打印消耗在JIT编译的时间
-XX:ErrorFile=./hs_err_pid<pid>.log 保存错误日志或者数据到文件中
-XX:-ExtendedDTraceProbes 开启solaris特有的dtrace探针
-XX:HeapDumpPath=./java_pid<pid>.hprof 指定导出堆信息时的路径或
文件名
-XX:-HeapDumpOnOutOfMemoryError 当首次遭遇OOM时导出此时堆中相关信
息
-XX:OnError="<cmd args>;<cmd args>" 出现致命ERROR之后运行自定义命
令
-XX:OnOutOfMemoryError="<cmd args>;<cmd args>" 当首次遭遇OOM时
执行自定义命令
-XX:-PrintClassHistogram 遇到Ctrl-Break后打印类实例的柱状信
息,与jmap -histo功能相同
-XX:-PrintConcurrentLocks 遇到Ctrl-Break后打印并发锁的相关信
息,与jstack -l功能相同
-XX:-PrintCommandLineFlags 打印在命令行中出现过的标记
-XX:-PrintCompilation 当一个方法被编译时打印相关信息
-XX:-PrintGC 每次GC时打印相关信息
-XX:-PrintGCDetails 每次GC时打印详细信息
-XX:-PrintGCTimeStamps 打印每次GC的时间戳
-XX:-TraceClassLoading 跟踪类的加载信息
-XX:-TraceClassLoadingPreorder 跟踪被引用到的所有类的加载信息
-XX:-TraceClassResolution 跟踪常量池
-XX:-TraceClassUnloading 跟踪类的卸载信息
-XX:-TraceLoaderConstraints 跟踪类加载器约束的相关信息
参数查询
如果想要查看正在运行的jvm就需要借助于jinfo命令查看。
-
jps查到他的进程号
shawn@macpro:~ > jps
44561
2691 Launcher
2692 App
2693 Jps
-
查询看运行参数
shawn@macpro:~ > jinfo -flags 2692
VM Flags:
-XX:CICompilerCount=3 -XX:InitialHeapSize=134217728 -
XX:MaxHeapSize=2147483648 -XX:MaxNewSize=715653120 -
XX:MinHeapDeltaBytes=524288 -XX:NewSize=44564480 -
XX:OldSize=89653248 -XX:+UseCompressedClassPointers -
XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -
XX:+UseParallelGC
#查看某一参数的值,用法:jinfo -flag <参数名> <进程id>
shawn@macpro:~ > jinfo -flag MaxHeapSize 2692
-XX:MaxHeapSize=2147483648
垃圾回收
**回收地点:**主要是堆区
回收时间:
-
在堆内存存储达到一定阈值之后,当年轻代或者老年代达到一定阈值,Java虚拟机无法再为新的对象分配内存空间了,那么Java虚拟机就会触发一次GC去回收掉那些已经不会再被使用到的对象
-
主动调用System.gc() 后尝试进行回收,手动调用System.gc()方法,通常这样会触发一次的Full GC,所以一般不推荐这个东西的使用,你会干扰jvm的运作
回收对象:
引用计数法:
假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败时,对象A的引用计数器就-1,如果对象A的计数器的值为0,就说明对象A没有引用了,可以被回收。
优点:实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报outofmember 错误。区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象。
缺点:
每次对象被引用时,都需要去更新计数器,有一点时间开销。
浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。
无法解决循环引用问题。(最大的缺点)
可达性分析算法:
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(ReferenceChain),如果某个对象到GC Roots间没有任何引用链相连,就说明从GCRoots到这个对象不可达时,则证明此对象是不可能再被使用的,就是可以回收的对象。
在JVM虚拟机中,可作为GC Roots的对象包括以下几种:
在虚拟机栈(栈帧中的本地变量表)中引用的对象
在方法区中类静态属性引用的对象(类变量)。
在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
所有被同步锁(synchronized关键字)持有的对象。
Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码
缓存等。
对象的四类引用
在java中,对象的引用主要有4种,从上到下级别依次降低。不同的引用回收的态度不同
强引用
在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,内存用不回收,够就够,不够抛内存溢出异常。
软引用
用来描述一些还有用,但非必须的对象。被SoftReference包装的那些类先回收没用的对象,收完后发现还不够,再触发二次回收,对软引用对象下手。
弱引用
用来描述那些非必须对象,强度比软引用更弱。被WeakReference包装的那些类无论当前内存是否足够,垃圾收集一旦发生,弱引用直接回收。
虚引用(实际开发基本不用)
最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
回收算法
标记清除算法
标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
标记:从根节点开始标记引用的对象。
清除:未被标记引用的对象就是垃圾对象,清理掉。
标记清除法可以说是最基础的收集算法,因为后续的收集算法大多都是以
标记-清除算法为基础,对其缺点进行改进而得到的。
缺点:
- 执行效率较低,标记和清除两个动作都需要遍历所有的对象,并且在
GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体
验是非常差的。 - 通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对
象可能存在于内存的各个角落,所以清理出来的内存是不连贯的。
标记整理算法
在标记清除算法的基础之上,做了优化改进的算法。
和标记清除算法一样,也是从根节点开始,对对象的引用进行标记在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。
特点
- 该算法解决了标记清除算法的碎片化的问题,下一步分配内存的时候更方便
- 多了一步整理操作,对象需要移动内存位置,效率也好不到哪去。
标记复制算法
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
优缺点
优点:
-
在垃圾对象多的情况下,效率较高,因为要把存活的全部移动一遍
-
清理后,内存无碎片
缺点:
- 在垃圾对象比例少的情况下,不适用,如:年轻代这么用可以,老年代就不合适
- 分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低
年轻代的标记复制算法
年轻代内存的回收就是典型的标记复制法
- sruvivor区有两个,一个from,另一个叫to,这俩交替互换角色
- 在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
- 紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。
- 年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置) 的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。
- 经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。
- GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
分代
在堆内存中,有些对象短暂存活有些则是长久存活,所以需要将堆内存进行分代,将短暂存活的对象放到一起,进行高频率的回收,长久存活的对象集中放到一起,进行低频率的回收细粒度的控制不同区域,调节不同的回收频率,节约系统资源(回收期间系统要额外干活的!)。
分代算法其实就是这样的,根据回收对象的特点进行选择,在jvm中,年轻代适合使用复制算法,老年代适合使用标记清除或标记压缩算法。
相关概念
- 部分收集(Partial GC)
- 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
- 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。(CMS收集器)
- 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。(G1收集器)
- 整堆收集(Full GC)
- 所有的内存整理一遍,包括堆和方法区。轻易不要触发
回收器(执行者)
在jvm中,实现了多种垃圾收集器,这些收集器种类繁多,看似乱七八糟,其实理清楚后很简单
-
先明白几件事情
- 用户线程:java程序运行后,用户不停请求操作jvm内存,这些称为用户线程
- GC线程:jvm系统进行垃圾回收启动的线程
- 串行:GC采用单线程,收集时停掉用户线程
- 并行:GC采用多线程,收集时同样要停掉用户线程
- 并发:用户线程和GC线程同步进行,这意义就不一样了
- STW:stop the world ,暂停响应用户线程,只提供给GC线程工作来回收垃圾(很不爽的事情)
- 分代:垃圾收集器是要工作在某个代上的,可能是年轻代,老年代,有的可能两个代都能工作
- 组合:因为分代,所以得有组合,你懂得……
-
准备案例
在开始前,我们先准备一个内存堆积的案例,下面学习收集器,只需要在启动时指定不同的XX参数即可:
package cn.itcast.jvm; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.Random; public class TestGC {public static void main(String[] args) throws Exception {List<Object> list = new ArrayList<Object>();//模拟web中不停请求while (true){int sleep = new Random().nextInt(100);if(System.currentTimeMillis() % 2 ==0){//模拟释放,如果恰好请求时间是偶数,清空列表list.clear();}else{//模拟业务,从db中查询了10000条记录for (int i = 0; i < 10000; i++) {Properties properties = new Properties();properties.put("key_"+i, "value_" + System.currentTimeMillis() + i);list.add(properties);}}System.out.println("list大小为:" + list.size());//模拟请求间隔,0-100ms随机Thread.sleep(sleep);}} }
串行
1)概述
其实是两个收集器,年轻代的叫 Serial , 老年代的叫 Serial Old,很好记!
这是最基础的,历史最悠久的收集器。听名字就知道,这个属于串行收集器,即:GC时,停掉用户线程,同时,
GC本身也是只有一个线程在跑
2)原理
很简单,GC时暂停用户进程,新生代 Serial 采用复制算法,Serial Old采用标记整理算法。
3)优缺点
单线程 + STW,那么这个收集器还有存在的价值吗?
答案是:有!我们在吐槽单线程的同时,不要忘了,单线程带来的便捷性
实际上,Serial收集器依然是hotspot在客户端模式下的默认收集器,因为它足够简单有效,没有多线程GC的协调和额外开销,在单核或资源有限的环境下,单线程甚至比多线程还要高效。
而Serial Old则作为下面几款垃圾收集器的兜底措施,比如CMS、G1等处理不了老年代时,他们会自动启用SOld来做FullGC进行收集。
4)配置参数
- -XX:+UseSerialGC
指定年轻代和老年代都使用串行垃圾收集器 - -XX:+PrintGCDetails
打印垃圾回收的详细信息
5)操作案例
# 为了测试GC,将堆的初始和最大内存都设置为16M
# java代码使用本节开头的例子
-XX:+UseSerialGC -XX:+PrintGCDetails -Xms16m -Xmx16m
6)启动程序,可以看到下面信息:
[GC (Allocation Failure) [DefNew: 4416K->512K(4928K),
0.0046102 secs] 4416K->1973K(15872K), 0.0046533 secs] [Times:
user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 10944K-
>3107K(10944K), 0.0085637 secs] 15871K->3107K(15872K),
[Metaspace: 3496K->3496K(1056768K)], 0.0085974 secs] [Times:
user=0.02 sys=0.00, real=0.01 secs]
DefNew:表示使用的是串行垃圾收集器。
4416K->512K (4928K):表示,年轻代GC前,占有4416K内存,GC后,占有512K内存,总大小4928K
0.0046102 secs:表示,GC所用的时间,单位为秒。
4416K->1973K (15872K):表示,GC前,堆内存占有4416K,GC后,占有1973K,总大小为15872K
Full GC:表示,内存空间全部进行GC,老年代、元空间
并行
1)概述
- ParNew收集器:
新生代的,无非就是将Serial的单线程换成多线程,它现在存在的唯一价值就是作为新生代收集器配合老年代的CMS收集器一起工作,并且在jdk9里也已不再推荐这套组合,而是推荐G1。我们只需要知道的是:曾经,它存在过。 - 另外一对并行收集器:Parallel Scavenge (新生代的) / Parallel Old (老年代的)
关于并行收集器,我们重点看Parallel这一对,这一对也是主流的jdk8下默
认收集器
2)详解
Parallel这一对,它所关注的是系统的吞吐量。所谓吞吐量,反映的是用户线程在系统整体时间里可用的比例:
即: 吞吐量 = 用户代码运行时间 / ( 用户代码运行时间 + 垃圾收集器运行时间 )
这一点,从它的配置参数上直接就能看出来
3)参数
-
-XX:+UseParallelGC
- 年轻代使用ParallelGC垃圾回收器,老年代使用串行回收器。
-
-XX:+UseParallelOldGC
- 年轻代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC
垃圾回收器。
- 年轻代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC
-
-XX:MaxGCPauseMillis
- 设置最大的垃圾收集时的停顿时间,单位为毫秒
- 需要注意,ParallelGC为了达到设置的停顿时间,可能会调整
堆大小或其他的参数,如果堆的大小设置的较小,就会导致
GC工作变得很频繁,反而可能会影响到性能。 - 该参数使用需谨慎。
-
-XX:GCTimeRatio
-
直接设置垃圾回收时间占程序运行时间的最大百分比,公式为1/(1+n)。
-
它的值为0~100之间的数字,默认值为99,也就是垃圾回收时间不能超过1%
-
-
-XX:UseAdaptiveSizePolicy
- 自适应GC模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的平衡。
- 一般用于,手动调整参数比较困难的场景,让收集器自动进行
调整。
4)调试
#参数
-XX:+UseParallelGC -XX:+UseParallelOldGC -
XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -Xms16m -Xmx16m
#打印的信息
[GC (Allocation Failure) [PSYoungGen: 4096K->480K(4608K)]
4096K->1840K(15872K), 0.0034307 secs] [Times: user=0.00
sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 505K->0K(4608K)]
[ParOldGen: 10332K->10751K(11264K)] 10837K->10751K(15872K),
[Metaspace: 3491K->3491K(1056768K)], 0.0793622 secs] [Times:
user=0.13 sys=0.00, real=0.08 secs]
PSYoungGen:年轻代,Parallel Scavenge
ParOldGen:老年代,Parallel Old
并发 - CMS
1)简介
CMS收集器,工作在老年代。
前面的收集器都是要停止用户线程的,而CMS收集器这是真正意义上的并
行处理器,也就是用户线程和GC线程在同一时间一起工作。
2)执行过程
- 初始化标记(CMS-initial-mark) :标记root直接关联的对象,会导致stw,但是这个没多少对象,时间短
- 并发标记(CMS-concurrent-mark):沿着上一步的root,往下追踪,这步耗时最长,但是与用户线程同时运行
- 重新标记(CMS-remark) :因为上一步是并发进行的,所以再增量过一遍有变化的,会导致stw,但比上一步少很多
- 并发清除(CMS-concurrent-sweep):标记完的干掉,因为是标记-清除算法,不需要移动存活对象,所以这一步与用户线程同时运行
- 重置线程:重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行
3)测试
#设置启动参数
-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xms16m -Xmx16m
#运行日志
#注意,cms默认搭配的新生代是 parnew :
[GC (Allocation Failure) [ParNew: 4926K->512K(4928K),
0.0041843 secs] 9424K->6736K(15872K), 0.0042168 secs] [Times:
user=0.00 sys=0.00, real=0.00 secs]
#老年代开始:
#第一步,初始标记
[GC (CMS Initial Mark) [1 CMS-initial-mark: 6224K(10944K)]
6824K(15872K), 0.0004209 secs] [Times: user=0.00 sys=0.00,
real=0.00 secs]
#第二步,并发标记
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.002/0.002 secs] [Times: user=0.00
sys=0.00, real=0.00 secs]
#第三步,预处理
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00
sys=0.00, real=0.00 secs]
#第四步,重新标记
[GC (CMS Final Remark) [YG occupancy: 1657 K (4928 K)][Rescan
(parallel) , 0.0005811 secs][weak refs processing, 0.0000136
secs][class unloading, 0.0003671 secs][scrub symbol table,
0.0006813 secs][scrub string table, 0.0001216 secs][1 CMS-
remark: 6224K(10944K)] 7881K(15872K), 0.0018324 secs] [Times:
user=0.00 sys=0.00, real=0.00 secs]
#第五步,并发清理
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.004/0.004 secs] [Times: user=0.00
sys=0.00, real=0.00 secs]
#第六步,重置
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00
sys=0.00, real=0.00 secs]
由以上日志信息,可以看出CMS执行的过程。
4)优缺点
优点:
- 不可否认,一款优秀的收集器,并发收集,低停顿。
- 互联网服务器上低停顿的现实要求很吻合,一个网站总不能告诉用户你
用10分钟,歇会再来用。
但是,CMS也不是完美的:
- 它不能等到内存吃紧了才启动收集。因为收集期间用户线程还在跑,得
预留。 - 浮动垃圾干不掉,在并发标记、并发清理时,产生的新垃圾必须到下一
次收集时处理。 - 标记-清除算法,免不了产生碎片,可以开启压缩但这些参数在jdk9里
也已废弃掉 - 最后,搭配CMS的年轻代现在只剩下了ParNew,是那么的苍白无力。
实际上,jdk9开始已经把它逐步淘汰
那么替代它的是谁呢?G1出场……
并发 - G1
1)概述
为解决CMS算法产生空间碎片和其它一系列的问题缺陷,G1(Garbage=First)算法,在JDK 7u4版本被正式推出
oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS。
JDK9默认G1为垃圾收集器的提案:https://openjdk.java.net/jeps/248
将CMS标记为丢弃的提案:https://openjdk.java.net/jeps/291
G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:
- 第一步,开启G1垃圾收集器
- 第二步,设置堆的最大内存
- 第三步,设置最大的停顿时间
2)原理
G1打破了之前的传统观念,它依然把内存划分为eden、survivor、old,
同时多了一个humongous(巨大的)区来存巨型对象。
但是,这些区在物理地址上不再连续。而是把整个物理地址分成一个个大
小相等的region,每一个region可以是上面角色中的一个,还可以在某个时刻
转变角色,从eden变成old !(就是个标签)
这样收集的时候,它收集某些性价比高的region回收就可以了。所以某个
时刻,G1可能连老带少一起收拾。
这是一个划时代的改变!
那它是怎么做的呢?收拾哪些区块呢?
先看两个概念,容易搞混:
- Remembered Set:记忆集,简称RS,每个 Region关联一个。RS 比
较复杂,简单来说就是记录Region之间对象的引用关系。 - Collection Set:简称CSet,在一次收集中,那些性价比高的Region揪
出来组成一个回收集,将来一口气回收掉。这个集合里是筛选出来的一
些Region,至于Region里面剩下的存活的对象,多个Region压缩到一个空闲
Region里去,这样就完成了一次收集。
3)模式
G1中提供了三种模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,
在不同的条件下被触发。
所谓的模式,其实也就是G1收集的时候,Region选哪种,是只选年轻代的
Region?还是两种都筛选?
-
Young GC
选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。参数 含义 -XX:MaxGCPauseMillis 设置G1收集过程目标时间,默认值200ms -XX:G1NewSizePercent 新生代最小值,默认值5% XX:G1MaxNewSizePercent 新生代最大值,默认值60% -
Mixed GC
选定所有年轻代里的Region,外加统计的在用户指定的开销目标范围内选择收益高的老年代Region
参数 含义 XX:InitiatingHeapOccupancyPercent 当老年代大小占整个堆大小百分比达到该阈值时,会触发一次mixed gc. -
full GC
严格意义上讲,这不属于G1的模式。但是使用G1时是有可能发生的。当mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会改为使用serial old GC(full GC)来收集整
个堆。
4) 运行过程
- 初始标记:标记出 GC Roots 直接关联的对象,这个阶段速度较快,
STW,单线程执行。
- 并发标记:从 GC Root 开始对堆中的对象进行可达新分析,找出存活
对象,这个阶段耗时较长,但可以和用户线程并发执行。
- 重新标记:修正在并发标记阶段因用户程序执行而产生变动的标记记
录。STW,并发执行。
- 筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排
序,根据用户所期望的 GC 停顿时间来制定回收计划,筛出CSet后移动
合并存活对象到空Region,清除旧的,完工。因为这个阶段需要移动
对象内存地址,所以必须STW。
思考一下,这属于什么算法呢???
答:从Region的动作来看G1使用的是标记-复制算法。而在全局视角上,类似标记 - 整理
总结:G1前面的几步和CMS差不多,只有在最后一步,CMS是标记清除,G1需要
合并Region属于标记整理
5)优缺点
- 并发性:继承了CMS的优点,可以与用户线程并发执行。当然只是在并
发标记阶段。其他还是需要STW
- 分代GC:G1依然是一个分代回收器,但是和之前的各类回收器不同,
它同时兼顾年轻代和老年代。而其他回收器,或者工作在年轻代,或者
工作在老年代;
- 空间整理:G1在回收过程中,会进行适当的对象移动,不像CMS只是
简单地标记清理对象。在若干次GC后,CMS必须进行一次碎片整理。
而G1不同,它每次回收都会有效地复制对象,减少空间碎片,进而提
升内部循环速度。
- 可预见性:为了缩短停顿时间,G1建立可预存停顿的模型,这样在用
户设置的停顿时间范围内,G1会选择适当的区域进行收集,确保停顿
时间不超过用户指定时间。
6)建议
-
如果应用程序追求低停顿,可以尝试选择G1;
-
经验值上,小内存6G以内,CMS优于G1,超过8G,尽量选择G1
-
是否代替CMS只有需要实际场景测试才知道。(如果使用G1后发现性能还不如CMS,那么还是选择CMS)
7)附:配置参数清单
=======G1 让垃圾回收配置简单很多,只需要打开并指定你预计的时间要求即
可=======
指定使用G1收集器:
"-XX:+UseG1GC"
为G1设置暂停时间目标,默认值为200毫秒;这个值不是越小越好。
太小的话会造成可供收集的Region数量偏少,跟不上对象产生的速度,反而会频
繁触发GC降低吞吐量
G1会根据这个目标决定收集行为:
"-XX:MaxGCPauseMillis"
=======附:其他参数,一般采用默认即可=======
设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约
2048个Region:
"-XX:G1HeapRegionSize"
新生代最小值,默认值5%:
"-XX:G1NewSizePercent"
新生代最大值,默认值60%:
"-XX:G1MaxNewSizePercent"
设置STW期间,并行GC线程数:
"-XX:ParallelGCThreads"
设置并发标记阶段,并行执行的线程数:
"-XX:ConcGCThreads"
当整个Java堆的占用率达到参数值时,开始触发mix gc;默认为45:"-XX:InitiatingHeapOccupancyPercent"
8)操作案例
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -
Xmx256m
#G1的日志不像CMS是严格按照事件顺序来的。
#属于分类统计,包含子操作
#总停顿时间
[GC pause (G1 Evacuation Pause) (young), 0.0044882 secs]
#并发处理耗时,线程数
[Parallel Time: 3.7 ms, GC Workers: 3]
#各个子项耗时情况……
[GC Worker Start (ms): Min: 14763.7, Avg: 14763.8, Max:
14763.8, Diff: 0.1][Ext Root Scanning (ms): Min: 0.2, Avg: 0.3, Max: 0.3,
Diff: 0.1, Sum: 0.8]
[Update RS (ms): Min: 1.8, Avg: 1.9, Max: 1.9, Diff:
0.2, Sum: 5.6]
[Processed Buffers: Min: 1, Avg: 1.7, Max: 3, Diff:
2, Sum: 5]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0,
Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0,
Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 1.1, Avg: 1.2, Max: 1.3, Diff:
0.2, Sum: 3.6]
[Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff:
0.2, Sum: 0.2]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1,
Diff: 0, Sum: 3]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0,
Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 3.4, Avg: 3.4, Max: 3.5,
Diff: 0.1, Sum: 10.3]
[GC Worker End (ms): Min: 14767.2, Avg: 14767.2, Max:
14767.3, Diff: 0.1]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.7 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.5 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
#重点:总的各个区的收集情况 收集前使用空间(总空间) -> 收集后使
用空间(总空间)
[Eden: 7168.0K(7168.0K)->0.0B(13.0M) Survivors: 2048.0K-
>2048.0K Heap: 55.5M(192.0M)->48.5M(192.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
归纳总结
1)垃圾收集器总结
名称 | 算法 | 工作区域 | 线程 | 模式 | 适合场合 | 优缺点 |
---|---|---|---|---|---|---|
Serial | 复制 | 新生代 | 单 | 串行 | 单CPU;Client模式下 | 缺:STW 优:简单高效,没有线程交互开销,专注于GC |
ParNew | 复制 | 新生代 | 多 | 并行 | 多CPU;Server模式下 | 缺:STW 优:并行并发GC |
Parallel Scavenge | 复制 | 新生代 | 多 | 并行 | 吞吐量控制,Client,Server均可以 | 主要关注吞吐量,通过吞吐量的设置控制停顿时间,适应不同的场景 |
SerialOld | 整理 | 老年代 | 单 | 串行 | 主要Client模式下 | 缺:STW,其它收集器搞不定时的保底选择 |
Parallle Old | 整理 | 老年代 | 多 | 并行 | 吞吐量控制,Client,Server均可以 | 主要关注吞吐量,通过吞间,适应不同的场景 |
CMS | 清除 | 老年代 | 多 | 并行 | 互联网站;B/S系统服务端 | 缺:CPU资源敏感,无法处理浮动垃圾,产生大量内存碎片; 优:并发收集,低停顿 |
G1 | 整理 | 均可 | 多 | 并行 | 面向服务端应用 | 优:并行与并发,分代收集,空间整合(标记整理算法),可预测停顿 |
2)一些规律
- 新生代都是标记 - 复制算法,老年代采用标记 - 整理,或清除(CMS)
- 历史性的收集器大多针对某个代,但是G1,以及未来的ZGC都是全代可用
- 没有绝对好用的收集器,需要在 吞吐量、延迟性、内存占用量上做权衡
- 数据分析、科学计算等场合,偏重吞吐量
- 互联网服务器、web网站,偏重服务的延迟度,不能出现严重顿挫
- 客户端、微型终端、嵌入式应用,内存占用低是关键
3)搭配组合
除了G1和ZGC这些全能选手,其他垃圾收集器需要搭配工作
但是组合不是想怎么来就怎么来的,下图展示可用组合,以及在某些版本中废弃掉的组合:
4)如何查看当前jdk的垃圾回收器呢?
java -XX:+PrintCommandLineFlags -version
#jdk8,默认Parallel
shawn@macpro:~ > java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -
XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -
XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed
mode)
#jdk11,默认换成了G1
shawn@macpro:~ > java -XX:+PrintCommandLineFlags -version
-XX:G1ConcRefinementThreads=4 -XX:GCDrainStackTargetSize=64 -
XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -
XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240
-XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -
XX:+UseCompressedOops -XX:+UseG1GC
java version "11.0.2" 2019-01-15 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.2+9-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.2+9-LTS,
mixed mode)
#jdk14,默认还是G1,但是已经支持ZGC了,需要打开实验性参数开关
shawn@macpro:~ > java -XX:+PrintCommandLineFlags -version
-XX:G1ConcRefinementThreads=4 -XX:GCDrainStackTargetSize=64 -
XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -
XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -
XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -
XX:+UseCompressedClassPointers -XX:+UseCompressedOops -
XX:+UseG1GC
java version "14.0.2" 2020-07-14
Java(TM) SE Runtime Environment (build 14.0.2+12-46)
Java HotSpot(TM) 64-Bit Server VM (build 14.0.2+12-46, mixed
mode, sharing)
调优实战
环境
-
我们依然使用上面收集器章节相同的案例,这次针对内存来做详细分析。
-
使用企业里主流的jdk8运行,其他jdk版本参数可能略有不同。
-
这里我们使用在线、简洁、高效的图形化分析工具: http://gceasy.io
-
gc日志参数
-XX:+PrintGC 输出GC日志 -XX:+PrintGCDetails 输出GC的详细日志 -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式) -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05- 04T21:53:59.234+0800) -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息 -Xloggc:gc.log jdk8日志文件的输出 -Xlog:gc*:gc.log jdk11日志输出方式略有不同
package cn.itcast.jvm;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Random;
public class TestGC {public static void main(String[] args) throws Exception {List<Object> list = new ArrayList<Object>();//模拟web中不停请求while (true){int sleep = new Random().nextInt(100);if(System.currentTimeMillis() % 2 ==0){//模拟释放,如果恰好请求时间是偶数,清空列表list.clear();}else{//模拟业务,从db中查询了10000条记录for (int i = 0; i < 10000; i++) {Properties properties = new Properties();properties.put("key_"+i, "value_" +
System.currentTimeMillis() + i);list.add(properties);}}System.out.println("list大小为:" + list.size());//模拟请求间隔,0-100ms随机Thread.sleep(sleep);}}
}
初始状态
参数
-Xmx32m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log
执行日志
运行一段时间后,在项目的目录下会看到gc.log
日志分析
打开 gceasy 的主页,上传后点击 Analyze 即可看到分析结果
1)整体内存概况
young与old全部吃满,均明显不足,需要调大,meta闲置,可以缩减
2)整体时间情况
收集时间绝大多数在10ms以内,说明收集的速度还可以,但是回收次数明显偏多,整体吞吐量94
3)回收前后的heap明显抖动频繁,整体偏高
4)GC间隔时间长的点,发生大量FullGC
5)大批量空间的有效释放在FullGC上
6)回收释放情况
年轻代和年老代回收效果一般,回收前后的两条线甚至发生交叉,应该偏离较远才说明有明显内存下降
Metaspace比较稳定
7)A&P:对象从年轻代晋升到老年代的情况
频繁晋升,跨代移动,说明年轻代不够用,老年代一旦堆积,极容易引发fullGC
8)GC次数及时间统计
发生76次GC,里面竟然有8是FullGC,不可容忍!
总GC耗时,410ms,FullGC占了将近一半
9)GC停顿情况
总耗时、平均每次GC耗时 5.39ms
10)GC被触发的原因
内存分配失败引发回收达到8次,这个会影响正常业务执行。
初步调优
参数
分析上面的情况,明显年轻代老年代内存均严重不足,那么最简单粗暴的方式,我们加大内存
-Xms256m -Xmx256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log
二次分析
重复上面的步骤,新开一个浏览器页签,方便对比分析日志,重点关注几个点:
1)总内存够用了,但是年轻代依然被吃爆。年老代闲置。
2)吞吐量上升,耗时特别长的gc分部区间明显减少,甚至消失
3)gc前后的空间曲线对比明显
4)FullGC消失!
5)GC大批量的内存释放发生在了年轻代
6)年轻代的回收前后两条曲线不再交叉,被明显剥离
7)老年代表示情绪稳定
8)年轻到老年代的晋升明显减少
9)FullGC完全消失,总GC次数明显减少到4,总停顿时间从上次的0.41s降低到0.02s
10)晋升的对象明显减少,创建速度提升
11)不再发生内存分配失败造成gc的现象
二次调优
参数
结合上次调优,我们发现,年轻代依然不够用,年老代闲置,对象还是会频繁从年轻代晋升到年老代。
结合我们的业务场景,大批量对象在请求后会被释放,属于短生命周期。包括我们现实中从数据库请求发送到网页后对象就完成了实名,属于同类场景。
所以,加大年轻代比例!
-Xms256m -Xmx256m -XX:NewSize=250m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log
日志分析
同样是256的内存,我们再次跑出日志分析看看差异
1)年轻代已基本够用,很少有对象再跑到老年代
2)吞吐量进一步上升
思考一下,那为什么pause的平均时间还变长了呢???
答:次数变少了,单次需要收集的对象多了,所以肯定要占时间,我们接着往下看总耗时
3)堆回收空间变化明显
4)gc次数明显下降,总时间进一步降低到20ms
lmRvYd-1755000948816)]
10)GC被触发的原因
内存分配失败引发回收达到8次,这个会影响正常业务执行。
[外链图片转存中…(img-rBAha3hi-1755000948816)]
初步调优
参数
分析上面的情况,明显年轻代老年代内存均严重不足,那么最简单粗暴的方式,我们加大内存
-Xms256m -Xmx256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log
二次分析
重复上面的步骤,新开一个浏览器页签,方便对比分析日志,重点关注几个点:
1)总内存够用了,但是年轻代依然被吃爆。年老代闲置。
[外链图片转存中…(img-fn4bnilD-1755000948816)]
2)吞吐量上升,耗时特别长的gc分部区间明显减少,甚至消失
[外链图片转存中…(img-DKOnQNSz-1755000948816)]
3)gc前后的空间曲线对比明显
[外链图片转存中…(img-QZrCCN5X-1755000948816)]
4)FullGC消失!
[外链图片转存中…(img-JABW8R5a-1755000948816)]
5)GC大批量的内存释放发生在了年轻代
[外链图片转存中…(img-fVf4WFbB-1755000948816)]
6)年轻代的回收前后两条曲线不再交叉,被明显剥离
[外链图片转存中…(img-ZBHXQz3c-1755000948816)]
7)老年代表示情绪稳定
[外链图片转存中…(img-VY6AOH7G-1755000948816)]
8)年轻到老年代的晋升明显减少
[外链图片转存中…(img-CPHol1o1-1755000948816)]
9)FullGC完全消失,总GC次数明显减少到4,总停顿时间从上次的0.41s降低到0.02s
[外链图片转存中…(img-bZ6rCfsI-1755000948816)]
10)晋升的对象明显减少,创建速度提升
[外链图片转存中…(img-B71WPIhw-1755000948816)]
11)不再发生内存分配失败造成gc的现象
[外链图片转存中…(img-mJnhKBet-1755000948816)]
二次调优
参数
结合上次调优,我们发现,年轻代依然不够用,年老代闲置,对象还是会频繁从年轻代晋升到年老代。
结合我们的业务场景,大批量对象在请求后会被释放,属于短生命周期。包括我们现实中从数据库请求发送到网页后对象就完成了实名,属于同类场景。
所以,加大年轻代比例!
-Xms256m -Xmx256m -XX:NewSize=250m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log
日志分析
同样是256的内存,我们再次跑出日志分析看看差异
1)年轻代已基本够用,很少有对象再跑到老年代
[外链图片转存中…(img-DYMSMTPt-1755000948816)]
2)吞吐量进一步上升
[外链图片转存中…(img-AHxD0qgG-1755000948816)]
思考一下,那为什么pause的平均时间还变长了呢???
答:次数变少了,单次需要收集的对象多了,所以肯定要占时间,我们接着往下看总耗时
3)堆回收空间变化明显
[外链图片转存中…(img-uwc3Ty6n-1755000948816)]
4)gc次数明显下降,总时间进一步降低到20ms