文章目录
一、JVM基础
1.1 Java虚拟机简介
1、是什么:
- 是一种抽象化的计算机,有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统
- 是一种规范,可用不同的方式加以实现,对应不同的虚拟机
2、内容:一个指令集、堆栈、 “垃圾堆”和一个方法区
3、Java语言跨平台
-
Sun以及其提供商提供了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码(.class文件)
-
.Java文件可以被被编译成.class
-
一旦一个JVM在给定的平台上运行,此.java文件都能在这个平台上运行,如图2所示
-
特点:JVM仅与字节码.class文件有关,与任何语言无关
4、Java和Jvm的关系:
- Java发布之初,就考虑不止让Java也要让其他语言运行在JVM上(时至今日,可以在JVM上执行的常见语言有:Python、Groovy、JS、PHP、Lua)
- java规范分为java语言规范和JVM规范。
5、常见的Jvm实现
-
JRockit:BEA公司的虚拟机,全部代码都靠即时编译器编译执行由于在GC方面的优异表现,被Oracle收购
-
HotSpot: 由于LongView技术公司设计,后来因为其在JIT编译上的优异表现被Sun收购,06年ORacle收购了Sun
现在我们使用的HotSpot = 原HotSpot(取JIT) + JRockit(取GC)
-
Zing:Azul公司的虚拟机,是真正的高性能虚拟机。一个VM实例可以管理至少数十个CPU+几百G内存资源,实现可控的GC时间
1.2 指令
1、定义
指令 = 操作码 + 操作数
getstatic #2 :表示去.class文件常量池中[02]处,获取静态字段的值
1.3 执行引擎
混合模式:解释器和JIT即时编译器的共存,使得HotSpot执行引擎能够平衡启动速度和运行效率
1、解释器解释执行
特点:逐条解释字节码指令进行执行
2、JIT及时编译器编译执行
特点:一整段执行
3、HotSpot执行引擎执行过程
1)每当执行完一条指令(操作码 + 操作数)后,PC寄存器会更新下一条需要被执行的指定的地址
2)解释执行阶段
-
当Java程序启动时,HotSpot执行引擎首先采用解释器,逐条解释执行字节码指令
即读取字节码指令,将其翻译成对应的机器码,并直接由硬件执行
-
优势
可以快速启动程序(不需要等待编译器编译整个程序)、节省内存(只需要加载和执行当前需要的代码)
3)编译执行阶段
- 热点代码探测:在执行过程中,HotSpot执行引擎会收集代码的执行情况,特别是那些执行频率高的代码块(称为“热点代码”)。热点代码探测通常基于计数器(如方法调用计数器)来实现
- 即时编译
- 一旦热点代码被探测到,HotSpot执行引擎就会将这些代码提交给JIT即时编译器进行编译
- JIT编译器会将热点代码编译成与本地平台相关的机器码,并进行优化以提高执行效率
- 编译后的代码会被缓存起来,以便后续再次执行时直接使用。这样,热点代码的执行速度就会大大提高
1.4 Jvm基于栈的指令集架构
源于: Java语言的跨平台特性
优点
- 可移植性好:基于栈的指令集架构不依赖于特定的硬件寄存器,因此可以更容易地实现跨平台的操作
- 实现简单:基于栈的指令集架构在设计和实现上相对简单
缺点
- 执行性能:与基于寄存器的指令集架构相比,执行相同操作时需要更多的指令数量,影响执行性能
二 、class文件
2.1 字节码简介
1、是什么:二进制01文件
2、如何生成: .class文件即字节码,是.java文件被编译后(javac)生成的文件,如图3所示
3、使用IDEA查看:
- IDEA安装jclassLib Bytecode插件
- IDEA View栏下使用Show Bytecode with jclassLib ,如图5所示
2.2 class文件内容
2.2.1 一般信息
如图6所示
- 主版本号:52-即Java8
- 访问标志
类是public的、final等修饰的
- 本类索引:#9即在常量池[09]中:全类名
- 字段计数:成员变量个数
- 方法计数:方法的个数
2.2.2 常量池
1、是什么:class文件的资源库
2、内容:14种不同结构的表数据
如:符号引用和字面量等
2.2.2.1 符号引用
class_info
类的全限定名:com/mjp/类名
Fieldref_info
- 字段所述的类|接口:字段所在类信息
- 字段名(name)和字段描述符(java/lang/String)
methodRef_info
名字和描述符:无参构造方法,在常量池[33]处有描述符
- 名字<<init>>:即构造方法
- 描述符()V:方法入参为空()、方法返回值为V即void
2.2.2.2 字面量
string_info
private String name = "mjp";
字符串字面量:cp_info #29 <mjp>
Integer_info
private final int a = 10;
Integer:10
2.2.2.3 字面量和符号引用
1、字面量
1)类型:string_info、Integer_info等
2)分类
- 文本字符串:String name = “mjp”; cp_info # 4
- 常量:final int a = 1;
二者均存在于常量池中,一个是[13]CONSTANT_Integer_info处、一个是[29]CONSTANT_utf8_info字符串常量池中
2、符号引用
1)类型:class_info、Fieldref_info、methodRef_info等
2)分类
- class_info:类、接口全限定名,cp_info # 1
- Fieldref_info:字段名称 和 字段描述符,cp_info # 2
- methodRef_info:方法名、方法入参、方法返回值类型,cp_info # 3
3、符号引用的作用
- 符号引用,保存了方法(所属类、描述符)的信息
- 在类加载过程时,可以通过符号引用,动态链接到直接内存地址引用
这样使得Xxx.class文件更小
2.2.3 字段
private String name;
- 名称:name
- 描述符:String
- 访问标志:public
2.2.4 方法
1、默认的无参构造方法
- 名称:<<init>>
- 描述符:<()V>表示方法的返回值类型是void、方法的入参为空
- 访问标志:public
2、自定义方法
public void func(Integer i, String s) {
this.map.put(s, i);
}
- 名称:<func>
- 描述符:(String;Integer;)V表示方法的返回值类型是void、方法的入参为String 和 Integer类型
- 访问标志:public
3、方法调用字节码指令invoke
public class Demo{
private void func(Animal animal, Map<String, Integer> map) {
// invokevirtual,调用的多态方法
animal.eat();
// invokeinterface,调用的接口方法
map.put("mjp", 18);
// invokestatic,调用的类方法
f1();
// invokespecial,调用的对象私有方法
f2();
// invokevirtual,调用的实力方法
f3();
}
// invokestatic
public static void f1() {
}
// invokespecial
private void f2() {
}
// invokevirtual
public void f3() {
}
}
-
invokevirutal: 当你调用一个对象的非静态、非私有方法,并且这个方法在编译时不能确定具体调用哪个类的方法时(即存在继承和多态的情况) 或者 实力方法(非静态、非私有)
上述animal.eat()即多态在编译时期无法确定调用是父类还是子类的eat方法、对象本身的实力方法-f3()
-
invokespecial:调用对象的private私有方法、构造方法、父类继承方法-f2
-
invokeinterface:调用接口方法,上述map.put即调用的Map接口的put方法
-
invokestatic:调用的类方法-f1()
-
invkoedynamic:使用了lambda表达式
三、类加载
3.1 类加载过程
分为三个阶段:加载、链接、初始化
1、加载(Loading)
- 通过类的全限定名,获取此类的二进制字节流class文件
- 将class文件存储的静态结构,转换为方法区的运行时数据结构
- 并在堆中生成一个代表此类的Class对象,作为这个类在方法区中各种字段、方法的访问入口
2、链接
1)验证(Verification)
确保Class文件的字节流中包含的信息符合JVM规范,保证这些信息被当作代码运行后不会危害虚拟机自身的安全
验证内容
2)准备(Preparation)
- 在方法区中为静态变量分配内存、并设置默认值
public static int value = 1;
在准备阶段过后,value的值为0,而不是1。赋值1的动作在初始化阶段进行
3)解析(Resolution)
动态链接:将常量池中的符号引用转化为直接引用的过程。
3、初始化(Initialization):<clinit>()
此时Java程序代码才开始真正执行
-
为类的静态变量赋予初始值
- 正常情况下:在编译时就完成了赋值
public static final int a = 1;
- 特殊情况:对于复杂表达式,即使是static final修饰,也不在编译时完成赋值,而是在此处初始化阶段
public static final int a = new Random().next(10);
-
执行 static {} ,仅在此执行一次
-
执行其父类的<clinit>()
4、使用
除了上述1-3类的加载过程外,类的生命周期还包括4-5
- new 类
5、卸载
当堆中代表类的Class
对象不再被引用,即不可达时,表示类的生命周期结束
- GC会回收这部分堆内存
- 同时卸载类在方法区的数据(方法、字段)
JVM自带的类加载器所加载的类(String类等核心类),在虚拟机的生命周期中通常不会被卸载
3.2 类加载时机
JVM规定了五种情况,需立即对类进行初始化(自然的,加载和链接则需要在此之前开始)
1、如果类没有进行过初始化,此时遇到new
、getstatic
、putstatic
或invokestatic
这四条字节码指令时,则触发此类的初始化
2、首次访问(读取或者设置)这个类的静态变量(被final
修饰、已在编译期把结果放入常量池的静态字段除外)和调用静态方法的时候。
3、如果类型没有进行过初始化,使用了反射获取此类的class对象,则触发此类的初始化
A.class;
Class.forName(xxx.xxx.A);
4、当类进行初始化的时,如果其父类还未初始化过,则对其父类进行初始化
5、当虚拟机启动时,会先初始化main方法所在的主类
3.3 类加载器的层次结构
Java的类加载器采用了双亲委派模型(Parents Delegation Model)。这个模型要求除了顶层的启动类加载器(Bootstrap ClassLoader)外,其余的类加载器都应当有自己的父类加载器。
1、启动类加载器(Bootstrap ClassLoader)
- 根类加载器负责加载Java核心库,如
rt.jar
(javax、lang、util、io等) - 由JVM使用C++实现的
2、扩展类加载器(Extension ClassLoader)
- 负责加载JRE的扩展目录(
lib/ext
)中的jar包和类库 - Java实现,由根类加载器完成加载
3、系统类加载器(System ClassLoader):应用类加载器(Application ClassLoader),加载类路径CLASSPATH下的类。一般来说,用户编写的类都是由它来完成加载的
4、自定义类加载器
1)自定义
-
如果不打破双亲委派机制-参考:ApClassLoader(本质继承ClassLoader,重写findClass方法)
-
如果打破双亲委派机制-参考:WebappClassLoader
2)场景
- 用于加载非标准路径下的类,如网络上的类文件
- 隔离记载类,尤其是不同版本的相同类
5、上下文类加载器
1)定义
- 是Thread类的一个属性
- 每个线程都可以有自己的上下文类加载器,在运行时动态加载与当前线程上下文相关的类
- 在JDBC -SPI机制中,使用当前线程的上下文类加载器来加载JDBC驱动-con.mysql.driver.Driver
2)显示设置
// 1.显示设置-自定义类加载器为当前线程的上下文加载器
MyClassLoader myClassLoader = new MyClassLoader();
Thread.currentThread().setContextClassLoader(myClassLoader);
// 2.显示设置-系统类加载器为当前线程的上下文加载器
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
3)默认为系统类加载器关系
- 如果没有显式地为当前线程设置上下文类加载器,则默认继承自其父线程-通常是main线程
- 如果main线程也未显式设置上下文类加载器,则使用系统类加载器作为上下文类加载器
3.4 类型安全机制
Java的类型安全机制依赖于
- 类的完全限定名(包括包名和类名)和类加载器
- 两个类如果由不同的类加载器加载,即使它们的完全限定名相同,它们也被视为不同的类型
- 这意味着,如果一个接口是由一个类加载器加载的,而其实现类是由另一个类加载器加载的,那么这些类之间就无法正确地实现多态和类型转换,因为JVM会认为它们是两种完全不同的类型
- 所以,通常情况下接口和实现类应由相同的类加载器加载
3.5 双亲委派机制
3.5.1 流程
当需要加载一个类时
- 首先委托给父类加载器去加载
- 如果父类加载器能够加载该类,就成功返回
- 如果父类加载器加载不到,子类加载器才会尝试自己去加载
3.5.2 api
-
Class<?> loadClass(String name) :加载name类,返回此类的class对象
-
Class<?> findLoadedClass(String name):查找name类是否被加载,若为null则说明未被加载。如果已被加载则返回class对象
-
Class<?> findClass(String name):调用子类加载器去加载name类
- 若子类加载器加载范围包括name类,则可以加载,并返回class对象
- 否则返回null
- 具体实现,可以参考ApClassLoader,底层调用defineClass
- 根据类的全路径名称,找到.class文件
- 将.class文件转换为运行时方法区的动态数据结构
- 返回此类在堆中的class对象,作为此类在方法区字段、方法的访问入口
-
Class<?> defineClass(String name, byte[] b, int off, int len):将.class(二进制字节数组) --> .java(class对象)
-
ClassLoader getClassLoader():使用class对象调用此方法,返回加载此类的类加载器
-
getSystemClassLoader():获取系统类加载器
3.5.3 双亲委派机制源码-loadClass
protected Class<?> loadClass(String name, boolean resolve){
synchronized (getClassLoadingLock(name)) {
// 1.校验下这个类是否已经被加载过了
Class<?> c = findLoadedClass(name);
// 2.没有被加载过
if (c == null) {
// 2.向上委派
// 2.1如果当前类加载器不是启动类根类加载器,则向上委派给其父类加载器去加载
if (parent != null) {
// 递归-委派
c = parent.loadClass(name, false);
} else {
// 2.2如果当前类加载器已经是根类加载器了,则让其尝试去加载
c = findBootstrapClassOrNull(name);
}
// 3.向下尝试
// 如果向上委派直到根类加载器都没能加载,则不断执行此句,尝试子类加载器加载
if (c == null) {
// 自定义类加载器需要重写此方法,当父类加载器无法完成加载时,需自定义加载逻辑去自己加载
c = findClass(name);
}
}
return c;
}
3.5.4 双亲委派机制的作用
1)安全性
防止用户自定义的类(可能含有恶意代码)覆盖掉Java核心库中的类
2)唯一性
loadClass时,一旦发现c != null,则直接返回class对象,保证了一个类在Jvm中只会被加载一次
3)可扩展性
支持自定义类加载器,通过继承ClassLoader,重写findClass方法,实现自定义加载逻辑
3.5.5 打破双亲委派机制
场景一:SPI
1、缺陷:无法直接加载第三方类
2、缺陷描述
- JDBC规范中的Driver接口,是在java.sql下的即rt.jar中的sql包下的,故通过启动类加载器去加载
- 而JDBC驱动的实现类如MySQL-com.mysql.jdbc包下的具体实现Driver类
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
public class Driver implements java.sql.Driver{
}
鉴于类型安全机制,正常情况下实现类应该和接口一样由启动类加载器去加载,但是实现类在类路径下,无法被根类加载器直接加载
- 导致了在JDBC 4.0之前,需要通过
Class.forName("com.mysql.jdbc.Driver")
方法显式的使用系统类加载器加载驱动
3、解决-SPI机制
JDBC 4.0后,支持SPI方式来注册数据库驱动,无需再通过Class.forName("com.mysql.jdbc.Driver")
方法显式加载驱动
1)代码实现
String url = "jdbc:mysql://localhost:3306/day21?useUnicode=true&characterEncoding=UTF-8&useSSL=false";
//一、获取mysql的数据库连接对象
Connection conn = DriverManager.getConnection(url,"root", "941123");
//二、获取SQL语句执行对象
Statement statement = conn.createStatement();
//三、执行SQL语句
ResultSet set = statement.executeQuery("select * from tb_user");
2)SPI实现机制原理
3)打破双亲委派机制
- 在加载Driver驱动时,使用的是SPI服务发现机制
private static void loadInitialDrivers() {
// 一、load
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// 二、hasNext
while(driversIterator.hasNext()) {
// 三、next
driversIterator.next();
}
}
- load:使用上下文类加载器去加载驱动Driver类
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
- 因为上下文类加载器没有显示指定,所以默认为系统类加载器,所以本质就是使用系统类加载器去加载的类路径下的Driver实现类
Class<?> c = Class.forName("com.mysql.driver.Driver", false, loader);
场景二:Tomcat
1、背景
-
在容器化环境,如Docker中,每个容器运行自己的Web服务和Jvm实例
-
传统的应用服务器,如Tomcat中,多个Web服务,是共享一个Jvm实例的
- 则两个不同Web服务(如web1和web2)中分别调用
ClassLoader.getSystemClassLoader()
时,返回的是相同的系统类加载器实例 - 因为系统类加载器是JVM级别的,而非应用程序级别的
- 则两个不同Web服务(如web1和web2)中分别调用
2、Tomcat中可能存在的缺陷
如图8,Web1和Web2共享一个Jvm实例时
- web1服务使用系统类加载器去加载Spring4中的某个类A,并获取aClass对象
- 当web2服务使用到类A时,发现全限定名称 和 类加载器(系统类加载器)都相同,则不会再去加载Spring5中的类A,而是直接返回aClass对象
3、 解决-自定义类加载器
Tomcat为每个Web应用程序创建了一个独立的WebAppClassLoader类加载器 ,并打破双亲委派机制
如图9所示
1)打破双亲委派机制
- 当要加载一个类时,tomcat不会按照双亲委派机制,优先让父类加载器去加载,而是优先使用自定义WebAppClassLoader类加载器去优先加载类路径下的全限定类名的类
- 当自定义加载器无法加载到时,才会走双亲委派
具体代码为:WebappClassLoader --继承> WebappClassLoaderBase --继承> URLClassLoader#findClass
2)自定义类加载器加载过程
- web1服务使用自定义类加载器WebApp1加载器去加载Spring4中的某个类A,并获取aClass对象
- 当web2服务使用到类A时,使用自定义类加载器WebApp2加载器去加载Spring5中的类A
3.6 Launcher
1、内容
- static class ExtClassLoader:扩展类加载器
- static class AppClassLoader:系统类加载器
2、构造器
public Launcher() {
// 1.创建扩展类加载器
ExtClassLoader var1 = Launcher.ExtClassLoader.getExtClassLoader();
// 2.创建系统类加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
// 3.设置上下文类加载器为:系统类加载器
Thread.currentThread().setContextClassLoader(this.loader);
}
四、运行时数据区-栈
4.1 PC寄存器
1、特点
一个线程对应一个栈,当然也对应一个PC寄存器。Java中唯一一块无OOM的内存区域
2、内容
下一条该执行的指令地址
3、作用
当CPU在多个线程之间切换时,PC寄存器能够确保每个线程都能从上次中断的位置继续执行
4.2 本地方法栈
1、作用
管理Java中native方法的调用
- JNI:是Java调用本地方法的一种机制
- 通过JNI,Java程序可以加载并执行用其他语言(如C或C++)编写的库
- 本地方法栈保存JNI调用过程中的状态信息,确保JNI调用的正确性和稳定性
2、特点
- 可能OOM
- 也可以StackOverFlow
4.3 Java虚拟机栈
4.3.1 Java栈
1、定义
- Java栈是线程私有的,它的生命周期与线程相同,在创建线程时,会创建一个Java栈用于存放该线程执行方法的所有栈帧。
2、特点
- 无GC、但是可能有OOM和StackOverFlow
4.3.2 栈帧
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。这个引用用于支持当前方法的代码能够实现动态链接
1、定义
- 是一种数据结构,是Java栈的基本单位
2、作用
-
一个线程对应一个Java栈,一个线程可能有多个执行方法,每个方法对应一个栈帧
-
栈帧中存储了方法的
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
等信息
-
栈帧中存储的方法信息,用于支持Jvm对方法的调用和执行
3、局部变量表
1)作用
- 存储方法的参数
- 存储方法内部定义的局部变量
2)存储基本单元:slot- 变量槽
- 存储内容
- 8中基本类型的值(long、double占用2个slot来存储值)
- 引用类型的堆内存地址值
3)局部变量名
- 局部变量名主要是在.java源代码中,用于帮助开发者理解和编写代码的,是编译时概念
- 在.class文件中,局部变量名会被转换成JVM内部使用的索引index
// 其中d = 1.0、s = "m"
public void testMethod(double d, String s) {
int i = 5;
User user = new User();
// 其他代码
}
slot和局部变量的关系如图10所示
其中index即.java源码中的局部变量名
- index = 11表示d变量名称
- slot1和slot2两个槽用于存储double类型的值,即存储的d变量的值
- double d = 10,即上述的index = 11指向槽,这样就可以通过index访问槽中的值了
一般this的index = 0,即当前对象的堆地址值,在index = 0 指向的slot中
4)局部变量是否线程安全
- 线程安全
public String func1() {
StringBuilder sb = new StringBuilder();
sb.append("a");
String s = sb.toString();
return s;
}
sb在方法结束后,栈帧随着方法的结束,生命周期也结束了
- 线程不安全-逃逸
public StringBuilder func2() {
StringBuilder sb = new StringBuilder();
sb.append("a");
return sb;
}
sb是堆中的对象,非线程安全
4、操作数栈
1)作用
保存计算过程中临时变量
2)特点
FIFO
3)实战
public int add() {
int a = 8;
int b = 15;
int c = a + b;
return c;
}
.class文件中的方法-code字节码信息如图11所示
0- bipush:
- PC寄存器,去指令地址0,获取字节码指令: bipush
- 执行字节码指令bipush 8 : 将8压至操作数栈顶
2- istore_1:
- PC寄存器,去指令地址2,获取字节码指令: istore_1
- 执行字节码指令istore_1 : 将操作数栈顶的int类型值(8)存储到局部变量表中索引index = 1处
- 弹出操作数栈栈顶int值8
- 将int值8,存入局部变量表中的slot槽中(值为8),指向此槽的index = 1(即局部变量名a)
- 此时局部变量表中内容为,如图12所示
3-bipush:同上0,操作数栈,操作值15
5- istore_2:同上2,局部变量表,slot存储值15,index = 2指向此slot
6、7:iload_1和iload_2
- 获取index = 1对应的slot中的值:8,存入操作数栈
- 获取index = 2对应的slot中的值:15,存入操作数栈
8-iadd:iadd 由于JIT即时编译器负责执行,从操作数栈中弹出两个int类型的值进行相加,并将结果压回操作数栈
- 从操作数栈中拿出元素:8
- 从操作数栈中拿出元素:15
- 8 + 15 = 23
- 23压入操作数栈中
9-istore_3:同上2,局部变量表,slot存储值23,index = 3指向此slot
10-iload_3:获取index = 3对应的slot中的值:23,存入操作数栈
11-ireturn:从操作数栈中拿出元素:23
- 将返回值23压入调用者func的操作数栈
public void func() {
// pc = n
add();
// other,pc n + 1
}
public int add() {
int a = 8;
int b = 15;
int c = a + b;
return c;
}
- 并将方法返回地址(PC = n + 1)加载到调用者func方法的PC寄存器中
- 然后退出方法
5、动态链接
5.1 定义
在运行时解析方法的引用
5.2 作用
多态:运行时确定调用哪个类的方法
5.3 实现过程
将符号引用 -> 直接引用
public void func() {
Map<String, Integer> map = new HashMap<>();
map.put("mjp",18);
}
0 new #4 <java/util/HashMap>
3 dup
4 invokespecial #5 <java/util/HashMap.<init> : ()V>
7 astore_1
8 aload_1
9 ldc #2 <mjp>
11 bipush 18
13 invokestatic #6 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
16 invokeinterface #7 <java/util/Map.put : (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;> count 3
21 pop
22 return
1)0 - new #4 <java/util/HashMap>
- 动态链接:符号引用new #4,转为直接引用时,可以获取到HashMap类地址
- 对象类型检查:在执行invokeinterface 之前HashMap的实例已被推送到了操作数栈上
2)16- invokeinterface #7 <java/util/Map.put
- 背景
- invokeinterface:在编译时无法确定具体调用哪个实现类的方法,所以此处只能先用符号引用#7表示
- #7 : .class文件常量池中的InterfaceMethodRef_info符号引用
<java/util/Map>
<put : (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;>
-
执行过程
HashMap在运行时常量池中,如图7所示:class对象地址为0x03,put方法的引用地址为0x13
- 当执行invokeinterface时,JVM会检查这个对象引用的实际类型 ,即HashMap:0x03
- 查找方法实现:
- Jvm会查找
HashMap
类的vtable
虚拟方法表(该表包含了HashMap中所有实例方法的直接地址 )
- Jvm会查找
- 去匹配和
Map.put
方法签名相匹配的方法(方法名、方法参数个数和类型、返回值类型)地址0x13
6、方法返回地址
public int add() {
// pc = 20
int a = 1;
// pc = 21
int b = func();
// pc = 22
return a + b;
}
1)定义
func方法栈帧的方法返回值地址,存储的是调用者add的PC寄存器的值
2)内容
值 = 调用者add方法,调用func方法后的下一条指令的地址 22
五、堆
5.1 参数
模块 | 参数 | 大小 |
---|---|---|
初始化堆内存 | -Xms | 物理内存大小 * 1/64 |
堆最大内存 | -Xmx | 内存大小 * 1/4 |
新生代内存 | -Xmn | -XX:newRatio = 2,新生代1/3,老年代2/3 |
1、ms和mx
建议ms = mx,防止频繁GC以及从ms <–> mx扩容缩容(但是过大也可能会造成STW变长)
2、totalMemory 和 maxMemory
Runtime runtime = Runtime.getRuntime();
long l = runtime.maxMemory();
long l2 = runtime.totalMemory();
- max:Jvm可用的最大堆内存
- total,Jvm从启动到运行到此刻,总共分配的内存大小(可能未完全使用)。随着程序不断运行,total会增加
3、新生代
-XX:SurvivorRatio=n :设置两个Survivor区(From和To)与Eden区的比例(默认8:1:1)
5.2 创建对象方式
1)new
2)反射
- newInstance
- 构造器
3)clone克隆
4)反序列化
5.3 对象存放位置
堆中
5.3.1 新生代
几乎所有的对象都在新生代Eden区创建的
5.3.2 老年代
-
场景1:MinorGC(YoungGC),GC了15次,仍存活的对象
-
场景2:动态判断
一次Minor GC后,Survivor区中一批对象的总大小 > 50%Survivor区,则这批对象中年龄最大的被直接晋升到老年代
5.3.3 TLAB
正常来说,堆中数据,是线程共享的
但是堆中有一块很小的区域TLAB,本地线程分配缓存
1)定义:线程私有的堆空间
2)作用:当分配的对象很小时,则直接放入TLAB
- 私有,则无竞争,空间换时间
TLAB放不下,才放入新生代的eden区
5.4 对象结构
结构 | 大小(字节) |
---|---|
对象头 | markword-8、KclassPoint-4 |
实例数据 | 4 |
对齐填充数据 | 8的倍数,是对象大小最终为8的整数倍 |
5.4.1 对象头
5.4.1.1 markword
如图15所示
- 分代年龄4bit,max = 1111即15,表示minorGC15次仍存活,则年->老年代
- Epoch:此pid获取到偏向锁的次数
+XX:BiasedLockingStartupDelay=n
表示系统启动 n 毫秒之后,启用偏向锁+XX:PreInflateSpin=n
表示自旋 n 次之后,轻量锁升级为重量级锁
5.4.1.2 klassPointer指针
class对象和klassPointer指针
class对象 | klassPointer指针 | |
---|---|---|
定义 | 类加载完成后返回的方法区的访问入口。反射获取的class对象 | 指针 |
作用 | 程序员可以通过反射可以获取运行时类的信息 | 此指针指向对象类型信息-InstanceKlass对象 |
存储位置 | 堆 | 堆内对象头 |
InstanceKlass对象
1、定义
- 是JVM中用于表示Java类的一种数据结构
- 类被加载到内存中时,都会创建一个对应的InstanceKlass对象来表示这个类
2、作用
- 主要存储类的元数据( 类的名称、父类、接口、字段、方法等 )信息
- 也包含了类的静态字段和方法代码等详细信息
- 支持类的实例化、方法调用等运行时操作
Class对象,是InstanceKlass对象的“镜像”或“代理”(Class对象对InstanceKlass对象进行了封装和抽象,隐藏了JVM内部的实现细节,为Java代码提供了一个更加高级和易于使用的接口)
- 程序员通过Class对象可以获取类的运行时数据
- Jvm通过InstanceKlass对象管理类的元数据
对象访问定位
如图16所示,动态链接的完整版
5.4.2 实例数据
类的实例变量(即非static的字段)的值 或 地址引用
- 字段的元数据(修饰符、类型、名称)是存在于方法区的
- 字段的值,是同对象一起,在对象结构-实例数据中的(具体值或地址引用)
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
version>0.17</version>
</dependency>
public class Demo{
private int a1;
private int a2 = 10;
private String s = "mjp";
private Object obj;
private double d1;
private Double d2;
public static void main(String[] args) {
Demo obj = new Demo();
System.out.println(ClassLayout.parseInstance(obj).toPrintable()
);
}
}
-
基本类型,除了double和long外,其余都占4字节,存的是值
-
引用类型,占4个字节,存的是地址引用
正常情况下,64位Jvm中对象的引用大小是8B,但是因为 -XX:+UseCompressedOops 压缩成4B
压缩参数,如图41所示
- s:指向堆中字符串常量池对象
- obj
- d2
5.4.3 对齐填充数据
- 对象的大小必须为8的整数倍(有助于提升内存访问的效率)
5.4.4 Object类对象占用16字节
1、pom
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
version>0.17</version>
</dependency>
2、code
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable()
3、结果
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
4、解析对象内部结构object internals
1)对象头(Object Header)
-
Mark Word(标记字)
- 从偏移量0开始,占用8个字节
- 0x0000000000000001对象地址
- non-biasable是一个非偏向锁的对象
- age 0 :GC年龄为0
-
Class Pointer(类指针)
-
从偏移量8开始,占用4个字节,但在64位JVM上
-
0xf80001e5是这个类指针的值,它指向
Object
类的元数据instancekclass
-
2)对齐间隙(Alignment Gap)
- 偏移量12开始占4个字节(确保对象的大小是8的倍数, 12 + 4 = 16 % 8 == 0)
3)实例变量(这里Object类无成员变量)
实例大小(Instance Size):16
5.5 new过程字节码指令分析
- 方法调用
public User func() {
User u = new User("wxx");
return u;
}
- 类定义
public class User{
private String name;
private User(String name) {
this.name= name;
}
}
- .class文件-方法code
0 new #2 <com/mjp/lean/User>
3 dup
4 ldc #6 <wxx>
6 invokespecial #4 <com/mjp/lean/User.<init> : (Ljava/lang/String;)V>
9 astore_1
10 areturn
1、类加载过程
new触发类的首次初始化,完成User类的加载、链接、初始化。然后使用new
1)方法区内容
如上图16所示,此时方法区已经有了表示类的instanceKClass对象。即已经有了类的元数据、方法详情Vtab等信息
2)实例初始化
执行构造函数之前,先执行实例初始化(如果有的话)
public class User{
private Integer age;
{
age = 10;
System.out.println("实例初始化");
}
}
2、执行 new字节码指令
0 - new
- 为对象分配内存
内存分配的方式主要有两种:指针碰撞(Pointer Bumping)和空闲列表,期间还可能设置Jvm的CAS和TLAB本地线程分配缓存
- 设置user对象的内存地址为0x01
- 设置对象结构(此时堆中已有对象结构)
- 对象头
- 实例数据:将User类的普通(非静态)成员变量String name跟随类进入0x01地址处,属性默认值为null
- 对齐填充数据
- 将user对象内存地址引用0x01,存入操作数栈顶
3 - dup
-
复制操作数栈顶的引用 0x01
-
将复制的0x01也一同压入操作数栈
4- ldc #6 <wxx>
从运行时常量池中加载常量值"wxx"的引用地址,压入操作数栈顶 。如图14所示
6- invokespecial #4 <com/mjp/lean/User.<init> : (Ljava/lang/String;)V>
-
从操作数栈中,弹出
- wxx常量值的引用地址,指向的值为字符串wxx
- user对象引用地址0x01
-
根据0x01,匹配User类的有参构造方法-User.<init> : (Ljava/lang/String;)V>
- 找到堆中user对象的地址
- 再根据对象头中的kclasspoint指针找到方法区表示User类的instancekclass对象
- 根据instancekclass对象中的元数据信息(类名User) 以及 方法详情信息(Vtab虚拟方法表中的有参构造),匹配到User#init地址
-
执行初始化
完成普通成员变量name的初始化
9- astore_1
- 将操作数栈顶剩余的0x01地址引用,弹出,存入局部变量表中
- 用一个slot槽存储0x01
- index = 1,指向引用地址0x01
10 - areturn
- 从局部变量表中获取局部变量user引用index = 1指向的引用地址0x01返回
- 并将方法返回地址值(调用者下一步应该执行的PC寄存器值)一同返回
5.6 字符串常量池
参考:十五、字符串
1、背景
jdk7之后,字符串常量池从方法区移到堆中了
2、原因
方法区内存管理
- 字符串大部分朝生夕死,容易称为垃圾,容易触发方法区的GC(法区GC困难)
3、打印字符串常量池信息
-XX:+PrintStringTableStatistics
优化思路:避免创建相同的字符串
- 使用StringBuilder
- 使用intern字符串常量池
六、方法区
6.1 方法区、永久代、元空间
6.1.1 方法区
1)定义
用于存储每个类的
-
元数据
- 类元数据
- 方法元数据
- 字段元数据(修饰符、类型、名称)
-
构造函数和普通方法详情
-
运行时常量池
- 存储了与类运行时相关的常量信息
- 便于高效的访问它们
-
异常表
-
JIT即时编译器执行的缓存热点代码
-
加载此类的类加载的对象引用
等数据的内存区域
2)特点
类似接口,是Jvm规范,不同jdk版本有不同的实现
6.1.2 永久代
1)定义
jdk8之前,方法区的具体实现
2)特点
- "代"即年轻代和老年代,所以可以通过FullGC间接回收方法区垃圾
6.1.3 元空间
1)背景
- jdk8之前Jvm虚拟机主要是HotSpot实现
- jdk8,HotSpot = 原HotSpot + JRocket
2)定义
使用本地内存作为方法区, 不再像永久代那样使用JVM的内存 (可以有效防止OOM)
3)大小
- 大小是动态变化的 (初始值 <-> 最大值)
大小 | 初始大小 | 最大大小 |
---|---|---|
默认值 | 接近21M | 本地内存大小 |
参数设置 | -XX:MetaspaceSize=N | -XX:MaxMetaspaceSize=N |
作用 | GC阈值 | OOM阈值 |
- 查看进程元空间大小
jps 获取进程id
jinfo -flag MetaspaceSize <PID>
4)使用元空间原因
- 永久代,容易频繁GC。元空间大小动态变化,不易频繁GC
- GC相对复杂
- 方法区一般存储类信息
- 堆中只要GCRoot不可达即为垃圾
- 方法区中类称为垃圾,需满足
- 自身类型的所有实例均被GC
- 加载自身的类加载器被GC(一般很难)
- class对象无引用
既然永久代容易GC而且GC过程又复杂,所以换成元空间
6.2 变量和常量
1、局部变量|常量
2、实例变量|常量
局部变量|常量 | 实例变量|常量 | |
---|---|---|
名存储位置 | 栈帧局部变量表中-index索引即等效名称 | 方法区字段元数据 |
值存储位置 | 栈帧局部变量表slot中 | 堆中对象结构实例数据中 |
值内容 | 具体值、堆中对象引用地址 | 具体值、堆中对象引用地址 |
3、类变量|常量
public class Demo{
private static final int a1 = 1;
private static int a2 = 2;
private static final ZoneId a3 = ZoneId.systemDefault();
private static ZoneId a4 = ZoneId.systemDefault();
}
1)名:都是存储在方法区中,字段元数据(修饰符、类型、名)
2)值
- a1的值1
- 编译时就已经确定,无需加载到运行时方法区,故不在栈、堆、方法区
- 直接在编译时内嵌入到类的二进制中
- a2-a4:都存在元空间的静态字段区域中 ,引用的对象存在于堆中
七、GC
7.1 如何定位垃圾
7.1.1 垃圾定义
没有任何引用指向此对象,即对象已"死亡"
7.1.2 可达性分析
1、目的
判断对象是否还存活,即判断对象是否已为垃圾
2、定义
- GCRoots对象作为起点
- 从这些点向下搜索
- 当一个对象到GCRoot没有任何路径相连,则认为此对象为垃圾。即GCRoot不可达
3、GCRoot节点
1)栈
-
栈帧
-
本地方法栈-通过JNI引用的对象
-
RSet
2)方法区-类属性引用的对象
private static User u = new User();
u本身存在元空间的静态字段区域中 ,引用的对象存在于堆中
3)堆
-
字符串常量池对象
-
被同步锁(synchronized)持有的对象
-
Jvm内部-如类加载器也是GCRoot点,被它引用的对象也认为是存活对象
4、STW
GCRoot可达性分析过程中,所有用户线程都会被stop。
防止分析过程中,对象的存活状态又变化了
- 原本GCRoot可达的,变为不可达了
- 原本不可达的,产生新的引用,可达了
7.1.3 对象真正的死亡
当对象GCRoot不可达时,则认为此对象不再被任何引用,认为是垃圾,可以回收了。
但是在回收前,垃圾还有一次机会可以复活
1、对象复活
参考:finalize
2、守护线程-finalize
2.1、用户线程
main以及main线程汇总创建的线程
2.2、守护线程
Thread daemonThread = new Thread(() -> {
// 线程任务
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();
1)定义:后台线程
2)作用:为用户线程提供服务
- 垃圾收集Finilize线程
- 性能监控线程
3)特点:
- 当用户线程数 >= 1时,Jvm不会终止
- Jvm无需等待所有守护线程结束,才终止,所以守护线程不能持有需要持续执行的任务,因为JVM可能随时停止它们
7.2 垃圾收集算法
7.2.1 标记清除(mark-sweep)
1、过程
1)标记
标记出所有被GCRoot可达的对象
2)清除
- 遍历运行时内存
- 将GCRoot不可达的对象回收,释放地址
过程如图17所示
2、缺点
- 内存碎片:清除后可用空间不是连续的,需要维护一个可用空间列表记录哪些空间可用
- 可能会stw
3、适合场景:老年代内存回收
7.2.2 复制(copying)
1、定义
将内存可用地址一分为二(From和To),每次使用其中的一块
2、过程
加入正在使用的From块,要发生GC了
1)标记
在From块中,标记出GCRoot可达对象
2)复制
- GC时,直接将From块中这些GCRoot可达对象实体,完整的复制到To块中
- 更新对象引用地址,使其指向To区中的新对象
3)清除
将From块中全部内容都清除
4)角色转换
现在To变成原本的From,用于给对象分配内存
如果18所示
3、缺点
优点是解决了标记-清楚的缺点
- 内存的可用大小降低一半
- 需要维护引用关系
原本对象在From块中的地址引用为0x01,当copy过程中,将对象实体copy到To块中了,在To块中对象地址为0x02,
所以对象的引用地址需要更新为0x02
4、解决缺点
不是严格按照一分为二,新生代而是按照,8:1:1的分配方式,如上图13所示
- 当eden + from survior作为可用内存,存储对象。当发生GC时,将存活的对象copy到to survior区域
- 此时,可用区域为eden全部区域 + to survior中未被使用区域
- 保证了 > 80%的内存可用
5、使用场景
新生代区域适合此算法
- 对象朝生夕死,快速的完成copy
- 而如果是老年代,对象本身基本上都是"老不死"对象,会出现大量无意义的copy
7.2.3 标记-整理(mark-compact)
1、背景
优化标记-清除的碎片化问题
2、过程
1)标记
标记出GCRoot可达的对象
2)整理
- 将所有存活的对象移动到内存的一端(保持存活对象之间的相对位置不变)
- 移动过程中,更新指向对象的地址引用
- 移动完成后,将未标记的垃圾对象所占用的内存空间释放回收
如图19所示
3、优点
- 减少空间碎片
- 在将存活对象移动到内存的另一端过程中,保持了对象之间的相对位置不变,减少了对象引用地址的变更
4、缺点
- 当内存较大、存活对象较多时,整理过程比较耗时
5、适用场景
老年代
7.2.4 分代算法
1、定义
不同于上述三种垃圾回收算法,分代算法是一种思想。
即将内存分为年轻代(存储朝生夕死的对象)和老年代("老不死"的对象)。根据年轻代和老年代对象特点,选取合适的垃圾回收算法。eg:年轻代使用复制算法
7.3 垃圾回收相关概念
7.3.1 MinorGC
1.定义:YoungGC
2.触发条件:
新生代中的Eden区空间不足时
3.回收对象:
回收Eden区和Survivor区中的垃圾对象,并将存活的对象复制到另一个Survivor区或晋升到老年代
4.特点:
- Minor GC通常比较频繁,因为新生代中的对象生命周期较短
- 执行速度快,因为新生代中的对象大多数很快变为垃圾
7.3.2 MajorGC
1.定义:OldGC
2.触发条件:
- 当老年代内存空间不足时
3.回收对象:主要回收老年代中的垃圾对象
4.特点:
- 标记-整理算法,执行速度相对较慢
7.3.3 MixedGC
1.定义:G1独有混合GC
2.触发条件:
- 当新生代 region不足。
- 当老年代内存占据到一定比例(如45%)时
3.回收对象:同时回收新生代和老年代中的垃圾对象
7.3.4 FullGC
1.定义:对整个堆(新生代和老年代)、元空间进行垃圾回收
2.触发条件:
-
调用System.gc()时
-
老年代空间不足时
-
元空间不足时
-
老年代内存担保机制
1)目的
防止老年代内存溢出,影响程序稳定
2)过程
在MinorGC之前,Jvm会判断
-
如果此时新生代中所有对象的总大小 < 老年代可用空间大小,则允许MinorGC
-
如果此时新生代中所有对象的总大小 > 老年代可用空间大小
&& 历史MinorGC晋升至老年代的总对象的平均大小 < 老年代可用空间大小 ,也允许MinorGC(但是存在风险,因为本次的MinorGC大部分对象需要晋升到老年代)
-
如果二者都 > 老年代可用空间大小,则本次不允许本次MinorGC执行,因为可能会触发FullGC 或者直接导致老年代OOM等异常。此时直接进行FullGC,回收老年代垃圾
-
3.特点:
- 是最耗时的垃圾回收过程,因为它需要扫描整个堆、元空间
- STW
7.3.5 安全点|区域
1、安全点
1)定义
即在程序运行过程中,可以全部正确的枚举出所有根的状态
- 这里的根即GCRoot,有了GCRoot就不会漏掉存活的对象
- 全部正确,要想实现这一点,就不能在枚举过程中改变根,所以就需要暂停所有Java程序
- 不是随时随地就可以暂停的,在暂停线程之前,必须将属于自己的根放到 GC 能看见的位置。以免GC无法找到所有的根
2)过程
-
在GC发生时,JVM会对所有用户线程,发出中断请求
-
线程接收到中断请求后,如果自己在安全点,则将自己中断挂起
-
如果自己不再安全点,则继续执行到安全点,再将自己挂起
-
如果所有线程都已到达安全点,JVM将执行GC操作
3)作用
保证所有存活的数据不会被漏掉
4)安全点选择
- 方法中执行时间较长的指令(如循环结束、方法调用前后、异常跳转等)
2、安全区域
1)背景
当Jvm要GC时,有些线程并未运行到安全点,且这些线程被Block住了,无法继续执行指令到安全点
2)定义
- 如果一段代码内,对象的引用关系不会发生改变,即为安全区域
3)过程
- 当线程进入安全区域时,它会标识自己已经进入了安全区域
- 当JvmGC时,有些阻断的线程无法响应,但是Jvm获知他们已经达到安全区域,此时Jvm仍可以安全地进行GC
- 当线程获取到执行时间片,准备离开安全区域时,它会检查GC是否完成
- 若GC已经完成,线程将继续执行
- 否则,等待可以离开安全区域继续执行的信号
7.4 并行和并发
7.4.1 并发- Concurrency
1)过程
单处理器中,同一时间段,用户线程和GC线程,交替获取cpu时间片执行
2)优缺点
- stw很短,用户几乎无感,对用户体验和程序响应友好
- 用户线程和GC线程交替执行,则二者需要交互(比如安全点),这样才能够安全GC
7.4.2 并行- Parallel
1)过程
- 垃圾收集器会将GC任务分解成多个子任务,分配给不同的CPU核心上的线程并行执行
- 同一时刻,多个GC线程, 在不同的处理器或处理器核心上,独立的执行
2)优缺点
- 用户线程完全STW
- 但可以减少垃圾收集总时间,吞吐量高
7.5 G1前置
7.5.1 指标
1、吞吐量
用户线程执行时间
÷
(用户线程执行时间
+
G
C
线程执行时间)
用户线程执行时间 ÷ (用户线程执行时间 + GC线程执行时间)
用户线程执行时间÷(用户线程执行时间+GC线程执行时间)
吞吐量越高,用户线程执行时间越长,尽可能快的完成任务
2、单次STW时长
3、吞吐量和STW与内存大小关系
-
大
- 提高吞吐量(够用,很少GC)
- 暂停时间长(一旦GC,则STW时间就长)
-
小年轻代
-
暂停时间短
-
牺牲吞吐量
-
7.5.2 垃圾收集器历史版本
并发Concurrency收集器减少暂停时间,并行Parallel收集器提高吞吐量
1、Jdk8垃圾收集器
- -XX:+UseParallelGC
即Parallel Scavenge(新生代-复制算法) + Parallel Old(老生代-标记整理算法) ,吞吐量大,但是STW也长
- 吞吐量是首要任务
- 对STW无感,可以接受一秒或更长
2、G1
- Java9的默认垃圾收集器G1
- 一直到24年6月的jdk22,默认都是G1
3、CMS
- jdk9标记为废弃(不建议使用,推荐G1)
- jdk14移除,即从jdk15开始无法再使用cms
4、ZGC
- jdk13发布
- jdk17基本可稳定使用
- STW < 1ms
7.5.3 G1特点
1、适用于大的堆内存 > 6G
2、目标
G1在暂停时间可控的情况下,获得最大的吞吐量
3、能力
1)期望暂停时间可控(软实时性)
这个能力即G-First,垃圾优先回收的名称由来
- -XX:MaxGCPauseMillis= 200
- 暂停时间只是一个目标,并不能保证总能达到
避免使用-Xmn、-XX:NewRatio 等,这些设置会将年轻一代大小限制在特定值,导致可控延时功能失效
所以,G1只能在有限时间内,选取更有价值的region进行回收
2)region内无内存碎片
在转移时,region内不会出现碎片化
- 即将转移后的对象从内存的另外一侧开始存放
但是会出现以region为单位 (整个堆)的碎片化,和普通的复制算法相比,算是一缺点
2)可预测性
预测下次GC暂停时长的功能
根据预测结果,G1使用延迟GC、拆分GC目标等手段,来达到期望暂定时间可控的功能终极目标
4、参数调整
- 如果想提升吞吐量,则将XX:MaxGCPauseMillis调大
- 如果想降低STW,则将XX:MaxGCPauseMillis调小
7.5.4 region
1、背景
G1,在保留了新生代和老年代的概念下,将堆分割成等大的region区域
所以新生代和老年代在G1中是虚拟的
- 年轻代 动态 占比-XX:G1NewSizePercent = 5- Max60%,是一个动态的(因为年轻region可能变为old region)
如图25所示
实际内存中,是排成一排的空间。上图二维只是便于展示
2、特点
大约 2000 个区域,大小从 1 到 32Mb 不等
-
新生代和老年代表示的region区 并不要求连续
此region本次放新生代对象,GC一次后可能放老年代对象
-
默认region大小为堆总大小 / 2048
个数多-单个小
- 需要疏散的实时对象少,但是容易出现大对象
个数少-单个大
- 处理跨区域引用的工作量相对减少、但需要疏散的实时对象越多
3、作用
为了期望暂定时间可控
4、Humongous
1)定义
专门用于存放大小为region 50%或更大的对象。
2)本质
也是region,只不过在old region中每个Humongous都是以连续region形式分配的
5、CollectedHeap
CollectedHeap的3个主要成员变量结构如图53所示
_hrs:通过数组维护所有HeapRegion
_young_list:新生代HeapRegion的链表
_free_region_list:空闲HeapRegion的链表
老年代的HeapRegion没有与任何东西连接在一起
6、HeapRegion类的5个成员变量结构如图54所示
_bottom :region头地址
_end:region头地址和尾地址
_top:region内空闲内存空间的头地址
_next_young_region:单向链表,指向下一个新生代region
_next_in_special_set:单向链表
- 当前region在空闲region链表_free_region_list上时,它指向下一个空闲region
- 当前region在回收集CSet链表上时,它指向下一个回收对象所在region
7.5.5 Vm Options
多个参数,使用空格隔开
1、查看默认的垃圾收集器
-XX:+PrintCommandLineFlags -version
- java -XX:+PrintCommandLineFlags -version在服务器终端查看
2、查看运行时方法区总大小 和 使用率
-XX:+PrintGCDetails -version
3、使用G1垃圾收集器
-XX: +UseG1GC
4、G1目标暂停时间
-XX:MaxGCPauseMillis= 200
- ms
5、设置每个region大小
-XX:G1HeapRegionSize=24M
- 2的幂次方
- 默认为堆大小 / 2048
6、触发MixedGC阈值
-XX:InitiatingHeapOccupancyPercent = 45
- 默认堆占用45%触发MixedGC
- 调小为40%,可能会降低FullGC的可能
6、吞吐量
-XX: GCTimeRatio = 99
用户线程执行时间占99%
7、新生代和老年代占比
XX:NewRatio=2 默认为2,即新生代1/3
8、 -XX:ParallelGCThreads = n
参与并行收集垃圾的线程数(过小无法并行处理,过多则线程切换开销)
-
如果CPU核心数n <= 8,则就为n
-
如果CPU核心数大于8,则为8 + ((N - 8) * 5/8)
9、-XX:G1MixedGCLiveThresholdPercent=85
- 在MixedGC时,老年代region中,只有存活对象占比 < 85%的region才会加入CSet作为收集候选region
- 某个old region LiveThresholdPercent值越小,说明存活对象越少,越具有回收价值
10、-XX:G1MixedGCCountTarget = 8
在一次混合垃圾收集周期中,G1会尝试最多执行8次的混合回收
- 过大:GC总耗时变长(可通过此参数,降低Mixed时长)
- 过小:频繁GC
八、纯G1GC
8.1 并发标记-ConcurrentMark
并发标记是G1GC最主要的两个过程(并发标记和转移evacuation)之一
8.1.0 前置
1、作用
提供evacuation转移过程中所需信息
- 在标记完成后,需计算出每个region中存活对象的个数(在evacuation中要用)
2、 目标
- 在尽量不暂停用户线程的情况下,标记处存活对象,即并发标记
3、步骤
并发标记过程包括以下 5 个步骤:
① 初始标记阶段 ② 并发标记阶段 ③ 最终标记阶段 ④ 存活对象计数 ⑤ 收尾工作
8.1.0.1 标记位图
标记位图是标记的载体,并发标记并不是直接在对象上添加GCRoot可达的标记,而是在标记位图上添加标记
结构如图33
1、黑、白、×
- 黑色表示已标记,是存活对象
- 白色表示未标记
- 带有叉号的是死亡对象
2、next 、 prev
每个region都有
- next 是本次标记的标记位图
- prev 是上次标记的标记位图,保存了上次标记的结果
3、关联关系
标记位图中每个bit都对应着关联区域内的对象的开头部分
- 假设 单个对象的大小都是 8 个字节
- 则每 8 个字节就会对应标记位图中的 1 个比特
- 黑色表示比特值为 1,白色表示比特值为 0
4、bottom 、top 、nextTAMS 、prevTAMS
- bottom 表示区域内众多对象的末尾
- top 表示开头
- TAMS 是“Top At Marking Start”(标记开始时的 缩写)
- nextTAMS ,本次标记开始时的 top所在的位置
- prevTAMS ,上次标记开始时的 top所在的位置
8.1.0.2 三色标记算法
1、定义
G1的并发标记阶段,使用三色标记算法,进行可达性分析
白色:该对象没有被标记过(垃圾)
灰色:该对象已经被标记过了,但该对象下的所有子对象没有被标记完( -> )
黑色:该对象已经被标记过了,且该对象下的所有子对象也被标记完
2、并发过程
如图32所示
1)起始阶段:所有节点都是白色的
2)init mark-STW
- A,直接被GCRoot指向,所以A本身被标记过了,且A下无子对象,故为黑色
- D,直接被GCRoot指向,所以D本身被标记过了,但子对象(E)尚未标记,故为灰色
3)并发标记
- A,本身和所有属性都标记完成了,A变为黑色
- D,本身和所有属性都标记完成了,D变为黑色
- E,本身和所有属性都标记完成了,E变为黑色
- F,变为灰色
黑色和灰色,GCRoot可达,白色表示垃圾
3、优点
并发标记
4、问题
在并发标记期间,用户线程的活动导致了可达性关系发生了变化
4.1 问题1 -浮动垃圾
1)描述:D -> E 引用断了
- 原本E被认为是存活的,现在实际是垃圾
2)解决:本次GC可以忽略,下一次GC处理
4.2 问题2 - 漏标(新增对象)
1)描述:新增 H
- H可能是存活对象,本次要处理
2)解决漏标(新增对象):STAB
4.3 问题3 - 漏标(变更对象引用)
1)描述:
原本D-> E,现在D -> B,所以B肯定是存活对象了
- 原本B被认为是垃圾,本次GC就要回收了,但现在是存活对象
- 本次就要处理
2)解决漏标(变更对象引用):Write Barrier写屏障技术
8.1.0.3 SATB
1、定义
Snapshot-At-The-Beginning,保存并发标记开始时,对象间的引用关系,的逻辑快照
2、原理
- 在并发标记过程中产生的新对象,会被看作"已完成扫描和标记",故在标记位图中是黑色的
如图35所示
- J和K的引用关系在STAB快照中不存在 ,所以能够判断出他们是在并发标记过程中产生的新对象,故标记为黑色
8.1.0.4 Write Barrier写屏障
1、定义
捕获对象引用关系的变更,并记录这些变更
2、作用
确保能够准确地识别出存活对象
- G1在并发标记阶段,用户线程和GC线程是并发执行的,可能导致:GC线程在标记存活对象时,用户线程改变了对应的引用关系,使得原本认为的垃圾变成存活对象(这样就可能漏标了)影响标记结果。
3、过程
在对象属性引用另一个对象时,写屏障会被触发,捕获这一引用变更
4、G1的具体实现- SATB专用写屏障技术
写屏障是用于SATB 的,因此称之为 SATB 专用写屏障
5、STAB专用写屏障原理
obj0.filed = obj2;
5.1 satb_write_barrier() 函数源码
def satb_write_barrier(field, newobj):
2: if $gc_phase == GC_CONCURRENT_MARK:
3: oldobj = *field
4: if oldobj != Null:
5: enqueue($current_thread.stab_local_queue, oldobj)
6:
7: *field = newobj
参数 field 表示被写入对象的域(属性),参数 newobj 表示被写入域(属性)的值
上述代码中即,obj0对象的域-filed,field的值obj2
-
2 :
的GC_CONCURRENT_MARK 表示并发标记阶段的标志位 (flag)
-
4 :
- 检查当前是否处于并发标记阶段且field 非 Null
- 检查通过,执行 5 :将 oldobj 添加到 $current_thread.stab_local_queue 当前线程的本地STAB队列中
- 最终,执行 7 :执行实际的写入操作
此函数不是由用户线程去执行,而是交由 GC 线程在并发标记过程中处理,防止影响用户线程
5.2 这里将oldobj加入queue的原因
1)首先不是newobj加入queue分析如下
- 如果newobj是在并发标记过程中新创建的对象,STAB算法会将其标记为存活对象
- 如果newobj也有别处引用,那么也会标记为存活对象
- 如果newobj也有别处引用,但是也在并发标记过程中被别的引用替换了,satb_write_barrier函数也会将newobj(对于别人而言是oldobj)放入queue,再最终标记阶段再进行对象分析
所以,newobj一定不会被错误当做垃圾回收掉
2)将oldobj加入queue的原因
如果不将oldobj加入queue,将oldobj当做垃圾回收,会有什么影响?
- 对于本次的filed而言,是没有影响,因为不再引用了
- 但是对于并发标记阶段,其它filed,将oldobj作为newobj进行引用
- 本次并发标记过程感知不到(因为oldobj不是新增对象,无法通过STAB快照标记为存活对象)
- 这样直接将oldobj当做垃圾,就影响到其它filed了
所以,需要将oldobj加入queue,再最终标记阶段,再分析一次
5.3 数据结构:SATB 本地队列queue 和 队列集合
二者关系如图36所示
1)本地队列内容
存储并发标记阶段的待标记对象的引用
如图32中
原本A -> null ,写,A ->B
- 因为oldobj == null
- 不会添加到STAB本地队列
- 但是会执行A ->B
原本D -> E,写, D->H
- 因为oldobj是E != null
- 添加到STAB本地队列
- 执行D->H
2)队列集合内容
填充满了的本地队列
5.3 本地队列作用
是用户线程私有的,无线程安全问题
5.4 过程
- 并发标记阶段,GC线程会定期检查队列集合
- 如果发现队列集合中有本地队列,则会对本地队列中的全部对象引用,进行标记和扫描
8.1.1初始标记阶段
8.1.1.1 定义
STW,并行,标记GCRoot直接引用的对象
8.1.1.2 过程
1、为每个region创建标记位图next
nextTAMS大小 = (top-bottom)/8 字节
如图34所示
2、根扫描
1)定义
标记GCRoot直接引用的对象
2)特点
STW
- 虽然,G1中采用写屏障技术可以获知对象的修改
- 但是大多数Root不是对象,他们的修改无法被写屏障获知
- 所以,为了防止漏标,跟扫描过程必须STW
3)过程
三色标记算法的init阶段
- Root -> C,但是C的子对象(A和E)尚未被标记,所以C是灰色的
8.1.2 并发标记阶段
8.1.2.1 定义
并发,标记初始化标记结果对象所引用的对象
8.1.2.2 过程
1、并发标记
继续顺着GCRoot直接引用的对象(如初始化标记中被标出来的C对象)继续标记(子对象),即三色标记算法的并发标记阶段
并发标记结束后如图35所示
- C:初始化标记时被标记的,当时因为子对象A和E尚未被标记,故为灰色。而经历完并发标记阶段后,子对象A和E也均被标记,故为黑色,表示存活对象
- A:本身被标记,且无子对象,故为黑色
- E:同理A,E对应了标记位图中多个位,但只对其起始标记位(mark bit)涂黑
- J和K:在并发标记期间创建的对象,G1通过STAB算法也完成了二者的标记,故J和K也是黑色,也是存活对象
1)针对并发过程中,新增对象(J和K)
通过STAB算法完成标记
2)针对并发过程中,引用关系发生变化的对象
通过STAB写屏障技术,完成扫描和标记,保证了不漏标
2、优先级
前者:顺着GCRoot直接引用的对象,继续扫描和标记
后者:一旦STAB队列集合中有本地队列,也需要扫描本地队列中变更引用关系的对象
二者优先级 : STAB本地队列 > 前者
原因:
- 虽然写屏障会不断地往 SATB 本地队列中添加对象
- 但是对象间引用关系的变化并不会改变存活对象的触达链路(—>)的总条数
8.1.3 最终标记阶段
1、remark定义
STW,并行,扫描并发阶段没有标记的对象,此阶段完成后,堆内所有存活对象都会被标记。
此时不带标记的对象都可以判定为死亡对象
2、内容
- 并发标记后,STACK队列集合中的本地STACK都被扫描了
- 仍有未填满的本地SATB 队列,待扫描
3、STW的原因
因为尚未扫描的(未满)SATB 本地队列中的数据会被用户线程操作,所以必须STW
8.1.4 存活对象计数
1、定义
并发(用户线程和GC线程之间是并发交替执行的,但是GC任务本身是并行的),对每个region中被标记的对象进行计数
2、过程
如图37所示
- 标记位图中本次nextTAMS 所有存活的对象:A、B、E 和 GH(STACK本地队列中的对象)
- J、K不是,因为他们是在并发标记阶段新产生的对象(但他们是存活对象)
- L、M不是,因为他们是在存活对象计数过程中产生的新对象
- 故,本次标记出来的存活对象总共大小 8 + 8 + 16 + 8 + 8 + 8 = 56字节
3、特点
- 转移处理可能在计数过程中启动,所以需要先暂停记忆集合(remembered set)线程,因为RSet线程也会使用这些待计算的对象
8.1.5 收尾工作
1、定义
STW,并行,对并发标记过程进行收尾工作,为下次并发标记做准备
2、STW的原因
收尾工作中操作的数据,有的 和 用户线程是共享的
3、过程
GC 线程会扫描每个region
1)当前 -> 上一个
如图38所示
- 将标记位图 next 中的并发标记结果移动到标记位图 prev 中
- 对并发标记中使用过的标记值进行重置,下次并发标记做好准备
- prevTAMS 被移到了 原nextTAMS处
- 原nextTAMS 会移动到 bottom处
- 在下次并发标记开始时,nextTAMS 会重新移动到Top处
2)执行标记-清除中的清除动作
清除定义:释放那些没有存活对象的region(整个region全是死对象),即以region为单位进行整个region清除
一旦发现会立即进行回收
回收一些完全空闲的区域,可以避免许多不必要的垃圾回收,这样就可以不费吹灰之力释放大量空间
3)计算每个区域的转移效率
定义:region的转移效率 = 区域内死亡对象的字节数 ÷ 转移此region所需时间
并按照该效率对region进行降序排序
- 死亡对象的字节数
- 转移时间,即预测时间,可以根据过往的转移时间进行来计算
一般来说死亡对象越多,转移效率就越高(死的多,则分子大,活的就少,则分母小,结果值就大)
8.2 转移-evacuation
8.2.0 前置
8.2.0.1定义
转移对象 + 重置region
- 将region中所有存活对象转移到空闲region
- 被转移的region中就只剩下死亡对象了,就可以重置region
故转移后的region如图39所示
8.2.0.2 特点
1)STW
转移是STW
- 期间即使有并发标记,也需要暂定并发标记,优先让转移先执行
2)依赖性
- 并发标记和转移在处理上是相互独立的
- 并发标记的结果信息对于转移来说并不是必须的
因此,转移处理可能发生在并发标记开始之前,也可能发生在并发标记的过程中
3)触发转移的时机
当新生代region数达到上限时,转移就会被执行
当申请内存时,发现没有足够的内存,就会尝试执行do_collection_pause回收一些内存。此时就会在安全点状态下执行转移操作。如果转移后,仍无法满足申请的内存大小,则进行FullGC或者OOM
8.2.0.3Card Table-卡表
1、数据结构
如图40所示
1)数据结构:
数组,元素大小为 1 B (Byte) 即 8(bit)
2)数组内容
- 物理内容:1 和 0
- 1:代表脏Dirty卡片,脏卡片是已经被转移专用写屏障添加到RSet日志中的卡片
- 0:代表干净卡片
- 逻辑内容:卡片
- 堆中大小为512B的一段存储空间
- 逻辑映射计算公式
堆中的对象所对应(所在的)的卡片,在卡表中的索引值,计算公式
(
对象的地址-堆的头部地址
)
/
512
(对象的地址 - 堆的头部地址)/512
(对象的地址-堆的头部地址)/512
-
eg1:假如obj1在堆中的位置是0x258(十六进制),对应十进制600,根据公式(600 - 0) / 512 = 1
所以obj1(0x258)所在的卡片,在卡表中的index值 为1
2、卡表大小
假如堆大小 = 1G,则卡片的个数 = 1G / 512B = 2^21 (即这么多连续的卡片,组成了堆)即Card Table数组的index个数,此值即Card Table的大小为2M
8.2.0.4 RSet-记忆集
remember set转移专用记忆集
1、定义
RSet是每个region都具有的,用于记录region和region之间的引用关系的一种map数据结构
- key:region地址,引用本region的其他region的地址
- val:数组[卡表index1,卡表index2],引用方的对象所对应的卡片索引
2、作用
空间换时间,即使不扫描堆内所有region内的对象,也可以确定待转移对象所在区域内的存活对象
3、具体实现
G1GC 是通过卡表(card table)来实现RSet的
4、原理
如图31所示
当遍历region2的RSet记忆集时
- entry1,k1是region1的地址,v1是卡表index 666
- 然后去卡表index = 666处查看,值 = 1是脏表
- 遍历region1中,idnex为666的卡片(512B)中全部对象的引用
- 遍历其中obj3对象时,从对象结构-实时数据中获取obj3的属性值即地址引用
- 发现地址引用指向region2中的对象obj2
- 则可以判断,obj2被引用,是存活对象(除非没有一个活对象引用obj2)
8.2.0.5 转移专用写屏障
1、背景
引用对象发生了变化
2、evacuation_write_barrier() 函数源码
def evacuation_write_barrier(obj, field, newobj):
2: check = obj ^ newobj
3: check = check >> LOG_OF_HEAP_REGION_SIZE
4: if newobj == Null:
5: check = 0
6: if check == 0:
7: return
8:
9: if not is_dirty_card(obj):
10: to_dirty(obj)
11: enqueue($current_thread.rs_log, obj)
12:
13: *field = newobj
2:
检测两个对象地址的高位部分是否相
本质是为了判断obj 和 newobj是否为同一块region
如果二者同处于同一块region,则check的值一定小于region大小
3:
当check值小于region大小,又执行check >> 右移region的大小(2^n),则3返回check的结果一定为0
6:
如果obj 和 newobj是为同一块region,则check值为0,所以满足if判断,直接return
含义就是,虽然引用发生了变化,但是变化后的引用对象,就在我本身的region内
4:
说明新的引用为null
obj0.filed = null;
9-11:
当对象 obj 的属性被修改时,写屏障就会获知,并会将对象obj 所对应的卡片索引,添加到RSet日志中
- 9
(obj地址 - 0 ) / 512 = index,判断CartTable[index] == 1即判断obj对应的卡片是否为脏卡片:
已经被转移专用写屏障添加到RSet日志中的卡片,目的是为了防止添加重复卡片。
- 10
如果不是脏卡片,则在第10行将卡片变成脏卡片
- 11
加入RSet日志中
所以转移专属写屏障函数的根本作用,就是将脏卡片添加到RSet日志集中,而RSet日志集的根本作用就是维护RSet
3、使用RSet日志 维护Rset
过程如图43所示
- 从日志中取出脏卡片
- 将脏卡片置为干净卡片
- 检查卡片中的所有对象的属性
- 往属性中地址所指向的region的RSet中,添加卡片,即<region, [卡片index]>
- 对于region-A而言,region-B-卡片索引2048中对象b,引用了我当中的对象
- 对于region-C而言,region-B-卡片索引2048中对象c,引用了我当中的对象
8.2.0.6 用户线程和GC线程执行关系
如图52所示
- GC线程(并发标记:初始、最终、收尾 ,整个转移)阶段,导致用户线程STW
- 转移可能发生在并发标记中暂停处理以外的所有时刻(比如并发、计数)
8.2.1 选择回收集合CSet
g1_policy()->choose_collection_set(target_pause_time_ms);
1、定义
参考并发标记后,提供的信息(每个region的转移效率),来选择要转移的区域,即CSet(eden、survior、old region均有)
2、选择标准
1)转移效率高
2)转移的预测暂停时间在用户的容忍范围内(用户指定了期望暂定的时间)
当所有已选区域预测暂停时间总和,快要超过用户的期望暂定的时间时,不再选择
8.2.2 转移
8.2.2.1 转移对象的定义
CSet中转移对象总共二大类
1、根对象 以及其子子孙孙对象
- 在根对象转移过程中,会尝试将其子对象加入转移队列
- 一旦对象加入转移队列,则会被转移,在转移过程中,会尝试将其子对象加入转移队列 ----
- 一旦 ----
2、根据RSet确定的对象以及其子子孙孙对象
- 在转移根据RSet确定的对象时,会转移其子对象
- 在转移其子对象的过程中,会尝试将其子对象加入转移队列
- 一旦对象加入转移队列,则会被转移,在转移过程中,会尝试将其子对象加入转移队列 ----
- 一旦 –
8.2.2.2 "根"对象
1、"根"对象的定义
"根"对象,包含以下二类对象:
① 由根直接引用的对象
- 被根直接引用,却不在CSet内的对象会被直接忽略
② 并发标记处理中的对象
并发标记中
- SATB 队列集合中的oldobj
- SATB 本地队列的引用也会被转移
这部分可能不在①中。如果他们是存活的,也应该被转移
8.2.2.3 根据RSet确定的对象
obj1 -> obj2
1、定义
被CSet内其他region中存活的对象引用的对象(obj2)
2、分析
for遍历CSet中的所有region
-
for扫描region中的RSet(如region2)中的所有values即所有卡表索引值
-
for扫描卡表内存中的所有obj对象,即scan_card方法
-
如果obj1引用了obj2,且obj1不是死亡对象,则转移此对象obj2
-
for遍历此对象的所有子对象
如果子对象也在CSet中,则转移子对象
-
-
-
如图46所示
仅有region-B在CSet中
遍历region-B的RSet
- k1-region-A,发现a虽然引用b,但是a是死亡对象,如果b仅被死亡对象引用,则不转移
- k2-region-C,发现e引用d‘,而且e是存活对象(无需考虑region-C在不在CSet),所以d’要转移
- 转移到目标region-D,需要更新region-D中的RSet
8.2.2.4 两大类对象的转移源码-evacuate_roots()
1: def evacuate_roots():
// 一、"根"对象,直接调用evacuate_obj转移对象函数,完成对象转移,返回转移后的新地址
2: for r in $roots:
3: if is_into_collection_set(*r):
4: *r = evacuate_obj(r)
5: // 二、根据RSet找到的对象
6: force_update_rs()
7: for region in $collection_set:
8: for card in region.rs_cards:
9: scan_card(card)
10:
11: def scan_card(card):
12: for obj in objects_in_card(card):
// 忽略掉死亡对象,以及只被死亡对象引用的对象
13: if is_marked(obj):
14: for child in children(obj):
15: if is_into_collection_set(*child):
16: *child = evacuate_obj(child)
8.2.2.5 对象的转移-evacuate_obj
书接上回:evacuate_roots方法中
- 第16行
*child = evacuate_obj(child)
- 第4行
*r = evacuate_obj(r)
底层都是对象的转移**evacuate_obj()**函数
1、转移对象的源码
def evacuate_obj(ref):
2: from = *ref
// 未被标记的对象,不会参与
3: if not is_marked(from):
4: return from
// 如果对象被转移过了,则直接返回对象转移后的新地址
5: if from.forwarded:
6: add_reference(ref, from.forwarded)
// 返回转移对象的新地址值,即to
7: return from.forwarding
8: // 一、将对象复制到转移目标region
9: to = allocate($free_region, from.size)
10: copy_data(new, from, from.size)
11: // 将对象转移后的地址存入 forwarding 指针中
12: from.forwarding = to
13: from.forwarded = True
14: // 二、扫描已转移完成的对象的子对象
15: for child in children(to):
// 2.1如果子对象也在CSet,则加入转移队列
16: if is_into_collection_set(*child):
17: enqueue($evacuate_queue, child)
18: else:
// 2.2 如果不在CSet,则
// 参数:子对象的引用方child 和子对象 *child
19: add_reference(child, *child)
20:
// 三、将待转移对象所对应的卡片,添加到转移目标region的RSet中(图45中⑤)
21: add_reference(ref, to)
// 七、返回对象转移后的新地址值
23: return to
这里为了便于理解参数含义,使用具体代码举例
d.field = a;
-
参数 ref 是待转移对象的引用地址,即d.field的值
-
from 是待转移对象,a
-
from.forwarded:转移后的地址引用,这里简记为to,即转移对象的新地址值
-
add_reference (from,to): 获取 from 所对应的卡片,将其添加到to的region的RSet中
push(card(from), to_region.rs_cards)
如果from和to位于相同的region,就没要更新了
2、源码中,转移对象伴随的执行动作分析
其中a是转移对象
步骤一、
- 将对象复制到转移目标region(copy_data物理转移对象)
步骤二、
2.1逻辑如图44所示
假如对象a为根转移对象,完成了转移到目标region后为a‘
添加子对象(a引用的对象)至转移队列:对象 a 引用的所有位于CSet内的对象(b),会被添加到转移队列中
后续动作
-
后续会将b对象转移到目标region
- 然后继续扫描b的子对象
-
会将a.filed值改为新的b‘地址值
-
更新RSet,对于b而言:
- 谁引用了我:更新b’所在region的RSet
- 我又引用了谁:更新原本被b引用的对象所在region的RSet
1)转移队列定义
用来保存待转移对象引用值
2)处理转移队列的源码- evacuate()
def evacuate():
2: while $evacuate_queue != Null:
3: ref = dequeue($evacuate_queue)
4: *ref = evacuate_obj(ref)
- 从转移队列中取出对象
- 然后执行evacuate_obj方法
2.2 逻辑,如图42所示
a 被转移到了 a’,所以有必要更新c-region-RSet中的key(a’所在region地址)以及val(a’的卡片在卡表中的index)
步骤三:
如图45所示
- a’的RSet,需要添加d对应的卡片
- d.field1 的地址被替换成了对象 a’
8.3 软实时性
8.3.1 GC暂停处理
1、暂停处理时刻的选择(起始和结束)
依据2个时间值
-
用户期望GC 暂停时间上限
-
GC 单位时间
准则:
单位时间内,累计的总GC暂定处理时间,不超过用户期望暂停时间
(eg:500ms、3s:即在3s内暂停时间总和不超过500ms)
2、依赖数据
G1中有一个队列记录了之前的暂停处理,调度开始时间和调度处理结束时间。G1本次的调度(起始和结束时刻)会参考之前的
3、符合准则举例
如图48所示
在单位时间内(a、b、c,长度都是单位时间),总的暂定时间(a的暂停时间、b的暂定时间、c的暂定时间),都不超过期望暂停时间(ok),所以图中GC暂停处理时刻的选择,没问题,遵循
4、不符合准则的场景
- 预测转移时间不准
- 堆内可用空间不足
- 并发标记阶段暂停时间就已超用户期望值
8.3.2 预测转移时间
1、定义
因为用户设置了期望暂停时间,所以再将region放入CSet时,就要预测转移CSet中的这些region中的对象,需要的时间,如果预测时间已经 > 用户期望的暂停时间了,就不要再放入CSet中了
2、CSet转移时间计算公式
如果47所示
基本大头2个
- 扫描RSet中的卡片
- 对象转移
九、分代G1GC
上述分析的都是纯G1模式,OpenJDK对外开放给用户的是分代G1模式
9.1 前置
9.1.1 和纯G1的区别
- region分为年轻代和老年代
- CSet的选择也是分代的
9.1.2 G1新生代GC 和 G1老年代GC
G1新生代GC:full_young_gc
- 新生代GC:minor GC
- G1新生代GC:完全新生代GC
- 将所有新生代region加入CSet(符合软实时性准则的前提下)
G1老年代GC
- 老年代GC:MajorGC
- G1老年代GC:部分新生代GC,或MixedGC
- 将所有新生代region加入CSet(符合软实时性准则的前提下)
- 以及部分老年代region加入CSet
9.1.3 新生代region分类
新生代区域又分为两类:创建region和存活region
1、创建region
定义:存放刚刚生成,一次都没有转移过的对象
2、存活region
非创建region
9.1.4 转移专用写屏障生|失效场景
1、生效场景
obj1 -> obj2
如果obj1是老年代region中对象,obj2是新生代中对象,则新生代region2的RSet中,记录有老年代region1的卡片信息
但:
- 如果obj1是新生代region中对象,obj2是老年代region中对象,region2的RSet中不记录卡表
一句话概括:
只有老年代引用新生代,新生代region的RSet中才会记录卡表
如图49所示
2、失效原因
首先对于新生代的对象,在G1新生代GC时,所有新生代region都会加入加入CSet,然后去扫描
-
图49中右,需要通过regionA的RSet中记录谁引用了我,然后通过这个谁,能够快速的定位出A中的存活对象,所以必要要有这个数据
-
图49中左,即使regionB中RSet不记录任何数据,因为regionA一定被加入CSet,在遍历整个regionA的过程中,也可以发现regionA中有对象引用了b,也能够判断b是存活对象。所以没必要记录,否则就重复记录数据了
3、总结
每个region中都有RSet,但只有老引用新的时候,才会在新region的RSet记录数据
9.1.5 转移对象存放的目标region
存放位置 | 条件 |
---|---|
新生代-存活region | 对象年龄 < 阈值(15) |
老年代region(对象晋升到老年代) | 对象年龄 >= 阈值(15) |
- 如果目标region满了,则会选择一个空闲的区域,把它修改成存活region或old region来使用
- 如果对象本身就在老年代,那么其年龄肯定 > 15了,所以下一次转移肯定还是在老年代(这个不算晋升)
9.1.6 不同代GC时的软实时性考量
1、背景
因为G1GC,无论是G1新生代GC,还是G1老年代GC,CSet中都含有全部的年轻代region。
因为G1认为年轻代中的对象是朝生夕死的,所以回收年轻代,性价比很高。
但是,如果新生代总region很大,就有可能违背软实时性准则,即:
GC单位时间内,累计总GC暂停处理时间 < 用户期望暂停时间
2、G1新生代GC
在遵循软实时性准则前提下,尽可能多的添加新生代region(优先选择价值高的region)
3、G1老年代GC
在遵循软实时性准则前提下,尽可能少的添加新生代region,然后添加部分老年代region
在并发标记结束之后设置个数
9.2 G1GC过程
9.2.1 G1新生代GC过程
如图50所示
将所有年轻代region(创建region 和 存活region)中存活对象,转移到目标region
- 如果有对象晋升成功,则存入old region
- 其它的正常存入新生代-存活region
9.2.2 G1老年代GC过程-MixedGC
1、过程
如图51所示
将所有年轻代region(创建region 和 存活region)中,以及部分老年代region中存活对象,转移到目标region
- 所有新生代(创建region、存活region)中存活对象
- 如果有对象晋升成功,则存入old region
- 其它的正常存入新生代-存活region
- 部分老年代中存活对象,存入old region
2、触发时机
“-XX:InitiatingHeapOccupancyPercent”,其默认值是45%,当老年代占据堆内存的45%时,触发MixedGC
9.2.3 二者选择
1、结论
垃圾回收器通常会选择G1老年代GC即MixedGC,只有在认为使用G1新生代GC效率更高时,才会切换成此
切换的时机:也只是在并发标记结束之后。
2、评估过程
- 参考并发标记中标记出的死亡对象个数,预测出下次部分新生代 GC 的转移效率
- 根据过去的完全新生代 GC 的转移效率,预测出下次完全新生代 GC 的转移效率
- 如果预测出完全新生代 GC 的转移效率更高,则切换为完全新生代 GC
9.2.4 并发标记触发时机
当转移完成并通过以下 4 项检查之后,会开始执行并发标记。
① 不在并发标记执行过程中 (避免重复并发标记)
② 并发标记的结果已被上次转移使用完
③ 已经使用了一定量的堆内存(默认是全部堆内存的 45% 以上)
④ 相比上次转移完成之后,堆内存的使用量有所增加
9.2.5 FullGC
在执行Full GC时,G1可能会退化为使用串行收集器,能会导致较长的STW时间
1、容易引发场景
1)如果分配了许多巨大的对象,就更有可能出现满 GC 的情况
Humongous ,必须为它们找到一组连续的region,因此在所有 Java 堆内存耗尽之前就可能发生 Full GC
-
减少大对象
- 将大对象分割成小对象,如日志打印分割
-
增加region大小,让大对象 相对region"变成"小对象
-XX:G1HeapRegionSize
2)并发标记无法及时完成,无法启动空间回收阶段
- 提高并发标记线程数-XX:ConcGCThreads
3) System.gc()
2、执行过程
标记存活对象:从GC Roots开始遍历堆中的所有对象,标记出所有存活的对象
整理内存空间:将存活对象移动到堆的一端,并清理掉未使用的内存空间
十、JVM调优
jvm中与性能有关的组件,如图24所示
- JIT即时编译器
- 堆(内存泄露、内存溢出)
- GC(日志、参数)
10.1 调优工具-GUI
10.1.1 工具
-
Arths
-
JProfiler、 MAT、VisualVM
-
GC日志分析工具:GC Easy、GCViewer
这里以JProfiler安装和使用教程
10.1.1 JProfiler
1、监控内容:CPU、内存、堆、线程、GC
2、离线观察快照
1)dump一份hprof文件
2)使用JProfiler打开、或在JProfiler的start center中选择open snapshot
3)观察Classes情况
因为内存、cpu等都是动态变化的,只有class对象快照可观察
4)class对象
如图57所示:类名、实例个数、实例总大小
5)对象实例的引用关系
选中对象,右键-use selected objects,结果如图61所示
-
查看我被谁引用的引用链
如图62
在图中可以查看当前实例被谁引用,也可以查看当前实例的GCRoot(右键,show path to GCRoot)
3、本机Idea项目
如图64所示
1)整体性能指标Overview
如图55所示
2)内存
如图56所示
3)cpu
-
整体情况如图58所示
-
线程占用cpu资源情况:record cpu data(会影响服务器性能)
如图63所示
包|类|方法级别的线程,占用cpu时间片的时间百分比、线程的状态(运行、阻塞)
(可以先以包、再以类、再以方法,逐步缩小线程范围)
4)排查内存泄露
- 开启:recorded objects(会影响服务器性能)
如图59所示
- 选中类,右击选择change liveness mode
如图60所示
- 选择Live objects模式,查看top20
- 再选择 -Garbage collected Objects模式,是否包含Live objects模式下的top20
- 如果不包含,说明有的Live objects,始终未变成垃圾,未被收集过,则有可能为内存泄露对象
5)Threads
主要查看blocked状态表示的红色线程
5、远程服务器实时观察
不推荐,会影响线上服务器性能
10.1.2 命令
top
1、作用
查看 CPU、内存使用情况、正在运行的进程 。定位 哪些进程正在消耗最多的系统资源 (占用cpu执行片、内存)
类似于Windows的任务管理器
2、使用
1)第一行:查看cpu负载率
load average: 值1,值2,值3
- 分别表示在前,值1 min、值2 min、值3 min的平均负载
- 如果服务器是8核,值 > 8,则说明cpu处于高负载了
2)第三行:30%id即cpu空闲率(低于20%说明cpu高负载)
ps
- 查看占用cpu负载最高的top5进程
ps -eo pid,user,%cpu,command --sort=-%cpu | head -n 6
或
ps -eo pid,user,%cpu,command | sort -nrk 3 | head -n 6
- 看进程下cpu负载最高的top5 线程
ps -eLf -p <PID> --sort=-%cpu | head -n 6
或
ps -eLf -p pid | awk 'NR>1 && $2==12345 {print $0}' | sort -nrk 9 | head -n 5
假设CPU使用率位于第9列
或
top -p pid 或 top pid : 默认按照cpu占用率降序,结果如果要按照内存降序展示:shift + m(Men即内存)
Windows环境下,建议使用 Git Bash 执行以下命令
jps
1、作用
Java进程pid、Idea中VM Options、JIT信息
9552 org.jetbrains.jps.cmdline.Launcher
9448 com.mjp.onjava.Demo
-Xms128m
-Xmx2024m
-XX:HeapDumpPath=C:\Users\admin\\java_error_in_idea64.hprof
-XX:+UseG1GC
-XX:+HeapDumpOnOutOfMemoryError
-XX:CICompilerCount=2
2、场景
- 列出Java进程,显示完整的包名或主类名
jps -l
- 列出Jvm启动参数(包含-l功能)
jps -lvm
结果输出 >> “C:\Users\admin\Desktop\my.txt”
jstat
1、作用
查看内存泄漏、GC(次数、时间)、内存使用Use情况、类加载等
2、使用
jstat 可选指令 pid xms(每个多少ms) ncount(打印几次)
3、可选指令
- -class:加载的类情况
- -gc:查看新、老region、元空间、GC次数和时间等情况
- 单位一般是kb
- 0.0并非0值,而是以kb表示,值很小,忽略
- C结尾表示Capacity即容量
- U结尾表示user使用量
- T结尾表示Time时间s
S0C, S1C: Survivor space 0 和 Survivor space 1 的容量(Capacity)即“From”和“To”
S0U, S1U: Survivor space 0 和 Survivor space 1 的使用量(Usage)
EC: Eden space的容量。
OC: Old Generation(老年代)的容量
MC: Metaspace(元空间)的容量
YGC, YGCT: 年轻代垃圾收集的次数和总时间
FGC, FGCT: 完全垃圾收集(Full GC)的次数和总时间
GCT: 垃圾收集的总时间,是YGCT和FGCT之和。
- -gccapacity
NGCMN 和 NGCMX:分别代表新生代(New Generation)的最小和最大容量
NGC:当前新生代已使用的容量
S0C 和 S1C
EC
OGCMN 和 OGCMX:分别代表老年代(Old Generation)的最小和最大容量
OGC:当前老年代已使用的容量
OC:老年代的总容量(或当前容量,取决于JVM的实现和配置)
MCMN 和 MCMX:分别代表元数据区的最小和最大容量
MC:当前元数据区已使用的容量
YGC 和 FGC:分别代表年轻代垃圾收集和Full GC的次数
jinfo
1、作用:查看Jvm启动参数
2、使用
- jinfo -flags pid:Jvm启动参数(region、新生代、堆初始、最大等)
-XX:ConcGCThreads=1:并发垃圾收集器的线程数
-XX:G1HeapRegionSize=1048576:单个region大小为1MB
-XX:InitialHeapSize=268435456:堆内存的初始大小为256MB
-XX:MaxHeapSize=4269801472:堆内存的最大大小为4GB
-XX:MaxNewSize=2561671168:年轻代(Young Generation)的最大大小为2.4GB
-XX:MinHeapDeltaBytes=1048576:堆内存扩展时的最小增量为1MB
-XX:+UseG1GC
- java -Xmx4088m -jar your-application.jar :修改jvm参数值
如: -XX:+HeapDumpOnOutOfMemoryError,即发生OOM时,自动将OOM时的堆内存使用情况,dump文件到-XX:HeapDumpPath = 的文件地址
jmap
1、作用:查看堆内存使用情况、类实例的创建情况、dump文件
2、使用
- jmap -heap pid:堆内存的使用情况
Heap Configuration:
MaxHeapSize = 4269801472 (4072.0MB)
NewSize = 1363144 (1.2999954223632812MB)
MaxNewSize = 2561671168 (2443.0MB)
OldSize = 5452592 (5.1999969482421875MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
G1HeapRegionSize = 1048576 (1.0MB)
Heap Usage:
G1 Heap:
regions = 4072
capacity = 4269801472 (4072.0MB)
used = 46269088 (44.125640869140625MB)
free = 4223532384 (4027.8743591308594MB)
1.0836355812657323% used
G1 Young Generation:
Eden Space:
regions = 1
capacity = 12582912 (12.0MB)
used = 1048576 (1.0MB)
free = 11534336 (11.0MB)
8.333333333333334% used
Survivor Space:
regions = 2
capacity = 2097152 (2.0MB)
used = 2097152 (2.0MB)
free = 0 (0.0MB)
100.0% used
G1 Old Generation:
regions = 84
capacity = 253755392 (242.0MB)
used = 42074784 (40.125640869140625MB)
free = 211680608 (201.87435913085938MB)
16.58084333435563% used
4027 interned Strings occupying 308088 bytes.
列出Java堆中每个类的实例数量、占用的内存大小以及该类实例的总大小
num序号 #instances实例个数 #bytes实例大小 class name类
----------------------------------------------
-
jmap -histo <pid> | sort -nr -k2 | head -n 10 :实例个数最多的top10的类
-
jmap -histo <pid> | sort -nr -k3 | head -n 10 :实例总大小最多的top10的类
-
jmap -dump:live,format=b,file=d:\Demo.hprof PID 生成JVM堆内存的快照(命令执行时刻堆内存中使用情况 ) ,影响服务器性能
下载后的快照,可以使用JProfiler进行分析
jstack
1、作用:定位线程出现长时间停顿的原因 (死锁、死循环、IO等待)
2、使用
1)死循环
- 代码
public class Demo{
private void func() {
while (true) {
System.out.println("s");
}
}
}
- 命令行输出:关键字locked
"main" #1 prio=5 os_prio=0 tid=0x000000000204c000 nid=0x2450 runnable [0x000000000253f000]
java.lang.Thread.State: RUNNABLE
- locked <0x00000006c7e5f358> (a java.io.PrintStream)
at com.mjp.onjava.Demo.func(Demo.java:27)
at com.mjp.onjava.Demo.main(Demo.java:15)
main线程是在执行,状态为RUNNABLE
但是有死循环locked情况发生,在Demo类的第15行func方法,方法的第27行
System.out.println(“s”);出现了死循环
2)死锁
-
产生死锁的Java代码
-
命令行输出:关键字 deadlock 、BLOCKED
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x000000001bd1c988 (object 0x000000076b57d7c0, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x000000001bd1de28 (object 0x000000076b57d7d0, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.mjp.onjava.DeadlockDemo$2.run(DeadlockDemo.java:48)
- waiting to lock <0x000000076b57d7c0> (a java.lang.Object)
- locked <0x000000076b57d7d0> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at com.mjp.onjava.DeadlockDemo$1.run(DeadlockDemo.java:27)
- waiting to lock <0x000000076b57d7d0> (a java.lang.Object)
- locked <0x000000076b57d7c0> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
- 生产了1个死锁
- 产生死锁的线程0-1
- 产生死锁的位置DeadlockDemo.java:48、DeadlockDemo.java:27
netstat
tcp、udp查看端口和ip
1、查看mysql服务,是哪个端口在监听
sudo netstat -tulnp | grep mysql
-t
显示TCP端口-u
显示UDP端口-l
仅显示监听状态的端口-n
直接显示IP地址和端口号-p
显示监听端口的程序名和PID(需要root权限)
2、 查看3306端口,正在被那个进程占用
sudo netstat -tulnp | grep 3306
3、 查看本机的3306端口,正在被哪个服务器访问
sudo netstat -an | grep ':3306'
- -a : all
结果显示
127.0.0.1:3306 192.168.163.10:14800 established
10.2 Heap
10.2.1 内存泄漏
定义
因为code不准确,导致无用的对象,仍被引用,一直处于GCRoot可达,无法被回收,占用的内层无法被释放
场景
1)逃逸
原本在方法内部的局部变量,因为逃逸可能变成了被引用的对象,也无法被回收
2)成员内部类
非静态内部类会持有外部类的信息,导致外部类无法被回收,建议使用静态内部类
3)静态集合持有对象
定义了静态集合,且集合中持有的是大量朝生夕死的对象(如字符串),这些对象也无法被正常回收
4)finally中未正确的关闭资源
建议使用try-with
5)单例
- 单例对象的生命周期和应用程序一样长
- 如果单例对象持有其他对象的引用,这个引用的对象将无法被回收
6)ThreadLocal使用不当
强软弱虚引用
1、强引用
强引用使得对象GCRoot可达
2、软引用 SoftReference
1)内存回收策略
- 正常情况下不会回收软引用对象
- 只有当内存不够了,且又申请了内存,此时会清除掉所有的软引用数据
2)场景
- 场景1-自定义LRU
public class MyLRU<K, V> extends LinkedHashMap<K, SoftReference<V>> {
private Integer limit;
public MyLRU(Integer limit) {
super(16, 0.75f, true);
this.limit = limit;
}
@Override
public boolean removeEldestEntry(Map.Entry<K, SoftReference<V>> eldest) {
return size() > limit;
}
public static void main(String[] args) {
MyLRU<String, Object> lru = new MyLRU<>(3);
lru.put("1", new SoftReference<>("mjp"));
}
}
- 场景2
SoftReference<byte[]> sf = new SoftReference<>(new byte[1024 * 1024 * 1024]);
byte[] byte1 = new byte[1024 * 1024];
System.gc();//此时堆内存仍够用,所以不会回收软引用内存
System.out.println(sf.get());//[B@34c45dca
byte[] byte2 = new byte[1024 * 1024 * 1024];
// 因为byte2申请了很大一块内存,导致GC,会回收软引用sf指向的内存
System.out.println(sf.get());//null
3)实战
- 网页图片缓存对象,使用软引用持有
- 快速加载
- 防止OOM
3、弱引用
1)内存回收策略
-
GC即回收
但不一定是立即回收
2)场景
WeakReference<byte[]> sf = new WeakReference<>(new byte[1]);
System.out.println(sf.get());//[B@34c45dca
System.gc();//只有GC,必回收
System.out.println(sf.get());//null
3)实战
- WeakHashMap
适用于缓存数量较多的大对象
CglibAopProxy中
private static final Map<Class<?>, Boolean> validatedClasses = new WeakHashMap<>();
4、虚引用
4.1 作用
管理直接内存
4.2 直接内存
1)作用-零拷贝
-
直接内存分配方式绕过了Java堆,直接在操作系统的内存中分配空间
对于网络IO场景, 减少数据在Java堆和操作系统之间的复制次数 ,即零拷贝
-
但直接内存需要程序员自己去回收,这里是通过ByteBuffer类中Clean虚引用机制完成直接内存的回收
2)实战
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
底层源码
// 分配直接内存
long base = unsafe.allocateMemory(size);
Unsafe unsafe.setMemory(base, size, (byte) 0);
// ByteBuffer成员变量address
long address = base + ps - (base & (ps - 1));
// 虚引用类Cleaner(extends PhantomReference<Object>)
Cleaner cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
成员变量address通过虚引用Cleaner,指向直接引用地址,如图20所示
4.3 内存回收策略
当ByteBuffer类的byteBuffer对象不再被引用时,我们也应该将os分配的直接内存释放掉。流程如下
- 当GC回收byteBuffer对象时,Clean虚引用会向Queue队列中发送信息,byteBuffer被回收了
- GC线程会对Queue一直监视,一旦发现queue队列中有byteBuffer元素
- 获取byteBuffer对象的address成员变量信息,得知还有一个虚引用指向直接内存地址
- 此时Jvm会调用线程,回收这块区域
ThreadLocal
一、原理
1、定义
本质是线程级别的全局变量
2、作用
在线程的生命周期中,在不同的模块内,传递值。避免了复杂的参数传递逻辑
3、概念
1)ThreadLocal类结构如图21所示
ThreadLocal类
静态内部类ThreadLocalMap
静态内部类Entry extends WeakReference<ThreadLocal<?>>
Key: this即ThreadLocal-tl
val: 即tl.set(值)的值
2)ThreadLocal 和 Thread关系如图22所示
每个Thread线程,都会通过createMap方法,创建一个ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
并将其作为成员变量
ThreadLocal.ThreadLocalMap threadLocals = null;
4、set
ThreadLocal<Object> tl = new ThreadLocal<>();
tl.set(1);//等价map.put(tl,1);
源码
public void set(T value) {
// 一、获取当前线程
Thread t = Thread.currentThread();
// 二、获取当前线程的ThreadLocalMap属性
ThreadLocalMap map = getMap(t);
if (map != null)
// 三.有的话,则map设置k-v
map.set(this, value);
else
// 如果没有则new一个
createMap(t, value);
}
- 步骤三、 map.set设置key-val
// 2.1 计算本次的key(this对象-ThreadLocal对象)在长度为16的Entry数组中的下标index索引
Entry[] tab = table;
int len = tab.length;
// 底层是通过AtomicInteger计算的
int i = key.threadLocalHashCode & (len-1);
// ---
// 省略了判断i下是否已有Entry元素,元素值覆盖等情况
// 假设i下无元素,则直接存储
// 2.2 设置key-val
tab[i] = new Entry(key, value);
- 2.2 new Entry构造函数
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
// 2.2.1 key则继续向上-> WeakReference -> Reference作为构造方法的参数
super(k);
// 2.2.2 val为Entry的成员变量
value = v;
}
}
- 2.2.1弱引用
Reference(T referent) {
this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
// 这里的referent即ThreadLocal对象
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
这里的tl是weakreferent,指向堆中分配的TreadLocal内存
- 2.2.2 Entry对象持有k-v(v是自己的属性、k是其爷爷的属性值)
5、get
Object o = tl.get();//等价map.get(tl);
源码
public T get() {
// 一、获取当前线程
Thread t = Thread.currentThread();
// 二、获取当前线程的属性map
ThreadLocalMap map = getMap(t);
if (map != null) {
// 三、根据tl对象,获取map中的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 四、返回entry的属性值val
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
- 步骤三、Entry e = map.getEntry(this)
private Entry getEntry(ThreadLocal<?> key) {
// 3.1通过key即tl计算在Entry数组中的下标index
int i = key.threadLocalHashCode & (table.length - 1);
// 3.2 获取元素
Entry e = table[i];
return e;
}
二、Entry必须为弱引用
1、目的
方式内存泄露
2、过程-假如Entry不是弱引用,而是正常的强引用,分析如图23所示
1)Demo类成员变量tl,不再引用0x01,即①断掉
正常情况下,此时地址0x01对应的tl对象,GCRoot不可达,应该被回收,但是
2)由于线程池ref仍然引用Thread对象
- Thread中持有ThreadLocalMap
- ThreadLocalMap持有Entry
- 如果Entry是强引用,则由于③这条线的存在,导致0x01地址内存无法被回收
3、Entry弱引用
- 由于Entry是弱引用,则只要GC,③这条线就会断掉,即③指向的内存地址会被回收
三、 注意事项
1、内存泄露
如上图23所示,即使Entry是弱引用,因为④引用的存在,仍可能导致内存泄露问题
1)分析
- ①断了
- ②存在
- GC时,③断了,0x01被回收了,此时Entry中的key即tl = null
- 正常情况下tl.get()可以获取key(tl)对应的Entry中的val,但是key为null了,无法再获取val了
- 导致val引用④无法被回收,即byte[]数据占用10M的空间无法被回收,导致内存泄露
2)解决
方式一:当确定不再使用tl时,手动将val删除
ThreadLocal<Integer> tl = new ThreadLocal<>();
tl.set(1);
Integer integer = tl.get();
System.out.println(integer);//1
tl.remove();
System.out.println(tl.get());//null
- remove:会将val对应的内存回收
- 弱引用: 会将key对应的内存回收
不会存在内存泄露问题
方式二:将val也变为弱引用
ThreadLocal<WeakReference<byte[]>> tl = new ThreadLocal<>();
2、并发时,多个多个请求公用一个线程
背景
100个请求,并发使用5个线程,其中req1使用的t1处理的,后续req21也是使用的t1处理的。此时t1中的tl可能被req1中逻辑改变了
3、父线程不会传递tl副本到子线程
public static void main(String[] args) throws InterruptedException {
final ThreadLocal<String> threadLocal=new ThreadLocal<>();
threadLocal.set("Java");
System.out.println("父线程的值:"+threadLocal.get());
new Thread(() ->
System.out.println("子线程的值:"+threadLocal.get())//null
).start();
}
- new的子线程,不会获取到父线程(main)的tl副本
解决: 使用TransmittableThreadLocal
四、实战
1、Spring实战
在 Spring 的事务管理中,使用ThreadLocal
存储当前线程的事务信息(主要是Connection连接), 当方法调用链中需要获取当前事务con时,可直接从tl中获取,避免复杂的参数传递
- 源码位置DataSourceTransactionManager#doBegin:开启事务
// 创建完成事务后,会将当前的连接Connection,绑定到当前的线程上(ThreadLocal)
TransactionSynchronizationManager.bindResource(
obtainDataSource(), txObject.getConnectionHolder()
);
// resources即ThreadLocal<Map<Object, Object>>
Map<Object, Object> map = resources.get();//即tl.get => val(map)
// 为val(map)绑定内容(k:datasource、val:con)
Object oldValue = map.put(actualKey, value);
2、Mybatis实战
1)背景
-
Mybatis中使用相同的SqlSession创建Mapper,不同的Mapper都使用相同的SqlSession,存在线程安全问题
-
Spring整合Mybatis,使用ThredLcoal解决上述问题(每个线程有单独的DefaultSqlSession)
2)源码
SqlSessionUtils#getSqlSession下registerSessionHolder
TransactionSynchronizationManager.bindResource(sessionFactory, holder);
底层和Spring一样
Map<Object, Object> map = resources.get();
Object oldValue = map.put(actualKey, value);
排查泄露
方式一:使用上述JProfiler
方式二:使用jps + jmap(实例情况)
10.2.2 OOM
1、原因
申请不到足够的内存
2、堆触发OOM过程
1)创建对象
新创建的对象,正常情况下存于新生代的eden区
2)MinorGC
- 当新生代的eden区满了,触发YGC,将存活对象分放入to survior
- 继续向eden区 和 to survior区存对象
- 当eden满了,触发YGC,将eden和to survior存活对象,再放入from surivor区
- 直到内存担保机制,老年代可用空间 < (新生代对象大小总和,新生代历次晋升对象大小平均值),触发FullGC
3)FullGC
- FullGC触发完成后,继续执行上述1)和2)动作,直到老年代也彻底满了,整个堆无法再分配足够的空间
3、堆避免OOM
1)内存加载的数据过大
- 场景1:mysql取数据,不使用分页条件,大量的数据放入运行时内存,直接OOM
- 场景2:excel的导出
2)while死循环
- 场景1:while循环结束条件 不是 >=而是==,在高并发的时候,可能死循环
3)http-head设置过大
默认是4096即4k,设置过大,高并发大量请求也会占用大量内存
4)不正确的使用lambda表达式创建大量的对象
- 循环中、高并发场景下、使用lambda表达式操作大集合、表达式中持有外部对象的引用
5)内存泄露
导致大量空间被占用,无法示释放
4、方法区OOM过程
1)触发过程
本地内存大小已经超过了 -XX:MaxMetaspaceSize=N 值
- 当加载的类过多或者动态生成的类过多时
5、方法区避免OOM
1)瞬时加载大量类
-
使用了大量的动态代理
-
第三方库
导致瞬时向运行时元空间中加载了大量的类信息(注意:避免引入不必要的依赖和类)
2)内存泄露
自定义的类加载器没有正确实现资源释放,导致已加载的类无法被卸载
6、排查OOM
10.3 GC
10.3.1 GC日志
10.3.1.1 打印日志参数
**-XX:+PrintGCDetails
**将日系详细程度设置为更精细
- 显示每个阶段的平均时间、最短时间和最长时间
- 根扫描、RSet 更新、RSet 扫描、对象复制
- 释放 CSet 的时间
- 显示 eden、survior和 堆总占用率
输出样本
[Ext Root Scanning (ms): Avg: 1.7 Min: 0.0 Max: 3.7 Diff: 3.7]
[Eden: 818M(818M)->0B(714M) Survivors: 0B->104M Heap: 836M(4096M)->409M(4096M)]
-XX:+PrintGCDateStamps
- 为每条日志添加发生时间戳 yyyy-MM-dd HH:mm:ss.SSS
2024-05-02T11:16:32.057+0200: [GC pause (young) 46M->35M(1332M), 0.0317225 secs]
-XX:+PrintGCDateStamps
- GC事件自JVM启动以来的时间s
10.3.1.2 获取GC日志
1、获取本地Idea中运行项目
配置如图65和如图66所示
1)配置VM Options参数
-XX:+UseG1GC -Xloggc:./gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
-XX:+PrintGCDateStamps
gc.log内容输出在此Java项目目录下
2、获取线上运行服务的gc.log
一般线上服务器的GC情况都是写在gc.log文件中,可以直接查看此文件内容进行GC日志分析
分析过程,可以参考下文,或者直接使用工具:GCEasy、GCViewer
3、 当本地SpringBoot项目启动耗时特别长,这时可以打印GC日志看一下 原因
是否有FullGC
[Full GC (Metadata GC Threshold)
因为项目刚刚启动的时候,要加载很多类,如果元数据空间不足,可能引发FullGC。这个时候可以适当调大一下元空间
‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M
10.3.1.3 GCEasy-日志分析
1、直接将gc.log上传到GCEasy中
http://gceasy.io/
2、分析结果说明参考
一文学会使用GCeasy——一款超好用的在线分析GC日志的网站_gc在线分析-CSDN博客
3、其它分析工具:
https://github.com/chewiebug/GCViewer
10.3.1.4 人工-日志分析
1、YGC日志分析
CommandLine flags: -XX:InitialHeapSize=266802112(初始堆内存256M) -XX:MaxHeapSize=4268833792
2024-07-28T10:57:29.710+0800: 0.176: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0197663 secs]
[Parallel Time: 12.1 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 175.7, Avg: 176.0, Max: 176.3, Diff: 0.7]
// 1.1初始化标记
[Ext Root Scanning (ms): Min: 0.0, Avg: 2.3, Max: 7.9, Diff: 7.9, Sum: 9.2]
// 1.2、并发标记中-更新RS
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
// 2.1、转移节点源RSet
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
// 2.2、转移节点源“根”节点
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
// 2.3 转移-转移对象-复制对象
[Object Copy (ms): Min: 0.0, Avg: 0.5, Max: 0.7, Diff: 0.7, Sum: 2.0]
[Termination (ms): Min: 0.0, Avg: 6.1, Max: 9.8, Diff: 9.8, Sum: 24.2]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 7.9, Avg: 8.9, Max: 11.2, Diff: 3.3, Sum: 35.5]
[GC Worker End (ms): Min: 184.2, Avg: 184.9, Max: 186.8, Diff: 2.7]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]:清理卡表
[Other: 7.6 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 7.4 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.1 ms]
[Free CSet: 0.0 ms]
[Eden: 4096.0K(14.0M)->0.0B(10.0M) Survivors: 0.0B->2048.0K Heap: 61.0M(256.0M)->1098.8K(256.0M)]
1)触发时间
2024-07-28T10:57:29.710+0800: 0.176:[]
时间戳、在Jvm启动了0.176s后执行的此次GC
2)GC类型、原因、GC 对应用造成的实际停顿时间 (STW)
[GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0197663 secs]
-
原因:大对象Humongous分配申请不到足够内层,导致的无法存入老年代region中,触发了GC
常见的触发GC的其它原因:
- G1 Evacuation Pause (转移:复制+清理后的对象,没有足够的空间来存储他们)
-
GC的类型:YGC
-
对应用造成的实际停顿时间: 0.01977s
-
(initial-mark)表示触发YGC后,开始初始化标记步骤
3)转移
[Object Copy (ms): Min: 0.0, Avg: 0.5, Max: 0.7, Diff: 0.7, Sum: 2.0]//2.转移-copy
[Other: 7.6 ms]
[Choose CSet: 0.0 ms] //1.选择CSet
[Humongous Reclaim: 0.1 ms]:大对象回收
[Free CSet: 0.0 ms]:清空CSet
4)堆使用情况: GC之后打印的
[Eden: 4096.0K(14.0M)->0.0B(10.0M) Survivors: 0.0B->2048.0K Heap: 61.0M(256.0M)->1098.8K(256.0M)]
- Eden:年轻代中的Eden区从
4096.0K(14.0M)
减少到0.0B(10M)
- 表明Eden区在这次GC后被完全清空
- Eden区的总容量从14.0M变为10.0M,可能是因为GC后调整了Eden区的大小
- Survivor区从
0.0B
增加到2048.0K
- 堆内存从
61.0M(256.0M)
减少到1098.8K(256.0M)
,但实际总堆大小(256.0M)未变,只是使用中的部分减少了
2、MixedGC
29.268: [GC pause (G1 Evacuation Pause) (mixed), 0.0059011 secs]
[Parallel Time: 5.6 ms, GC Workers: 4]
... ...
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.3 ms]
... ...
[Eden: 14.0M(14.0M)->0.0B(156.0M) Survivors: 10.0M->4096.0K Heap: 165.9M(512.0M)->148.7M(512.0M)]
3、FullGC
1)人为调用
System.gc();
2)回收无效果
837.762: [GC pause (G1 Evacuation Pause) (mixed)-- 99G->99G(100G), 32.0781615 secs]
869.842: [Full GC (Allocation Failure) 99G->62G(100G), 169.3897706 secs]
第一行回收后,还是99G,没有效果,堆还是马上要满了。这时FullGC
3)元空间不足
[Full GC (Metadata GC Threshold)
10.3.2 GC日志调优实战
1、背景
发现在14:00 - 14:05之间,服务下载接口偶发性超时
2、分析过程
1)dowp日志
- 使用公司工具dump一份日志 .hprof
2)分析日志
- 发现在14:00 - 14:05之间,频繁有FullGC,FullGC时长1s多
- 发现FullGC的信息不正常,G1中old region的使用率不到45%,就触发了FullGC
- 说明有大对象
- 大对象(>50%的region)即需要用humongous即连续的region存储,当没有足够的连续region,会提前触发FullGC
- 当时我们物理内存是16G,16G / 2048,即每个region差不多 = 8M, 50%region = 4M
- 使用公司工具(类似MAT和JProfile)
- 查看 .hprof内容视图,根据视图 对象类型、实例数量和占用空间大小的详细信息,发现了大对象
- 同时看火焰图,高度表示栈深、宽度表示占用cpu执行时间片长度(序列化方法一般都比较长时间)
- 大对象业务:这5min内一般是业务在页面下载excel内容的发频发时间
- 支持80多个仓 * 3000 sku的信息 = 240000 即 20w多条数据
- 并发查询查询获取sku先关数据response(List<DTO> list),并且将req和resp全部打印了
- sku详情信息特别多(规格、品类、温层、价格、销量、库存等等),整个下来妥妥的大对象
- 触发条件
- 有的业务方,只下载部分仓的部分数据,这部分数据的日志非大对象
- 大区管理员,会下载全国所有数据,这部分日志会形成大对象,触发FullGC
3、解决
- 业务角度
- 推广,尽量下载有用的数据,减少无目的的全量数据下载
- 研发角度
- Jvm调优
- 通过HeapRegionSize参数调整region大小,使得大对象相对于region变成小对象(风险不可控,未采用)
- 代码调优
- 优化打印日志,将DTO ->优化成部分有用参数的DTO(因为库存、销量等属性经常涉及到日志排查,所以不能直接忽略)
- 再次基础上,再将日志进行切分,将FullGC优化成执行更快的YongGC
- Jvm调优
@Slf4j
@Component
public class PartLogUtils {
public <V> void partLog(List<V> info,
int count,
Logger logger,
String format) {
List<List<V>> result = splitInfo(info, count);
logInfo(result, logger, format);
}
private <V> List<List<V>> splitInfo(List<V> info, int count) {
List<List<V>> result = Lists.newArrayList();
if (CollectionUtils.isEmpty(info) || info.size() < count) {
result.add(info);
return result;
}
List<V> temp = Lists.newArrayList();
int index = 0;
for (V item : info) {
index++;
temp.add(item);
if (index % count == 0) {
result.add(temp);
temp = Lists.newArrayList();
}
}
if (temp.size() > 0) {
result.add(temp);
}
return result;
}
private <V> void logInfo(List<V> infoList, Logger logger, String format) {
for (V item : infoList) {
logger.info(format, GsonUtils.toJsonStr(item));
}
}
}
- 使用
private final static Logger logger = LoggerFactory.getLogger(A.class);
PartLogUtils.partLog(resp, 2000, logger,"queryUserInfo:[{}]");
- 后续
将同步导出,优化为异步导出
10.3.3 G1 VM Options调优
堆
设置-XX:G1ReservePercent=n
值更大一些,默认为 10
-
预留一部分堆内存
-
用于在并发标记周期,暂时存储那些“晋升失败”的对象
- 晋升失败:老年代没有足够的空间来容纳晋升对象
- 晋升失败代价很高
直到下一次垃圾收集发生
-
或使用**
-XX:ConcGCThreads=n
**选项增加标记线程的数量
设置堆ms = mx
- 除非遇到了STW(G1可控),都则建议设置
- 默认内存往往太小
- 频繁GC
- ms <–> mx扩容缩容
年轻代
不要 通过-Xmn
设置年轻一代的大小
设置年轻一代的大小会禁用暂停时间目标,即G1的STW可控功能失效
Yong Only Phase STW时长变长
-
Jvm表现:主要是转移-CSet 阶段耗时过长(尤其是对象复制子阶段)
-
根因:大概率是年轻代占比变大了,可以降低
-XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent
参考资料
- 周志明老师的《深入理解Java虚拟机》
- 中村成洋老师的《深入java虚拟机:jvm+g1gc的算法与实现》
- 尚硅谷康师傅的《详解java虚拟机》
- Oracle官网
《hotspot-virtual-machine-garbage-collection-tuning-guide》
《java-virtual-machine-guide》
《jvms22》 - 及其其它网上资料
如:Getting Started with the G1 Garbage Collector、CSDN、博客等