From e8ee01e5d103823e10507f8aa1f91aec42c1f10d Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 20 Feb 2022 22:35:52 +0800 Subject: [PATCH 01/57] Update Java Note --- DB.md | 8 +- Java.md | 468 ++++++++++++++++++++++++++------------------------------ Prog.md | 2 +- Web.md | 4 +- 4 files changed, 222 insertions(+), 260 deletions(-) diff --git a/DB.md b/DB.md index f51e5c2..bdd5d85 100644 --- a/DB.md +++ b/DB.md @@ -5757,7 +5757,7 @@ InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 * Read Committed 级别,增删改操作会加写锁(行锁),读操作不加锁 - MySQL 做了优化,在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引,需要在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象,这种情况同样适用于 RR + MySQL 做了优化,在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引(全表扫描),在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象,这种情况同样适用于 RR * Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,**加了读锁后其他事务就无法修改数据**,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 @@ -6135,7 +6135,7 @@ RC、RR 级别下的 InnoDB 快照读区别 - 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** - 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 **Read View 并不能阻止事务去更新数据**,并且把这条新记录的 trx_id 给变为当前的事务 id,所以对当前事务就是可见的 + 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1 去 UPDATE 该行会发现更新成功,因为 **Read View 并不能阻止事务去更新数据**,并且把这条新记录的 trx_id 变为当前的事务 id,所以对当前事务就是可见的 - 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 @@ -6790,9 +6790,9 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合,可以保护当前记录和前面的间隙 * 加锁遵循左开右闭原则 -* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会对 (10,11] 加锁 +* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,正无穷),锁住索引 11 会对 (10,11] 加锁 -间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化 +间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙。 间隙锁危害:当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害 diff --git a/Java.md b/Java.md index 91ff8a8..be48f40 100644 --- a/Java.md +++ b/Java.md @@ -104,7 +104,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - char 类型是一个单一的 16 位两个字节的 Unicode 字符 - 最小值是 **`\u0000`**(即为 0) - 最大值是 **`\uffff`**(即为 65535) -- char 数据类型可以**存储任何字符** +- char 数据类型可以存储任何字符 - 例子:`char c = 'A'`,`char c = '张'` 上下转型 @@ -187,8 +187,8 @@ Java 为包装类做了一些特殊功能,具体来看特殊功能主要有: * 把字符串类型的数值转换成对应的基本数据类型的值(**重要**) - 1. Xxx.parseXxx("字符串类型的数值") → Integer.parseInt(numStr) - 2. Xxx.valueOf("字符串类型的数值") → Integer.valueOf(numStr) (推荐使用) + 1. Xxx.parseXxx("字符串类型的数值") → `Integer.parseInt(numStr)` + 2. Xxx.valueOf("字符串类型的数值") → `Integer.valueOf(numStr)` (推荐使用) ```java public class PackageClass02 { @@ -219,7 +219,35 @@ Java 为包装类做了一些特殊功能,具体来看特殊功能主要有: } ``` - + + + +*** + + + +##### 类型对比 + +* 有了基本数据类型,为什么还要引用数据类型? + + > 引用数据类型封装了数据和处理该数据的方法,比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型 + > + > Java 中大部分类和方法都是针对引用数据类型,包括泛型和集合 + +* 引用数据类型那么好,为什么还用基本数据类型? + + > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高。逻辑上来讲,Java 只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的 + +* Java 集合不能存放基本数据类型,只存放对象的引用? + + > 不能放基本数据类型是因为不是 Object 的子类。泛型思想,如果不用泛型要写很多参数类型不同的但功能相同的函数(方法重载) + +* == + + > == 比较基本数据类型:比较的是具体的值 + > == 比较引用数据类型:比较的是对象地址值 + + *** @@ -242,8 +270,8 @@ public class PackegeClass { Integer c = 100 ; int c1 = c ; // 自动拆箱 - Integer it = Integer.valueOf(12); // 手工装箱! - // Integer it1 = new Integer(12); // 手工装箱! + Integer it = Integer.valueOf(12); // 手工装箱! + // Integer it1 = new Integer(12); // 手工装箱! Integer it2 = 12; Integer it3 = 111 ; @@ -257,6 +285,7 @@ public class PackegeClass { ```java public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) + // 【缓存池】,本质上是一个数组 return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } @@ -304,10 +333,10 @@ valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中 - Integer values between -128 and 127 - Character in the range \u0000 to \u007F (0 and 127) -在 jdk 1.8 所有的数值类缓冲池中,**Integer 的缓存池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127**,但是上界是可调的,在启动 jvm 的时候,通过 AutoBoxCacheMax= 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界 +在 jdk 1.8 所有的数值类缓冲池中,**Integer 的缓存池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127**,但是上界是可调的,在启动 JVM 时通过 `AutoBoxCacheMax=` 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界 ```java -Integer x = 100; //自动装箱,底层调用 Integer.valueOf(1) +Integer x = 100; // 自动装箱,底层调用 Integer.valueOf(1) Integer y = 100; System.out.println(x == y); // true @@ -335,8 +364,8 @@ System.out.println(x == y); // true,因为 y 会调用 intValue 自动拆箱 一般使用 `sc.nextInt()` 或者 `sc.nextLine()` 接受整型和字符串,然后转成需要的数据类型 -Scanner:`BufferedReader br = new BufferedReader(new InputStreamReader(System.in))` -print:`PrintStream.write()` +* Scanner:`BufferedReader br = new BufferedReader(new InputStreamReader(System.in))` +* print:`PrintStream.write()` > 使用引用数据类型的API @@ -351,31 +380,6 @@ public static void main(String[] args) { -*** - - - -#### 面试题 - -* 有了基本数据类型,为什么还要引用数据类型? - - > 引用数据类型封装了数据和处理该数据的方法,比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型 - > - > Java 中大部分类和方法都是针对引用数据类型,包括泛型和集合 - -* 引用数据类型那么好,为什么还用基本数据类型? - - > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高。逻辑上来讲,Java 只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的 - -* Java 集合不能存放基本数据类型,只存放对象的引用? - - > 不能放基本数据类型是因为不是 Object 的子类。泛型思想,如果不用泛型要写很多参数类型不同的但功能相同的函数(方法重载) - -* == - - > == 比较基本数据类型:比较的是具体的值 - > == 比较引用数据类型:比较的是对象地址值 - **** @@ -386,12 +390,12 @@ public static void main(String[] args) { #### 初始化 -数组就是存储数据长度固定的容器,存储多个数据的**数据类型要一致**,数组也是一个对象 +数组就是存储数据长度固定的容器,存储多个数据的数据类型要一致,**数组也是一个对象** 创建数组: -* 数据类型[] 数组名:`int[] arr;` (常用) -* 数据类型 数组名[]:`int arr[];` +* 数据类型[] 数组名:`int[] arr` (常用) +* 数据类型 数组名[]:`int arr[]` 静态初始化: @@ -406,9 +410,9 @@ public static void main(String[] args) { #### 元素访问 -* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从 **0** 开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。 +* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从 **0** 开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。 -* **访问格式**:数组名[索引] `arr[0]` +* **访问格式**:数组名[索引],`arr[0]` * **赋值:**`arr[0] = 10` @@ -419,19 +423,19 @@ public static void main(String[] args) { #### 内存分配 -内存是计算机中的重要原件,临时存储区域,作用是运行程序。我们编写的程序是存放在硬盘中的,在硬盘中的程序是不会运行的。必须放进内存中才能运行,运行完毕后会清空内存。 Java虚拟机要运行程序,必须要对内存进行空间的分配和管理。 +内存是计算机中的重要器件,临时存储区域,作用是运行程序。我们编写的程序是存放在硬盘中,在硬盘中的程序是不会运行的,必须放进内存中才能运行,运行完毕后会清空内存。 Java 虚拟机要运行程序,必须要对内存进行空间的分配和管理。 -| 区域名称 | 作用 | -| ---------- | -------------------------------------------------------- | -| 寄存器 | 给CPU使用,和我们开发无关 | -| 本地方法栈 | JVM在使用操作系统功能的时候使用,和我们开发无关 | -| 方法区 | 存储可以运行的class文件 | -| 堆内存 | 存储对象或者数组,new来创建的,都存储在堆内存 | -| 方法栈 | 方法运行时使用的内存,比如main方法运行,进入方法栈中执行 | +| 区域名称 | 作用 | +| ---------- | ---------------------------------------------------------- | +| 寄存器 | 给 CPU 使用 | +| 本地方法栈 | JVM 在使用操作系统功能的时候使用 | +| 方法区 | 存储可以运行的 class 文件 | +| 堆内存 | 存储对象或者数组,new 来创建的,都存储在堆内存 | +| 方法栈 | 方法运行时使用的内存,比如 main 方法运行,进入方法栈中执行 | -**内存分配图**: +**内存分配图**:Java 内存分配 -* Java内存分配-一个数组内存图 +* 一个数组内存图 ![](https://gitee.com/seazean/images/raw/master/Java/数组内存分配-一个数组内存图.png) @@ -464,7 +468,7 @@ public static void main(String[] args) { } ``` - arr = null,表示变量arr将不再保存数组的内存地址,也就不允许再操作数组,因此运行的时候会抛出空指针异常。在开发中,空指针异常是不能出现的,一旦出现了,就必须要修改我们编写的代码。 + arr = null,表示变量 arr 将不再保存数组的内存地址,也就不允许再操作数组,因此运行的时候会抛出空指针异常。在开发中,空指针异常是不能出现的,一旦出现了,就必须要修改编写的代码。 解决方案:给数组一个真正的堆内存空间引用即可! @@ -482,14 +486,14 @@ public static void main(String[] args) { * 动态初始化: - 数据类型[][] 变量名 = new 数据类型[m] [n] : `int[][] arr = new int[3][3];` + 数据类型[][] 变量名 = new 数据类型[m] [n] : `int[][] arr = new int[3][3]` * m 表示这个二维数组,可以存放多少个一维数组,行 * n 表示每一个一维数组,可以存放多少个元素,列 * 静态初始化 * 数据类型[][] 变量名 = new 数据类型[][]{ {元素1, 元素2...} , {元素1, 元素2...} - * 数据类型[][] 变量名 = { {元素1, 元素2...} , {元素1, 元素2...} ...} - * `int[][] arr = {{11,22,33}, {44,55,66}};` + * 数据类型[][] 变量名 = {{元素1, 元素2...}, {元素1, 元素2...}...} + * `int[][] arr = {{11,22,33}, {44,55,66}}` 遍历: @@ -525,13 +529,13 @@ public class Test1 { ### 运算 -* i++ 与++i 的区别? +* i++ 与 ++i 的区别? i++ 表示先将 i 放在表达式中运算,然后再加 1 ++i 表示先将 i 加 1,然后再放在表达式中运算 * || 和 |,&& 和& 的区别,逻辑运算符 - **&和| 称为布尔运算符,位运算符。&&和|| 称为条件布尔运算符,也叫短路运算符**。 + **& 和| 称为布尔运算符,位运算符。&& 和 || 称为条件布尔运算符,也叫短路运算符**。 如果 && 运算符的第一个操作数是 false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是 false;同样,如果第一个操作数是 true,|| 运算符就返回 true,无需考虑第二个操作数的值;但 & 和 | 却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用 && 和 || 运算符** @@ -570,14 +574,14 @@ public class Test1 { ``` * 负数: - 原码:最高位为1,其余位置和正数相同 + 原码:最高位为 1,其余位置和正数相同 反码:保证符号位不变,其余位置取反 补码:保证符号位不变,其余位置取反加 1,即反码 +1 ```java - -100原码: 10000000 00000000 00000000 01100100 //32位 - -100反码: 11111111 11111111 11111111 10011011 - -100补码: 11111111 11111111 11111111 10011100 + -100 原码: 10000000 00000000 00000000 01100100 //32位 + -100 反码: 11111111 11111111 11111111 10011011 + -100 补码: 11111111 11111111 11111111 10011100 ``` 补码 → 原码:符号位不变,其余位置取反加 1 @@ -621,13 +625,11 @@ public class Test1 { #### 可变参数 -可变参数用在形参中可以接收多个数据。 +可变参数用在形参中可以接收多个数据,在方法内部**本质上就是一个数组** -可变参数的格式:数据类型... 参数名称 +格式:数据类型... 参数名称 -可变参数的作用:传输参数非常灵活,方便。可以不传输参数、传输一个参数、或者传输一个数组。 - -可变参数在方法内部本质上就是一个数组。 +作用:传输参数非常灵活,方便,可以不传输参数、传输一个参数、或者传输一个数组。 可变参数的注意事项: @@ -667,7 +669,7 @@ public static void sum(int... nums){ 在方法内部定义的叫局部变量,局部变量不能加 static,包括 protected、private、public 这些也不能加 -原因:局部变量是保存在栈中的,而静态变量保存于方法区(JDK8 在堆中),局部变量出了方法就被栈回收了,而静态变量不会,所以在局部变量前不能加 static 关键字,静态变量是定义在类中,又叫类变量 +原因:局部变量是保存在栈中的,而静态变量保存于方法区(JDK8 在堆中),局部变量出了方法就被栈回收了,而静态变量不会,所以**在局部变量前不能加 static 关键字**,静态变量是定义在类中,又叫类变量 @@ -677,7 +679,7 @@ public static void sum(int... nums){ #### 定义调用 -定义格式 +定义格式: ```java public static 返回值类型 方法名(参数) { @@ -686,11 +688,10 @@ public static 返回值类型 方法名(参数) { } ``` -调用格式 +调用格式: ```java -数据类型 变量名 = 方法名 ( 参数 ) ; -//注意:方法的返回值通常会使用变量接收,否则该返回值将无意义 +数据类型 变量名 = 方法名 (参数) ; ``` * 方法名:调用方法时候使用的标识 @@ -700,10 +701,10 @@ public static 返回值类型 方法名(参数) { 如果方法操作完毕 -* void 类型的方法,直接调用即可,而且方法体中一般不写return +* void 类型的方法,直接调用即可,而且方法体中一般不写 return * 非 void 类型的方法,推荐用变量接收调用 -原理:每个方法在被调用执行的时候,都会进入栈内存,并且拥有自己独立的内存空间,方法内部代码调用完毕之后,会从栈内存中弹栈消失。 +原理:每个方法在被调用执行的时候,都会进入栈内存,并且拥有自己独立的内存空间,方法内部代码调用完毕之后,会从栈内存中弹栈消失 @@ -785,9 +786,9 @@ public class MethodDemo { 重载的方法在编译过程中即可完成识别,方法调用时 Java 编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段: -* 在不考虑对基本类型自动装拆箱 (auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法 -* 如果第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法 -* 如果第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法 +* 一阶段:在不考虑对基本类型自动装拆箱 (auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法 +* 二阶段:如果第一阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法 +* 三阶段:如果第二阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法 如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么会选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系,**一般会选择形参为参数类型的子类的方法,因为子类时更具体的实现**: @@ -796,7 +797,7 @@ public class MethodDemo { void invoke(Object obj, Object... args) { ... } void invoke(String s, Object obj, Object... args) { ... } - invoke(null, 1); // 调用第二个invoke方法,选取的第二阶段 + invoke(null, 1); // 调用第二个invoke方法,选取的第二阶段 invoke(null, 1, 2); // 调用第二个invoke方法,匹配第一个和第二个,但String是Object的子类 invoke(null, new Object[]{1}); // 只有手动绕开可变长参数的语法糖,才能调用第一个invoke方法 @@ -932,7 +933,7 @@ Java 的参数是以**值传递**的形式传入方法中 // 获取索引 Season s = Season.SPRING; System.out.println(s); //SPRING - System.out.println(s.ordinal()); // 0,代表索引,summer 就是 1 + System.out.println(s.ordinal()); // 0,该值代表索引,summer 就是 1 s.s.doSomething(); // 获取全部枚举 Season[] ss = Season.values(); @@ -988,15 +989,15 @@ Debug 是供程序员使用的程序调试工具,它可以用于查看程序 ### 概述 -**Java是一种面向对象的高级编程语言。** +**Java 是一种面向对象的高级编程语言。** **三大特征:封装,继承,多态** 面向对象最重要的两个概念:类和对象 -* 类:相同事物共同特征的描述。类只是学术上的一个概念并非真实存在的,只能描述一类事物 -* 对象:是真实存在的实例, 实例==对象,**对象是类的实例化** -* 结论:有了类和对象就可以描述万千世界所有的事物。 必须先有类才能有对象 +* 类:相同事物共同特征的描述,类只是学术上的一个概念并非真实存在的,只能描述一类事物 +* 对象:是真实存在的实例, 实例 == 对象,**对象是类的实例化** +* 结论:有了类和对象就可以描述万千世界所有的事物,必须先有类才能有对象 @@ -1017,7 +1018,7 @@ Debug 是供程序员使用的程序调试工具,它可以用于查看程序 1. 类名的首字母建议大写,满足驼峰模式,比如 StudentNameCode 2. 一个 Java 代码中可以定义多个类,按照规范一个 Java 文件一个类 -3. 一个 Java 代码文件中,只能有一个类是 public 修饰,**public修饰的类名必须成为当前Java代码的文件名称** +3. 一个 Java 代码文件中,只能有一个类是 public 修饰,**public 修饰的类名必须成为当前 Java 代码的文件名称** ```java 类中的成分:有且仅有五大成分 @@ -1086,6 +1087,7 @@ public class ClassDemo { ### 封装 封装的哲学思维:合理隐藏,合理暴露 + 封装最初的目的:提高代码的安全性和复用性,组件化 封装的步骤: @@ -1120,9 +1122,7 @@ this 关键字的作用: #### 基本介绍 -Java 是通过成员变量是否有 static 修饰来区分是类的还是属于对象的。 - -static 静态修饰的成员(方法和成员变量)属于类本身的。 +Java 是通过成员变量是否有 static 修饰来区分是类的还是属于对象的 按照有无 static 修饰,成员变量和方法可以分为: @@ -1140,7 +1140,7 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 -#### static用法 +#### static 用法 成员变量的访问语法: @@ -1175,8 +1175,8 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 inAddr(); // b.对象.实例方法 // Student.eat(); // 报错了! - Student zbj = new Student(); - zbj.eat(); + Student sea = new Student(); + sea.eat(); } } ``` @@ -1199,14 +1199,14 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 访问问题: -* 实例方法是否可以直接访问实例成员变量?可以的,因为它们都属于对象 -* 实例方法是否可以直接访问静态成员变量?可以的,静态成员变量可以被共享访问 -* 实例方法是否可以直接访问实例方法? 可以的,实例方法和实例方法都属于对象 -* 实例方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问 -* 静态方法是否可以直接访问实例变量? 不可以的,实例变量必须用对象访问!! -* 静态方法是否可以直接访问静态变量? 可以的,静态成员变量可以被共享访问。 -* 静态方法是否可以直接访问实例方法? 不可以的,实例方法必须用对象访问!! -* 静态方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问!! +* 实例方法是否可以直接访问实例成员变量?可以,因为它们都属于对象 +* 实例方法是否可以直接访问静态成员变量?可以,静态成员变量可以被共享访问 +* 实例方法是否可以直接访问实例方法? 可以,实例方法和实例方法都属于对象 +* 实例方法是否可以直接访问静态方法?可以,静态方法可以被共享访问 +* 静态方法是否可以直接访问实例变量? 不可以,实例变量必须用对象访问!! +* 静态方法是否可以直接访问静态变量? 可以,静态成员变量可以被共享访问。 +* 静态方法是否可以直接访问实例方法? 不可以,实例方法必须用对象访问!! +* 静态方法是否可以直接访问静态方法?可以,静态方法可以被共享访问!! @@ -1220,8 +1220,8 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 继承是 Java 中一般到特殊的关系,是一种子类到父类的关系 -* 被继承的类称为:父类/超类。 -* 继承父类的类称为:子类。 +* 被继承的类称为:父类/超类 +* 继承父类的类称为:子类 继承的作用: @@ -1235,7 +1235,7 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 2. **单继承**:一个类只能继承一个直接父类 3. 多层继承:一个类可以间接继承多个父类(家谱) 4. 一个类可以有多个子类 -5. 一个类要么默认继承了 Object 类,要么间接继承了 Object 类,Object 类是 Java 中的祖宗类 +5. 一个类要么默认继承了 Object 类,要么间接继承了 Object 类,**Object 类是 Java 中的祖宗类** 继承的格式: @@ -1356,7 +1356,7 @@ class Animal{ -#### 面试问题 +#### 常见问题 * 为什么子类构造器会先调用父类构造器? @@ -1365,17 +1365,18 @@ class Animal{ 3. 参考 JVM → 类加载 → 对象创建 ```java - class Animal{ - public Animal(){ + class Animal { + public Animal() { System.out.println("==父类Animal的无参数构造器=="); } } - class Tiger extends Animal{ - public Tiger(){ + + class Tiger extends Animal { + public Tiger() { super(); // 默认存在的,根据参数去匹配调用父类的构造器。 System.out.println("==子类Tiger的无参数构造器=="); } - public Tiger(String name){ + public Tiger(String name) { //super(); 默认存在的,根据参数去匹配调用父类的构造器。 System.out.println("==子类Tiger的有参数构造器=="); } @@ -1421,8 +1422,8 @@ class Animal{ 总结与拓展: -* this 代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)**可以根据参数匹配访问本类其他构造器。 -* super 代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...)可以根据参数匹配访问父类的构造器 +* this 代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)** 可以根据参数匹配访问本类其他构造器 +* super 代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...) 可以根据参数匹配访问父类的构造器 **注意:** @@ -1461,7 +1462,7 @@ class Student{ this.age = age; this.schoolName = schoolName; } -// .......get + set + // .......get + set } ``` @@ -1481,7 +1482,7 @@ final 用于修饰:类,方法,变量 * final 可以修饰方法,方法就不能被重写 * final 修饰变量总规则:变量有且仅能被赋值一次 -**面试题**:final 和 abstract 的关系是互斥关系,不能同时修饰类或者同时修饰方法! +final 和 abstract 的关系是**互斥关系**,不能同时修饰类或者同时修饰方法 @@ -1505,7 +1506,7 @@ final 修饰静态成员变量可以在哪些地方赋值: ```java public class FinalDemo { -//常量:public static final修饰,名称字母全部大写,下划线连接。 + //常量:public static final修饰,名称字母全部大写,下划线连接。 public static final String SCHOOL_NAME = "张三" ; public static final String SCHOOL_NAME1; @@ -1565,9 +1566,9 @@ public class FinalDemo { > 父类知道子类要完成某个功能,但是每个子类实现情况不一样。 -抽象方法:没有方法体,只有方法签名,必须用**abstract**修饰的方法就是抽象方法 +抽象方法:没有方法体,只有方法签名,必须用 abstract 修饰的方法就是抽象方法 -抽象类:拥有抽象方法的类必须定义成抽象类,必须用**abstract**修饰,抽象类是为了被继承 +抽象类:拥有抽象方法的类必须定义成抽象类,必须用 abstract 修饰,**抽象类是为了被继承** 一个类继承抽象类,**必须重写抽象类的全部抽象方法**,否则这个类必须定义成抽象类,因为拥有抽象方法的类必须定义成抽象类 @@ -1597,12 +1598,11 @@ abstract class Animal{ -#### 面试问题 +#### 常见问题 一、抽象类是否有构造器,是否可以创建对象? -答:抽象类作为类一定有构造器,而且必须有构造器,提供给子类继承后调用父类构造器使用的 -* 抽象类有构造器,但是抽象类不能创建对象,类的其他成分它都具备 +* 抽象类有构造器,但是抽象类不能创建对象,类的其他成分它都具备,构造器提供给子类继承后调用父类构造器使用 * 抽象类中存在抽象方法,但不能执行,**抽象类中也可没有抽象方法** > 抽象在学术上本身意味着不能实例化 @@ -1678,9 +1678,9 @@ abstract class Template{ #### 基本介绍 -接口,是 Java 语言中一种引用类型,是方法的集合。 +接口是 Java 语言中一种引用类型,是方法的集合。 -接口是更加彻底的抽象,接口中只有抽象方法和常量,没有其他成分,jdk1.8 前 +接口是更加彻底的抽象,接口中只有抽象方法和常量,没有其他成分 ```java 修饰符 interface 接口名称{ @@ -1691,11 +1691,11 @@ abstract class Template{ } ``` -* 抽象方法:接口中的抽象方法默认会加上public abstract修饰,所以可以省略不写 +* 抽象方法:接口中的抽象方法默认会加上 public abstract 修饰,所以可以省略不写 * 静态方法:静态方法必须有方法体 -* 常量:常量是public static final修饰的成员变量,仅能被赋值一次,值不能改变。常量的名称规范上要求全部大写,多个单词下划线连接。public static final可以省略不写。 +* 常量:是 public static final 修饰的成员变量,仅能被赋值一次,值不能改变。常量的名称规范上要求全部大写,多个单词下划线连接,public static final 可以省略不写 ```java public interface InterfaceDemo{ @@ -1717,14 +1717,11 @@ abstract class Template{ #### 实现接口 -作用:**接口是用来被类实现的。** +**接口是用来被类实现的。** -类与类是继承关系:一个类只能直接继承一个父类,单继承 -类与接口是实现关系:一个类可以实现多个接口,多实现,接口不能继承类 -接口与接口继承关系:**多继承** - ->子类 继承 父类 ->实现类 实现 接口 +* 类与类是继承关系:一个类只能直接继承一个父类,单继承 +* 类与接口是实现关系:一个类可以实现多个接口,多实现,接口不能继承类 +* 接口与接口继承关系:**多继承** ```java 修饰符 class 实现类名称 implements 接口1,接口2,接口3,....{ @@ -1741,9 +1738,9 @@ abstract class Template{ 2. 当一个类实现多个接口时,多个接口中存在同名的默认方法,实现类必须重写这个方法 -3. 当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类**就近选择执行父类**的成员方法 +3. 当一个类既继承一个父类,又实现若干个接口时,父类中成员方法与接口中默认方法重名,子类**就近选择执行父类**的成员方法 -4. 接口中,没有构造器,**不能创建对象**,接口是更彻底的抽象,连构造器都没有,自然不能创建对象!! +4. 接口中,没有构造器,**不能创建对象**,接口是更彻底的抽象,连构造器都没有,自然不能创建对象 ```java public class InterfaceDemo { @@ -1783,10 +1780,12 @@ jdk1.8 以后新增的功能: * 默认方法(就是普通实例方法) * 必须用 default 修饰,默认会 public 修饰 * 必须用接口的实现类的对象来调用 + * 必须有默认实现 * 静态方法 * 默认会 public 修饰 * 接口的静态方法必须用接口的类名本身来调用 * 调用格式:ClassName.method() + * 必须有默认实现 * 私有方法:JDK 1.9 才开始有的,只能在**本类中**被其他的默认方法或者私有方法访问 ```java @@ -1846,7 +1845,7 @@ interface InterfaceJDK8{ | 实现 | 子类使用 **extends** 关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字 **implements** 来实现接口。它需要提供接口中所有声明的方法的实现 | | 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | | 与正常Java类的区别 | 除了不能实例化抽象类之外,和普通 Java 类没有任何区别 | 接口是完全不同的类型 | -| 访问修饰符 | 抽象方法有 **public**、**protected** 和 **default** 这些修饰符 | 接口方法默认修饰符是 **public**,别的修饰符需要有方法体 | +| 访问修饰符 | 抽象方法有 **public**、**protected** 和 **default** 这些修饰符 | 接口默认修饰符是 **public**,别的修饰符需要有方法体 | | main方法 | 抽象方法可以有 main 方法并且我们可以运行它 | jdk8 以前接口没有 main 方法,不能运行;jdk8 以后接口可以有 default 和 static 方法,可以运行 main 方法 | | 多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口可以继承一个或多个其它接口,接口不可继承类 | | 速度 | 比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法 | @@ -1928,21 +1927,22 @@ class Animal{ #### 上下转型 >基本数据类型的转换: -> 1.小范围类型的变量或者值可以直接赋值给大范围类型的变量。 -> 2.大范围类型的变量或者值必须强制类型转换给小范围类型的变量。 +> +>1. 小范围类型的变量或者值可以直接赋值给大范围类型的变量 +>2. 大范围类型的变量或者值必须强制类型转换给小范围类型的变量 -引用数据类型的**自动**类型转换语法:子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量。 +引用数据类型的**自动**类型转换语法:子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量 **父类引用指向子类对象** -- **向上转型(upcasting)**:通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换 -- **向下转型(downcasting)**:通过父类对象(大范围)实例化子类对象(小范围),这种属于强制转换 +- **向上转型 (upcasting)**:通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换 +- **向下转型 (downcasting)**:通过父类对象(大范围)实例化子类对象(小范围),这种属于强制转换 ```java public class PolymorphicDemo { public static void main(String[] args){ - Animal a = new Cat();//向上转型 - Cat c = (Cat)a;//向下转型 + Animal a = new Cat(); // 向上转型 + Cat c = (Cat)a; // 向下转型 } } class Animal{} @@ -2007,7 +2007,7 @@ class Animal{} #### 静态内部类 -定义:有static修饰,属于外部类本身,会加载一次 +定义:有 static 修饰,属于外部类本身,会加载一次 静态内部类中的成分研究: @@ -2017,7 +2017,7 @@ class Animal{} 静态内部类的访问格式:外部类名称.内部类名称 -静态内部类创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器; +静态内部类创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器 静态内部类的访问拓展: @@ -2050,22 +2050,20 @@ static class Outter{ #### 实例内部类 -定义:无static修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载。 +定义:无 static 修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载。 实例内部类的成分特点:实例内部类中不能定义静态成员,其他都可以定义 实例内部类的访问格式:外部类名称.内部类名称 -创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类构造器.new 内部构造器; +创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类构造器.new 内部构造器 -* `Outter.Inner in = new Outter().new Inner();` +* `Outter.Inner in = new Outter().new Inner()` -拓展:**实例内部类可以访问外部类的全部成员** +**实例内部类可以访问外部类的全部成员** -> * 实例内部类中是否可以直接访问外部类的静态成员? -> 可以,外部类的静态成员可以被共享访问! -> * 实例内部类中是否可以访问外部类的实例成员? -> 可以,实例内部类属于外部类对象,可以直接访问外部类对象的实例成员! +* 实例内部类中可以直接访问外部类的静态成员,外部类的静态成员可以被共享访问 +* 实例内部类中可以访问外部类的实例成员,实例内部类属于外部类对象,可以直接访问外部类对象的实例成员 @@ -2075,7 +2073,7 @@ static class Outter{ #### 局部内部类 -局部内部类:定义在方法中,在构造器中,代码块中,for循环中定义的内部类。 +局部内部类:定义在方法中,在构造器中,代码块中,for 循环中定义的内部类 局部内部类中的成分特点:只能定义实例成员,不能定义静态成员 @@ -2101,7 +2099,6 @@ public class InnerClass{ #### 匿名内部类 匿名内部类:没有名字的局部内部类 -作用:简化代码,是开发中常用的形式 匿名内部类的格式: @@ -2114,7 +2111,7 @@ new 类名|抽象类|接口(形参){ * 匿名内部类不能定义静态成员 * 匿名内部类一旦写出来,就会立即创建一个匿名内部类的对象返回 -* **匿名内部类的对象的类型相当于是当前new的那个的类型的子类类型** +* **匿名内部类的对象的类型相当于是当前 new 的那个的类型的子类类型** * 匿名内部类引用局部变量必须是**常量**,底层创建为内部类的成员变量(原因:JVM → 运行机制 → 代码优化) ```java @@ -2149,7 +2146,7 @@ abstract class Animal{ ### 权限符 权限修饰符:有四种**(private -> 缺省 -> protected - > public )** -可以修饰成员变量,修饰方法,修饰构造器,内部类,不同修饰符修饰的成员能够被访问的权限将受到限制! +可以修饰成员变量,修饰方法,修饰构造器,内部类,不同修饰符修饰的成员能够被访问的权限将受到限制 | 四种修饰符访问权限 | private | 缺省 | protected | public | | ------------------ | :-----: | :--: | :-------: | :----: | @@ -2184,12 +2181,12 @@ static { ``` * 静态代码块特点: - * 必须有static修饰 + * 必须有 static 修饰 * 会与类一起优先加载,且自动触发执行一次 * 只能访问静态资源 * 静态代码块作用: * 可以在执行类的方法等操作之前先在静态代码块中进行静态资源的初始化 - * **先执行静态代码块,在执行main函数里的操作** + * **先执行静态代码块,在执行 main 函数里的操作** ```java public class CodeDemo { @@ -2234,7 +2231,7 @@ main方法被执行 ``` * 实例代码块的特点: - * 无static修饰,属于对象 + * 无 static 修饰,属于对象 * 会与类的对象一起加载,每次创建类的对象的时候,实例代码块都会被加载且自动触发执行一次 * 实例代码块的代码在底层实际上是提取到每个构造器中去执行的 @@ -2305,7 +2302,7 @@ public boolean equals(Object o) { **面试题**:== 和 equals 的区别 * == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的**地址**是否相同,即是否是指相同一个对象,比较的是真正意义上的指针操作 -* Object 类中的方法,默认比较两个对象的引用,重写 equals 方法比较的是两个对象的**内容**是否相等,所有的类都是继承自 java.lang.Object 类,所以适用于所有对象 +* Object 类中的方法,**默认比较两个对象的引用**,重写 equals 方法比较的是两个对象的**内容**是否相等,所有的类都是继承自 java.lang.Object 类,所以适用于所有对象 hashCode 的作用: @@ -2331,7 +2328,7 @@ Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(), * 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 -Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括clone()),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常 +Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括 clone),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常 * Clone & Copy:`Student s = new Student` @@ -2343,7 +2340,7 @@ Cloneable 接口是一个标识性接口,即该接口不包含任何方法( 浅克隆:Object 中的 clone() 方法在对某个对象克隆时对其仅仅是简单地执行域对域的 copy - * 对基本数据类型和包装类的克隆是没有问题的。String、Integer 等包装类型在内存中是不可以被改变的对象,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 + * 对基本数据类型和包装类的克隆是没有问题的。String、Integer 等包装类型在内存中是**不可以被改变的对象**,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 * 如果对一个引用类型进行克隆时只是克隆了它的引用,和原始对象共享对象成员变量 ![](https://gitee.com/seazean/images/raw/master/Java/Object浅克隆.jpg) @@ -2376,12 +2373,12 @@ SDP → 创建型 → 原型模式 ### Objects -Objects 类与 Object 是继承关系。 +Objects 类与 Object 是继承关系 -Objects的方法: +Objects 的方法: -* `public static boolean equals(Object a, Object b)` : 比较两个对象是否相同。 - 底层进行非空判断,从而可以避免空指针异常,更安全!!推荐使用!! +* `public static boolean equals(Object a, Object b)`:比较两个对象是否相同。 + 底层进行非空判断,从而可以避免空指针异常,更安全,推荐使用! ```java public static boolean equals(Object a, Object b) { @@ -2389,11 +2386,11 @@ Objects的方法: } ``` -* `public static boolean isNull(Object obj)` : 判断变量是否为null ,为null返回true ,反之! +* `public static boolean isNull(Object obj)`:判断变量是否为 null ,为 null 返回 true -* `public static String toString(对象)` : 返回参数中对象的字符串表示形式 +* `public static String toString(对象)`:返回参数中对象的字符串表示形式 -* `public static String toString(对象, 默认字符串)` : 返回对象的字符串表示形式。 +* `public static String toString(对象, 默认字符串)`:返回对象的字符串表示形式 ```java public class ObjectsDemo { @@ -2464,7 +2461,7 @@ s = s + "cd"; //s = abccd 新对象 * `public char[] toCharArray()`:将字符串拆分为字符数组后返回 * `public boolean startsWith(String prefix)`:测试此字符串是否以指定的前缀开头 * `public int indexOf(String str)`:返回指定子字符串第一次出现的字符串内的索引,没有返回 -1 -* `public int lastIndexOf(String str)`:返回字符串最后一次出现str的索引,没有返回 -1 +* `public int lastIndexOf(String str)`:返回字符串最后一次出现 str 的索引,没有返回 -1 * `public String substring(int beginIndex)`:返回子字符串,以原字符串指定索引处到结尾 * `public String substring(int i, int j)`:指定索引处扩展到 j - 1 的位置,字符串长度为 j - i * `public String toLowerCase()`:将此 String 所有字符转换为小写,使用默认语言环境的规则 @@ -2493,7 +2490,7 @@ s.replace("-","");//12378 直接赋值:`String s = "abc"` 直接赋值的方式创建字符串对象,内容就是 abc - 通过构造方法创建:通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同,**返回堆内存中对象的引用** -- 直接赋值方式创建:以“ ”方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 +- 直接赋值方式创建:以 `" "` 方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 `String str = new String("abc")` 创建字符串对象: @@ -2574,7 +2571,7 @@ public class Demo { System.out.println(s3 == s5); // true String x2 = new String("c") + new String("d"); // new String("cd") - // 虽然 new,但是在字符串常量池没有 cd 对象,toString() 方法 + // 虽然 new,但是在字符串常量池没有 cd 对象,因为 toString() 方法 x2.intern(); String x1 = "cd"; @@ -2612,7 +2609,7 @@ public static void main(String[] args) { String s2 = s.intern(); //jdk6:串池中创建一个字符串"ab" - //jdk8:串池中没有创建字符串"ab",而是创建一个引用指向new String("ab"),将此引用返回 + //jdk8:串池中没有创建字符串"ab",而是创建一个引用指向 new String("ab"),将此引用返回 System.out.println(s2 == "ab");//jdk6:true jdk8:true System.out.println(s == "ab");//jdk6:false jdk8:true @@ -2641,7 +2638,7 @@ public static void main(String[] args) { } ``` -* Version类初始化时需要对静态常量字段初始化,被 launcher_name 静态常量字段所引用的"java"字符串字面量就被放入的字符串常量池: +* Version 类初始化时需要对静态常量字段初始化,被 launcher_name 静态常量字段所引用的 `"java"` 字符串字面量就被放入的字符串常量池: ```java package sun.misc; @@ -2664,15 +2661,15 @@ public static void main(String[] args) { ##### 内存位置 -Java 7之前,String Pool 被放在运行时常量池中,它属于永久代;Java 7以后,String Pool 被移到堆中,这是因为永久代的空间有限,在大量使用字符串的场景下会导致OutOfMemoryError 错误 +Java 7 之前,String Pool 被放在运行时常量池中,属于永久代;Java 7以后,String Pool 被移到堆中,这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误 演示 StringTable 位置: -* `-Xmx10m`设置堆内存10m +* `-Xmx10m` 设置堆内存 10m -* 在jdk8下设置: `-Xmx10m -XX:-UseGCOverheadLimit`(运行参数在Run Configurations VM options) +* 在 JDK8 下设置: `-Xmx10m -XX:-UseGCOverheadLimit`(运行参数在 Run Configurations VM options) -* 在jdk6下设置: `-XX:MaxPermSize=10m` +* 在 JDK6 下设置: `-XX:MaxPermSize=10m` ```java public static void main(String[] args) throws InterruptedException { @@ -2824,7 +2821,7 @@ public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { ### Arrays -Array 的工具类 +Array 的工具类 Arrays 常用API: @@ -3755,7 +3752,7 @@ public class RegexDemo { 压栈 == 入栈、弹栈 == 出栈 场景:手枪的弹夹 -* 数组:数组是内存中的连续存储区域,分成若干等分的小区域(每个区域大小是一样的)。元素存在索引 +* 数组:数组是内存中的连续存储区域,分成若干等分的小区域(每个区域大小是一样的)元素存在索引 特点:**查询元素快**(根据索引快速计算出元素的地址,然后立即去定位) **增删元素慢**(创建新数组,迁移元素) @@ -3765,12 +3762,11 @@ public class RegexDemo { * 树: * 二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) - 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差 - 为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 - - * 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 - - 特点:**红黑树的增删查改性能都好** + 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差,为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 + +* 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 + + 特点:**红黑树的增删查改性能都好** 各数据结构时间复杂度对比: @@ -3790,7 +3786,7 @@ public class RegexDemo { #### 概述 -Java 中集合的代表是Collection,Collection 集合是 Java 中集合的祖宗类 +Java 中集合的代表是 Collection,Collection 集合是 Java 中集合的祖宗类 Collection 集合底层为数组:`[value1, value2, ....]` @@ -3979,8 +3975,6 @@ public static void main(String[] args){ } ``` -![ArrayList源码分析](https://gitee.com/seazean/images/raw/master/Java/ArrayList添加元素源码解析.png) - *** @@ -3997,8 +3991,8 @@ public class ArrayList extends AbstractList ``` - `RandomAccess` 是一个标志接口,表明实现这个这个接口的 List 集合是支持**快速随机访问**的。在 `ArrayList` 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 -- `ArrayList` 实现了 `Cloneable` 接口 ,即覆盖了函数`clone()`,能被克隆 -- `ArrayList` 实现了 `Serializable `接口,这意味着`ArrayList`支持序列化,能通过序列化去传输 +- `ArrayList` 实现了 `Cloneable` 接口 ,即覆盖了函数 `clone()`,能被克隆 +- `ArrayList` 实现了 `Serializable ` 接口,这意味着 `ArrayList` 支持序列化,能通过序列化去传输 核心方法: @@ -4082,8 +4076,8 @@ public class ArrayList extends AbstractList MAX_ARRAY_SIZE:要分配的数组的最大大小,分配更大的**可能**会导致 - * OutOfMemoryError:Requested array size exceeds VM limit(请求的数组大小超出VM限制) - * OutOfMemoryError: Java heap space(堆区内存不足,可以通过设置JVM参数 -Xmx 来调节) + * OutOfMemoryError:Requested array size exceeds VM limit(请求的数组大小超出 VM 限制) + * OutOfMemoryError: Java heap space(堆区内存不足,可以通过设置 JVM 参数 -Xmx 来调节) * 删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,在旧数组上操作,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的, @@ -4121,7 +4115,7 @@ public class ArrayList extends AbstractList } ``` -* **Fail-Fast**:快速失败,modCount 用来记录 ArrayList **结构发生变化**的次数,结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 +* **Fail-Fast**:快速失败,modCount 用来记录 ArrayList **结构发生变化**的次数,结构发生变化是指添加或者删除至少一个元素的操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,改变了抛出 ConcurrentModificationException 异常 @@ -4186,7 +4180,7 @@ public class ArrayList extends AbstractList 2. Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍 -3. 底层都是 `Object[]`数组存储 +3. 底层都是 `Object[]` 数组存储 @@ -4214,7 +4208,7 @@ LinkedList 除了拥有 List 集合的全部功能还多了很多操作首尾元 * `public void push(E e)`:将元素推入此列表所表示的堆栈 * `public int indexOf(Object o)`:返回此列表中指定元素的第一次出现的索引,如果不包含返回 -1 * `public int lastIndexOf(Object o)`:从尾遍历找 -* ` public boolean remove(Object o)`:一次只删除一个匹配的对象,如果删除了匹配对象返回true +* ` public boolean remove(Object o)`:一次只删除一个匹配的对象,如果删除了匹配对象返回 true * `public E remove(int index)`:删除指定位置的元素 ```java @@ -4247,9 +4241,9 @@ public class ListDemo { } ``` -![](https://gitee.com/seazean/images/raw/master/Java/LinkedList添加元素源码解析.png) +*** @@ -4336,7 +4330,7 @@ Set 系列集合:添加的元素是无序,不重复,无索引的 * LinkedHashSet:添加的元素是有序,不重复,无索引的 * TreeSet:不重复,无索引,按照大小默认升序排序 -**面试问题**:没有索引,不能使用普通 for 循环遍历 +**注意**:没有索引,不能使用普通 for 循环遍历 @@ -4355,7 +4349,7 @@ Set 系列集合:添加的元素是无序,不重复,无索引的 - 哈希值的特点 - 同一个对象多次调用 hashCode() 方法返回的哈希值是相同的 - - 默认情况下,不同对象的哈希值是不同的。而重写 hashCode() 方法,可以实现让不同对象的哈希值相同 + - 默认情况下,不同对象的哈希值是不同的,而重写 hashCode() 方法,可以实现让不同对象的哈希值相同 **HashSet 底层就是基于 HashMap 实现,值是 PRESENT = new Object()** @@ -4382,16 +4376,17 @@ Set 集合添加的元素是无序,不重复的。 不重复 重复了 ``` -* Set系列集合元素无序的根本原因 +* Set 系列集合元素无序的根本原因 - Set系列集合添加元素无序的根本原因是因为**底层采用了哈希表存储元素**。 - JDK 1.8 之前:哈希表 = 数组(初始容量16) + 链表 + (哈希算法) - JDK 1.8 之后:哈希表 = 数组(初始容量16) + 链表 + 红黑树 + (哈希算法) - 当链表长度超过阈值8且当前数组的长度 > 64时,将链表转换为红黑树,减少了查找时间 - 当链表长度超过阈值8且当前数组的长度 < 64时,扩容 + Set 系列集合添加元素无序的根本原因是因为**底层采用了哈希表存储元素**。 + + * JDK 1.8 之前:哈希表 = 数组(初始容量16) + 链表 + (哈希算法) + * JDK 1.8 之后:哈希表 = 数组(初始容量16) + 链表 + 红黑树 + (哈希算法) + * 当链表长度超过阈值 8 且当前数组的长度 > 64时,将链表转换为红黑树,减少了查找时间 + * 当链表长度超过阈值 8 且当前数组的长度 < 64时,扩容 ![](https://gitee.com/seazean/images/raw/master/Java/HashSet底层结构哈希表.png) - + 每个元素的 hashcode() 的值进行响应的算法运算,计算出的值相同的存入一个数组块中,以链表的形式存储,如果链表长度超过8就采取红黑树存储,所以输出的元素是无序的。 * 如何设置只要对象内容一样,就希望集合认为它们重复了:**重写 hashCode 和 equals 方法** @@ -4426,9 +4421,9 @@ TreeSet 集合自排序的方式: * 直接为**对象的类**实现比较器规则接口 Comparable,重写比较方法: - 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` + 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` - * 比较者大于被比较者,返回正数(升序) + * 比较者大于被比较者,返回正数 * 比较者小于被比较者,返回负数 * 比较者等于被比较者,返回 0 @@ -4436,7 +4431,7 @@ TreeSet 集合自排序的方式: 方法:`public int compare(Employee o1, Employee o2): o1 比较者, o2 被比较者` - * 比较者大于被比较者,返回正数(升序) + * 比较者大于被比较者,返回正数 * 比较者小于被比较者,返回负数 * 比较者等于被比较者,返回 0 @@ -4475,7 +4470,7 @@ public class Student implements Comparable{ } ``` -比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边。(红黑树) +比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边(红黑树) @@ -4642,7 +4637,7 @@ Map集合的遍历方式有:3种。 1. “键找值”的方式遍历:先获取 Map 集合全部的键,再根据遍历键找值。 2. “键值对”的方式遍历:难度较大,采用增强 for 或者迭代器 -3. JDK 1.8 开始之后的新技术:foreach,采用 Lambda表 达式 +3. JDK 1.8 开始之后的新技术:foreach,采用 Lambda 表达式 集合可以直接输出内容,因为底层重写了 toString() 方法 @@ -4732,7 +4727,7 @@ JDK7 对比 JDK8: ##### 继承关系 -HashMap继承关系如下图所示: +HashMap 继承关系如下图所示: ![](https://gitee.com/seazean/images/raw/master/Java/HashMap继承关系.bmp) @@ -4740,9 +4735,9 @@ HashMap继承关系如下图所示: 说明: -* Cloneable 空接口,表示可以克隆, 创建并返回HashMap对象的一个副本。 -* Serializable 序列化接口,属于标记性接口,HashMap对象可以被序列化和反序列化。 -* AbstractMap 父类提供了Map实现接口,以最大限度地减少实现此接口所需的工作 +* Cloneable 空接口,表示可以克隆, 创建并返回 HashMap 对象的一个副本。 +* Serializable 序列化接口,属于标记性接口,HashMap 对象可以被序列化和反序列化。 +* AbstractMap 父类提供了 Map 实现接口,以最大限度地减少实现此接口所需的工作 @@ -4773,7 +4768,7 @@ HashMap继承关系如下图所示: * 为什么必须是 2 的 n 次幂? - HashMap 中添加元素时,需要根据 key 的 hash 值,确定在数组中的具体位置。为了存取高效,要尽量较少碰撞,把数据尽可能分配均匀,每个链表长度大致相同,实现该方法的算法就是取模,hash%length,计算机中直接求余效率不如位移运算,所以源码中使用 hash&(length-1),实际上**hash % length == hash & (length-1)的前提是 length 是 2 的n次幂** + HashMap 中添加元素时,需要根据 key 的 hash 值,确定在数组中的具体位置。为了存取高效,要尽量较少碰撞,把数据分配均匀,每个链表长度大致相同,实现该方法的算法就是取模,hash%length,计算机中直接求余效率不如位移运算,所以源码中使用 hash&(length-1),实际上**hash % length == hash & (length-1)的前提是 length 是 2 的n次幂** 散列平均分布:2 的 n 次方是 1 后面 n 个 0,2 的 n 次方 -1 是 n 个 1,可以**保证散列的均匀性**,减少碰撞 @@ -4833,7 +4828,7 @@ HashMap继承关系如下图所示: * 其他说法 红黑树的平均查找长度是 log(n),如果长度为 8,平均查找长度为 log(8)=3,链表的平均查找长度为 n/2,当长度为 8 时,平均查找长度为 8/2=4,这才有转换成树的必要;链表长度如果是小于等于 6,6/2=3,而 log(6)=2.6,虽然速度也很快的,但转化为树结构和生成树的时间并不短 -6. 当链表的值小 于 6 则会从红黑树转回链表 +6. 当链表的值小于 6 则会从红黑树转回链表 ```java // 当桶(bucket)上的结点数小于这个值时树转链表 @@ -4847,7 +4842,7 @@ HashMap继承关系如下图所示: static final int MIN_TREEIFY_CAPACITY = 64; ``` - 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于 64 时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于 8 并且数组长度大于 64 时,链表才转换为红黑树,效率也变的更高效 + 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于 64 时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于 8 并且数组长度大于等于 64 时,链表才转换为红黑树,效率也变的更高效 8. table 用来初始化(必须是二的 n 次幂) @@ -4858,7 +4853,7 @@ HashMap继承关系如下图所示: jdk8 之前数组类型是 Entry类型,之后是 Node 类型。只是换了个名字,都实现了一样的接口 Map.Entry,负责存储键值对数据的 - 9. HashMap 中**存放元素的个数**(**重点**) + 9. HashMap 中**存放元素的个数** ```java // 存放元素的个数,HashMap中K-V的实时数量,不是table数组的长度 @@ -4899,7 +4894,7 @@ HashMap继承关系如下图所示: loadFactor 太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 **0.75f 是官方给出的一个比较好的临界值** - * threshold 计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认 0.75)。这个值是当前已占用数组长度的最大值。**当 size>=threshold** 的时候,那么就要考虑对数组的 resize(扩容),这就是衡量数组是否需要扩增的一个标准, 扩容后的 HashMap 容量是之前容量的**两倍**. + * threshold 计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认 0.75)。这个值是当前已占用数组长度的最大值。**当 size>=threshold** 的时候,那么就要考虑对数组的 resize(扩容),这就是衡量数组是否需要扩增的一个标准, 扩容后的 HashMap 容量是之前容量的**两倍** @@ -5013,7 +5008,7 @@ HashMap继承关系如下图所示: 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,扰动运算 - 原因:当数组长度很小,假设是 16,那么 n-1 即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 + 原因:当数组长度很小,假设是 16,那么 n-1 即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后 4 位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 哈希冲突的处理方式: @@ -5320,7 +5315,7 @@ HashMap继承关系如下图所示: 3. 桶上的 key 不是要找的 key,则查看后续的节点: - * 如果后续节点是红黑树节点,通过调用红黑树的方法根据 key 获取v alue + * 如果后续节点是红黑树节点,通过调用红黑树的方法根据 key 获取 value * 如果后续节点是链表节点,则通过循环遍历链表根据 key 获取 value @@ -5443,7 +5438,7 @@ LinkedHashMap 是 HashMap 的子类 源码解析: -* 内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序 +* **内部维护了一个双向链表**,用来维护插入顺序或者 LRU 顺序 ```java transient LinkedHashMap.Entry head; @@ -5672,7 +5667,7 @@ TreeMap 集合指定大小规则有 2 种方式: WeakHashMap 是基于弱引用的 -内部的 Entry 继承 WeakReference,被弱引用关联的对象在下一次垃圾回收时会被回收,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 +内部的 Entry 继承 WeakReference,被弱引用关联的对象在**下一次垃圾回收时会被回收**,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 ```java private static class Entry extends WeakReference implements Map.Entry { @@ -5734,41 +5729,6 @@ Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能,Conc -*** - - - -#### 面试题 - -输出一个字符串中每个字符出现的次数。 - -```java -/* - (1)键盘录入一个字符串。aabbccddaa123。 - (2)定义一个Map集合,键是每个字符,值是其出现的次数。 {a=4 , b=2 ,...} - (3)遍历字符串中的每一个字符。 - (4)拿着这个字符去Map集合中看是否有这个字符键,有说明之前统计过,其值+1 - 没有这个字符键,说明该字符是第一次统计,直接存入“该字符=1” -*/ -public class MapDemo{ - public static void main(String[] args){ - String s = "aabbccddaa123"; - Map infos = new HashMap<>(); - for (int i = 0; i < s.length(); i++){ - char ch = datas.charAt(i); - if(infos.containsKey(ch)){ - infos.put(ch,infos.get(ch) + 1); - } else { - infos.put(ch,1); - } - } - System.out.println("结果:"+infos); - } -} -``` - - - *** @@ -14234,7 +14194,7 @@ public static void main(String[] args) { } ``` -注意:如果调用了 foo() 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 +注意:如果调用了 `foo()` 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 diff --git a/Prog.md b/Prog.md index c09c693..8d5634a 100644 --- a/Prog.md +++ b/Prog.md @@ -14077,7 +14077,7 @@ ServerSocket 类: -相当于客户端和服务器建立一个数据管道,管道一般不用 close +**相当于**客户端和服务器建立一个数据管道(虚连接,不是真正的物理连接),管道一般不用 close diff --git a/Web.md b/Web.md index 3236ec1..ea0ed9d 100644 --- a/Web.md +++ b/Web.md @@ -2119,8 +2119,10 @@ URL 和 URI * 进行 URL 解析,进行编码 * DNS 解析,顺序是先查 hosts 文件是否有记录,有的话就会把相对应映射的 IP 返回,然后去本地 DNS 缓存中寻找,然后去找计算机上配置的 DNS 服务器上有或者有缓存,最后去找全球的根 DNS 服务器,直到查到为止 -* 查找到 IP 之后,进行 TCP 协议的三次握手,发出 HTTP 请求 +* 查找到 IP 之后,进行 TCP 协议的三次握手建立连接 +* 发出 HTTP 请求,取文件指令 * 服务器处理请求,返回响应 +* 释放 TCP 连接 * 浏览器解析渲染页面 From c6e1467b4466acb9b6686b8008dc844443b8de01 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 20 Feb 2022 23:20:36 +0800 Subject: [PATCH 02/57] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ac1828..211ff15 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ * DB:MySQL、Redis * Frame:Maven、Netty、RocketMQ -* Java:JavaSE、JVM、Algorithm、Design Pattern +* Java:JavaSE、JVM、Algorithm * Prog:Concurrent、Network Programming * SSM:MyBatis、Spring、SpringMVC、SpringBoot * Tool:Git、Linux、Docker From 83f54324a1b289a542e899a5b0520710481e806e Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 21 Feb 2022 00:43:50 +0800 Subject: [PATCH 03/57] Update Java Note --- Java.md | 15131 ++++++++++++++++++++---------------------------------- 1 file changed, 5599 insertions(+), 9532 deletions(-) diff --git a/Java.md b/Java.md index be48f40..77f9179 100644 --- a/Java.md +++ b/Java.md @@ -2260,6 +2260,8 @@ public class CodeDemo { + + *** @@ -2872,6 +2874,48 @@ public class MyArraysDemo { +*** + + + +### System + +System 代表当前系统 + +静态方法: + +* `public static void exit(int status)`:终止 JVM 虚拟机,**非 0 是异常终止** + +* `public static long currentTimeMillis()`:获取当前系统此刻时间毫秒值 + +* `static void arraycopy(Object var0, int var1, Object var2, int var3, int var4)`:数组拷贝 + 参数一:原数组 + 参数二:从原数组的哪个位置开始赋值 + 参数三:目标数组 + 参数四:从目标数组的哪个位置开始赋值 + 参数五:赋值几个 + +```java +public class SystemDemo { + public static void main(String[] args) { + //System.exit(0); // 0代表正常终止!! + long startTime = System.currentTimeMillis();//定义sdf 按照格式输出 + for(int i = 0; i < 10000; i++){输出i} + long endTime = new Date().getTime(); + System.out.println( (endTime - startTime)/1000.0 +"s");//程序用时 + + int[] arr1 = new int[]{10 ,20 ,30 ,40 ,50 ,60 ,70}; + int[] arr2 = new int[6]; // [ 0 , 0 , 0 , 0 , 0 , 0] + // 变成arrs2 = [0 , 30 , 40 , 50 , 0 , 0 ] + System.arraycopy(arr1, 2, arr2, 1, 3); + } +} +``` + + + + + *** @@ -2889,8 +2933,8 @@ public class MyArraysDemo { 时间记录的两种方式: -1. Date日期对象 -2. 时间毫秒值:从1970-01-01 00:00:00开始走到此刻的总的毫秒值。 1s = 1000ms +1. Date 日期对象 +2. 时间毫秒值:从 `1970-01-01 00:00:00` 开始走到此刻的总的毫秒值,1s = 1000ms ```java public class DateDemo { @@ -2934,10 +2978,10 @@ DateFormat 是一个抽象类,不能直接使用,使用它的子类:Simple SimpleDateFormat 简单日期格式化类: -* `public SimpleDateFormat(String pattern)` : 指定时间的格式创建简单日期对象 -* `public String format(Date date) ` : 把日期对象格式化成我们喜欢的时间形式,返回字符串 -* `public String format(Object time)` : 把时间毫秒值格式化成设定的时间形式,返回字符串! -* `public Date parse(String date)` : 把字符串的时间解析成日期对象 +* `public SimpleDateFormat(String pattern)`:指定时间的格式创建简单日期对象 +* `public String format(Date date) `:把日期对象格式化成我们喜欢的时间形式,返回字符串 +* `public String format(Object time)`:把时间毫秒值格式化成设定的时间形式,返回字符串! +* `public Date parse(String date)`:把字符串的时间解析成日期对象 >yyyy年MM月dd日 HH:mm:ss EEE a" 周几 上午下午 @@ -2972,12 +3016,12 @@ Calendar 日历类创建日历对象:`Calendar rightNow = Calendar.getInstance Calendar 的方法: -* `public static Calendar getInstance()`: 返回一个日历类的对象 +* `public static Calendar getInstance()`:返回一个日历类的对象 * `public int get(int field)`:取日期中的某个字段信息 * `public void set(int field,int value)`:修改日历的某个字段信息 * `public void add(int field,int amount)`:为某个字段增加/减少指定的值 -* `public final Date getTime()`: 拿到此刻日期对象 -* `public long getTimeInMillis()`: 拿到此刻时间毫秒值 +* `public final Date getTime()`:拿到此刻日期对象 +* `public long getTimeInMillis()`:拿到此刻时间毫秒值 ```java public static void main(String[] args){ @@ -3048,23 +3092,10 @@ public class JDK8DateDemo2 { } ``` -| 方法名 | 说明 | -| --------------------------------------------------- | ------------------------------ | -| public LocalDateTime plusYears (long years) | 添加或者减去年 | -| public LocalDateTime plusMonths(long months) | 添加或者减去月 | -| public LocalDateTime plusDays(long days) | 添加或者减去日 | -| public LocalDateTime plusHours(long hours) | 添加或者减去时 | -| public LocalDateTime plusMinutes(long minutes) | 添加或者减去分 | -| public LocalDateTime plusSeconds(long seconds) | 添加或者减去秒 | -| public LocalDateTime plusWeeks(long weeks) | 添加或者减去周 | -| public LocalDateTime minusYears (long years) | 减去或者添加年 | -| public LocalDateTime withYear(int year) | 直接修改年 | -| public LocalDateTime withMonth(int month) | 直接修改月 | -| public LocalDateTime withDayOfMonth(int dayofmonth) | 直接修改日期(一个月中的第几天) | -| public LocalDateTime withDayOfYear(int dayOfYear) | 直接修改日期(一年中的第几天) | -| public LocalDateTime withHour(int hour) | 直接修改小时 | -| public LocalDateTime withMinute(int minute) | 直接修改分钟 | -| public LocalDateTime withSecond(int second) | 直接修改秒 | +| 方法名 | 说明 | +| ------------------------------------------- | -------------- | +| public LocalDateTime plusYears (long years) | 添加或者减去年 | +| public LocalDateTime withYear(int year) | 直接修改年 | @@ -3142,6 +3173,10 @@ public class MathDemo { +**** + + + ### DecimalFormat 使任何形式的数字解析和格式化 @@ -3175,42 +3210,6 @@ public static void main(String[]args){ -*** - - - -### System - -System代表当前系统。 - -静态方法: - -1. `public static void exit(int status)` : 终止JVM虚拟机,非0是异常终止 -2. `public static long currentTimeMillis()` : 获取当前系统此刻时间毫秒值 -3. `static void arraycopy(Object var0, int var1, Object var2, int var3, int var4)` : 数组拷贝 - 参数一:原数组 - 参数二:从原数组的哪个位置开始赋值。 - 参数三:目标数组 - 参数四:从目标数组的哪个位置开始赋值 - 参数五:赋值几个。 - -```java -public class SystemDemo { - public static void main(String[] args) { - //System.exit(0); // 0代表正常终止!! - long startTime = System.currentTimeMillis();//定义sdf 按照格式输出 - for(int i = 0; i < 10000; i++){输出i} - long endTime = new Date().getTime(); - System.out.println( (endTime - startTime)/1000.0 +"s");//程序用时 - - int[] arr1 = new int[]{10 ,20 ,30 ,40 ,50 ,60 ,70}; - int[] arr2 = new int[6]; // [ 0 , 0 , 0 , 0 , 0 , 0] - // 变成arrs2 = [0 , 30 , 40 , 50 , 0 , 0 ] - System.arraycopy(arr1, 2, arr2, 1, 3); - } -} -``` - *** @@ -3233,8 +3232,8 @@ Java 在 java.math 包中提供的 API 类,用来对超过16位有效位的数 * `public BigDecimal subtract(BigDecimal value)`:减法运算 * `public BigDecimal multiply(BigDecimal value)`:乘法运算 * `public BigDecimal divide(BigDecimal value)`:除法运算 -* `public double doubleValue()`:把BigDecimal转换成double类型。 -* `public int intValue()`:转为int 其他类型相同 +* `public double doubleValue()`:把 BigDecimal 转换成 double 类型 +* `public int intValue()`:转为 int 其他类型相同 * `public BigDecimal divide (BigDecimal value,精确几位,舍入模式)`:除法 ```java @@ -3261,20 +3260,20 @@ public class BigDecimalDemo { } ``` -总结 +总结: 1. BigDecimal 是用来进行精确计算的 -2. 创建 BigDecimal 的对象,构造方法使用参数类型为字符串的。 -3. 四则运算中的除法,如果除不尽请使用 divide 的三个参数的方法。 +2. 创建 BigDecimal 的对象,构造方法使用参数类型为字符串的 +3. 四则运算中的除法,如果除不尽请使用 divide 的三个参数的方法 ```java BigDecimal divide = bd1.divide(参与运算的对象,小数点后精确到多少位,舍入模式); -参数1:表示参与运算的BigDecimal 对象。 -参数2:表示小数点后面精确到多少位 -参数3:舍入模式 - BigDecimal.ROUND_UP 进一法 - BigDecimal.ROUND_FLOOR 去尾法 - BigDecimal.ROUND_HALF_UP 四舍五入 +//参数1:表示参与运算的BigDecimal 对象。 +//参数2:表示小数点后面精确到多少位 +//参数3:舍入模式 +// BigDecimal.ROUND_UP 进一法 +// BigDecimal.ROUND_FLOOR 去尾法 +// BigDecimal.ROUND_HALF_UP 四舍五入 ``` @@ -3718,6 +3717,8 @@ public class RegexDemo { + + ## 集合 ### 集合概述 @@ -3763,7 +3764,7 @@ public class RegexDemo { * 二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差,为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 - + * 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 特点:**红黑树的增删查改性能都好** @@ -5748,15 +5749,13 @@ Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能,Conc * **泛型和集合都只能支持引用数据类型,不支持基本数据类型。** ```java -{ - ArrayList lists = new ArrayList<>(); - lists.add(99.9); - lists.add('a'); - lists.add("Java"); - ArrayList list = new ArrayList<>(); - lists1.add(10); - lists1.add(20); -} +ArrayList lists = new ArrayList<>(); +lists.add(99.9); +lists.add('a'); +lists.add("Java"); +ArrayList list = new ArrayList<>(); +lists1.add(10); +lists1.add(20); ``` 优点:泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常,体现的是 Java 的严谨性和规范性 @@ -5913,73 +5912,13 @@ class Dog{} -### 不可变 - -在 List、Set、Map 接口中都存在 of 方法,可以创建一个不可变的集合 -+ 这个集合不能添加,不能删除,不能修改 -+ 但是可以结合集合的带参构造,实现集合的批量添加 - -在 Map 接口中,还有一个 ofEntries 方法可以提高代码的阅读性 -+ 首先会把键值对封装成一个 Entry 对象,再把这个 Entry 对象添加到集合当中 - -````java -public class MyVariableParameter4 { - public static void main(String[] args) { - // static List of(E…elements) 创建一个具有指定元素的List集合对象 - //static Set of(E…elements) 创建一个具有指定元素的Set集合对象 - //static Map of(E…elements) 创建一个具有指定元素的Map集合对象 - - //method1(); - //method2(); - //method3(); - //method4(); - - } - - private static void method4() { - Map map = Map.ofEntries( - Map.entry("zhangsan", "江苏"), - Map.entry("lisi", "北京")); - System.out.println(map); - } - - private static void method3() { - Map map = Map.of("zhangsan", "江苏", "lisi", "北京"); - System.out.println(map); - } - - private static void method2() { - //传递的参数当中,不能存在重复的元素。 - Set set = Set.of("a", "b", "c", "d","a"); - System.out.println(set); - } - - private static void method1() { - List list = List.of("a", "b", "c", "d"); - System.out.println(list); - - //集合的批量添加。 - //首先是通过调用List.of方法来创建一个不可变的集合,of方法的形参就是一个可变参数。 - //再创建一个ArrayList集合,并把这个不可变的集合中所有的数据,都添加到ArrayList中。 - ArrayList list3 = new ArrayList<>(List.of("a", "b", "c", "d")); - System.out.println(list3); - } -} -```` - - - - - -*** - ## 异常 ### 基本介绍 -异常:程序在"编译"或者"执行"的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表。 +异常:程序在编译或者执行的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表。 错误:Error ,程序员无法处理的错误,只能重启系统,比如内存奔溃,JVM 本身的奔溃 @@ -5998,7 +5937,7 @@ Java 中异常继承的根类是:Throwable Exception 异常的分类: * 编译时异常:继承自 Exception 的异常或者其子类,编译阶段就会报错 -* 运行时异常: 继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,在运行时阶段可能出现,编译阶段是不会出错的,但是运行阶段可能出现,建议提前处理 +* 运行时异常:继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,在运行阶段出错 @@ -6011,9 +5950,9 @@ Exception 异常的分类: 异常的产生默认的处理过程解析:(自动处理的过程) 1. 默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException(算术异常) -2. 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机 +2. 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给 JVM 虚拟机 3. 虚拟机接收到异常对象后,先在控制台直接输出**异常栈**信息数据 -4. 直接从当前执行的异常点干掉当前程序 +4. 直接从当前执行的异常点终止当前程序 5. 后续代码没有机会执行了,因为程序已经死亡 ```java @@ -6040,12 +5979,9 @@ public class ExceptionDemo { #### 基本介绍 -编译时异常:继承自Exception的异常或者其子类,没有继承 RuntimeException,编译时异常是编译阶段就会报错,必须程序员编译阶段就处理的。否则代码编译就报错 - -编译时异常的作用是什么: +编译时异常:继承自 Exception 的异常或者其子类,没有继承 RuntimeException,编译时异常是编译阶段就会报错 -* 是担心程序员的技术不行,在编译阶段就爆出一个错误, 目的在于提醒 -* 提醒程序员这里很可能出错,请检查并注意不要出bug +编译时异常的作用是什么:在编译阶段就爆出一个错误,目的在于提醒,请检查并注意不要出 BUG ```java public static void main(String[] args) throws ParseException { @@ -6066,12 +6002,9 @@ public static void main(String[] args) throws ParseException { ##### throws -在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给JVM虚拟机,JVM 虚拟机输出异常信息,直接干掉程序,这种方式与默认方式是一样的。 +在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给 JVM 虚拟机,JVM 虚拟机输出异常信息,直接终止掉程序,这种方式与默认方式是一样的 -* 优点:可以解决代码编译时的错误 -* 运行时出现异常,程序还是会立即死亡! - -**Exception是异常最高类型可以抛出一切异常!** +**Exception是异常最高类型可以抛出一切异常** ```java public static void main(String[] args) throws Exception { @@ -6091,7 +6024,7 @@ public static void main(String[] args) throws Exception { ##### try/catch -可以处理异常,并且出现异常后代码也不会死亡。 +可以处理异常,并且出现异常后代码也不会死亡 * 自己捕获异常和处理异常的格式:**捕获处理** @@ -6107,9 +6040,8 @@ public static void main(String[] args) throws Exception { } ``` -* 监视捕获处理异常企业级写法: - Exception可以捕获处理一切异常类型! - +* 监视捕获处理异常写法:Exception 可以捕获处理一切异常类型 + ```java try{ // 可能出现异常的代码! @@ -6119,9 +6051,10 @@ public static void main(String[] args) throws Exception { ``` **Throwable成员方法:** - `public String getMessage()` : 返回此 throwable 的详细消息字符串 - `public String toString()` : 返回此可抛出的简短描述 - `public void printStackTrace()` : 把异常的错误信息输出在控制台 + +* `public String getMessage()`:返回此 throwable 的详细消息字符串 +* `public String toString()`:返回此可抛出的简短描述 +* `public void printStackTrace()`:把异常的错误信息输出在控制台 ```java public static void main(String[] args) { @@ -6146,8 +6079,7 @@ public static void main(String[] args) { ##### 规范做法 -在出现异常的地方把异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理!(**规范做法**) -这种方案最外层调用者可以知道底层执行的情况,同时程序在出现异常后也不会立即死亡(最好的方案) +在出现异常的地方把异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理 ```java public class ExceptionDemo{ @@ -6174,16 +6106,16 @@ public class ExceptionDemo{ #### 基本介绍 -继承自RuntimeException的异常或者其子类,编译阶段是不会出错的,它是在运行时阶段可能出现的错误,运行时异常编译阶段可以处理也可以不处理,代码编译都能通过!! +继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,是在运行时阶段可能出现的错误,运行时异常编译阶段可以处理也可以不处理,代码编译都能通过 **常见的运行时异常**: -1. 数组索引越界异常: ArrayIndexOutOfBoundsException -2. 空指针异常 : NullPointerException,直接输出没问题,调用空指针的变量的功能就会报错! +1. 数组索引越界异常:ArrayIndexOutOfBoundsException +2. 空指针异常:NullPointerException,直接输出没问题,调用空指针的变量的功能就会报错 3. 类型转换异常:ClassCastException 4. 迭代器遍历没有此元素异常:NoSuchElementException 5. 算术异常(数学操作异常):ArithmeticException -6. 数字转换异常: NumberFormatException +6. 数字转换异常:NumberFormatException ```java public class ExceptionDemo { @@ -6267,11 +6199,9 @@ catch:0-N次 (如果有finally那么catch可以没有!!) finally: 0-1次 ``` +**finally 的作用**:可以在代码执行完毕以后进行资源的释放操作 - -**finally的作用**:可以在代码执行完毕以后进行资源的释放操作 - -资源:资源都是实现了 Closeable 接口的,都自带 close() 关闭方法! +资源:资源都是实现了 Closeable 接口的,都自带 close() 关闭方法 注意:如果在 finally 中出现了 return,会吞掉异常 @@ -6322,10 +6252,10 @@ public class FinallyDemo { 自定义异常: -* 自定义编译时异常:定义一个异常类继承 Exception,重写构造器,在出现异常的地方用throw new 自定义对象抛出 -* 自定义运行时异常:定义一个异常类继承 RuntimeException,重写构造器,在出现异常的地方用 throw new 自定义对象抛出! +* 自定义编译时异常:定义一个异常类继承 Exception,重写构造器,在出现异常的地方用 throw new 自定义对象抛出 +* 自定义运行时异常:定义一个异常类继承 RuntimeException,重写构造器,在出现异常的地方用 throw new 自定义对象抛出 -**throws: 用在方法上,用于抛出方法中的异常** +**throws:用在方法上,用于抛出方法中的异常** **throw: 用在出现异常的地方,创建异常对象且立即从此处抛出** @@ -6412,17 +6342,19 @@ public class Demo{ + + ## λ ### lambda #### 基本介绍 -Lambda表达式是JDK1.8开始之后的新技术,是一种代码的新语法,一种特殊写法 +Lambda 表达式是 JDK1.8 开始之后的新技术,是一种代码的新语法,一种特殊写法 作用:为了简化匿名内部类的代码写法 -Lambda表达式的格式: +Lambda 表达式的格式: ```java (匿名内部类被重写方法的形参列表) -> { @@ -6430,11 +6362,11 @@ Lambda表达式的格式: } ``` -Lambda表达式并不能简化所有匿名内部类的写法,只能简化**函数式接口的匿名内部类** +Lambda 表达式并不能简化所有匿名内部类的写法,只能简化**函数式接口的匿名内部类** 简化条件:首先必须是接口,接口中只能有一个抽象方法 -@FunctionalInterface函数式接口注解:一旦某个接口加上了这个注解,这个接口只能有且仅有一个抽象方法 +@FunctionalInterface 函数式接口注解:一旦某个接口加上了这个注解,这个接口只能有且仅有一个抽象方法 @@ -6444,9 +6376,9 @@ Lambda表达式并不能简化所有匿名内部类的写法,只能简化**函 #### 简化方法 -Lambda表达式的省略写法(进一步在Lambda表达式的基础上继续简化) +Lambda 表达式的省略写法(进一步在 Lambda 表达式的基础上继续简化) -* 如果Lambda表达式的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号;如果这行代码是return语句,必须省略return不写 +* 如果 Lambda 表达式的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号;如果这行代码是 return 语句,必须省略 return 不写 * 参数类型可以省略不写 * 如果只有一个参数,参数类型可以省略,同时()也可以省略 @@ -6486,35 +6418,7 @@ names.forEach(s -> System.out.println(s) ); #### 常用简化 -##### Runnable - -```java -//1. -Thread t = new Thread(new Runnable() { - @Override - public void run() { - System.out.println(Thread.currentThread().getName()+":执行~~~"); - } -}); -t.start(); - -//2. -Thread t1 = new Thread(() -> { - System.out.println(Thread.currentThread().getName()+":执行~~~"); -}); -t1.start(); -//3. -new Thread(() -> { - System.out.println(Thread.currentThread().getName()+":执行~~~"); -}).start(); - -//4.一行代码 -new Thread(() -> System.out.println(Thread.currentThread().getName()+":执行~~~")).start(); -``` - - - -##### Comparator +Comparator ```java public class CollectionsDemo { @@ -6550,7 +6454,7 @@ public class CollectionsDemo { #### 基本介绍 -方法引用:方法引用是为了进一步简化Lambda表达式的写法 +方法引用:方法引用是为了进一步简化 Lambda 表达式的写法 方法引用的格式:类型或者对象::引用的方法 @@ -6708,6 +6612,8 @@ public class ConstructorDemo { + + ## I/O ### Stream @@ -6895,9 +6801,9 @@ public static void main(String[] args) { File 类:代表操作系统的文件对象,是用来操作操作系统的文件对象的,删除文件,获取文件信息,创建文件(文件夹),广义来说操作系统认为文件包含(文件和文件夹) File 类构造器: - `public File(String pathname)`:根据路径获取文件对象 - `public File(String parent , String child)`:根据父路径和文件名称获取文件对象! - `public File(File parent , String child)` + +* `public File(String pathname)`:根据路径获取文件对象 +* `public File(String parent , String child)`:根据父路径和文件名称获取文件对象 File 类创建文件对象的格式: @@ -6945,17 +6851,19 @@ public class FileDemo{ ##### 常用方法 -`public String getAbsolutePath()` : 返回此File的绝对路径名字符串。 -`public String getPath()` : 获取创建文件对象的时候用的路径 -`public String getName()` : 返回由此File表示的文件或目录的名称。 -`public long length()` : 返回由此File表示的文件的长度(大小)。 -`public long length(FileFilter filter)` : 文件过滤器。 +| 方法 | 说明 | +| ------------------------------ | -------------------------------------- | +| String getAbsolutePath() | 返回此 File 的绝对路径名字符串 | +| String getPath() | 获取创建文件对象的时候用的路径 | +| String getName() | 返回由此 File 表示的文件或目录的名称 | +| long length() | 返回由此 File 表示的文件的长度(大小) | +| long length(FileFilter filter) | 文件过滤器 | ```java public class FileDemo { public static void main(String[] args) { // 1.绝对路径创建一个文件对象 - File f1 = new File("E:/图片/meinv.jpg"); + File f1 = new File("E:/图片/test.jpg"); // a.获取它的绝对路径。 System.out.println(f1.getAbsolutePath()); // b.获取文件定义的时候使用的路径。 @@ -6967,7 +6875,7 @@ public class FileDemo { System.out.println("------------------------"); // 2.相对路径 - File f2 = new File("Day09Demo/src/dlei01.txt"); + File f2 = new File("Demo/src/test.txt"); // a.获取它的绝对路径。 System.out.println(f2.getAbsolutePath()); // b.获取文件定义的时候使用的路径。 @@ -6983,14 +6891,20 @@ public class FileDemo { +*** + + + ##### 判断方法 -`public boolean exists()` : 此File表示的文件或目录是否实际存在。 -`public boolean isDirectory()` : 此File表示的是否为目录。 -`public boolean isFile()` : 此File表示的是否为文件 +方法列表: + +* `boolean exists()`:此 File 表示的文件或目录是否实际存在 +* `boolean isDirectory()`:此 File 表示的是否为目录 +* `boolean isFile()`:此 File 表示的是否为文件 ```java -File f = new File("Day09Demo/src/dlei01.txt"); +File f = new File("Demo/src/test.txt"); // a.判断文件路径是否存在 System.out.println(f.exists()); // true // b.判断文件对象是否是文件,是文件返回true ,反之 @@ -7001,17 +6915,23 @@ System.out.println(f.isDirectory()); // false +**** + + + ##### 创建删除 -`public boolean createNewFile()` : 当且仅当具有该名称的文件尚不存在时, 创建一个新的空文件。 -`public boolean delete()` : 删除由此File表示的文件或目录。 (只能删除空目录) -`public boolean mkdir()` : 创建由此File表示的目录。(只能创建一级目录) -`public boolean mkdirs()` : 可以创建多级目录(建议使用的) +方法列表: + +* `boolean createNewFile()`:当且仅当具有该名称的文件尚不存在时, 创建一个新的空文件 +* `boolean delete()`:删除由此 File 表示的文件或目录(只能删除空目录) +* `boolean mkdir()`:创建由此 File 表示的目录(只能创建一级目录) +* `boolean mkdirs()`:可以创建多级目录(建议使用) ```java public class FileDemo { public static void main(String[] args) throws IOException { - File f = new File("Day09Demo/src/dlei02.txt"); + File f = new File("Demo/src/test.txt"); // a.创建新文件,创建成功返回true ,反之 System.out.println(f.createNewFile()); @@ -7041,7 +6961,7 @@ public class FileDemo { #### 遍历目录 - `public String[] list()`:获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。 -- `public File[] listFiles()(常用)`:获取当前目录下所有的"一级文件对象"到一个**文件对象数组**中去返回(**重点**) +- `public File[] listFiles()`:获取当前目录下所有的"一级文件对象"到一个**文件对象数组**中去返回(**重点**) - `public long lastModified`:返回此抽象路径名表示的文件上次修改的时间。 ```java @@ -7060,7 +6980,7 @@ public class FileDemo { } // c - File f1 = new File("D:\\it\\图片资源\\beautiful.jpg"); + File f1 = new File("D:\\图片资源\\beautiful.jpg"); long time = f1.lastModified(); // 最后修改时间! SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(sdf.format(time)); @@ -7168,30 +7088,23 @@ IO 输入输出流:输入/输出流 * Input:输入 * Output:输出 -引入:File类只能操作文件对象本身,不能读写文件对象的内容,读写数据内容,应该使用IO流 +引入:File 类只能操作文件对象本身,不能读写文件对象的内容,读写数据内容,应该使用 IO 流 IO 流是一个水流模型:IO 理解成水管,把数据理解成水流 IO 流的分类: * 按照流的方向分为:输入流,输出流。 - * 输出流:以内存为基准,把内存中的数据写出到磁盘文件或者网络介质中去的流称为输出流 - 输出流的作用:写数据到文件,或者写数据发送给别人 - * 输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据读入到内存中的流称为输入流 - 输入流的作用:读取数据到内存 + * 输出流:以内存为基准,把内存中的数据**写出到磁盘文件**或者网络介质中去的流称为输出流 + * 输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据**读入到内存**中的流称为输入流 * 按照流的内容分为:字节流,字符流 * 字节流:流中的数据的最小单位是一个一个的字节,这个流就是字节流 * 字符流:流中的数据的最小单位是一个一个的字符,这个流就是字符流(**针对于文本内容**) -流大体分为四大类: - -* 字节输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字节的形式读入到内存中去的流称为字节输入流 -* 字节输出流:以内存为基准,把内存中的数据以一个一个的字节写出到磁盘文件或者网络介质中去的流称为字节输出流 -* 字符输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字符的形式读入到内存中去的流称为字符输入流 -* 字符输出流:以内存为基准,把内存中的数据以一个一个的字符写出到磁盘文件或者网络介质中去的流称为字符输出流 +流大体分为四大类:字节输入流、字节输出流、字符输入流、字符输出流 ```java -IO流的体系: +IO 流的体系: 字节流 字符流 字节输入流 字节输出流 字符输入流 字符输出流 InputStream OutputStream Reader Writer (抽象类) @@ -7211,25 +7124,25 @@ ObjectInputStream ObjectOutputStream ##### 字节输入 -FileInputStream 文件字节输入流: +FileInputStream 文件字节输入流:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流 + +构造方法: -* 作用:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流 +* `public FileInputStream(File path)`:创建一个字节输入流管道与源文件对象接通 +* `public FileInputStream(String pathName)`:创建一个字节输入流管道与文件路径对接,底层实质上创建 File 对象 -* 构造器: - `public FileInputStream(File path)` : 创建一个字节输入流管道与源文件对象接通 - `public FileInputStream(String pathName)` : 创建一个字节输入流管道与文件路径对接,底层实质上创建了File对象 - -* 方法: - `public int read()` : 每次读取一个字节返回,读取完毕会返回-1 - `public int read(byte[] buffer)` : 从字节输入流中读取字节到字节数组中去,返回读取的字节数量,没有字节可读返回-1,**byte中新读取的数据默认是覆盖原数据**,构造String需要设定长度 - `public String(byte[] bytes,int offset,int length)` : 构造新的String - `public long transferTo(OutputStream out) ` : 从输入流中读取所有字节,并按读取的顺序,将字节写入给定的输出流`is.transferTo(os)` +方法: + +* `public int read()`:每次读取一个字节返回,读取完毕会返回-1 +* `public int read(byte[] buffer)`:从字节输入流中读取字节到字节数组中去,返回读取的字节数量,没有字节可读返回 -1,**byte 中新读取的数据默认是覆盖原数据**,构造 String 需要设定长度 +* `public String(byte[] bytes,int offset,int length)`:构造新的 String +* `public long transferTo(OutputStream out) `:从输入流中读取所有字节,并按读取的顺序,将字节写入给定的输出流 ```java public class FileInputStreamDemo01 { public static void main(String[] args) throws Exception { // 1.创建文件对象定位dlei01.txt - File file = new File("Day09Demo/src/dlei01.txt"); + File file = new File("Demo/src/dlei01.txt"); // 2.创建一个字节输入流管道与源文件接通 InputStream is = new FileInputStream(file); // 3.读取一个字节的编号返回,读取完毕返回-1 @@ -7246,14 +7159,14 @@ public class FileInputStreamDemo01 { } ``` -一个一个字节读取英文和数字没有问题。但是一旦读取中文输出无法避免乱码,因为会截断中文的字节。一个一个字节的读取数据,性能也较差,所以**禁止使用上面的方案** +一个一个字节读取英文和数字没有问题,但是读取中文输出无法避免乱码,因为会截断中文的字节。一个一个字节的读取数据,性能也较差,所以**禁止使用上面的方案** 采取下面的方案: ```java public static void main(String[] args) throws Exception { //简化写法,底层实质上创建了File对象 - InputStream is = new FileInputStream("Day09Demo/src/dlei01.txt"); + InputStream is = new FileInputStream("Demo/src/test.txt"); byte[] buffer = new byte[3];//开发中使用byte[1024] int len; while((len = is.read(buffer)) !=-1){ @@ -7265,17 +7178,9 @@ public static void main(String[] args) throws Exception { ``` ```java -//定义一个字节数组与文件的大小刚刚一样大,然后一桶水读取全部字节数据再输出! -//可以避免中文读取输出乱码,但是如果读取的文件过大,会出现内存溢出!! -//字节流并不适合读取文本文件内容输出,读写文件内容建议使用字符流。 -/* - byte[] buffer = new byte[(int) f.length()]; - int len = is.read(buffer); - String rs = new String(buffer); -*/ - -File f = new File("Day09Demo/src/dlei03.txt"); +File f = new File("Demo/src/test.txt"); InputStream is = new FileInputStream(f); +// 读取全部的 byte[] buffer = is.readAllBytes(); String rs = new String(buffer); System.out.println(rs); @@ -7283,35 +7188,39 @@ System.out.println(rs); +**** + + + ##### 字节输出 -FileOutputStream 文件字节输出流: +FileOutputStream 文件字节输出流:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去 + +构造方法: + +* `public FileOutputStream(File file)`:创建一个字节输出流管道通向目标文件对象 +* `public FileOutputStream(String file) `:创建一个字节输出流管道通向目标文件路径 +* `public FileOutputStream(File file, boolean append)` : 创建一个追加数据的字节输出流管道到目标文件对象 +* `public FileOutputStream(String file, boolean append)` : 创建一个追加数据的字节输出流管道通向目标文件路径 -* 作用:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去 +API: -* 构造器: - `public FileOutputStream(File file)` : 创建一个字节输出流管道通向目标文件对象 - `public FileOutputStream(String file) ` : 创建一个字节输出流管道通向目标文件路径 - `public FileOutputStream(File file , boolean append)` : 追加数据的字节输出流管道到目标文件对象 - `public FileOutputStream(String file , boolean append)` : 创建一个追加数据的字节输出流管道通向目标文件路径 -* API: - `public void write(int a)` : 写一个字节出去 - `public void write(byte[] buffer)` :写一个字节数组出去 - `public void write(byte[] buffer , int pos , int len)` : 写一个字节数组的一部分出去 - 参数一,字节数组;参数二:起始字节索引位置,参数三:写多少个字节数出去。 +* `public void write(int a)`:写一个字节出去 +* `public void write(byte[] buffer)`:写一个字节数组出去 +* `public void write(byte[] buffer , int pos , int len)`:写一个字节数组的一部分出去,从 pos 位置,写出 len 长度 -* FileOutputStream字节输出流每次启动写数据的时候都会先清空之前的全部数据,重新写入: - `OutputStream os = new FileOutputStream("Day09Demo/out05")` : 覆盖数据管道 - `OutputStream os = new FileOutputStream("Day09Demo/out05" , true)` : 追加数据的管道 +* FileOutputStream 字节输出流每次启动写数据的时候都会先清空之前的全部数据,重新写入: + * `OutputStream os = new FileOutputStream("Demo/out05")`:覆盖数据管道 + * `OutputStream os = new FileOutputStream("Demo/out05" , true)`:追加数据的管道 说明: -* 字节输出流只能写字节出去,字节输出流默认是**覆盖**数据管道。 -* 换行用: **os.write("\r\n".getBytes());** -* 关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了! +* 字节输出流只能写字节出去,字节输出流默认是**覆盖**数据管道 +* 换行用:**os.write("\r\n".getBytes())** +* 关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了 ```java -OutputStream os = new FileOutputStream("Day09Demo/out05"); +OutputStream os = new FileOutputStream("Demo/out05"); os.write(97);//a os.write('b'); os.write("\r\n".getBytes()); @@ -7323,14 +7232,7 @@ os.close(); ##### 文件复制 -思想:字节是计算机中一切文件的组成,所以字节流适合做一切文件的复制 - -分析步骤: - (1)创建一个字节输入流管道与源文件接通。 - (2)创建一个字节输出流与目标文件接通。 - (3)创建一个字节数组作为桶 - (4)从字节输入流管道中读取数据,写出到字节输出流管道即可。 - (5)关闭资源! +字节是计算机中一切文件的组成,所以字节流适合做一切文件的复制 ```java public class CopyDemo01 { @@ -7339,9 +7241,9 @@ public class CopyDemo01 { OutputStream os = null ; try{ //(1)创建一个字节输入流管道与源文件接通。 - is = new FileInputStream("D:\\seazean\\图片资源\\meinv.jpg"); + is = new FileInputStream("D:\\seazean\\图片资源\\test.jpg"); //(2)创建一个字节输出流与目标文件接通。 - os = new FileOutputStream("D:\\seazean\\meimei.jpg"); + os = new FileOutputStream("D:\\seazean\\test.jpg"); //(3)创建一个字节数组作为桶 byte buffer = new byte[1024]; //(4)从字节输入流管道中读取数据,写出到字节输出流管道即可 @@ -7375,32 +7277,30 @@ public class CopyDemo01 { ##### 字符输入 -FileReader:文件字符输入流 +FileReader:文件字符输入流,以内存为基准,把磁盘文件的数据以字符的形式读入到内存,读取文本文件内容到内存中去 + +构造器: + +* `public FileReader(File file)`:创建一个字符输入流与源文件对象接通。 +* `public FileReader(String filePath)`:创建一个字符输入流与源文件路径接通。 + +方法: + +* `public int read()`:读取一个字符的编号返回,读取完毕返回 -1 +* `public int read(char[] buffer)`:读取一个字符数组,读取多少个就返回多少个,读取完毕返回 -1 + +结论: + +* 字符流一个一个字符的读取文本内容输出,可以解决中文读取输出乱码的问题,适合操作文本文件,但是一个一个字符的读取文本内容性能较差 +* 字符流按照**字符数组循环读取数据**,可以解决中文读取输出乱码的问题,而且性能也较好 - * 作用:以内存为基准,把磁盘文件的数据以字符的形式读入到内存,读取文本文件内容到内存中去。 - * 构造器: - `public FileReader(File file)` : 创建一个字符输入流与源文件对象接通。 - `public FileReader(String filePath)` : 创建一个字符输入流与源文件路径接通。 - * 方法: - `public int read()` : 读取一个字符的编号返回! 读取完毕返回 -1 - `public int read(char[] buffer)` : 读取一个字符数组,读取多少个就返回多少个,读取完毕返回 -1 - * 结论: - 字符流一个一个字符的读取文本内容输出,可以解决中文读取输出乱码的问题,适合操作文本文件。 - 但是:一个一个字符的读取文本内容性能较差!! - 字符流按照**字符数组循环读取数据**,可以解决中文读取输出乱码的问题,而且性能也较好!! - * **字符流不能复制图片,视频等类型的文件**。字符流在读取完了字节数据后并没有直接往目的地写,而是先查编码表,查到对应的数据就将该数据写入目的地。如果查不到,则码表会将一些未知区域中的数据去map这些字节数据,然后写到目的地,这样的话就造成了源数据和目的数据的不一致。 +**字符流不能复制图片,视频等类型的文件**。字符流在读取完了字节数据后并没有直接往目的地写,而是先查编码表,查到对应的数据就将该数据写入目的地。如果查不到,则码表会将一些未知区域中的数据去 map 这些字节数据,然后写到目的地,这样的话就造成了源数据和目的数据的不一致。 ```java public class FileReaderDemo01{//字符 public static void main(String[] args) throws Exception { - // 1.创建一个文件对象定位源文件 - // File f = new File("Day10Demo/src/dlei01.txt"); - // 2.创建一个字符输入流管道与源文件接通 - // Reader fr = new FileReader(f); - // 3.简化写法:创建一个字符输入流管道与源文件路径接通 - Reader fr = new FileReader("Day10Demo/src/dlei01.txt"); - //int code1 = fr.read(); - //System.out.print((char)code1); + // 创建一个字符输入流管道与源文件路径接通 + Reader fr = new FileReader("Demo/src/test.txt"); int ch; while((ch = fr.read()) != -1){ System.out.print((char)ch); @@ -7409,13 +7309,8 @@ public class FileReaderDemo01{//字符 } public class FileReaderDemo02 {//字符数组 public static void main(String[] args) throws Exception { - Reader fr = new FileReader("Day10Demo/src/dlei01.txt"); + Reader fr = new FileReader("Demo/src/test.txt"); - //char[] buffer = new char[3]; - //int len = fr.read(buffer); - //System.out.println("字符数:"+len); - //String rs = new String(buffer,0,len); - //System.out.println(rs); char[] buffer = new char[1024]; int len; while((len = fr.read(buffer)) != -1) { @@ -7427,30 +7322,33 @@ public class FileReaderDemo02 {//字符数组 +*** + + + ##### 字符输出 -FileWriter:文件字符输出流 - -* 作用:以内存为基准,把内存中的数据按照字符的形式写出到磁盘文件中去 -* 构造器: - `public FileWriter(File file)` : 创建一个字符输出流管道通向目标文件对象 - `public FileWriter(String filePath)` : 创建一个字符输出流管道通向目标文件路径 - `public FileWriter(File file,boolean append)` : 创建一个追加数据的字符输出流管道通向文件对象 - `public FileWriter(String filePath,boolean append)` : 创建一个追加数据的字符输出流管道通向目标文件路径 -* 方法: - `public void write(int c)` : 写一个字符出去 - `public void write(String c)` : 写一个字符串出去 - `public void write(char[] buffer)` : 写一个字符数组出去 - `public void write(String c ,int pos ,int len)` : 写字符串的一部分出去 - `public void write(char[] buffer ,int pos ,int len)` : 写字符数组的一部分出去 -* 说明: - 覆盖数据管道:`Writer fw = new FileWriter("Day10Demo/src/dlei03.txt")` - 追加数据管道:`Writer fw = new FileWriter("Day10Demo/src/dlei03.txt",true)` - 换行:fw.write("\r\n"); // 换行 - 读写字符文件数据建议使用字符流 - -```java -Writer fw = new FileWriter("Day10Demo/src/dlei03.txt"); +FileWriter:文件字符输出流,以内存为基准,把内存中的数据按照字符的形式写出到磁盘文件中去 + +构造器: + +* `public FileWriter(File file)`:创建一个字符输出流管道通向目标文件对象(覆盖数据管道) +* `public FileWriter(String filePath)`:创建一个字符输出流管道通向目标文件路径 +* `public FileWriter(File file, boolean append)`:创建一个追加数据的字符输出流管道通向文件对象(追加数据管道) +* `public FileWriter(String filePath, boolean append)`:创建一个追加数据的字符输出流管道通向目标文件路径 + +方法: + +* `public void write(int c)`:写一个字符出去 +* `public void write(char[] buffer)`:写一个字符数组出去 +* `public void write(String c, int pos, int len)`:写字符串的一部分出去 +* `public void write(char[] buffer, int pos, int len)`:写字符数组的一部分出去 +* `fw.write("\r\n")`:换行 + +读写字符文件数据建议使用字符流 + +```java +Writer fw = new FileWriter("Demo/src/test.txt"); fw.write(97); // 字符a fw.write('b'); // 字符b fw.write("Java是最优美的语言!"); @@ -7468,14 +7366,14 @@ fw.close; ##### 基本介绍 -作用:缓冲流可以提高字节流和字符流的读写数据的性能。 +缓冲流可以提高字节流和字符流的读写数据的性能 缓冲流分为四类: -* BufferedInputStream:字节缓冲输入流,可以提高字节输入流读数据的性能。 -* BufferedOutStream: 字节缓冲输出流,可以提高字节输出流写数据的性能。 -* BufferedReader: 字符缓冲输入流,可以提高字符输入流读数据的性能。 -* BufferedWriter: 字符缓冲输出流,可以提高字符输出流写数据的性能。 +* BufferedInputStream:字节缓冲输入流,可以提高字节输入流读数据的性能 +* BufferedOutStream:字节缓冲输出流,可以提高字节输出流写数据的性能 +* BufferedReader:字符缓冲输入流,可以提高字符输入流读数据的性能 +* BufferedWriter:字符缓冲输出流,可以提高字符输出流写数据的性能 @@ -7487,7 +7385,7 @@ fw.close; 字节缓冲输入流:BufferedInputStream -作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道, 提高字节输入流读数据的性能 +作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道,提高字节输入流读数据的性能 构造器:`public BufferedInputStream(InputStream in)` @@ -7497,7 +7395,7 @@ fw.close; public class BufferedInputStreamDemo01 { public static void main(String[] args) throws Exception { // 1.定义一个低级的字节输入流与源文件接通 - InputStream is = new FileInputStream("Day10Demo/src/dlei04.txt"); + InputStream is = new FileInputStream("Demo/src/test.txt"); // 2.把低级的字节输入流包装成一个高级的缓冲字节输入流。 BufferInputStream bis = new BufferInputStream(is); // 3.定义一个字节数组按照循环读取。 @@ -7525,13 +7423,13 @@ public class BufferedInputStreamDemo01 { 构造器:`public BufferedOutputStream(OutputStream os)` -原理:缓冲字节输出流自带了8KB缓冲池,数据就直接写入到缓冲池中去,性能极高了 +原理:缓冲字节输出流自带了 8KB 缓冲池,数据就直接写入到缓冲池中去,性能提高了 ```java public class BufferedOutputStreamDemo02 { public static void main(String[] args) throws Exception { // 1.写一个原始的字节输出流 - OutputStream os = new FileOutputStream("Day10Demo/src/dlei05.txt"); + OutputStream os = new FileOutputStream("Demo/src/test.txt"); // 2.把低级的字节输出流包装成一个高级的缓冲字节输出流 BufferedOutputStream bos = new BufferedOutputStream(os); // 3.写数据出去 @@ -7548,14 +7446,14 @@ public class BufferedOutputStreamDemo02 { ##### 字节流性能 -利用字节流的复制统计各种写法形式下缓冲流的性能执行情况。 +利用字节流的复制统计各种写法形式下缓冲流的性能执行情况 复制流: -* 使用低级的字节流按照一个一个字节的形式复制文件。 -* 使用低级的字节流按照一个一个字节数组的形式复制文件。 -* 使用高级的缓冲字节流按照一个一个字节的形式复制文件。 -* 使用高级的缓冲字节流按照一个一个字节数组的形式复制文件。 +* 使用低级的字节流按照一个一个字节的形式复制文件 +* 使用低级的字节流按照一个一个字节数组的形式复制文件 +* 使用高级的缓冲字节流按照一个一个字节的形式复制文件 +* 使用高级的缓冲字节流按照一个一个字节数组的形式复制文件 高级的缓冲字节流按照一个一个字节数组的形式复制文件,性能最高,建议使用 @@ -7573,14 +7471,14 @@ public class BufferedOutputStreamDemo02 { 构造器:`public BufferedReader(Reader reader)` -原理:缓冲字符输入流默认会有一个8K的字符缓冲池,可以提高读字符的性能 +原理:缓冲字符输入流默认会有一个 8K 的字符缓冲池,可以提高读字符的性能 -按照行读取数据的功能:`public String readLine()` 读取一行数据返回,读取完毕返回null +按照行读取数据的功能:`public String readLine()` 读取一行数据返回,读取完毕返回 null ```java public static void main(String[] args) throws Exception { // 1.定义一个原始的字符输入流读取源文件 - Reader fr = new FileReader("Day10Demo/src/dlei06.txt"); + Reader fr = new FileReader("Demo/src/test.txt"); // 2.把低级的字符输入流管道包装成一个高级的缓冲字符输入流管道 BufferedReader br = new BufferedReader(fr); // 定义一个字符串变量存储每行数据 @@ -7611,13 +7509,13 @@ public static void main(String[] args) throws Exception { 构造器:`public BufferedWriter(Writer writer)` - 原理:高级的字符缓冲输出流多了一个8k的字符缓冲池,写数据性能极大提高了 + 原理:高级的字符缓冲输出流多了一个 8K 的字符缓冲池,写数据性能极大提高了 字符缓冲输出流多了一个换行的特有功能:`public void newLine()` **新建一行** ```java public static void main(String[] args) throws Exception { - Writer fw = new FileWriter("Day10Demo/src/dlei07.txt",true);//追加 + Writer fw = new FileWriter("Demo/src/test.txt",true);//追加 BufferedWriter bw = new BufferedWriter(fw); bw.write("我爱学习Java"); @@ -7667,8 +7565,8 @@ GBK GBK 不乱码! UTF-8 GBK 乱码! ``` -如果代码编码和读取的文件编码一致,字符流读取的时候不会乱码。 -如果代码编码和读取的文件编码不一致,字符流读取的时候会乱码。 +* 如果代码编码和读取的文件编码一致,字符流读取的时候不会乱码 +* 如果代码编码和读取的文件编码不一致,字符流读取的时候会乱码 @@ -7684,8 +7582,8 @@ UTF-8 GBK 乱码! 构造器: -* `public InputStreamReader(InputStream is)` : 使用当前代码默认编码 UTF-8 转换成字符流 -* `public InputStreamReader(InputStream is, String charset)` : 指定编码把字节流转换成字符流 +* `public InputStreamReader(InputStream is)`:使用当前代码默认编码 UTF-8 转换成字符流 +* `public InputStreamReader(InputStream is, String charset)`:指定编码把字节流转换成字符流 ```java public class InputStreamReaderDemo{ @@ -7719,11 +7617,11 @@ public class InputStreamReaderDemo{ 构造器: -* `public OutputStreamWriter(OutputStream os)` : 用默认编码 UTF-8 把字节输出流转换成字符输出流 -* `public OutputStreamWriter(OutputStream os, String charset)` : 指定编码把字节输出流转换成 +* `public OutputStreamWriter(OutputStream os)`:用默认编码 UTF-8 把字节输出流转换成字符输出流 +* `public OutputStreamWriter(OutputStream os, String charset)`:指定编码把字节输出流转换成 ```Java -OutputStream os = new FileOutputStream("Day10Demo/src/dlei07.txt"); +OutputStream os = new FileOutputStream("Demo/src/test.txt"); OutputStreamWriter osw = new OutputStreamWriter(os,"GBK"); osw.write("我在学习Java"); osw.close(); @@ -7739,11 +7637,11 @@ osw.close(); ##### 基本介绍 -对象序列化:把Java对象转换成字节序列的过程,将对象写入到IO流中。 对象 => 文件中 +对象序列化:把 Java 对象转换成字节序列的过程,将对象写入到 IO 流中。对象 => 文件中 -对象反序列化:把字节序列恢复为Java对象的过程,从IO流中恢复对象。 文件中 => 对象 +对象反序列化:把字节序列恢复为 Java 对象的过程,从 IO 流中恢复对象,文件中 => 对象 -transient 关键字修饰的成员变量,将不参与序列化! +transient 关键字修饰的成员变量,将不参与序列化 @@ -7755,7 +7653,7 @@ transient 关键字修饰的成员变量,将不参与序列化! 对象序列化流(对象字节输出流):ObjectOutputStream -作用:把内存中的Java对象数据保存到文件中去 +作用:把内存中的 Java 对象数据保存到文件中去 构造器:`public ObjectOutputStream(OutputStream out)` @@ -7769,7 +7667,7 @@ public class SerializeDemo01 { // 1.创建User用户对象 User user = new User("seazean","980823","七十一"); // 2.创建低级的字节输出流通向目标文件 - OutputStream os = new FileOutputStream("Day10Demo/src/obj.dat"); + OutputStream os = new FileOutputStream("Demo/src/obj.dat"); // 3.把低级的字节输出流包装成高级的对象字节输出流 ObjectOutputStream ObjectOutputStream oos = new ObjectOutputStream(os); // 4.通过对象字节输出流序列化对象: @@ -7787,7 +7685,7 @@ class User implements Serializable { private String loginName; private transient String passWord; private String userName; - ///get+set + // get+set } ``` @@ -7812,19 +7710,20 @@ byte[] bytes = bos.toByteArray(); 对象反序列化(对象字节输入流):ObjectInputStream -作用:读取序列化的对象文件恢复到Java对象中 +作用:读取序列化的对象文件恢复到 Java 对象中 构造器:`public ObjectInputStream(InputStream is)` 方法:`public final Object readObject()` 序列化版本号:`private static final long serialVersionUID = 2L` -说明:序列化使用的版本号和反序列化使用的版本号一致才可以正常反序列化,否则报错 + +注意:序列化使用的版本号和反序列化使用的版本号一致才可以正常反序列化,否则报错 ```java public class SerializeDemo02 { public static void main(String[] args) throws Exception { - InputStream is = new FileInputStream("Day10Demo/src/obj.dat"); + InputStream is = new FileInputStream("Demo/src/obj.dat"); ObjectInputStream ois = new ObjectInputStream(is); User user = (User)ois.readObject();//反序列化 System.out.println(user); @@ -7859,15 +7758,14 @@ class User implements Serializable { * `public PrintStream(OutputStream os)` * `public PrintStream(String filepath)` -System类: +System 类: * `public static void setOut(PrintStream out)`:让系统的输出流向打印流 ```java public class PrintStreamDemo01 { public static void main(String[] args) throws Exception { - PrintStream ps = new PrintStream("Day10Demo/src/dlei.txt"); - //PrintWriter pw = new PrintWriter("Day10Demo/src/dlei08.txt"); + PrintStream ps = new PrintStream("Demo/src/test.txt"); ps.println(任何类型的数据); ps.print(不换行); ps.write("我爱你".getBytes()); @@ -7877,7 +7775,7 @@ public class PrintStreamDemo01 { public class PrintStreamDemo02 { public static void main(String[] args) throws Exception { System.out.println("==seazean0=="); - PrintStream ps = new PrintStream("Day10Demo/src/log.txt"); + PrintStream ps = new PrintStream("Demo/src/log.txt"); System.setOut(ps); // 让系统的输出流向打印流 //不输出在控制台,输出到文件里 System.out.println("==seazean1=="); @@ -7937,23 +7835,23 @@ try( ### Properties -Properties:属性集对象。就是一个Map集合,一个键值对集合 +Properties:属性集对象。就是一个 Map 集合,一个键值对集合 -核心作用:Properties代表的是一个属性文件,可以把键值对数据存入到一个属性文件 +核心作用:Properties 代表的是一个属性文件,可以把键值对数据存入到一个属性文件 -属性文件:后缀是.properties结尾的文件,里面的内容都是 key=value +属性文件:后缀是 `.properties` 结尾的文件,里面的内容都是 key=value -Properties方法: +Properties 方法: -| 方法名 | 说明 | -| --------------------------------------------------- | ------------------------------------------- | -| public Object setProperty(String key, String value) | 设置集合的键和值,底层调用Hashtable方法 put | -| public String getProperty(String key) | 使用此属性列表中指定的键搜索属性 | -| public Set stringPropertyNames() | 所有键的名称的集合 | -| public synchronized void load(Reader r) | 从输入字符流读取属性列表(键和元素对) | -| public synchronized void load(InputStream inStream) | 加载属性文件的数据到属性集对象中去 | -| public void store(Writer w, String comments) | 将此属性列表(键和元素对)写入 Properties表 | -| public void store(OutputStream os, String comments) | 保存数据到属性文件中去 | +| 方法名 | 说明 | +| -------------------------------------------- | --------------------------------------------- | +| Object setProperty(String key, String value) | 设置集合的键和值,底层调用 Hashtable 方法 put | +| String getProperty(String key) | 使用此属性列表中指定的键搜索属性 | +| Set stringPropertyNames() | 所有键的名称的集合 | +| synchronized void load(Reader r) | 从输入字符流读取属性列表(键和元素对) | +| synchronized void load(InputStream inStream) | 加载属性文件的数据到属性集对象中去 | +| void store(Writer w, String comments) | 将此属性列表(键和元素对)写入 Properties 表 | +| void store(OutputStream os, String comments) | 保存数据到属性文件中去 | ````java public class PropertiesDemo01 { @@ -7962,7 +7860,7 @@ public class PropertiesDemo01 { Properties properties = new Properties();//{} properties.setProperty("admin" , "123456"); // b.把属性集对象的数据存入到属性文件中去(重点) - OutputStream os = new FileOutputStream("Day10Demo/src/users.properties"); + OutputStream os = new FileOutputStream("Demo/src/users.properties"); properties.store(os,"i am very happy!!我保存了用户数据!"); //参数一:被保存数据的输出管道 //参数二:保存心得。就是对象保存的数据进行解释说明! @@ -7974,7 +7872,7 @@ public class PropertiesDemo01 { public class PropertiesDemo02 { public static void main(String[] args) throws Exception { Properties properties = new Properties();//底层基于map集合 - properties.load(new FileInputStream("Day10Demo/src/users.properties")); + properties.load(new FileInputStream("Demo/src/users.properties")); System.out.println(properties); System.out.println(properties.getProperty("admin")); @@ -7998,13 +7896,15 @@ public class PropertiesDemo02 { RandomAccessFile 类:该类的实例支持读取和写入随机访问文件 构造器: -RandomAccessFile(File file, String mode):创建随机访问文件流,从File参数指定的文件读取,可选择写入 -RandomAccessFile(String name, String mode):创建随机访问文件流,从指定名称文件读取,可选择写入文件 + +* `RandomAccessFile(File file, String mode)`:创建随机访问文件流,从 File 参数指定的文件读取,可选择写入 +* `RandomAccessFile(String name, String mode)`:创建随机访问文件流,从指定名称文件读取,可选择写入文件 常用方法: -`public void seek(long pos)` : 设置文件指针偏移,从该文件开头测量,发生下一次读取或写入(插入+覆盖) -`public void write(byte[] b)` : 从指定的字节数组写入 b.length个字节到该文件 -`public int read(byte[] b)` : 从该文件读取最多b.length个字节的数据到字节数组 + +* `public void seek(long pos)`:设置文件指针偏移,从该文件开头测量,发生下一次读取或写入(插入+覆盖) +* `public void write(byte[] b)`:从指定的字节数组写入 b.length 个字节到该文件 +* `public int read(byte[] b)`:从该文件读取最多 b.length 个字节的数据到字节数组 ```java public static void main(String[] args) throws Exception { @@ -8024,16 +7924,16 @@ public static void main(String[] args) throws Exception { ### Commons -commons-io 是apache开源基金组织提供的一组有关IO操作的类库,可以挺提高IO功能开发的效率。 +commons-io 是 apache 提供的一组有关 IO 操作的类库,可以挺提高 IO 功能开发的效率。 commons-io 工具包提供了很多有关 IO 操作的类: -| 包 | 功能描述 | -| ----------------------------------- | :------------------------------------------- | -| org.apache.commons.io | 有关Streams、Readers、Writers、Files的工具类 | -| org.apache.commons.io.input | 输入流相关的实现类,包含Reader和InputStream | -| org.apache.commons.io.output | 输出流相关的实现类,包含Writer和OutputStream | -| org.apache.commons.io.serialization | 序列化相关的类 | +| 包 | 功能描述 | +| ----------------------------------- | :---------------------------------------------- | +| org.apache.commons.io | 有关 Streams、Readers、Writers、Files 的工具类 | +| org.apache.commons.io.input | 输入流相关的实现类,包含 Reader 和 InputStream | +| org.apache.commons.io.output | 输出流相关的实现类,包含 Writer 和 OutputStream | +| org.apache.commons.io.serialization | 序列化相关的类 | IOUtils 和 FileUtils 可以方便的复制文件和文件夹 @@ -8041,18 +7941,18 @@ IOUtils 和 FileUtils 可以方便的复制文件和文件夹 public class CommonsIODemo01 { public static void main(String[] args) throws Exception { // 1.完成文件复制! - IOUtils.copy(new FileInputStream("Day13Demo/src/books.xml"), - new FileOutputStream("Day13Demo/new.xml")); + IOUtils.copy(new FileInputStream("Demo/src/books.xml"), + new FileOutputStream("Demo/new.xml")); // 2.完成文件复制到某个文件夹下! - FileUtils.copyFileToDirectory(new File("Day13Demo/src/books.xml"), + FileUtils.copyFileToDirectory(new File("Demo/src/books.xml"), new File("D:/it")); // 3.完成文件夹复制到某个文件夹下! FileUtils.copyDirectoryToDirectory(new File("D:\\it\\图片服务器") , new File("D:\\")); // Java从1.7开始提供了一些nio, 自己也有一行代码完成复制的技术。 - Files.copy(Paths.get("Day13Demo/src/books.xml") - , new FileOutputStream("Day13Demo/new11.txt")); + Files.copy(Paths.get("Demo/src/books.xml") + , new FileOutputStream("Demo/new11.txt")); } } ``` @@ -8065,19 +7965,17 @@ public class CommonsIODemo01 { + + ## 反射 ### 测试框架 -> 单元测试是指程序员写的测试代码给自己的类中的方法进行预期正确性的验证。 -> 单元测试一旦写好了这些测试代码,就可以一直使用,可以实现一定程度上的自动化测试。 +单元测试的经典框架:Junit,是 Java 语言编写的第三方单元测试框架 -单元测试的经典框架:Junit - -* Junit : 是 Java 语言编写的第三方单元测试框架,可以帮助我们方便快速的测试我们代码的正确性。 -* 单元测试: - * 单元:在 Java 中,一个类就是一个单元 - * 单元测试:Junit 编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试 +单元测试: +* 单元:在 Java 中,一个类就是一个单元 +* 单元测试:Junit 编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试 Junit 单元测试框架的作用: @@ -8086,14 +7984,14 @@ Junit 单元测试框架的作用: 测试方法注意事项:**必须是 public 修饰的,没有返回值,没有参数,使用注解@Test修饰** -Junit常用注解(Junit 4.xxxx版本),@Test 测试方法: +Junit常用注解(Junit 4.xxxx 版本),@Test 测试方法: * @Before:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 * @After:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 * @BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前**只**执行一次 * @AfterClass:用来静态修饰方法,该方法会在所有测试方法之后**只**执行一次 -Junit常用注解(Junit5.xxxx版本),@Test 测试方法: +Junit 常用注解(Junit5.xxxx 版本),@Test 测试方法: * @BeforeEach:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 * @AfterEach:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 @@ -8180,7 +8078,7 @@ public class UserServiceTest { 核心思想:在运行时获取类编译后的字节码文件对象,然后解析类中的全部成分 -反射提供了一个Class类型:HelloWorld.java → javac → HelloWorld.class +反射提供了一个 Class 类型:HelloWorld.java → javac → HelloWorld.class * `Class c = HelloWorld.class` @@ -8188,16 +8086,16 @@ public class UserServiceTest { 作用:可以在运行时得到一个类的全部成分然后操作,破坏封装性,也可以破坏泛型的约束性。 -**反射的优点:** +反射的优点: - 可扩展性:应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类 - 类浏览器和可视化开发环境:一个类浏览器需要可以枚举类的成员,可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码 -- 调试器和测试工具: 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率 +- 调试器和测试工具: 调试器需要能够检查一个类里的私有成员,测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率 -**反射的缺点:** +反射的缺点: - 性能开销:反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化,反射操作的效率要比那些非射操作低得多,应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。 -- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行,如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了 +- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行,如果一个程序必须在有安全限制的环境中运行 - 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化 @@ -8260,9 +8158,9 @@ class Student{} 获取构造器的 API: * Constructor getConstructor(Class... parameterTypes):根据参数匹配获取某个构造器,只能拿 public 修饰的构造器 -* **Constructor getDeclaredConstructor(Class... parameterTypes)**:根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符 +* Constructor getDeclaredConstructor(Class... parameterTypes):根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符 * Constructor[] getConstructors():获取所有的构造器,只能拿 public 修饰的构造器 -* **Constructor[] getDeclaredConstructors()**:获取所有构造器,只要申明就可以定位,不关心权限修饰符 +* Constructor[] getDeclaredConstructors():获取所有构造器,只要申明就可以定位,不关心权限修饰符 Constructor 的常用 API: @@ -8356,12 +8254,12 @@ public class TestStudent02 { #### 获取变量 -获取Field成员变量API: +获取 Field 成员变量 API: -* Field getField(String name) : 根据成员变量名获得对应 Field 对象,只能获得 public 修饰 -* Field getDeclaredField(String name) : 根据成员变量名获得对应 Field 对象,所有申明的变量 -* Field[] getFields() : 获得所有的成员变量对应的Field对象,只能获得 public 的 -* Field[] getDeclaredFields() : 获得所有的成员变量对应的 Field 对象,只要申明了就可以得到 +* Field getField(String name):根据成员变量名获得对应 Field 对象,只能获得 public 修饰 +* Field getDeclaredField(String name):根据成员变量名获得对应 Field 对象,所有申明的变量 +* Field[] getFields():获得所有的成员变量对应的 Field 对象,只能获得 public 的 +* Field[] getDeclaredFields():获得所有的成员变量对应的 Field 对象,只要申明了就可以得到 Field 的方法:给成员变量赋值和取值 @@ -8440,6 +8338,10 @@ public class FieldDemo02 { +*** + + + #### 获取方法 获取 Method 方法 API: @@ -8546,10 +8448,9 @@ public class ReflectDemo { 注解:类的组成部分,可以给类携带一些额外的信息,提供一种安全的类似注释标记的机制,用来将任何信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联 -* 注解是 JDK1.5 的新特性 * 注解是给编译器或 JVM 看的,编译器或 JVM 可以根据注解来完成对应的功能 * 注解类似修饰符,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中 -* 父类中的注解是不能被子类继承的 +* **父类中的注解是不能被子类继承的** 注解作用: @@ -8665,29 +8566,25 @@ public class AnnotationDemo01{ 元注解有四个: -* @Target:约束自定义注解可以标记的范围,默认值为任何元素,表示该注解用于什么地方 - - 可使用的值定义在ElementType枚举类中: +* @Target:约束自定义注解可以标记的范围,默认值为任何元素,表示该注解用于什么地方,可用值定义在 ElementType 类中: - `ElementType.CONSTRUCTOR`:用于描述构造器 - - `ElementType.FIELD`:成员变量、对象、属性(包括enum实例) + - `ElementType.FIELD`:成员变量、对象、属性(包括 enum 实例) - `ElementType.LOCAL_VARIABLE`:用于描述局部变量 - `ElementType.METHOD`:用于描述方法 - `ElementType.PACKAGE`:用于描述包 - `ElementType.PARAMETER`:用于描述参数 - - `ElementType.TYPE`:用于描述类、接口(包括注解类型) 或enum声明 - -* @Retention:定义该注解的生命周期,申明注解的作用范围:编译时,运行时 - - 可使用的值定义在RetentionPolicy枚举类中: + - `ElementType.TYPE`:用于描述类、接口(包括注解类型)或 enum 声明 + +* @Retention:定义该注解的生命周期,申明注解的作用范围:编译时,运行时,可使用的值定义在 RetentionPolicy 枚举类中: - - `RetentionPolicy.SOURCE`:在编译阶段丢弃,这些注解在编译结束之后就不再有任何意义,只作用在源码阶段,生成的字节码文件中不存在。`@Override`, `@SuppressWarnings`都属于这类注解 + - `RetentionPolicy.SOURCE`:在编译阶段丢弃,这些注解在编译结束之后就不再有任何意义,只作用在源码阶段,生成的字节码文件中不存在,`@Override`、`@SuppressWarnings` 都属于这类注解 - `RetentionPolicy.CLASS`:在类加载时丢弃,在字节码文件的处理中有用,运行阶段不存在,默认值 - `RetentionPolicy.RUNTIME` : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息,自定义的注解通常使用这种方式 - + * @Inherited:表示修饰的自定义注解可以被子类继承 -* @Documented:表示是否将自定义的注解信息添加在 java 文档中 +* @Documented:表示是否将自定义的注解信息添加在 Java 文档中 ```java public class AnnotationDemo01{ @@ -8816,10 +8713,14 @@ public class TestDemo{ + + **** + + ## XML ### 概述 @@ -8966,11 +8867,11 @@ XML 文件中常见的组成元素有:文档声明、元素、属性、注释、 #### DTD -##### DTD定义 +##### DTD 定义 -DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及XML文档结构的其它详细信息。 +DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及 XML 文档结构的其它详细信息。 -##### DTD规则 +DTD 规则: * 约束元素的嵌套层级 @@ -9066,7 +8967,7 @@ DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XM -##### DTD引入 +##### DTD 引入 * 引入本地 dtd @@ -9142,7 +9043,7 @@ DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XM -##### DTD实现 +##### DTD 实现 persondtd.dtd 文件 @@ -9179,13 +9080,13 @@ persondtd.dtd 文件 #### Schema -##### XSD定义 +##### XSD 定义 1. Schema 语言也可作为 XSD(XML Schema Definition) -2. Schema 约束文件本身也是一个 xml 文件,符合 xml 的语法,这个文件的后缀名 .xsd -3. 一个 xml 中可以引用多个 Schema 约束文件,多个 Schema 使用名称空间区分(名称空间类似于 Java 包名) +2. Schema 约束文件本身也是一个 XML 文件,符合 XML 的语法,这个文件的后缀名 .xsd +3. 一个 XML 中可以引用多个 Schema 约束文件,多个 Schema 使用名称空间区分(名称空间类似于 Java 包名) 4. dtd 里面元素类型的取值比较单一常见的是 PCDATA 类型,但是在 Schema 里面可以支持很多个数据类型 -5. **Schema 文件约束 xml 文件的同时也被别的文件约束着** +5. **Schema 文件约束 XML 文件的同时也被别的文件约束着** @@ -9193,7 +9094,7 @@ persondtd.dtd 文件 -##### XSD规则 +##### XSD 规则 1. 创建一个文件,这个文件的后缀名为 .xsd 2. 定义文档声明 @@ -9244,7 +9145,7 @@ person.xsd -##### XSD引入 +##### XSD 引入 1. 在根标签上定义属性 xmlns="http://www.w3.org/2001/XMLSchema-instance" 2. **通过 xmlns 引入约束文件的名称空间** @@ -9273,7 +9174,7 @@ person.xsd -##### XSD属性 +##### XSD 属性 ```scheme @@ -9339,12 +9240,12 @@ DOM(Document Object Model):文档对象模型,把文档的各个组成 Dom4J 实现: * Dom4J 解析器构造方法:`SAXReader saxReader = new SAXReader()` -* SAXReader 常用API: +* SAXReader 常用 API: * `public Document read(File file)`:Reads a Document from the given File * `public Document read(InputStream in)`:Reads a Document from the given stream using SAX -* Java Class 类API: +* Java Class 类 API: * `public InputStream getResourceAsStream(String path)`:加载文件成为一个字节输入流返回 @@ -9401,6 +9302,10 @@ public class Dom4JDemo { +**** + + + #### 子元素 Element 元素的 API: @@ -9620,92 +9525,126 @@ public class XPathDemo { +**** -*** +## SDP +### 单例模式 -# JVM +#### 基本介绍 -## JVM概述 +创建型模式的主要关注点是怎样创建对象,将对象的创建与使用分离,降低系统的耦合度,使用者不需要关注对象的创建细节 -### 基本介绍 +创建型模式分为:单例模式、工厂方法模式、抽象工程模式、原型模式、建造者模式 -JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作 +单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,提供了一种创建对象的最佳方式 -特点: +单例设计模式分类两种: -* Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 -* JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** +* 饿汉式:类加载就会导致该单实例对象被创建 -Java 代码执行流程:java程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) +* 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建 -JVM 结构: - -JVM、JRE、JDK 对比: +*** - +#### 饿汉式 -参考书籍:https://book.douban.com/subject/34907497/ +饿汉式在类加载的过程导致该单实例对象被创建,**虚拟机会保证类加载的线程安全**,但是如果只是为了加载该类不需要实例,则会造成内存的浪费 -参考视频:https://www.bilibili.com/video/BV1PJ411n7xZ +* 静态变量的方式: -参考视频:https://www.bilibili.com/video/BV1yE411Z7AP + ```java + public final class Singleton { + // 私有构造方法 + private Singleton() {} + // 在成员位置创建该类的对象 + private static final Singleton instance = new Singleton(); + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return instance; + } + + // 解决序列化问题 + protected Object readResolve() { + return INSTANCE; + } + } + ``` + * 加 final 修饰,所以不会被子类继承,防止子类中不适当的行为覆盖父类的方法,破坏了单例 + * 防止反序列化破坏单例的方式: -*** + * 对单例声明 transient,然后实现 readObject(ObjectInputStream in) 方法,复用原来的单例 + 条件:访问权限为 private/protected、返回值必须是 Object、异常可以不抛 + * 实现 readResolve() 方法,当 JVM 从内存中反序列化地组装一个新对象,就会自动调用 readResolve 方法返回原来单例 -### 架构模型 + * 构造方法设置为私有,防止其他类无限创建对象,但是不能防止反射破坏 -Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,Java 的指令都是根据栈来设计的,不同平台 CPU 架构不同,所以不能设计为基于寄存器架构 + * 静态变量初始化在类加载时完成,由 JVM 保证线程安全,能保证单例对象创建时的安全 -* 基于栈式架构的特点: - * 设计和实现简单,适用于资源受限的系统 - * 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现 - * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器(ACC)中,指令可直接访问寄存器 - * 一地址指令:一个操作码对应一个地址码,通过地址码寻找操作数 - * 不需要硬件的支持,可移植性更好,更好实现跨平台 -* 基于寄存器架构的特点: - * 需要硬件的支持,可移植性差 - * 性能更好,执行更高效,寄存器比内存快 - * 以一地址指令、二地址指令、三地址指令为主 + * 提供静态方法而不是直接将 INSTANCE 设置为 public,体现了更好的封装性、提供泛型支持、可以改进成懒汉单例设计 +* 静态代码块的方式: + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + // 在成员位置创建该类的对象 + private static Singleton instance; + static { + instance = new Singleton(); + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return instance; + } + } + ``` -*** +* 枚举方式:枚举类型是所用单例实现中**唯一一种不会被破坏**的单例实现模式 + ```java + public enum Singleton { + INSTANCE; + public void doSomething() { + System.out.println("doSomething"); + } + } + public static void main(String[] args) { + Singleton.INSTANCE.doSomething(); + } + ``` + * 问题1:枚举单例是如何限制实例个数的?每个枚举项都是一个实例,是一个静态成员变量 + * 问题2:枚举单例在创建时是否有并发问题?否 + * 问题3:枚举单例能否被反射破坏单例?否,反射创建对象时判断是枚举类型就直接抛出异常 + * 问题4:枚举单例能否被反序列化破坏单例?否 + * 问题5:枚举单例属于懒汉式还是饿汉式?**饿汉式** + * 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做?添加构造方法 -### 生命周期 + 反编译结果: -JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 + ```java + public final class Singleton extends java.lang.Enum { // Enum实现序列化接口 + public static final Singleton INSTANCE = new Singleton(); + } + ``` -- **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 -- **运行**: - - - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 - - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 - - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 - - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 - - Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化;Client 模式启动的 JVM 采用的是轻量级的虚拟机 -- **死亡**: - - 当程序中的用户线程都中止,JVM 才会退出 - - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 - - 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 java 安全管理器允许这次 exit 或 halt 操作 - - @@ -9713,36 +9652,78 @@ JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 +#### 懒汉式 +* 线程不安全 -## 内存结构 - -### 内存概述 - -内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区 + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + // 在成员位置创建该类的对象 + private static Singleton instance; + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + if(instance == null) { + // 多线程环境,会出现线程安全问题,可能多个线程同时进入这里 + instance = new Singleton(); + } + return instance; + } + } + ``` -JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 +* 双端检锁机制 -* Java1.8 以前的内存结构图: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java7内存结构图.png) + 在多线程的情况下,可能会出现空指针问题,出现问题的原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作,所以需要使用 `volatile` 关键字 -* Java1.8 之后的内存结果图: + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + private static volatile Singleton instance; + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + // 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例 + if(instance == null) { + synchronized (Singleton.class) { + // 抢到锁之后再次判断是否为null + if(instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } + } + ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java8内存结构图.png) +* 静态内部类方式 -线程运行诊断: + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + private static class SingletonHolder { + private static final Singleton INSTANCE = new Singleton(); + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return SingletonHolder.INSTANCE; + } + } + ``` -* 定位:jps 定位进程 id -* jstack 进程 id:用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息 + * 内部类属于懒汉式,类加载本身就是懒惰的,首次调用时加载,然后对单例进行初始化 -常见OOM错误: + 类加载的时候方法不会被调用,所以不会触发 getInstance 方法调用 invokestatic 指令对内部类进行加载;加载的时候字节码常量池会被加入类的运行时常量池,解析工作是将常量池中的符号引用解析成直接引用,但是解析过程不一定非得在类加载时完成,可以延迟到运行时进行,所以静态内部类实现单例会**延迟加载** -* java.lang.StackOverflowError -* java.lang.OutOfMemoryError:java heap space -* java.lang.OutOfMemoryError:GC overhead limit exceeded -* java.lang.OutOfMemoryError:Direct buffer memory -* java.lang.OutOfMemoryError:unable to create new native thread -* java.lang.OutOfMemoryError:Metaspace + * 没有线程安全问题,静态变量初始化在类加载时完成,由 JVM 保证线程安全 @@ -9750,47 +9731,100 @@ JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理 -### JVM内存 - -#### 虚拟机栈 - -##### Java栈 - -Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所需要的内存 - -* 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(**一个方法一个栈帧**) +#### 破坏单例 -* Java 虚拟机规范允许 **Java 栈的大小是动态的或者是固定不变的** +##### 反序列化 -* 虚拟机栈是**每个线程私有的**,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程 +将单例对象序列化再反序列化,对象从内存反序列化到程序中会重新创建一个对象,通过反序列化得到的对象是不同的对象,而且得到的对象不是通过构造器得到的,**反序列化得到的对象不执行构造器** -* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着: +* Singleton - * 局部变量表:存储方法里的java基本数据类型以及对象的引用 - * 动态链接:也叫指向运行时常量池的方法引用 - * 方法返回地址:方法正常退出或者异常退出的定义 - * 操作数栈或表达式栈和其他一些附加信息 + ```java + public class Singleton implements Serializable { //实现序列化接口 + // 私有构造方法 + private Singleton() {} + private static class SingletonHolder { + private static final Singleton INSTANCE = new Singleton(); + } - - -设置栈内存大小:`-Xss size` `-Xss 1024k` + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return SingletonHolder.INSTANCE; + } + } + ``` -* 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M +* 序列化 -虚拟机栈特点: + ```java + public class Test { + public static void main(String[] args) throws Exception { + //往文件中写对象 + //writeObject2File(); + //从文件中读取对象 + Singleton s1 = readObjectFromFile(); + Singleton s2 = readObjectFromFile(); + //判断两个反序列化后的对象是否是同一个对象 + System.out.println(s1 == s2); + } + + private static Singleton readObjectFromFile() throws Exception { + //创建对象输入流对象 + ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C://a.txt")); + //第一个读取Singleton对象 + Singleton instance = (Singleton) ois.readObject(); + return instance; + } + + public static void writeObject2File() throws Exception { + //获取Singleton类的对象 + Singleton instance = Singleton.getInstance(); + //创建对象输出流 + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C://a.txt")); + //将instance对象写出到文件中 + oos.writeObject(instance); + } + } + ``` -* 栈内存**不需要进行GC**,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据 +* 解决方法: -* 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大) + 在 Singleton 类中添加 `readResolve()` 方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新创建的对象 -* 方法内的局部变量是否**线程安全**: - * 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析) - * 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全 + ```java + private Object readResolve() { + return SingletonHolder.INSTANCE; + } + ``` -异常: + ObjectInputStream 类源码分析: -* 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError 异常 -* 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常 + ```java + public final Object readObject() throws IOException, ClassNotFoundException{ + //... + Object obj = readObject0(false);//重点查看readObject0方法 + } + + private Object readObject0(boolean unshared) throws IOException { + try { + switch (tc) { + case TC_OBJECT: + return checkResolve(readOrdinaryObject(unshared)); + } + } + } + private Object readOrdinaryObject(boolean unshared) throws IOException { + // isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类 + obj = desc.isInstantiable() ? desc.newInstance() : null; + // 添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true + if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { + // 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量 + // 多次调用ObjectInputStream类中的readObject方法,本质调用定义的readResolve方法,返回的是同一个对象。 + Object rep = desc.invokeReadResolve(obj); + } + return obj; + } + ``` @@ -9798,21 +9832,62 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 -##### 局部变量 +##### 反射破解 -局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 +* 反射 -* 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题 -* 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中 -* 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁 -* 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收 + ```java + public class Test { + public static void main(String[] args) throws Exception { + //获取Singleton类的字节码对象 + Class clazz = Singleton.class; + //获取Singleton类的私有无参构造方法对象 + Constructor constructor = clazz.getDeclaredConstructor(); + //取消访问检查 + constructor.setAccessible(true); + + //创建Singleton类的对象s1 + Singleton s1 = (Singleton) constructor.newInstance(); + //创建Singleton类的对象s2 + Singleton s2 = (Singleton) constructor.newInstance(); + + //判断通过反射创建的两个Singleton对象是否是同一个对象 + System.out.println(s1 == s2); //false + } + } + ``` + +* 反射方式破解单例的解决方法: + + ```java + public class Singleton { + private static volatile Singleton instance; + + // 私有构造方法 + private Singleton() { + // 反射破解单例模式需要添加的代码 + if(instance != null) { + throw new RuntimeException(); + } + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + if(instance != null) { + return instance; + } + synchronized (Singleton.class) { + if(instance != null) { + return instance; + } + instance = new Singleton(); + return instance; + } + } + } + ``` -局部变量表最基本的存储单元是 **slot(变量槽)**: -* 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据 -* 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量 -* 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot -* 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 @@ -9820,97 +9895,172 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 -##### 操作数栈 +#### Runtime -栈:可以使用数组或者链表来实现 +Runtime 类就是使用的单例设计模式中的饿汉式 -操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop) +```java +public class Runtime { + private static Runtime currentRuntime = new Runtime(); + public static Runtime getRuntime() { + return currentRuntime; + } + private Runtime() {} + ... +} +``` -* 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区 +使用 Runtime -* Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈 -* 如果被调用的方法带有返回值的话,其**返回值将会被压入当前栈帧的操作数栈中** +```java +public class RuntimeDemo { + public static void main(String[] args) throws IOException { + //获取Runtime类对象 + Runtime runtime = Runtime.getRuntime(); -栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率 + //返回 Java 虚拟机中的内存总量。 + System.out.println(runtime.totalMemory()); + //返回 Java 虚拟机试图使用的最大内存量。 + System.out.println(runtime.maxMemory()); -基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术 + //创建一个新的进程执行指定的字符串命令,返回进程对象 + Process process = runtime.exec("ipconfig"); + //获取命令执行后的结果,通过输入流获取 + InputStream inputStream = process.getInputStream(); + byte[] arr = new byte[1024 * 1024* 100]; + int b = inputStream.read(arr); + System.out.println(new String(arr,0,b,"gbk")); + } +} +``` -*** +**** -##### 动态链接 -动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是**动态绑定** -* 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 +### 代理模式 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接符号引用.png) +#### 静态代理 -* 在 Java 源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中 - - 常量池的作用:提供一些符号和常量,便于指令的识别 - - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接运行时常量池.png) +代理模式:由于某些原因需要给某对象提供一个代理以控制对该对象的访问,访问对象不适合或者不能直接引用为目标对象,代理对象作为访问对象和目标对象之间的中介 +Java 中的代理按照代理类生成时机不同又分为静态代理和动态代理,静态代理代理类在编译期就生成,而动态代理代理类则是在 Java 运行时动态生成,动态代理又有 JDK 代理和 CGLib 代理两种 +代理(Proxy)模式分为三种角色: -*** +* 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法 +* 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象 +* 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,可以访问、控制或扩展真实主题的功能 +买票案例,火车站是目标对象,代售点是代理对象 +* 卖票接口: -##### 返回地址 + ```java + public interface SellTickets { + void sell(); + } + ``` -Return Address:存放调用该方法的 PC 寄存器的值 +* 火车站,具有卖票功能,需要实现SellTickets接口 -方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置 + ```java + public class TrainStation implements SellTickets { + public void sell() { + System.out.println("火车站卖票"); + } + } + ``` -* 正常:调用者的 pc 计数器的值作为返回地址,即调用该方法的指令的**下一条指令的地址** -* 异常:返回地址是要通过异常表来确定 +* 代售点: -正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者 + ```java + public class ProxyPoint implements SellTickets { + private TrainStation station = new TrainStation(); + + public void sell() { + System.out.println("代理点收取一些服务费用"); + station.sell(); + } + } + ``` -异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出 +* 测试类: -两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值 + ```java + public class Client { + public static void main(String[] args) { + ProxyPoint pp = new ProxyPoint(); + pp.sell(); + } + } + ``` + 测试类直接访问的是 ProxyPoint 类对象,也就是 ProxyPoint 作为访问对象和目标对象的中介 -##### 附加信息 -栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息 +**** -*** +#### JDK +##### 使用方式 +Java 中提供了一个动态代理类 Proxy,Proxy 并不是代理对象的类,而是提供了一个创建代理对象的静态方法 newProxyInstance() 来获取代理对象 -#### 本地方法栈 +`static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h) ` -本地方法栈是为虚拟机执行本地方法时提供服务的 +* 参数一:类加载器,负责加载代理类。传入类加载器,代理和被代理对象要用一个类加载器才是父子关系,不同类加载器加载相同的类在 JVM 中都不是同一个类对象 -JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植 +* 参数二:被代理业务对象的**全部实现的接口**,代理对象与真实对象实现相同接口,知道为哪些方法做代理 -* 不需要进行GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常 +* 参数三:代理真正的执行方法,也就是代理的处理逻辑 -* 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一 +代码实现: -* 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 +* 代理工厂:创建代理对象 -* 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限 + ```java + public class ProxyFactory { + private TrainStation station = new TrainStation(); + //也可以在参数中提供 getProxyObject(TrainStation station) + public SellTickets getProxyObject() { + //使用 Proxy 获取代理对象 + SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance( + station.getClass().getClassLoader(), + station.getClass().getInterfaces(), + new InvocationHandler() { + public Object invoke(Object proxy, Method method, Object[] args) { + System.out.println("代理点(JDK动态代理方式)"); + //执行真实对象 + Object result = method.invoke(station, args); + return result; + } + }); + return sellTickets; + } + } + ``` - * 本地方法可以通过本地方法接口来**访问虚拟机内部的运行时数据区** - * 直接从本地内存的堆中分配任意数量的内存 - * 可以直接使用本地处理器中的寄存器 - - - - +* 测试类: -图片来源:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md + ```java + public class Client { + public static void main(String[] args) { + //获取代理对象 + ProxyFactory factory = new ProxyFactory(); + //必须时代理ji + SellTickets proxyObject = factory.getProxyObject(); + proxyObject.sell(); + } + } + ``` @@ -9918,185 +10068,292 @@ JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可 -#### 程序计数器 - -Program Counter Register 程序计数器(寄存器) +##### 实现原理 -作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空) +JDK 动态代理方式的优缺点: -原理: +- 优点:可以为任意的接口实现类对象做代理,也可以为被代理对象的所有接口的所有方法做代理,动态代理可以在不改变方法源码的情况下,实现对方法功能的增强,提高了软件的可扩展性,Java 反射机制可以生成任意类型的动态代理类 +- 缺点:**只能针对接口或者接口的实现类对象做代理对象**,普通类是不能做代理对象的 +- 原因:**生成的代理类继承了 Proxy**,Java 是单继承的,所以 JDK 动态代理只能代理接口 -* JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程 -* 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号 +ProxyFactory 不是代理模式中的代理类,而代理类是程序在运行过程中动态的在内存中生成的类,可以通过 Arthas 工具查看代理类结构: -特点: +* 代理类($Proxy0)实现了 SellTickets 接口,真实类和代理类实现同样的接口 +* 代理类($Proxy0)将提供了的匿名内部类对象传递给了父类 +* 代理类($Proxy0)的修饰符是 public final -* 是线程私有的 -* **不会存在内存溢出**,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC +```java +// 程序运行过程中动态生成的代理类 +public final class $Proxy0 extends Proxy implements SellTickets { + private static Method m3; -Java 反编译指令:`javap -v Test.class` + public $Proxy0(InvocationHandler invocationHandler) { + super(invocationHandler);//InvocationHandler对象传递给父类 + } -#20:代表去 Constant pool 查看该地址的指令 + static { + m3 = Class.forName("proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]); + } -```java -0: getstatic #20 // PrintStream out = System.out; -3: astore_1 // -- -4: aload_1 // out.println(1); -5: iconst_1 // -- -6: invokevirtual #26 // -- -9: aload_1 // out.println(2); -10: iconst_2 // -- -11: invokevirtual #26 // -- + public final void sell() { + // 调用InvocationHandler的invoke方法 + this.h.invoke(this, m3, null); + } +} + +// Java提供的动态代理相关类 +public class Proxy implements java.io.Serializable { + protected InvocationHandler h; + + protected Proxy(InvocationHandler h) { + this.h = h; + } +} ``` +执行流程如下: + +1. 在测试类中通过代理对象调用 sell() 方法 +2. 根据多态的特性,执行的是代理类($Proxy0)中的 sell() 方法 +3. 代理类($Proxy0)中的 sell() 方法中又调用了 InvocationHandler 接口的子实现类对象的 invoke 方法 +4. invoke 方法通过反射执行了真实对象所属类(TrainStation)中的 sell() 方法 + **** -#### 堆 +##### 源码解析 -Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题 +```java +public static Object newProxyInstance(ClassLoader loader, + Class[] interfaces, + InvocationHandler h){ + // InvocationHandler 为空则抛出异常 + Objects.requireNonNull(h); -存放哪些资源: + // 复制一份 interfaces + final Class[] intfs = interfaces.clone(); + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + checkProxyAccess(Reflection.getCallerClass(), loader, intfs); + } -* 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 -* 字符串常量池: - * 字符串常量池原本存放于方法区,jdk7 开始放置于堆中 - * 字符串常量池**存储的是 String 对象的直接引用或者对象**,是一张 string table -* 静态变量:静态变量是有 static 修饰的变量,jdk7 时从方法区迁移至堆中 -* 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率 + // 从缓存中查找 class 类型的代理对象,会调用 ProxyClassFactory#apply 方法 + Class cl = getProxyClass0(loader, intfs); + //proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()) + + try { + if (sm != null) { + checkNewProxyPermission(Reflection.getCallerClass(), cl); + } -设置堆内存指令:`-Xmx Size` + // 获取代理类的构造方法,根据参数 InvocationHandler 匹配获取某个构造器 + final Constructor cons = cl.getConstructor(constructorParams); + final InvocationHandler ih = h; + // 构造方法不是 pubic 的需要启用权限,暴力p + if (!Modifier.isPublic(cl.getModifiers())) { + AccessController.doPrivileged(new PrivilegedAction() { + public Void run() { + // 设置可访问的权限 + cons.setAccessible(true); + return null; + } + }); + } + // cons 是构造方法,并且内部持有 InvocationHandler,在 InvocationHandler 中持有 target 目标对象 + return cons.newInstance(new Object[]{h}); + } catch (IllegalAccessException|InstantiationException e) {} +} +``` -内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常 +Proxy 的静态内部类: -堆内存诊断工具:(控制台命令) +```java +private static final class ProxyClassFactory { + // 代理类型的名称前缀 + private static final String proxyClassNamePrefix = "$Proxy"; -1. jps:查看当前系统中有哪些 java 进程 -2. jmap:查看堆内存占用情况 `jhsdb jmap --heap --pid 进程id` -3. jconsole:图形界面的,多功能的监测工具,可以连续监测 + // 生成唯一数字使用,结合上面的代理类型名称前缀一起生成 + private static final AtomicLong nextUniqueNumber = new AtomicLong(); -在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**: + //参数一:Proxy.newInstance 时传递的 + //参数二:Proxy.newInstance 时传递的接口集合 + @Override + public Class apply(ClassLoader loader, Class[] interfaces) { + + Map, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length); + // 遍历接口集合 + for (Class intf : interfaces) { + Class interfaceClass = null; + try { + // 加载接口类到 JVM + interfaceClass = Class.forName(intf.getName(), false, loader); + } catch (ClassNotFoundException e) { + } + if (interfaceClass != intf) { + throw new IllegalArgumentException( + intf + " is not visible from class loader"); + } + // 如果 interfaceClass 不是接口 直接报错,保证集合内都是接口 + if (!interfaceClass.isInterface()) { + throw new IllegalArgumentException( + interfaceClass.getName() + " is not an interface"); + } + // 保证接口 interfaces 集合中没有重复的接口 + if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "repeated interface: " + interfaceClass.getName()); + } + } -* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 -* Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区 -* Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理 + // 生成的代理类的包名 + String proxyPkg = null; + // 【生成的代理类访问修饰符 public final】 + int accessFlags = Modifier.PUBLIC | Modifier.FINAL; -分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能 + // 检查接口集合内的接口,看看有没有某个接口的访问修饰符不是 public 的 如果不是 public 的接口, + // 生成的代理类 class 就必须和它在一个包下,否则访问出现问题 + for (Class intf : interfaces) { + // 获取访问修饰符 + int flags = intf.getModifiers(); + if (!Modifier.isPublic(flags)) { + accessFlags = Modifier.FINAL; + // 获取当前接口的全限定名 包名.类名 + String name = intf.getName(); + int n = name.lastIndexOf('.'); + // 获取包名 + String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); + if (proxyPkg == null) { + proxyPkg = pkg; + } else if (!pkg.equals(proxyPkg)) { + throw new IllegalArgumentException( + "non-public interfaces from different packages"); + } + } + } -```java -public static void main(String[] args) { - //返回Java虚拟机中的堆内存总量 - long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; - //返回Java虚拟机使用的最大堆内存量 - long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; - - System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M - System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M + if (proxyPkg == null) { + // if no non-public proxy interfaces, use com.sun.proxy package + proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; + } + + // 获取唯一的编号 + long num = nextUniqueNumber.getAndIncrement(); + // 包名+ $proxy + 数字,比如 $proxy1 + String proxyName = proxyPkg + proxyClassNamePrefix + num; + + // 【生成二进制字节码,这个字节码写入到文件内】,就是编译好的 class 文件 + byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags); + try { + // 【使用加载器加载二进制到 jvm】,并且返回 class + return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); + } catch (ClassFormatError e) { } + } } ``` -*** +*** -#### 方法区 -方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) -方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式** +#### CGLIB -方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError) +CGLIB 是一个功能强大,高性能的代码生成包,为没有实现接口的类提供代理,为 JDK 动态代理提供了补充($$Proxy) -方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 +* CGLIB 是第三方提供的包,所以需要引入 jar 包的坐标: -为了**避免方法区出现 OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,**静态变量和字符串常量池等放入堆中** + ```xml + + cglib + cglib + 2.2.2 + + ``` -类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 +* 代理工厂类: -常量池表(Constant Pool Table)是 Class 文件的一部分,存储了**类在编译期间生成的字面量、符号引用**,JVM 为每个已加载的类维护一个常量池 + ```java + public class ProxyFactory implements MethodInterceptor { + private TrainStation target = new TrainStation(); + + public TrainStation getProxyObject() { + //创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数 + Enhancer enhancer = new Enhancer(); + //设置父类的字节码对象 + enhancer.setSuperclass(target.getClass()); + //设置回调函数 + enhancer.setCallback(new MethodInterceptor() { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + System.out.println("代理点收取一些服务费用(CGLIB动态代理方式)"); + Object o = methodProxy.invokeSuper(obj, args); + return null;//因为返回值为void + } + }); + //创建代理对象 + TrainStation obj = (TrainStation) enhancer.create(); + return obj; + } + } + ``` -- 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等 -- 符号引用:类、字段、方法、接口等的符号引用 +CGLIB 的优缺点 -运行时常量池是方法区的一部分 +* 优点: + * CGLIB 动态代理**不限定**是否具有接口,可以对任意操作进行增强 + * CGLIB 动态代理无需要原始被代理对象,动态创建出新的代理对象 + * **JDKProxy 仅对接口方法做增强,CGLIB 对所有方法做增强**,包括 Object 类中的方法,toString、hashCode 等 +* 缺点:CGLIB 不能对声明为 final 的类或者方法进行代理,因为 CGLIB 原理是**动态生成被代理类的子类,继承被代理类** -* 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池 -* 类在解析阶段将这些符号引用替换成直接引用 -* 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern() -*** +**** -### 本地内存 -#### 基本介绍 +#### 方式对比 -虚拟机内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM +三种方式对比: -本地内存:又叫做**堆外内存**,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM +* 动态代理和静态代理: -本地内存概述图: + * 动态代理将接口中声明的所有方法都被转移到一个集中的方法中处理(InvocationHandler.invoke),在接口方法数量比较多的时候,可以进行灵活处理,不需要像静态代理那样每一个方法进行中转 - + * 静态代理是在编译时就已经将接口、代理类、被代理类的字节码文件确定下来 + * 动态代理是程序**在运行后通过反射创建字节码文件**交由 JVM 加载 +* JDK 代理和 CGLIB 代理: + JDK 动态代理采用 ProxyGenerator.generateProxyClass() 方法在运行时生成字节码;CGLIB 底层采用 ASM 字节码生成框架,使用字节码技术生成代理类。在 JDK1.6之前比使用 Java 反射效率要高,到 JDK1.8 的时候,JDK 代理效率高于 CGLIB 代理。所以如果有接口或者当前类就是接口使用 JDK 动态代理,如果没有接口使用 CGLIB 代理 -*** +代理模式的优缺点: +* 优点: + * 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用 + * **代理对象可以增强目标对象的功能,内部持有原始的目标对象** + * 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度 +* 缺点:增加了系统的复杂度 -#### 元空间 +代理模式的使用场景: -PermGen 被元空间代替,永久代的**类信息、方法、常量池**等都移动到元空间区 - -元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制 +* 远程(Remote)代理:本地服务通过网络请求远程服务,需要实现网络通信,处理其中可能的异常。为了良好的代码设计和可维护性,将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能 -方法区内存溢出: +* 防火墙(Firewall)代理:当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网,当互联网返回响应时,代理服务器再把它转给你的浏览器 -* JDK1.8 以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space +* 保护(Protect or Access)代理:控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限 - ```sh - -XX:MaxPermSize=8m #参数设置 - ``` - -* JDK1.8 以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace - ```sh - -XX:MaxMetaspaceSize=8m #参数设置 - ``` -元空间内存溢出演示: -```java -public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码 - public static void main(String[] args) { - int j = 0; - try { - Demo1_8 test = new Demo1_8(); - for (int i = 0; i < 10000; i++, j++) { - // ClassWriter 作用是生成类的二进制字节码 - ClassWriter cw = new ClassWriter(0); - // 版本号, public, 类名, 包名, 父类, 接口 - cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); - // 返回 byte[] - byte[] code = cw.toByteArray(); - // 执行了类的加载 - test.defineClass("Class" + i, code, 0, code.length); // Class 对象 - } - } finally { - System.out.println(j); - } - } -} -``` @@ -10104,172 +10361,193 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 -#### 直接内存 -直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域 +# JVM +## JVM概述 -直接内存详解参考:NET → NIO → 直接内存 +### 基本介绍 +JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作 +特点: -*** +* Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 +* JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** +Java 代码执行流程:java程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) +JVM 结构: -### 变量位置 + -变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的**声明位置** +JVM、JRE、JDK 对比: -静态内部类和其他内部类: + -* **一个 class 文件只能对应一个 public 类型的类**,这个类可以有内部类,但不会生成新的 class 文件 -* 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到堆(待考证) -类变量: +参考书籍:https://book.douban.com/subject/34907497/ -* 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 java 进程产生和销毁 -* 在 java8 之前把静态变量存放于方法区,在 java8 时存放在堆中的静态变量区 +参考视频:https://www.bilibili.com/video/BV1PJ411n7xZ +参考视频:https://www.bilibili.com/video/BV1yE411Z7AP -实例变量: -* 实例(成员)变量是定义在类中,没有 static 修饰的变量,随着类的实例产生和销毁,是类实例的一部分 -* 在类初始化的时候,从运行时常量池取出直接引用或者值,**与初始化的对象一起放入堆中** -局部变量: +*** -* 局部变量是定义在类的方法中的变量 -* 在所在方法被调用时**放入虚拟机栈的栈帧**中,方法执行结束后从虚拟机栈中弹出, -类常量池、运行时常量池、字符串常量池有什么关系?有什么区别? -* 类常量池与运行时常量池都存储在方法区,而字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中 -* 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符 -* **在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池** -* 对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池 +### 架构模型 -什么是字面量?什么是符号引用? +Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,Java 的指令都是根据栈来设计的,不同平台 CPU 架构不同,所以不能设计为基于寄存器架构 -* 字面量:java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示 +* 基于栈式架构的特点: + * 设计和实现简单,适用于资源受限的系统 + * 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现 + * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器(ACC)中,指令可直接访问寄存器 + * 一地址指令:一个操作码对应一个地址码,通过地址码寻找操作数 + * 不需要硬件的支持,可移植性更好,更好实现跨平台 +* 基于寄存器架构的特点: + * 需要硬件的支持,可移植性差 + * 性能更好,执行更高效,寄存器比内存快 + * 以一地址指令、二地址指令、三地址指令为主 - ```java - int a = 1; //这个1便是字面量 - String b = "iloveu"; //iloveu便是字面量 - ``` -* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 +*** -*** +### 生命周期 +JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 +- **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 +- **运行**: + + - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 + - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 + - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 + - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 + + Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化;Client 模式启动的 JVM 采用的是轻量级的虚拟机 +- **死亡**: + + - 当程序中的用户线程都中止,JVM 才会退出 + - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 + - 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 java 安全管理器允许这次 exit 或 halt 操作 -## 内存管理 -### 内存分配 -#### 两种方式 +*** -不分配内存的对象无法进行其他操作,JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 -* 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 -* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 -*** +## 内存结构 +### 内存概述 +内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区 -#### 分代思想 +JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 -##### 分代介绍 +* Java1.8 以前的内存结构图: + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java7内存结构图.png) -Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代 +* Java1.8 之后的内存结果图: -- 新生代使用:复制算法 -- 老年代使用:标记 - 清除 或者 标记 - 整理 算法 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java8内存结构图.png) -**Minor GC 和 Full GC**: +线程运行诊断: -- Minor GC:回收新生代,新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度比较快 -- Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 很少执行,执行速度会比 Minor GC 慢很多 +* 定位:jps 定位进程 id +* jstack 进程 id:用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息 - Eden 和 Survivor 大小比例默认为 8:1:1 +常见OOM错误: - +* java.lang.StackOverflowError +* java.lang.OutOfMemoryError:java heap space +* java.lang.OutOfMemoryError:GC overhead limit exceeded +* java.lang.OutOfMemoryError:Direct buffer memory +* java.lang.OutOfMemoryError:unable to create new native thread +* java.lang.OutOfMemoryError:Metaspace +*** -*** +### JVM内存 +#### 虚拟机栈 -##### 分代分配 +##### Java栈 -工作机制: +Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所需要的内存 -* **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC -* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且当前对象的年龄会加 1,清空 Eden 区 -* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区 -* To 区永远是空 Survivor 区,From 区是有数据的,每次 MinorGC 后两个区域互换 -* From 区和 To 区 也可以叫做 S0 区和 S1 区 +* 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(**一个方法一个栈帧**) -晋升到老年代: +* Java 虚拟机规范允许 **Java 栈的大小是动态的或者是固定不变的** -* **长期存活的对象进入老年代**:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中 - - `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用 4 个 bit 存储,所以最大值是 15,默认也是 15 -* **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发 GC 以获取足够的连续空间分配给大对象 - - `-XX:PretenureSizeThreshold`:大于此值的对象直接在老年代分配 -* **动态对象年龄判定**:如果在 Survivor 区中相同年龄的对象的所有大小之和超过 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代 +* 虚拟机栈是**每个线程私有的**,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程 -空间分配担保: +* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着: -* 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的 -* 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 + * 局部变量表:存储方法里的java基本数据类型以及对象的引用 + * 动态链接:也叫指向运行时常量池的方法引用 + * 方法返回地址:方法正常退出或者异常退出的定义 + * 操作数栈或表达式栈和其他一些附加信息 + + +设置栈内存大小:`-Xss size` `-Xss 1024k` +* 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M -*** +虚拟机栈特点: +* 栈内存**不需要进行GC**,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据 +* 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大) -#### TLAB +* 方法内的局部变量是否**线程安全**: + * 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析) + * 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全 -TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用 TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** +异常: -- 栈上分配使用的是栈来进行对象内存的分配 -- TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存 +* 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError 异常 +* 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常 -堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 -问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配策略.jpg) +*** -JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在堆中分配内存 -栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存 -参数设置: +##### 局部变量 -* `-XX:UseTLAB`:设置是否开启 TLAB 空间 +局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 -* `-XX:TLABWasteTargetPercent`:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1% -* `-XX:TLABRefillWasteFraction`:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配 +* 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题 +* 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中 +* 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁 +* 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配过程.jpg) +局部变量表最基本的存储单元是 **slot(变量槽)**: + +* 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据 +* 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量 +* 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot +* 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 @@ -10277,223 +10555,183 @@ JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都 -#### 逃逸分析 +##### 操作数栈 -即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善性能的技术,在 HotSpot 实现中有多种选择:C1、C2 和 C1+C2,分别对应 client、server 和分层编译 +栈:可以使用数组或者链表来实现 -* C1 编译速度快,优化方式比较保守;C2 编译速度慢,优化方式比较激进 -* C1+C2 在开始阶段采用 C1 编译,当代码运行到一定热度之后采用 C2 重新编译 +操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop) -逃逸分析并不是直接的优化手段,而是一个代码分析方式,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 +* 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区 -* 方法逃逸:当一个对象在方法中定义之后,被外部方法引用 - * 全局逃逸:一个对象的作用范围逃出了当前方法或者当前线程,比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值 - * 参数逃逸:一个对象被作为方法参数传递或者被参数引用 -* 线程逃逸:如类变量或实例变量,可能被其它线程访问到 +* Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈 +* 如果被调用的方法带有返回值的话,其**返回值将会被压入当前栈帧的操作数栈中** -如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配 +栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率 -* 同步消除 +基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术 - 线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的**同步锁**,通过 `-XX:+EliminateLocks` 可以开启同步消除 ( - 号关闭) -* 标量替换 - * 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问 - * 标量 (scalar) :不可分割的量,如基本数据类型和 reference 类型 - - 聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量 - * 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替 - * 参数设置: - - * `-XX:+EliminateAllocations`:开启标量替换 - * `-XX:+PrintEliminateAllocations`:查看标量替换情况 +*** -* 栈上分配 - JIT 编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需 GC - User 对象的作用域局限在方法 fn 中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成 User 对象,大大减轻 GC 的压力 +##### 动态链接 - ```java - public class JVM { - public static void main(String[] args) throws Exception { - int sum = 0; - int count = 1000000; - //warm up - for (int i = 0; i < count ; i++) { - sum += fn(i); - } - System.out.println(sum); - System.in.read(); - } - private static int fn(int age) { - User user = new User(age); - int i = user.getAge(); - return i; - } - } - - class User { - private final int age; +动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是**动态绑定** + +* 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 + + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接符号引用.png) + +* 在 Java 源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中 - public User(int age) { - this.age = age; - } + 常量池的作用:提供一些符号和常量,便于指令的识别 - public int getAge() { - return age; - } - } - ``` + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接运行时常量池.png) - -*** +*** -### 回收策略 -#### 触发条件 +##### 返回地址 -内存垃圾回收机制主要集中的区域就是线程共享区域:**堆和方法区** +Return Address:存放调用该方法的 PC 寄存器的值 -Minor GC 触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC +方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置 -FullGC 同时回收新生代、老年代和方法区,只会存在一个 FullGC 的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件: +* 正常:调用者的 pc 计数器的值作为返回地址,即调用该方法的指令的**下一条指令的地址** +* 异常:返回地址是要通过异常表来确定 -* 调用 System.gc(): +正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者 - * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 - * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用 System.gc() +异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出 -* 老年代空间不足: +两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值 - * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组 - * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 `-XX:MaxTenuringThreshold` 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 -* 空间分配担保失败 -* JDK 1.7 及以前的永久代(方法区)空间不足 +##### 附加信息 -* Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC +栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息 -手动 GC 测试,VM参数:`-XX:+PrintGcDetails` -```java -public void localvarGC1() { - byte[] buffer = new byte[10 * 1024 * 1024];//10MB - System.gc(); //输出: 不会被回收, FullGC时被放入老年代 -} +*** -public void localvarGC2() { - byte[] buffer = new byte[10 * 1024 * 1024]; - buffer = null; - System.gc(); //输出: 正常被回收 -} - public void localvarGC3() { - { - byte[] buffer = new byte[10 * 1024 * 1024]; - } - System.gc(); //输出: 不会被回收, FullGC时被放入老年代 - } -public void localvarGC4() { - { - byte[] buffer = new byte[10 * 1024 * 1024]; - } - int value = 10; - System.gc(); //输出: 正常被回收,slot复用,局部变量过了其作用域 buffer置空 -} -``` +#### 本地方法栈 +本地方法栈是为虚拟机执行本地方法时提供服务的 -*** +JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植 +* 不需要进行GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常 +* 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一 -#### 安全区域 +* 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 -安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下 +* 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限 -- Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太多可能导致运行时的性能问题 -- 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等 + * 本地方法可以通过本地方法接口来**访问虚拟机内部的运行时数据区** + * 直接从本地内存的堆中分配任意数量的内存 + * 可以直接使用本地处理器中的寄存器 + + + + -在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法: +图片来源:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md -- 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 -- 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 -问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 -安全区域 (Safe Region):指在一段代码片段中,**对象的引用关系不会发生变化**,在这个区域中的任何位置开始 GC 都是安全的 +*** -运行流程: -- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生GC,JVM 会忽略标识为 Safe Region 状态的线程 -- 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC,如果完成了则继续运行,否则线程必须等待 GC 完成,收到可以安全离开 SafeRegion 的信号 +#### 程序计数器 +Program Counter Register 程序计数器(寄存器) +作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空) -*** +原理: +* JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程 +* 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号 +特点: -### 垃圾判断 +* 是线程私有的 +* **不会存在内存溢出**,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC -#### 垃圾介绍 +Java 反编译指令:`javap -v Test.class` -垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** +#20:代表去 Constant pool 查看该地址的指令 -作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象 +```java +0: getstatic #20 // PrintStream out = System.out; +3: astore_1 // -- +4: aload_1 // out.println(1); +5: iconst_1 // -- +6: invokevirtual #26 // -- +9: aload_1 // out.println(2); +10: iconst_2 // -- +11: invokevirtual #26 // -- +``` -垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 -在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** +**** -*** +#### 堆 +Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题 -#### 引用计数法 +存放哪些资源: -引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用) +* 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 +* 字符串常量池: + * 字符串常量池原本存放于方法区,jdk7 开始放置于堆中 + * 字符串常量池**存储的是 String 对象的直接引用或者对象**,是一张 string table +* 静态变量:静态变量是有 static 修饰的变量,jdk7 时从方法区迁移至堆中 +* 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率 -优点: +设置堆内存指令:`-Xmx Size` -- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为 0,可以直接回收 -- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报 OOM 错误 -- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象 +内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常 -缺点: +堆内存诊断工具:(控制台命令) -- 每次对象被引用时,都需要去更新计数器,有一点时间开销 +1. jps:查看当前系统中有哪些 java 进程 +2. jmap:查看堆内存占用情况 `jhsdb jmap --heap --pid 进程id` +3. jconsole:图形界面的,多功能的监测工具,可以连续监测 -- 浪费 CPU 资源,即使内存够用,仍然在运行时进行计数器的统计。 +在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**: -- **无法解决循环引用问题,会引发内存泄露**(最大的缺点) +* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 +* Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区 +* Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理 - ```java - public class Test { - public Object instance = null; - public static void main(String[] args) { - Test a = new Test();// a = 1 - Test b = new Test();// b = 1 - a.instance = b; // b = 2 - b.instance = a; // a = 2 - a = null; // a = 1 - b = null; // b = 1 - } - } - ``` +分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-循环引用.png) +```java +public static void main(String[] args) { + //返回Java虚拟机中的堆内存总量 + long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; + //返回Java虚拟机使用的最大堆内存量 + long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; + + System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M + System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M +} +``` @@ -10501,159 +10739,164 @@ public void localvarGC4() { -#### 可达性分析 +#### 方法区 -##### GC Roots +方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) -可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集 +方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式** -GC Roots 对象: +方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError) -- 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 -- 本地方法栈中引用的对象 -- 堆中类静态属性引用的对象 -- 方法区中的常量引用的对象 -- 字符串常量池(string Table)里的引用 -- 同步锁 synchronized 持有的对象 +方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 -**GC Roots 是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 +为了**避免方法区出现 OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,**静态变量和字符串常量池等放入堆中** +类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 +常量池表(Constant Pool Table)是 Class 文件的一部分,存储了**类在编译期间生成的字面量、符号引用**,JVM 为每个已加载的类维护一个常量池 -*** +- 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等 +- 符号引用:类、字段、方法、接口等的符号引用 +运行时常量池是方法区的一部分 +* 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池 +* 类在解析阶段将这些符号引用替换成直接引用 +* 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern() -##### 工作原理 -可达性分析算法以根对象集合(GCRoots)为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 -分析工作必须在一个保障**一致性的快照**中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World 的一个原因 +*** -基本原理: -- 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链 -- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象 +### 本地内存 -- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象 +#### 基本介绍 - +虚拟机内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM +本地内存:又叫做**堆外内存**,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM +本地内存概述图: -*** + -##### 三色标记 +*** -###### 标记算法 -三色标记法把遍历对象图过程中遇到的对象,标记成以下三种颜色: -- 白色:尚未访问过 -- 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问 -- 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问完成 - -当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为: - -1. 初始时,所有对象都在白色集合 -2. 将 GC Roots 直接引用到的对象挪到灰色集合 -3. 从灰色集合中获取对象: - * 将本对象引用到的其他对象全部挪到灰色集合中 - * 将本对象挪到黑色集合里面 -4. 重复步骤 3,直至灰色集合为空时结束 -5. 结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收 - - +#### 元空间 +PermGen 被元空间代替,永久代的**类信息、方法、常量池**等都移动到元空间区 +元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制 -参考文章:https://www.jianshu.com/p/12544c0ad5c1 +方法区内存溢出: +* JDK1.8 以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space + ```sh + -XX:MaxPermSize=8m #参数设置 + ``` + +* JDK1.8 以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace -**** + ```sh + -XX:MaxMetaspaceSize=8m #参数设置 + ``` +元空间内存溢出演示: +```java +public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码 + public static void main(String[] args) { + int j = 0; + try { + Demo1_8 test = new Demo1_8(); + for (int i = 0; i < 10000; i++, j++) { + // ClassWriter 作用是生成类的二进制字节码 + ClassWriter cw = new ClassWriter(0); + // 版本号, public, 类名, 包名, 父类, 接口 + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); + // 返回 byte[] + byte[] code = cw.toByteArray(); + // 执行了类的加载 + test.defineClass("Class" + i, code, 0, code.length); // Class 对象 + } + } finally { + System.out.println(j); + } + } +} +``` -###### 并发标记 -并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生 -**多标情况:**当 E 变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** +*** -* 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾 -* 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除 - -**漏标情况:** +#### 直接内存 -* 条件一:灰色对象断开了对一个白色对象的引用(直接或间接),即灰色对象原成员变量的引用发生了变化 -* 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 -* 结果:导致该白色对象当作垃圾被 GC,影响到了应用程序的正确性 +直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域 - -代码角度解释漏标: -```java -Object G = objE.fieldG; // 读 -objE.fieldG = null; // 写 -objD.fieldG = G; // 写 -``` +直接内存详解参考:NET → NIO → 直接内存 -为了解决问题,可以操作上面三步,**将对象 G 记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再遍历该集合(重新标记) -> 所以**重新标记需要 STW**,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 -解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理: +*** -* **写屏障 + 增量更新**:黑色对象新增引用,会将黑色对象变成灰色对象,最后对该节节点重新扫描 - 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 - 缺点:对黑色变灰的对象重新扫描所有引用,比较耗费时间 +### 变量位置 -* **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 +变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的**声明位置** - 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了),重新扫描该对象的引用关系 +静态内部类和其他内部类: - SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 +* **一个 class 文件只能对应一个 public 类型的类**,这个类可以有内部类,但不会生成新的 class 文件 -* **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 +* 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到堆(待考证) -以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下: +类变量: -- CMS:写屏障 + 增量更新 -- G1:写屏障 + SATB -- ZGC:读屏障 +* 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 java 进程产生和销毁 +* 在 java8 之前把静态变量存放于方法区,在 java8 时存放在堆中的静态变量区 +实例变量: -*** +* 实例(成员)变量是定义在类中,没有 static 修饰的变量,随着类的实例产生和销毁,是类实例的一部分 +* 在类初始化的时候,从运行时常量池取出直接引用或者值,**与初始化的对象一起放入堆中** +局部变量: +* 局部变量是定义在类的方法中的变量 +* 在所在方法被调用时**放入虚拟机栈的栈帧**中,方法执行结束后从虚拟机栈中弹出, -#### finalization +类常量池、运行时常量池、字符串常量池有什么关系?有什么区别? -Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑 +* 类常量池与运行时常量池都存储在方法区,而字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中 +* 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符 +* **在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池** +* 对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池 -垃圾回收此对象之前,会先调用这个对象的 finalize() 方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等 +什么是字面量?什么是符号引用? -生存 OR 死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于缓刑阶段。**一个无法触及的对象有可能在某个条件下复活自己**,这样对它的回收就是不合理的,所以虚拟机中的对象可能的三种状态: +* 字面量:java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示 -- 可触及的:从根节点开始,可以到达这个对象。 -- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize() 中复活 -- 不可触及的:对象的 finalize() 被调用并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为 **finalize() 只会被调用一次**,等到这个对象再被标记为可回收时就必须回收 + ```java + int a = 1; //这个1便是字面量 + String b = "iloveu"; //iloveu便是字面量 + ``` -永远不要主动调用某个对象的 finalize() 方法,应该交给垃圾回收机制调用,原因: +* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 -* finalize() 时可能会导致对象复活 -* finalize() 方法的执行时间是没有保障的,完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize() 方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 -* 一个糟糕的 finalize() 会严重影响 GC 的性能 @@ -10661,87 +10904,75 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 -#### 引用分析 -无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型 -1. 强引用:被强引用关联的对象不会被回收,只有所有 GCRoots 都不通过强引用引用该对象,才能被垃圾回收 +## 内存管理 - * 强引用可以直接访问目标对象 - * 虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象 - * 强引用可能导致**内存泄漏** +### 内存分配 - ```java - Object obj = new Object();//使用 new 一个新对象的方式来创建强引用 - ``` +#### 两种方式 -2. 软引用(SoftReference):被软引用关联的对象只有在内存不够的情况下才会被回收 +不分配内存的对象无法进行其他操作,JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 - * **仅(可能有强引用,一个对象可以被多个引用)**有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 - * 配合**引用队列来释放软引用自身**,在构造软引用时,可以指定一个引用队列,当软引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况 - * 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用;如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时不会耗尽内存 +* 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 +* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 - ```java - Object obj = new Object(); - SoftReference sf = new SoftReference(obj); - obj = null; // 使对象只被软引用关联 - ``` -3. 弱引用(WeakReference):被弱引用关联的对象一定会被回收,只能存活到下一次垃圾回收发生之前 - * 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 - * 配合引用队列来释放弱引用自身 - * WeakHashMap 用来存储图片信息,可以在内存不足的时候及时回收,避免了 OOM +*** - ```java - Object obj = new Object(); - WeakReference wf = new WeakReference(obj); - obj = null; - ``` -4. 虚引用(PhantomReference):也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个 - * 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象 - * 为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程,能在这个对象被回收时收到一个系统通知 - * 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存 +#### 分代思想 - ```java - Object obj = new Object(); - PhantomReference pf = new PhantomReference(obj, null); - obj = null; - ``` +##### 分代介绍 -5. 终结器引用(finalization) +Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代 +- 新生代使用:复制算法 +- 老年代使用:标记 - 清除 或者 标记 - 整理 算法 +**Minor GC 和 Full GC**: -*** +- Minor GC:回收新生代,新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度比较快 +- Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 很少执行,执行速度会比 Minor GC 慢很多 + Eden 和 Survivor 大小比例默认为 8:1:1 + -#### 无用属性 -##### 无用类 -方法区主要回收的是无用的类 -判定一个类是否是无用的类,需要同时满足下面 3 个条件: -- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例 -- 加载该类的 `ClassLoader` 已经被回收 -- 该类对应的 `java.lang.Class` 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 +*** -虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的**仅仅是可以**,而并不是和对象一样不使用了就会必然被回收 +##### 分代分配 -*** +工作机制: +* **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC +* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且当前对象的年龄会加 1,清空 Eden 区 +* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区 +* To 区永远是空 Survivor 区,From 区是有数据的,每次 MinorGC 后两个区域互换 +* From 区和 To 区 也可以叫做 S0 区和 S1 区 +晋升到老年代: -##### 废弃常量 +* **长期存活的对象进入老年代**:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中 + + `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用 4 个 bit 存储,所以最大值是 15,默认也是 15 +* **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发 GC 以获取足够的连续空间分配给大对象 + + `-XX:PretenureSizeThreshold`:大于此值的对象直接在老年代分配 +* **动态对象年龄判定**:如果在 Survivor 区中相同年龄的对象的所有大小之和超过 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代 -在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该常量,说明常量 "abc" 是废弃常量,如果这时发生内存回收的话**而且有必要的话**(内存不够用),"abc" 就会被系统清理出常量池 +空间分配担保: + +* 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的 +* 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 @@ -10749,163 +10980,215 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 -##### 静态变量 +#### TLAB -类加载时(第一次访问),这个类中所有静态成员就会被加载到静态变量区,该区域的成员一旦创建,直到程序退出才会被回收 +TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用 TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** -如果是静态引用类型的变量,静态变量区只存储一份对象的引用地址,真正的对象在堆内,如果要回收该对象可以设置引用为 null +- 栈上分配使用的是栈来进行对象内存的分配 +- TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存 +堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 +问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占 -参考文章:https://blog.csdn.net/zhengzhb/article/details/7331354 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配策略.jpg) +JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在堆中分配内存 +栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存 -*** +参数设置: +* `-XX:UseTLAB`:设置是否开启 TLAB 空间 +* `-XX:TLABWasteTargetPercent`:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1% +* `-XX:TLABRefillWasteFraction`:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配 -### 回收算法 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配过程.jpg) -#### 标记清除 -当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是 -- 标记清除算法(Mark-Sweep) -- 复制算法(copying) -- 标记压缩算法(Mark-Compact) +*** -标记清除算法,是将垃圾回收分为两个个阶段,分别是**标记和清除** -- **标记**:Collector 从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的 Header 中记录为可达对象,**标记的是引用的对象,不是垃圾** -- **清除**:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收,把分块连接到 **空闲列表**”的单向链表,判断回收后的分块与前一个空闲分块是否连续,若连续会合并这两个分块,之后进行分配时只需要遍历这个空闲列表,就可以找到分块 -- **分配阶段**:程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block,如果找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 block - size 的两部分,返回大小为 size 的分块,并把大小为 block - size 的块返回给空闲列表 +#### 逃逸分析 -算法缺点: +即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善性能的技术,在 HotSpot 实现中有多种选择:C1、C2 和 C1+C2,分别对应 client、server 和分层编译 -- 标记和清除过程效率都不高 -- 会产生大量不连续的内存碎片,导致无法给大对象分配内存,需要维护一个空闲链表 +* C1 编译速度快,优化方式比较保守;C2 编译速度慢,优化方式比较激进 +* C1+C2 在开始阶段采用 C1 编译,当代码运行到一定热度之后采用 C2 重新编译 - +逃逸分析并不是直接的优化手段,而是一个代码分析方式,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 +* 方法逃逸:当一个对象在方法中定义之后,被外部方法引用 + * 全局逃逸:一个对象的作用范围逃出了当前方法或者当前线程,比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值 + * 参数逃逸:一个对象被作为方法参数传递或者被参数引用 +* 线程逃逸:如类变量或实例变量,可能被其它线程访问到 +如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配 -*** - +* 同步消除 + 线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的**同步锁**,通过 `-XX:+EliminateLocks` 可以开启同步消除 ( - 号关闭) -#### 复制算法 +* 标量替换 -复制算法的核心就是,**将原有的内存空间一分为二,每次只用其中的一块**,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收 + * 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问 + * 标量 (scalar) :不可分割的量,如基本数据类型和 reference 类型 + + 聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量 + * 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替 + * 参数设置: + + * `-XX:+EliminateAllocations`:开启标量替换 + * `-XX:+PrintEliminateAllocations`:查看标量替换情况 -应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之则不适合 +* 栈上分配 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-复制算法.png) + JIT 编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需 GC -算法优点: + User 对象的作用域局限在方法 fn 中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成 User 对象,大大减轻 GC 的压力 -- 没有标记和清除过程,实现简单,运行高效 -- 复制过去以后保证空间的连续性,不会出现“碎片”问题。 + ```java + public class JVM { + public static void main(String[] args) throws Exception { + int sum = 0; + int count = 1000000; + //warm up + for (int i = 0; i < count ; i++) { + sum += fn(i); + } + System.out.println(sum); + System.in.read(); + } + private static int fn(int age) { + User user = new User(age); + int i = user.getAge(); + return i; + } + } + + class User { + private final int age; + + public User(int age) { + this.age = age; + } + + public int getAge() { + return age; + } + } + ``` -算法缺点: + -- 主要不足是**只使用了内存的一半** -- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小 +*** -现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 +### 回收策略 -*** +#### 触发条件 +内存垃圾回收机制主要集中的区域就是线程共享区域:**堆和方法区** +Minor GC 触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC -#### 标记整理 +FullGC 同时回收新生代、老年代和方法区,只会存在一个 FullGC 的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件: -标记整理(压缩)算法是在标记清除算法的基础之上,做了优化改进的算法 +* 调用 System.gc(): -标记阶段和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而**解决了碎片化**的问题 + * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 + * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用 System.gc() -优点:不会产生内存碎片 +* 老年代空间不足: -缺点:需要移动大量对象,处理效率比较低 + * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组 + * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 `-XX:MaxTenuringThreshold` 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 - +* 空间分配担保失败 -| | Mark-Sweep | Mark-Compact | Copying | -| -------- | ---------------- | -------------- | ----------------------------------- | -| 速度 | 中等 | 最慢 | 最快 | -| 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) | -| 移动对象 | 否 | 是 | 是 | +* JDK 1.7 及以前的永久代(方法区)空间不足 -- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。 -- 为了尽量兼顾三个指标,标记一整理算法相对来说更平滑一些 +* Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC +手动 GC 测试,VM参数:`-XX:+PrintGcDetails` -*** +```java +public void localvarGC1() { + byte[] buffer = new byte[10 * 1024 * 1024];//10MB + System.gc(); //输出: 不会被回收, FullGC时被放入老年代 +} +public void localvarGC2() { + byte[] buffer = new byte[10 * 1024 * 1024]; + buffer = null; + System.gc(); //输出: 正常被回收 +} + public void localvarGC3() { + { + byte[] buffer = new byte[10 * 1024 * 1024]; + } + System.gc(); //输出: 不会被回收, FullGC时被放入老年代 + } +public void localvarGC4() { + { + byte[] buffer = new byte[10 * 1024 * 1024]; + } + int value = 10; + System.gc(); //输出: 正常被回收,slot复用,局部变量过了其作用域 buffer置空 +} +``` -#### 增量收集 -增量收集算法:通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作,基础仍是标记-清除和复制算法,用于多线程并发环境 -工作原理:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,影响系统的交互性,所以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复直到垃圾收集完成 +*** -缺点:线程切换和上下文转换消耗资源,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降 +#### 安全区域 +安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下 +- Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太多可能导致运行时的性能问题 +- 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等 -*** +在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法: +- 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 +- 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 +问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 -### 垃圾回收器 +安全区域 (Safe Region):指在一段代码片段中,**对象的引用关系不会发生变化**,在这个区域中的任何位置开始 GC 都是安全的 -#### 概述 +运行流程: -垃圾收集器分类: +- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生GC,JVM 会忽略标识为 Safe Region 状态的线程 -* 按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器 - * 除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行 -* 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器 - * 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间 - * 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束 -* 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器 - * 压缩式垃圾回收器在回收完成后进行压缩整理,消除回收后的碎片,再分配对象空间使用指针碰撞 - * 非压缩式的垃圾回收器不进行这步操作,再分配对象空间使用空闲列表 -* 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器 +- 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC,如果完成了则继续运行,否则线程必须等待 GC 完成,收到可以安全离开 SafeRegion 的信号 -GC 性能指标: -- **吞吐量**:程序的运行时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间) -- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例 -- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间 -- 收集频率:相对于应用程序的执行,收集操作发生的频率 -- 内存占用:Java 堆区所占的内存大小 -- 快速:一个对象从诞生到被回收所经历的时间 -**垃圾收集器的组合关系**: +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器关系图.png) -新生代收集器:Serial、ParNew、Paralle1 Scavenge; -老年代收集器:Serial old、Parallel old、CMS; +### 垃圾判断 -整堆收集器:G1 +#### 垃圾介绍 -* 红色虚线在 JDK9 移除、绿色虚线在 JDK14 弃用该组合、青色虚线在 JDK14 删除 CMS 垃圾回收器 +垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** -查看默认的垃圾收回收器: +作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象 -* `-XX:+PrintcommandLineFlags`:查看命令行相关参数(包含使用的垃圾收集器) +垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 -* 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程 ID +在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** @@ -10913,209 +11196,199 @@ GC 性能指标: -#### Serial +#### 引用计数法 -Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法** +引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用) -**STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且 java 应用中的所有线程都要暂停,等待垃圾回收的完成 +优点: -**Serial old**:执行老年代垃圾回收的串行收集器,内存回收算法使用的是**标记-整理算法**,同样也采用了串行回收和 STW 机制 +- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为 0,可以直接回收 +- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报 OOM 错误 +- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象 -- Serial old 是 Client 模式下默认的老年代的垃圾回收器 -- Serial old 在 Server 模式下主要有两个用途: - - 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用 - - 作为老年代 CMS 收集器的**后备垃圾回收方案**,在并发收集发生 Concurrent Mode Failure 时使用 +缺点: -开启参数:`-XX:+UseSerialGC` 等价于新生代用 Serial GC 且老年代用 Serial old GC +- 每次对象被引用时,都需要去更新计数器,有一点时间开销 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-Serial收集器.png) +- 浪费 CPU 资源,即使内存够用,仍然在运行时进行计数器的统计。 -优点:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 +- **无法解决循环引用问题,会引发内存泄露**(最大的缺点) -缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如 JavaWeb 应用 + ```java + public class Test { + public Object instance = null; + public static void main(String[] args) { + Test a = new Test();// a = 1 + Test b = new Test();// b = 1 + a.instance = b; // b = 2 + b.instance = a; // a = 2 + a = null; // a = 1 + b = null; // b = 1 + } + } + ``` +![](https://gitee.com/seazean/images/raw/master/Java/JVM-循环引用.png) -**** +*** -#### Parallel -Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和 Stop the World 机制 +#### 可达性分析 -Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** +##### GC Roots -对比其他回收器: +可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集 -* 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间 -* Parallel 目标是达到一个可控制的吞吐量,被称为**吞吐量优先**收集器 -* Parallel Scavenge 对比 ParNew 拥有**自适应调节策略**,可以通过一个开关参数打开 GC Ergonomics +GC Roots 对象: -应用场景: +- 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 +- 本地方法栈中引用的对象 +- 堆中类静态属性引用的对象 +- 方法区中的常量引用的对象 +- 字符串常量池(string Table)里的引用 +- 同步锁 synchronized 持有的对象 -* 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验 -* 高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互 +**GC Roots 是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 -停顿时间和吞吐量的关系:新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降 -在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 收集器,在 Server 模式下的内存回收性能很好,**Java8 默认是此垃圾收集器组合** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParallelScavenge收集器.png) +*** -参数配置: -* `-XX:+UseParallelGC`:手动指定年轻代使用Paralle并行收集器执行内存回收任务 -* `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 - * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认 jdk8 是开启的 -* `-XX:+UseAdaptivesizepplicy`:设置 Parallel scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 -* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般最好与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 - * 在默认情况下,当 CPU 数量小于 8 个,ParallelGcThreads 的值等于 CPU 数量 - * 当 CPU 数量大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU Count]/8] -* `-XX:MaxGCPauseMillis`:设置垃圾收集器最大停顿时间(即 STW 的时间),单位是毫秒 - * 对于用户来讲,停顿时间越短体验越好;在服务器端,注重高并发,整体的吞吐量 - * 为了把停顿时间控制在 MaxGCPauseMillis 以内,收集器在工作时会调整 Java 堆大小或其他一些参数 -* `-XX:GCTimeRatio`:垃圾收集时间占总时间的比例 =1/(N+1),用于衡量吞吐量的大小 - * 取值范围(0,100)。默认值 99,也就是垃圾回收时间不超过1 - * 与 `-xx:MaxGCPauseMillis` 参数有一定矛盾性,暂停时间越长,Radio 参数就容易超过设定的比例 +##### 工作原理 +可达性分析算法以根对象集合(GCRoots)为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 -*** +分析工作必须在一个保障**一致性的快照**中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World 的一个原因 +基本原理: +- 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链 -#### ParNew +- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象 -Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 +- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象 -并行垃圾收集器在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间 + -对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有**ParNew GC 能与 CMS 收集器配合工作** - -相关参数: -* `-XX:+UseParNewGC`:表示年轻代使用并行收集器,不影响老年代 -* `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParNew收集器.png) -ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 -- 对于新生代,回收次数频繁,使用并行方式高效 -- 对于老年代,回收次数少,使用串行方式节省资源(CPU 并行需要切换线程,串行可以省去切换线程的资源) +##### 三色标记 +###### 标记算法 +三色标记法把遍历对象图过程中遇到的对象,标记成以下三种颜色: -**** +- 白色:尚未访问过 +- 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问 +- 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问完成 +当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为: +1. 初始时,所有对象都在白色集合 +2. 将 GC Roots 直接引用到的对象挪到灰色集合 +3. 从灰色集合中获取对象: + * 将本对象引用到的其他对象全部挪到灰色集合中 + * 将本对象挪到黑色集合里面 +4. 重复步骤 3,直至灰色集合为空时结束 +5. 结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收 -#### CMS + -CMS 全称 Concurrent Mark Sweep,是一款**并发的、使用标记-清除**算法、针对老年代的垃圾回收器,其最大特点是**让垃圾收集线程与用户线程同时工作** -CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验 -分为以下四个流程: +参考文章:https://www.jianshu.com/p/12544c0ad5c1 -- 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 -- 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行 -- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) -- 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 -Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因: -* Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行 +**** -* Mark Compact 更适合 Stop The World 场景 -在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-CMS收集器.png) +###### 并发标记 -优点:并发收集、低延迟 +并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生 -缺点: +**多标情况:**当 E 变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** -- 吞吐量降低:在并发阶段虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,CPU 利用率不够高 -- CMS 收集器**无法处理浮动垃圾**,可能出现 Concurrent Mode Failure 导致另一次 Full GC 的产生 - - 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾(产生了新对象),这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 -- 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配 +* 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾 +* 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除 -参数设置: + -* `-XX:+UseConcMarkSweepGC`:手动指定使用 CMS 收集器执行内存回收任务 +**漏标情况:** - 开启该参数后会自动将 `-XX:+UseParNewGC` 打开,即:ParNew + CMS + Serial old的组合 +* 条件一:灰色对象断开了对一个白色对象的引用(直接或间接),即灰色对象原成员变量的引用发生了变化 +* 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 +* 结果:导致该白色对象当作垃圾被 GC,影响到了应用程序的正确性 -* `-XX:CMSInitiatingoccupanyFraction`:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收 + - * JDK5 及以前版本的默认值为 68,即当老年代的空间使用率达到 68% 时,会执行一次CMS回收 - * JDK6 及以上版本默认值为 92% +代码角度解释漏标: -* `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完 Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 +```java +Object G = objE.fieldG; // 读 +objE.fieldG = null; // 写 +objD.fieldG = G; // 写 +``` -* `-XX:CMSFullGCsBeforecompaction`:**设置在执行多少次 Full GC 后对内存空间进行压缩整理** +为了解决问题,可以操作上面三步,**将对象 G 记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再遍历该集合(重新标记) -* `-XX:ParallelCMSThreads`:**设置CMS的线程数量** +> 所以**重新标记需要 STW**,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 - * CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数 - * 收集线程占用的 CPU 资源多于25%,对用户程序影响可能较大;当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 +解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理: +* **写屏障 + 增量更新**:黑色对象新增引用,会将黑色对象变成灰色对象,最后对该节节点重新扫描 + 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 -*** + 缺点:对黑色变灰的对象重新扫描所有引用,比较耗费时间 +* **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 + 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了),重新扫描该对象的引用关系 -#### G1 + SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 -##### G1特点 +* **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 -G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替 CMS,适用于较大的堆(>4 ~ 6G),在 JDK9 之后默认使用 G1 +以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下: -G1 对比其他处理器的优点: +- CMS:写屏障 + 增量更新 +- G1:写屏障 + SATB +- ZGC:读屏障 -* 并发与并行: - * 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW - * 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 - * 其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,JVM 的 GC 线程处理速度慢时,系统会**调用应用程序线程加速垃圾回收**过程 -* **分区算法**: - * 从分代上看,G1 属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区。从堆结构上看,**新生代和老年代不再物理隔离**,不用担心每个代内存是否足够,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC - * 将整个堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32 MB之间且为 2 的 N 次幂,所有 Region 大小相同,在 JVM 生命周期内不会被改变。G1 把堆划分成多个大小相等的独立区域,使得每个小空间可以单独进行垃圾回收 - * **新的区域 Humongous**:本身属于老年代区,当出现了一个巨型对象超出了分区容量的一半,该对象就会进入到该区域。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储,为了能找到连续的H区,有时候不得不启动 Full GC - * G1 不会对巨型对象进行拷贝,回收时被优先考虑,G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉 - - * Region 结构图: - -![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) +*** -- 空间整合: - - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 - - G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 -- **可预测的停顿时间模型(软实时 soft real-time)**:可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 +#### finalization - - 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 - - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率 +Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑 - * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 +垃圾回收此对象之前,会先调用这个对象的 finalize() 方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等 -G1垃圾收集器的缺点: +生存 OR 死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于缓刑阶段。**一个无法触及的对象有可能在某个条件下复活自己**,这样对它的回收就是不合理的,所以虚拟机中的对象可能的三种状态: -* 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高 -* 从经验上来说,在小内存应用上 CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势。平衡点在 6-8GB 之间 +- 可触及的:从根节点开始,可以到达这个对象。 +- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize() 中复活 +- 不可触及的:对象的 finalize() 被调用并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为 **finalize() 只会被调用一次**,等到这个对象再被标记为可回收时就必须回收 -应用场景: +永远不要主动调用某个对象的 finalize() 方法,应该交给垃圾回收机制调用,原因: -* 面向服务端应用,针对具有大内存、多处理器的机器 -* 需要低 GC 延迟,并具有大堆的应用程序提供解决方案 +* finalize() 时可能会导致对象复活 +* finalize() 方法的执行时间是没有保障的,完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize() 方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 +* 一个糟糕的 finalize() 会严重影响 GC 的性能 @@ -11123,282 +11396,278 @@ G1垃圾收集器的缺点: -##### 记忆集 +#### 引用分析 -记忆集 Remembered Set 在新生代中,每个 Region 都有一个 Remembered Set,用来被哪些其他 Region 里的对象引用(谁引用了我就记录谁) +无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型 - +1. 强引用:被强引用关联的对象不会被回收,只有所有 GCRoots 都不通过强引用引用该对象,才能被垃圾回收 -* 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(跨代引用),不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 -* 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 + * 强引用可以直接访问目标对象 + * 虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象 + * 强引用可能导致**内存泄漏** -垃圾收集器在新生代中建立了记忆集这样的数据结构,可以理解为它是一个抽象类,具体实现记忆集的三种方式: + ```java + Object obj = new Object();//使用 new 一个新对象的方式来创建强引用 + ``` -* 字长精度 -* 对象精度 -* 卡精度(卡表) +2. 软引用(SoftReference):被软引用关联的对象只有在内存不够的情况下才会被回收 -卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等,卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,JVM 对于卡页的维护也是通过写屏障的方式 + * **仅(可能有强引用,一个对象可以被多个引用)**有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 + * 配合**引用队列来释放软引用自身**,在构造软引用时,可以指定一个引用队列,当软引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况 + * 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用;如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时不会耗尽内存 -收集集合 CSet 代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中 + ```java + Object obj = new Object(); + SoftReference sf = new SoftReference(obj); + obj = null; // 使对象只被软引用关联 + ``` -* CSet of Young Collection -* CSet of Mix Collection +3. 弱引用(WeakReference):被弱引用关联的对象一定会被回收,只能存活到下一次垃圾回收发生之前 + * 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 + * 配合引用队列来释放弱引用自身 + * WeakHashMap 用来存储图片信息,可以在内存不足的时候及时回收,避免了 OOM + ```java + Object obj = new Object(); + WeakReference wf = new WeakReference(obj); + obj = null; + ``` -*** +4. 虚引用(PhantomReference):也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个 + * 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象 + * 为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程,能在这个对象被回收时收到一个系统通知 + * 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存 + ```java + Object obj = new Object(); + PhantomReference pf = new PhantomReference(obj, null); + obj = null; + ``` -##### 工作原理 +5. 终结器引用(finalization) -G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不同的条件下被触发 -* 当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程 -* 标记完成马上开始混合回收过程 - +*** -顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收 -* **Young GC**:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1 停止应用程序的执行 STW,把活跃对象放入老年代,垃圾对象回收 - **回收过程**: +#### 无用属性 - 1. 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口 - 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 - * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet - * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 - 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的分区放入 Young CSet 中进行回收 - 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 - 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 +##### 无用类 -* **Concurrent Mark **: +方法区主要回收的是无用的类 - * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC - * 根区域扫描 (Root Region Scanning):扫描 Survivor 区中指向老年代的,被初始标记标记了的引用及引用的对象,这一个过程是并发进行的,但是必须在 Young GC 之前完成 - * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 - * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(**防止漏标**) - * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 +判定一个类是否是无用的类,需要同时满足下面 3 个条件: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) +- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例 +- 加载该类的 `ClassLoader` 已经被回收 +- 该类对应的 `java.lang.Class` 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 -* **Mixed GC**:当很多对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会**回收一部分**的 old region,过程同 YGC +虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的**仅仅是可以**,而并不是和对象一样不使用了就会必然被回收 - 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些老年代 region 收集,对垃圾回收的时间进行控制 - 在 G1 中,Mixed GC 可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 -* **Full GC**:对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC +*** - 产生 Full GC 的原因: - * 晋升时没有足够的空间存放晋升的对象 - * 并发处理过程完成之前空间耗尽,浮动垃圾 +##### 废弃常量 +在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该常量,说明常量 "abc" 是废弃常量,如果这时发生内存回收的话**而且有必要的话**(内存不够用),"abc" 就会被系统清理出常量池 -*** +*** -##### 相关参数 -- `-XX:+UseG1GC`:手动指定使用 G1 垃圾收集器执行内存回收任务 -- `-XX:G1HeapRegionSize`:设置每个 Region 的大小。值是 2 的幂,范围是 1MB 到 32MB 之间,目标是根据最小的 Java 堆大小划分出约 2048 个区域,默认是堆内存的 1/2000 -- `-XX:MaxGCPauseMillis`:设置期望达到的最大 GC 停顿时间指标,JVM会尽力实现,但不保证达到,默认值是 200ms -- `-XX:+ParallelGcThread`:设置 STW 时 GC 线程数的值,最多设置为 8 -- `-XX:ConcGCThreads`:设置并发标记线程数,设置为并行垃圾回收线程数 ParallelGcThreads 的1/4左右 -- `-XX:InitiatingHeapoccupancyPercent`:设置触发并发 Mixed GC 周期的 Java 堆占用率阈值,超过此值,就触发 GC,默认值是 45 -- `-XX:+ClassUnloadingWithConcurrentMark`:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 -- `-XX:G1NewSizePercent`:新生代占用整个堆内存的最小百分比(默认5%) -- `-XX:G1MaxNewSizePercent`:新生代占用整个堆内存的最大百分比(默认60%) -- `-XX:G1ReservePercent=10`:保留内存区域,防止 to space(Survivor中的 to 区)溢出 +##### 静态变量 +类加载时(第一次访问),这个类中所有静态成员就会被加载到静态变量区,该区域的成员一旦创建,直到程序退出才会被回收 -*** +如果是静态引用类型的变量,静态变量区只存储一份对象的引用地址,真正的对象在堆内,如果要回收该对象可以设置引用为 null -##### 调优 +参考文章:https://blog.csdn.net/zhengzhb/article/details/7331354 -G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优: -1. 开启 G1 垃圾收集器 -2. 设置堆的最大内存 -3. 设置最大的停顿时间(STW) -不断调优暂停时间指标: +*** -* `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1会根据这个参数选择 CSet 来满足响应时间的设置 -* 设置到 100ms 或者 200ms 都可以(不同情况下会不一样),但设置成50ms就不太合理 -* 暂停时间设置的太短,就会导致出现 G1 跟不上垃圾产生的速度,最终退化成 Full GC -* 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 -不要设置新生代和老年代的大小: - -- 避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标 -- 设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们需要做的只是设置整个堆内存的大小,剩下的交给 G1 自己去分配各个代的大小 +### 回收算法 +#### 标记清除 -*** +当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是 +- 标记清除算法(Mark-Sweep) +- 复制算法(copying) +- 标记压缩算法(Mark-Compact) +标记清除算法,是将垃圾回收分为两个个阶段,分别是**标记和清除** -#### ZGC +- **标记**:Collector 从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的 Header 中记录为可达对象,**标记的是引用的对象,不是垃圾** +- **清除**:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收,把分块连接到 **空闲列表**”的单向链表,判断回收后的分块与前一个空闲分块是否连续,若连续会合并这两个分块,之后进行分配时只需要遍历这个空闲列表,就可以找到分块 -ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region 内存布局的,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现**可并发的标记压缩算法** +- **分配阶段**:程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block,如果找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 block - size 的两部分,返回大小为 size 的分块,并把大小为 block - size 的块返回给空闲列表 -* 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 -* 染色指针:直接**将少量额外的信息存储在指针上的技术**,从 64 位的指针中拿高 4 位来标识对象此时的状态 - * 染色指针可以使某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用 - * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集、是否被移动过(Remapped)、是否只能通过 finalize() 方法才能被访问到(Finalizable) - * 可以大幅减少在垃圾收集过程中内存屏障的使用数量,写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作 - * 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据 -* 内存多重映射:多个虚拟地址指向同一个物理地址 +算法缺点: -可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就根据转发表访问新的移动对象,并更新引用,不会像 G1 一样必须等待垃圾回收完成才能访问 +- 标记和清除过程效率都不高 +- 会产生大量不连续的内存碎片,导致无法给大对象分配内存,需要维护一个空闲链表 -ZGC 目标: + -- 停顿时间不会超过 10ms -- 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在 10ms 以下) -- 可支持几百 M,甚至几 T 的堆大小(最大支持4T) -ZGC 的工作过程可以分为 4 个阶段: -* 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过初始标记和最终标记,需要短暂停顿 -* 并发预备重分配( Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) -* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 -* 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象,这样合并节省了一次遍历的开销 +*** -ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 -优点:高吞吐量、低延迟 -缺点:浮动垃圾,当 ZGC 准备要对一个很大的堆做一次完整的并发收集,其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象产生浮动垃圾 +#### 复制算法 +复制算法的核心就是,**将原有的内存空间一分为二,每次只用其中的一块**,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收 +应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之则不适合 -参考文章:https://www.cnblogs.com/jimoer/p/13170249.html +![](https://gitee.com/seazean/images/raw/master/Java/JVM-复制算法.png) +算法优点: +- 没有标记和清除过程,实现简单,运行高效 +- 复制过去以后保证空间的连续性,不会出现“碎片”问题。 -*** +算法缺点: +- 主要不足是**只使用了内存的一半** +- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小 +现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 -#### 总结 -Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: -- 最小化地使用内存和并行开销,选 Serial GC -- 最大化应用程序的吞吐量,选 Parallel GC -- 最小化 GC 的中断或停顿时间,选 CMS GC +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器总结.png) +#### 标记整理 +标记整理(压缩)算法是在标记清除算法的基础之上,做了优化改进的算法 +标记阶段和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而**解决了碎片化**的问题 -*** +优点:不会产生内存碎片 +缺点:需要移动大量对象,处理效率比较低 + -### 内存泄漏 +| | Mark-Sweep | Mark-Compact | Copying | +| -------- | ---------------- | -------------- | ----------------------------------- | +| 速度 | 中等 | 最慢 | 最快 | +| 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) | +| 移动对象 | 否 | 是 | 是 | -#### 泄露溢出 +- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。 +- 为了尽量兼顾三个指标,标记一整理算法相对来说更平滑一些 -内存泄漏(Memory Leak):是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 -可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。由于代码的实现不同就会出现很多种内存泄漏问题,让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏 -内存溢出(out of memory)指的是申请内存时,没有足够的内存可以使用 +*** -内存泄漏和内存溢出的关系:内存泄漏的越来越多,最终会导致内存溢出 +#### 增量收集 -*** +增量收集算法:通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作,基础仍是标记-清除和复制算法,用于多线程并发环境 +工作原理:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,影响系统的交互性,所以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复直到垃圾收集完成 +缺点:线程切换和上下文转换消耗资源,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降 -#### 几种情况 -##### 静态集合 -静态集合类的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。原因是**长生命周期的对象持有短生命周期对象的引用**,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收 -```java -public class MemoryLeak { - static List list = new ArrayList(); - public void oomTest(){ - Object obj = new Object();//局部变量 - list.add(obj); - } -} -``` +*** -*** +### 垃圾回收器 +#### 概述 -##### 单例模式 +垃圾收集器分类: -单例模式和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏 +* 按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器 + * 除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行 +* 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器 + * 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间 + * 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束 +* 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器 + * 压缩式垃圾回收器在回收完成后进行压缩整理,消除回收后的碎片,再分配对象空间使用指针碰撞 + * 非压缩式的垃圾回收器不进行这步操作,再分配对象空间使用空闲列表 +* 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器 +GC 性能指标: +- **吞吐量**:程序的运行时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间) +- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例 +- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间 +- 收集频率:相对于应用程序的执行,收集操作发生的频率 +- 内存占用:Java 堆区所占的内存大小 +- 快速:一个对象从诞生到被回收所经历的时间 -**** +**垃圾收集器的组合关系**: +![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器关系图.png) +新生代收集器:Serial、ParNew、Paralle1 Scavenge; -##### 内部类 +老年代收集器:Serial old、Parallel old、CMS; -内部类持有外部类的情况,如果一个外部类的实例对象调用方法返回了一个内部类的实例对象,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象也不会被回收,造成内存泄漏 +整堆收集器:G1 +* 红色虚线在 JDK9 移除、绿色虚线在 JDK14 弃用该组合、青色虚线在 JDK14 删除 CMS 垃圾回收器 +查看默认的垃圾收回收器: -*** +* `-XX:+PrintcommandLineFlags`:查看命令行相关参数(包含使用的垃圾收集器) +* 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程 ID -##### 连接相关 -数据库连接、网络连接和 IO 连接等,当不再使用时,需要显式调用 close 方法来释放与连接,垃圾回收器才会回收对应的对象,否则将会造成大量的对象无法被回收,从而引起内存泄漏 +*** -**** +#### Serial +Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法** +**STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且 java 应用中的所有线程都要暂停,等待垃圾回收的完成 -##### 不合理域 +**Serial old**:执行老年代垃圾回收的串行收集器,内存回收算法使用的是**标记-整理算法**,同样也采用了串行回收和 STW 机制 -变量不合理的作用域,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏;如果没有及时地把对象设置为 null,也有可能导致内存泄漏的发生 +- Serial old 是 Client 模式下默认的老年代的垃圾回收器 +- Serial old 在 Server 模式下主要有两个用途: + - 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用 + - 作为老年代 CMS 收集器的**后备垃圾回收方案**,在并发收集发生 Concurrent Mode Failure 时使用 -```java -public class UsingRandom { - private String msg; - public void receiveMsg(){ - msg = readFromNet();// 从网络中接受数据保存到 msg 中 - saveDB(msg); // 把 msg 保存到数据库中 - } -} -``` +开启参数:`-XX:+UseSerialGC` 等价于新生代用 Serial GC 且老年代用 Serial old GC -通过 readFromNet 方法把接收消息保存在 msg 中,然后调用 saveDB 方法把内容保存到数据库中,此时 msg 已经可以被回收,但是 msg 的生命周期与对象的生命周期相同,造成 msg 不能回收,产生内存泄漏 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-Serial收集器.png) -解决: +优点:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 -* msg 变量可以放在 receiveMsg 方法内部,当方法使用完,msg 的生命周期也就结束,就可以被回收了 -* 在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。 +缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如 JavaWeb 应用 @@ -11406,255 +11675,262 @@ public class UsingRandom { -##### 改变哈希 - -当一个对象被存储进 HashSet 集合中以后,就**不能修改这个对象中的那些参与计算哈希值的字段**,否则对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值不同,这种情况下使用该对象的当前引用作为的参数去 HashSet 集合中检索对象返回 false,导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏 +#### Parallel +Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和 Stop the World 机制 +Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** -*** +对比其他回收器: +* 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间 +* Parallel 目标是达到一个可控制的吞吐量,被称为**吞吐量优先**收集器 +* Parallel Scavenge 对比 ParNew 拥有**自适应调节策略**,可以通过一个开关参数打开 GC Ergonomics +应用场景: -##### 缓存泄露 +* 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验 +* 高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互 -内存泄漏的一个常见来源是缓存,一旦把对象引用放入到缓存中,就会很容易被遗忘 +停顿时间和吞吐量的关系:新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降 -使用 WeakHashMap 代表缓存,当除了自身有对 key 的引用外没有其他引用,map 会自动丢弃此值 +在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 收集器,在 Server 模式下的内存回收性能很好,**Java8 默认是此垃圾收集器组合** +![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParallelScavenge收集器.png) +参数配置: -*** +* `-XX:+UseParallelGC`:手动指定年轻代使用Paralle并行收集器执行内存回收任务 +* `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 + * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认 jdk8 是开启的 +* `-XX:+UseAdaptivesizepplicy`:设置 Parallel scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 +* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般最好与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 + * 在默认情况下,当 CPU 数量小于 8 个,ParallelGcThreads 的值等于 CPU 数量 + * 当 CPU 数量大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU Count]/8] +* `-XX:MaxGCPauseMillis`:设置垃圾收集器最大停顿时间(即 STW 的时间),单位是毫秒 + * 对于用户来讲,停顿时间越短体验越好;在服务器端,注重高并发,整体的吞吐量 + * 为了把停顿时间控制在 MaxGCPauseMillis 以内,收集器在工作时会调整 Java 堆大小或其他一些参数 +* `-XX:GCTimeRatio`:垃圾收集时间占总时间的比例 =1/(N+1),用于衡量吞吐量的大小 + * 取值范围(0,100)。默认值 99,也就是垃圾回收时间不超过1 + * 与 `-xx:MaxGCPauseMillis` 参数有一定矛盾性,暂停时间越长,Radio 参数就容易超过设定的比例 -##### 监听器 +*** -监听器和其他回调情况,假如客户端在实现的 API 中注册回调,却没有显式的取消,那么就会一直积聚下去,所以确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,比如保存为 WeakHashMap 中的键 +#### ParNew -*** +Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 +并行垃圾收集器在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间 +对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有**ParNew GC 能与 CMS 收集器配合工作** -#### 案例分析 +相关参数: -```java -public class Stack { - private Object[] elements; - private int size = 0; - private static final int DEFAULT_INITIAL_CAPACITY = 16; +* `-XX:+UseParNewGC`:表示年轻代使用并行收集器,不影响老年代 - public Stack() { - elements = new Object[DEFAULT_INITIAL_CAPACITY]; - } +* `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 - public void push(Object e) { //入栈 - ensureCapacity(); - elements[size++] = e; - } +![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParNew收集器.png) - public Object pop() { //出栈 - if (size == 0) - throw new EmptyStackException(); - return elements[--size]; - } +ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 - private void ensureCapacity() { - if (elements.length == size) - elements = Arrays.copyOf(elements, 2 * size + 1); - } -} -``` +- 对于新生代,回收次数频繁,使用并行方式高效 +- 对于老年代,回收次数少,使用串行方式节省资源(CPU 并行需要切换线程,串行可以省去切换线程的资源) -程序并没有明显错误,但 pop 函数存在内存泄漏问题,因为 pop 函数只是把栈顶索引下移一位,并没有把上一个出栈索引处的引用置空,导致栈数组一直强引用着已经出栈的对象 -解决方法: -```java -public Object pop() { - if (size == 0) - throw new EmptyStackException(); - Object result = elements[--size]; - elements[size] = null; - return result; -} -``` +**** +#### CMS +CMS 全称 Concurrent Mark Sweep,是一款**并发的、使用标记-清除**算法、针对老年代的垃圾回收器,其最大特点是**让垃圾收集线程与用户线程同时工作** -*** +CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验 +分为以下四个流程: +- 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 +- 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行 +- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) +- 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 +Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因: +* Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行 -## 类加载 +* Mark Compact 更适合 Stop The World 场景 -### 对象访存 +在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿 -#### 存储结构 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-CMS收集器.png) -一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding) +优点:并发收集、低延迟 -对象头: +缺点: -* 普通对象:分为两部分 +- 吞吐量降低:在并发阶段虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,CPU 利用率不够高 +- CMS 收集器**无法处理浮动垃圾**,可能出现 Concurrent Mode Failure 导致另一次 Full GC 的产生 + + 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾(产生了新对象),这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 +- 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配 - * **Mark Word**:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等 +参数设置: - ```ruby - hash(25) + age(4) + lock(3) = 32bit #32位系统 - unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 - ``` +* `-XX:+UseConcMarkSweepGC`:手动指定使用 CMS 收集器执行内存回收任务 - * **Klass Word**:类型指针,**指向该对象的 Class 类对象的指针**,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 **Java 中的一个引用的大小**) + 开启该参数后会自动将 `-XX:+UseParNewGC` 打开,即:ParNew + CMS + Serial old的组合 - ```ruby - |-----------------------------------------------------| - | Object Header (64 bits) | - |---------------------------|-------------------------| - | Mark Word (32 bits) | Klass Word (32 bits) | - |---------------------------|-------------------------| - ``` +* `-XX:CMSInitiatingoccupanyFraction`:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收 -* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度(12 字节) + * JDK5 及以前版本的默认值为 68,即当老年代的空间使用率达到 68% 时,会执行一次CMS回收 + * JDK6 及以上版本默认值为 92% - ```ruby - |-------------------------------------------------------------------------------| - | Object Header (96 bits) | - |-----------------------|-----------------------------|-------------------------| - | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | - |-----------------------|-----------------------------|-------------------------| - ``` +* `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完 Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 -实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 +* `-XX:CMSFullGCsBeforecompaction`:**设置在执行多少次 Full GC 后对内存空间进行压缩整理** -对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求**对象起始地址必须是 8 字节的整数倍**,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 +* `-XX:ParallelCMSThreads`:**设置CMS的线程数量** -32 位系统: + * CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数 + * 收集线程占用的 CPU 资源多于25%,对用户程序影响可能较大;当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 -* 一个 int 在 java 中占据 4byte,所以 Integer 的大小为: - ```java - private final int value; - ``` - ```ruby - # 需要补位4byte - 4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte - ``` +*** -* `int[] arr = new int[10]` - ```ruby - # 由于需要8位对齐,所以最终大小为`56byte`。 - 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte - ``` +#### G1 +##### G1特点 -*** +G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替 CMS,适用于较大的堆(>4 ~ 6G),在 JDK9 之后默认使用 G1 +G1 对比其他处理器的优点: +* 并发与并行: + * 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW + * 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 + * 其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,JVM 的 GC 线程处理速度慢时,系统会**调用应用程序线程加速垃圾回收**过程 -#### 实际大小 +* **分区算法**: + * 从分代上看,G1 属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区。从堆结构上看,**新生代和老年代不再物理隔离**,不用担心每个代内存是否足够,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC + * 将整个堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32 MB之间且为 2 的 N 次幂,所有 Region 大小相同,在 JVM 生命周期内不会被改变。G1 把堆划分成多个大小相等的独立区域,使得每个小空间可以单独进行垃圾回收 + * **新的区域 Humongous**:本身属于老年代区,当出现了一个巨型对象超出了分区容量的一半,该对象就会进入到该区域。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储,为了能找到连续的H区,有时候不得不启动 Full GC + * G1 不会对巨型对象进行拷贝,回收时被优先考虑,G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉 + + * Region 结构图: + -浅堆(Shallow Heap):**对象本身占用的内存,不包括内部引用对象的大小**,32 位系统中一个对象引用占 4 个字节,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) -JDK7 中的 String:2个 int 值共占 8 字节,value 对象引用占用 4 字节,对象头 8 字节,对齐后占 24 字节,为 String 对象的浅堆大小,与 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节 +- 空间整合: -```java -private final char value[]; -private int hash; -private int hash32; -``` + - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 + - G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 -保留集(Retained Set):对象 A 的保留集指当对象 A 被垃圾回收后,可以被释放的所有的对象集合(包括 A 本身),所以对象 A 的保留集就是只能通过对象 A 被直接或间接访问到的所有对象的集合,就是仅被对象 A 所持有的对象的集合 +- **可预测的停顿时间模型(软实时 soft real-time)**:可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 -深堆(Retained Heap):指对象的保留集中所有的对象的浅堆大小之和,一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间 + - 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 + - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率 -对象的实际大小:一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小 + * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 -下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,**A 的实际大小为 A、C、D 三者之和**,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内 +G1垃圾收集器的缺点: - +* 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高 +* 从经验上来说,在小内存应用上 CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势。平衡点在 6-8GB 之间 -内存分析工具 MAT 提供了一种叫支配树的对象图,体现了对象实例间的支配关系 +应用场景: -基本性质: +* 面向服务端应用,针对具有大内存、多处理器的机器 +* 需要低 GC 延迟,并具有大堆的应用程序提供解决方案 -- 对象 A 的子树(所有被对象 A 支配的对象集合)表示对象 A 的保留集(retained set),即深堆 -- 如果对象 A 支配对象 B,那么对象 A 的直接支配者也支配对象 B -- 支配树的边与对象引用图的边不直接对应 +*** -左图表示对象引用图,右图表示左图所对应的支配树: -![](https://gitee.com/seazean/images/raw/master/Java/JVM-支配树.png) -比如:对象 F 与对象 D 相互引用,因为到对象 F 的所有路径必然经过对象 D,因此对象 D 是对象 F 的直接支配者 +##### 记忆集 +记忆集 Remembered Set 在新生代中,每个 Region 都有一个 Remembered Set,用来被哪些其他 Region 里的对象引用(谁引用了我就记录谁) + -参考文章:https://www.yuque.com/u21195183/jvm/nkq31c +* 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(跨代引用),不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 +* 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 +垃圾收集器在新生代中建立了记忆集这样的数据结构,可以理解为它是一个抽象类,具体实现记忆集的三种方式: +* 字长精度 +* 对象精度 +* 卡精度(卡表) -*** +卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等,卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,JVM 对于卡页的维护也是通过写屏障的方式 +收集集合 CSet 代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中 +* CSet of Young Collection +* CSet of Mix Collection -#### 节约内存 -* 尽量使用基本数据类型 -* 满足容量前提下,尽量用小字段 +*** -* 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil - 一个 ArrayList 集合,如果里面放了 10 个数字,占用多少内存: - ```java - private transient Object[] elementData; - private int size; - ``` +##### 工作原理 - Mark Word 占 4byte,Klass Word 占 4byte,一个 int 字段占 4byte,elementData 数组占 12byte,数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte(深堆) +G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不同的条件下被触发 -* 时间用 long/int 表示,不用 Date 或者 String +* 当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程 +* 标记完成马上开始混合回收过程 + +顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收 -*** +* **Young GC**:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1 停止应用程序的执行 STW,把活跃对象放入老年代,垃圾对象回收 + **回收过程**: + 1. 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口 + 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 + * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet + * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 + 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的分区放入 Young CSet 中进行回收 + 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 + 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 -#### 对象访问 +* **Concurrent Mark **: -JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: + * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC + * 根区域扫描 (Root Region Scanning):扫描 Survivor 区中指向老年代的,被初始标记标记了的引用及引用的对象,这一个过程是并发进行的,但是必须在 Young GC 之前完成 + * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 + * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(**防止漏标**) + * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 -* 句柄访问:Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 - - 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-句柄访问.png) +* **Mixed GC**:当很多对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会**回收一部分**的 old region,过程同 YGC -* 直接指针(HotSpot 采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址 - - 优点:速度更快,**节省了一次指针定位的时间开销** + 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些老年代 region 收集,对垃圾回收的时间进行控制 - 缺点:对象被移动时(如进行GC后的内存重新排列),对象的 reference 也需要同步更新 - - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-直接指针.png) + 在 G1 中,Mixed GC 可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 +* **Full GC**:对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC + 产生 Full GC 的原因: -参考文章:https://www.cnblogs.com/afraidToForget/p/12584866.html + * 晋升时没有足够的空间存放晋升的对象 + * 并发处理过程完成之前空间耗尽,浮动垃圾 @@ -11662,178 +11938,123 @@ JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: -### 对象创建 - -#### 生命周期 +##### 相关参数 -在 Java 中,对象的生命周期包括以下几个阶段: +- `-XX:+UseG1GC`:手动指定使用 G1 垃圾收集器执行内存回收任务 +- `-XX:G1HeapRegionSize`:设置每个 Region 的大小。值是 2 的幂,范围是 1MB 到 32MB 之间,目标是根据最小的 Java 堆大小划分出约 2048 个区域,默认是堆内存的 1/2000 +- `-XX:MaxGCPauseMillis`:设置期望达到的最大 GC 停顿时间指标,JVM会尽力实现,但不保证达到,默认值是 200ms +- `-XX:+ParallelGcThread`:设置 STW 时 GC 线程数的值,最多设置为 8 +- `-XX:ConcGCThreads`:设置并发标记线程数,设置为并行垃圾回收线程数 ParallelGcThreads 的1/4左右 +- `-XX:InitiatingHeapoccupancyPercent`:设置触发并发 Mixed GC 周期的 Java 堆占用率阈值,超过此值,就触发 GC,默认值是 45 +- `-XX:+ClassUnloadingWithConcurrentMark`:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 +- `-XX:G1NewSizePercent`:新生代占用整个堆内存的最小百分比(默认5%) +- `-XX:G1MaxNewSizePercent`:新生代占用整个堆内存的最大百分比(默认60%) +- `-XX:G1ReservePercent=10`:保留内存区域,防止 to space(Survivor中的 to 区)溢出 -1. 创建阶段 (Created): -2. 应用阶段 (In Use):对象至少被一个强引用持有着 -3. 不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 -4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括 GC Root 的强引用 -5. 收集阶段 (Collected):垃圾回收器对该对象的内存空间重新分配做好准备,该对象如果重写了 finalize() 方法,则会去执行该方法 -6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完 finalize()方 法后仍然处于不可达状态时进入该阶段 -7. 对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 +*** -参考文章:https://blog.csdn.net/sodino/article/details/38387049 +##### 调优 -*** +G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优: +1. 开启 G1 垃圾收集器 +2. 设置堆的最大内存 +3. 设置最大的停顿时间(STW) +不断调优暂停时间指标: -#### 创建时机 +* `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1会根据这个参数选择 CSet 来满足响应时间的设置 +* 设置到 100ms 或者 200ms 都可以(不同情况下会不一样),但设置成50ms就不太合理 +* 暂停时间设置的太短,就会导致出现 G1 跟不上垃圾产生的速度,最终退化成 Full GC +* 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 -类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 +不要设置新生代和老年代的大小: -Java 对象创建时机: +- 避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标 +- 设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们需要做的只是设置整个堆内存的大小,剩下的交给 G1 自己去分配各个代的大小 -1. 使用 new 关键字创建对象:由执行类实例创建表达式而引起的对象创建 -2. 使用 Class 类的 newInstance 方法(反射机制) -3. 使用 Constructor 类的 newInstance 方法(反射机制) +*** - ```java - public class Student { - private int id; - public Student(Integer id) { - this.id = id; - } - public static void main(String[] args) throws Exception { - Constructor c = Student.class.getConstructor(Integer.class); - Student stu = c.newInstance(123); - } - } - ``` - 使用 newInstance 方法的这两种方式创建对象使用的就是 Java 的反射机制,事实上 Class 的 newInstance 方法内部调用的也是 Constructor 的 newInstance 方法 -4. 使用 Clone 方法创建对象:用 clone 方法创建对象的过程中并不会调用任何构造函数,要想使用 clone 方法,我们就必须先实现 Cloneable 接口并实现其定义的 clone 方法 +#### ZGC -5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM 会创建一个**单独的对象**,在此过程中,JVM 并不会调用任何构造函数,为了反序列化一个对象,需要让类实现 Serializable 接口 +ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region 内存布局的,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现**可并发的标记压缩算法** -从 Java 虚拟机层面看,除了使用 new 关键字创建对象的方式外,其他方式全部都是通过转变为 invokevirtual 指令直接创建对象的 - - - -*** +* 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 +* 染色指针:直接**将少量额外的信息存储在指针上的技术**,从 64 位的指针中拿高 4 位来标识对象此时的状态 + * 染色指针可以使某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用 + * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集、是否被移动过(Remapped)、是否只能通过 finalize() 方法才能被访问到(Finalizable) + * 可以大幅减少在垃圾收集过程中内存屏障的使用数量,写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作 + * 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据 +* 内存多重映射:多个虚拟地址指向同一个物理地址 +可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就根据转发表访问新的移动对象,并更新引用,不会像 G1 一样必须等待垃圾回收完成才能访问 +ZGC 目标: -#### 创建过程 +- 停顿时间不会超过 10ms +- 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在 10ms 以下) +- 可支持几百 M,甚至几 T 的堆大小(最大支持4T) -创建对象的过程: +ZGC 的工作过程可以分为 4 个阶段: -1. 判断对象对应的类是否加载、链接、初始化 +* 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过初始标记和最终标记,需要短暂停顿 +* 并发预备重分配( Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) +* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 +* 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象,这样合并节省了一次遍历的开销 -2. 为对象分配内存:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量,即使从**隐藏变量**也会被分配空间(继承部分解释了为什么会隐藏) +ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 -3. 处理并发安全问题: +优点:高吞吐量、低延迟 - * 采用 CAS 配上自旋保证更新的原子性 - * 每个线程预先分配一块 TLAB +缺点:浮动垃圾,当 ZGC 准备要对一个很大的堆做一次完整的并发收集,其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象产生浮动垃圾 -4. 初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 -5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的 HashCode、对象的 GC 信息、锁信息等数据存储在对象头中 -6. 执行 init 方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化 +参考文章:https://www.cnblogs.com/jimoer/p/13170249.html - * 实例变量初始化与实例代码块初始化: - 对实例变量直接赋值或者使用实例代码块赋值,**编译器会将其中的代码放到类的构造函数中去**,并且这些代码会被放在对超类构造函数的调用语句之后 (Java 要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前 - * 构造函数初始化: +*** - **Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到 Object 类。然后从 Object 类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 +#### 总结 -*** +Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: +- 最小化地使用内存和并行开销,选 Serial GC +- 最大化应用程序的吞吐量,选 Parallel GC +- 最小化 GC 的中断或停顿时间,选 CMS GC +![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器总结.png) -#### 承上启下 -1. 一个实例变量在对象初始化的过程中会被赋值几次? - JVM 在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值 - 在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值 - 在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值 - 在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 - 在 Java 的对象初始化过程中,一个实例变量最多可以被初始化 4 次 -2. 类的初始化过程与类的实例化过程的异同? - 类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程 - 类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化,先初始化才能实例化) +*** -3. 假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(**经典案例**) - ```java - public class StaticTest { - public static void main(String[] args) { - staticFunction();//调用静态方法,触发初始化 - } - - static StaticTest st = new StaticTest(); - - static { //静态代码块 - System.out.println("1"); - } - - { // 实例代码块 - System.out.println("2"); - } - - StaticTest() { // 实例构造器 - System.out.println("3"); - System.out.println("a=" + a + ",b=" + b); - } - - public static void staticFunction() { // 静态方法 - System.out.println("4"); - } - - int a = 110; // 实例变量 - static int b = 112; // 静态变量 - }/* Output: - 2 - 3 - a=110,b=0 - 1 - 4 - *///:~ - ``` - `static StaticTest st = new StaticTest();`: +### 内存泄漏 - * 实例实例化不一定要在类初始化结束之后才开始 +#### 泄露溢出 - * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此在实例化上述程序中的 st 变量时,**实际上是把实例化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致 a 为 110,b 为 0 的原因 +内存泄漏(Memory Leak):是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 - 代码等价于: +可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。由于代码的实现不同就会出现很多种内存泄漏问题,让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏 - ```java - public class StaticTest { - (){ - a = 110; // 实例变量 - System.out.println("2"); // 实例代码块 - System.out.println("3"); // 实例构造器中代码的执行 - System.out.println("a=" + a + ",b=" + b); // 实例构造器中代码的执行 - 类变量st被初始化 - System.out.println("1"); //静态代码块 - 类变量b被初始化为112 - } - } - ``` +内存溢出(out of memory)指的是申请内存时,没有足够的内存可以使用 - +内存泄漏和内存溢出的关系:内存泄漏的越来越多,最终会导致内存溢出 @@ -11841,21 +12062,21 @@ Java 对象创建时机: -### 加载过程 - -#### 生命周期 - -类是在运行期间**第一次使用时动态加载**的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间 +#### 几种情况 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类的生命周期.png) +##### 静态集合 -包括 7 个阶段: +静态集合类的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。原因是**长生命周期的对象持有短生命周期对象的引用**,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收 -* 加载(Loading) -* 链接:验证(Verification)、准备(Preparation)、解析(Resolution) -* 初始化(Initialization) -* 使用(Using) -* 卸载(Unloading) +```java +public class MemoryLeak { + static List list = new ArrayList(); + public void oomTest(){ + Object obj = new Object();//局部变量 + list.add(obj); + } +} +``` @@ -11863,115 +12084,88 @@ Java 对象创建时机: -#### 加载阶段 +##### 单例模式 -加载是类加载的其中一个阶段,注意不要混淆 +单例模式和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏 -加载过程完成以下三件事: -- 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) -- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(Java 类模型) -- 在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口 -其中二进制字节流可以从以下方式中获取: +**** -- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 -- 从网络中获取,最典型的应用是 Applet -- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 -- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码 -将字节码文件加载至方法区后,会**在堆中**创建一个 java.lang.Class 对象,用来引用位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象 -方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构: +##### 内部类 -* `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 java 使用 -* `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 +内部类持有外部类的情况,如果一个外部类的实例对象调用方法返回了一个内部类的实例对象,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象也不会被回收,造成内存泄漏 -加载过程: -* 如果这个类还有父类没有加载,先加载父类 -* 加载和链接可能是交替运行的 -* Class 对象和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 - +*** -创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程: -- 如果数组的元素类型是引用类型,那么遵循定义的加载过程递归加载和创建数组的元素类型 -- JVM 使用指定的元素类型和数组维度来创建新的数组类 -- 基本数据类型由启动类加载器加载 +##### 连接相关 +数据库连接、网络连接和 IO 连接等,当不再使用时,需要显式调用 close 方法来释放与连接,垃圾回收器才会回收对应的对象,否则将会造成大量的对象无法被回收,从而引起内存泄漏 -*** +**** -#### 链接阶段 -##### 验证 -确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全 +##### 不合理域 -主要包括**四种验证**: +变量不合理的作用域,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏;如果没有及时地把对象设置为 null,也有可能导致内存泄漏的发生 -* 文件格式验证 +```java +public class UsingRandom { + private String msg; + public void receiveMsg(){ + msg = readFromNet();// 从网络中接受数据保存到 msg 中 + saveDB(msg); // 把 msg 保存到数据库中 + } +} +``` -* 语义检查,但凡在语义上不符合规范的,虚拟机不会给予验证通过 +通过 readFromNet 方法把接收消息保存在 msg 中,然后调用 saveDB 方法把内容保存到数据库中,此时 msg 已经可以被回收,但是 msg 的生命周期与对象的生命周期相同,造成 msg 不能回收,产生内存泄漏 - * 是否所有的类都有父类的存在(除了 Object 外,其他类都应该有父类) +解决: - * 是否一些被定义为 final 的方法或者类被重写或继承了 +* msg 变量可以放在 receiveMsg 方法内部,当方法使用完,msg 的生命周期也就结束,就可以被回收了 +* 在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。 - * 非抽象类是否实现了所有抽象方法或者接口方法 - * 是否存在不兼容的方法 -* 字节码验证,试图通过对字节码流的分析,判断字节码是否可以被正确地执行 +**** - * 在字节码的执行过程中,是否会跳转到一条不存在的指令 - * 函数的调用是否传递了正确类型的参数 - * 变量的赋值是不是给了正确的数据类型 - * 栈映射帧(StackMapTable)在这个阶段用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型 -* 符号引用验证,Class 文件在其常量池会通过字符串记录将要使用的其他类或者方法 +##### 改变哈希 +当一个对象被存储进 HashSet 集合中以后,就**不能修改这个对象中的那些参与计算哈希值的字段**,否则对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值不同,这种情况下使用该对象的当前引用作为的参数去 HashSet 集合中检索对象返回 false,导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏 -*** +*** -##### 准备 -准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: -* 类变量也叫静态变量,就是是被 static 修饰的变量 -* 实例变量也叫对象变量,即没加 static 的变量 +##### 缓存泄露 -说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 +内存泄漏的一个常见来源是缓存,一旦把对象引用放入到缓存中,就会很容易被遗忘 -类变量初始化: +使用 WeakHashMap 代表缓存,当除了自身有对 key 的引用外没有其他引用,map 会自动丢弃此值 -* static 变量分配空间和赋值是两个步骤:**分配空间在准备阶段完成,赋值在初始化阶段完成** -* 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值(方法区)就确定了,准备阶段会显式初始化 -* 如果 static 变量是 final 的,但属于引用类型或者构造器方法的字符串,赋值在初始化阶段完成 -实例: -* 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123: +*** - ```java - public static int value = 123; - ``` -* 常量 value 被初始化为 123 而不是 0: - ```java - public static final int value = 123; - ``` +##### 监听器 -* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故 boolean 的默认值就是 false +监听器和其他回调情况,假如客户端在实现的 API 中注册回调,却没有显式的取消,那么就会一直积聚下去,所以确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,比如保存为 WeakHashMap 中的键 @@ -11979,194 +12173,195 @@ Java 对象创建时机: -##### 解析 +#### 案例分析 -将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: +```java +public class Stack { + private Object[] elements; + private int size = 0; + private static final int DEFAULT_INITIAL_CAPACITY = 16; -* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** -* 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中 + public Stack() { + elements = new Object[DEFAULT_INITIAL_CAPACITY]; + } -例如:在 `com.demo.Solution` 类中引用了 `com.test.Quest`,把 `com.test.Quest` 作为符号引用存进类常量池,在类加载完后,**用这个符号引用去方法区找这个类的内存地址** + public void push(Object e) { //入栈 + ensureCapacity(); + elements[size++] = e; + } -解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 - -* 在类加载阶段解析的是非虚方法,静态绑定 -* 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** -* 通过解析操作,符号引用就可以转变为目标方法在类的虚方法表中的位置,从而使得方法被成功调用 + public Object pop() { //出栈 + if (size == 0) + throw new EmptyStackException(); + return elements[--size]; + } -```java -public class Load2 { - public static void main(String[] args) throws Exception{ - ClassLoader classloader = Load2.class.getClassLoader(); - // cloadClass 加载类方法不会导致类的解析和初始化,也不会加载D - Class c = classloader.loadClass("cn.jvm.t3.load.C"); - - // new C();会导致类的解析和初始化,从而解析初始化D - System.in.read(); + private void ensureCapacity() { + if (elements.length == size) + elements = Arrays.copyOf(elements, 2 * size + 1); } } -class C { - D d = new D(); -} -class D { -} ``` +程序并没有明显错误,但 pop 函数存在内存泄漏问题,因为 pop 函数只是把栈顶索引下移一位,并没有把上一个出栈索引处的引用置空,导致栈数组一直强引用着已经出栈的对象 +解决方法: -**** - +```java +public Object pop() { + if (size == 0) + throw new EmptyStackException(); + Object result = elements[--size]; + elements[size] = null; + return result; +} +``` -#### 初始化 -##### 介绍 -初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行 -在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 clinit,另一个是实例的初始化方法 init +*** -类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机**调用一次**,而实例构造器则会被虚拟机调用多次,只要程序员创建对象 -类在第一次实例化加载一次,把 class 读入内存,后续实例化不再加载,引用第一次加载的类 -*** +## 类加载 +### 对象访存 +#### 存储结构 -##### clinit +一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding) -():类构造器,由编译器自动收集类中**所有类变量的赋值动作和静态语句块**中的语句合并产生的 +对象头: -作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 +* 普通对象:分为两部分 -* 如果类中没有静态变量或静态代码块,那么 clinit 方法将不会被生成 -* clinit 方法只执行一次,在执行 clinit 方法时,必须先执行父类的clinit方法 -* static 变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定 -* static 不加 final 的变量都在初始化环节赋值 + * **Mark Word**:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等 -**线程安全**问题: + ```ruby + hash(25) + age(4) + lock(3) = 32bit #32位系统 + unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 + ``` -* 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都阻塞等待,直到活动线程执行 () 方法完毕 -* 如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽 + * **Klass Word**:类型指针,**指向该对象的 Class 类对象的指针**,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 **Java 中的一个引用的大小**) -特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问 + ```ruby + |-----------------------------------------------------| + | Object Header (64 bits) | + |---------------------------|-------------------------| + | Mark Word (32 bits) | Klass Word (32 bits) | + |---------------------------|-------------------------| + ``` -```java -public class Test { - static { - //i = 0; // 给变量赋值可以正常编译通过 - System.out.print(i); // 这句编译器会提示“非法向前引用” - } - static int i = 1; -} -``` +* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度(12 字节) -接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法,两者不同的是: + ```ruby + |-------------------------------------------------------------------------------| + | Object Header (96 bits) | + |-----------------------|-----------------------------|-------------------------| + | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | + |-----------------------|-----------------------------|-------------------------| + ``` -* 在初始化一个接口时,并不会先初始化它的父接口,所以执行接口的 () 方法不需要先执行父接口的 () 方法 -* 在初始化一个类时,不会先初始化所实现的接口,所以接口的实现类在初始化时不会执行接口的 () 方法 -* 只有当父接口中定义的变量使用时,父接口才会初始化 +实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 +对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求**对象起始地址必须是 8 字节的整数倍**,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 +32 位系统: -**** +* 一个 int 在 java 中占据 4byte,所以 Integer 的大小为: + ```java + private final int value; + ``` + ```ruby + # 需要补位4byte + 4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte + ``` -##### 时机 +* `int[] arr = new int[10]` -类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化 + ```ruby + # 由于需要8位对齐,所以最终大小为`56byte`。 + 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte + ``` -**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会发生): -* 当创建一个类的实例时,使用 new 关键字,或者通过反射、克隆、反序列化(前文讲述的对象的创建时机) -* 当调用类的静态方法或访问静态字段时,遇到 getstatic、putstatic、invokestatic 这三条字节码指令,如果类没有进行过初始化,则必须先触发其初始化 - * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) - * putstatic:程序给类的静态变量赋值 - * invokestatic :调用一个类的静态方法 -* 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行初始化,则需要先触发其初始化 -* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,但这条规则并**不适用于接口** -* 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 -* MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这两个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类 -* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 -**被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 +*** -* 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 -* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法 -* 常量(final 修饰)在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 -* 调用 ClassLoader 类的 loadClass() 方法加载一个类,并不是对类的主动使用,不会导致类的初始化 +#### 实际大小 -*** +浅堆(Shallow Heap):**对象本身占用的内存,不包括内部引用对象的大小**,32 位系统中一个对象引用占 4 个字节,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐 +JDK7 中的 String:2个 int 值共占 8 字节,value 对象引用占用 4 字节,对象头 8 字节,对齐后占 24 字节,为 String 对象的浅堆大小,与 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节 +```java +private final char value[]; +private int hash; +private int hash32; +``` -##### init +保留集(Retained Set):对象 A 的保留集指当对象 A 被垃圾回收后,可以被释放的所有的对象集合(包括 A 本身),所以对象 A 的保留集就是只能通过对象 A 被直接或间接访问到的所有对象的集合,就是仅被对象 A 所持有的对象的集合 -init 指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 +深堆(Retained Heap):指对象的保留集中所有的对象的浅堆大小之和,一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间 -实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行 +对象的实际大小:一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小 -类实例化过程:**父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数** +下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,**A 的实际大小为 A、C、D 三者之和**,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内 + +内存分析工具 MAT 提供了一种叫支配树的对象图,体现了对象实例间的支配关系 -*** +基本性质: +- 对象 A 的子树(所有被对象 A 支配的对象集合)表示对象 A 的保留集(retained set),即深堆 +- 如果对象 A 支配对象 B,那么对象 A 的直接支配者也支配对象 B -#### 卸载阶段 +- 支配树的边与对象引用图的边不直接对应 -时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止 +左图表示对象引用图,右图表示左图所对应的支配树: -卸载类即该类的 **Class 对象被 GC**,卸载类需要满足3个要求: +![](https://gitee.com/seazean/images/raw/master/Java/JVM-支配树.png) -1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象 -2. 该类没有在其他任何地方被引用 -3. 该类的类加载器的实例已被 GC,一般是可替换类加载器的场景,如 OSGi、JSP 的重加载等,很难达成 +比如:对象 F 与对象 D 相互引用,因为到对象 F 的所有路径必然经过对象 D,因此对象 D 是对象 F 的直接支配者 -在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,这些类始终是可及的 +参考文章:https://www.yuque.com/u21195183/jvm/nkq31c -**** +*** -### 类加载器 -#### 类加载 -类加载方式: +#### 节约内存 -* 隐式加载:不直接在代码中调用 ClassLoader 的方法加载类对象 - * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 - * 在 JVM 启动时,通过三大类加载器加载 class -* 显式加载: - * ClassLoader.loadClass(className):只加载和连接,**不会进行初始化** - * Class.forName(String name, boolean initialize, ClassLoader loader):使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 +* 尽量使用基本数据类型 -类的唯一性: +* 满足容量前提下,尽量用小字段 -* 在 JVM 中表示两个 class 对象判断为同一个类存在的两个必要条件: - - 类的完整类名必须一致,包括包名 - - 加载这个类的 ClassLoader(指 ClassLoader 实例对象)必须相同 -* 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true +* 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil -命名空间: + 一个 ArrayList 集合,如果里面放了 10 个数字,占用多少内存: -- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成 -- 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类 + ```java + private transient Object[] elementData; + private int size; + ``` -基本特征: + Mark Word 占 4byte,Klass Word 占 4byte,一个 int 字段占 4byte,elementData 数组占 12byte,数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte(深堆) -* **可见性**,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的 -* **单一性**,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,不会在子加载器中重复加载 +* 时间用 long/int 表示,不用 Date 或者 String @@ -12174,110 +12369,90 @@ init 指的是实例构造器,主要作用是在类实例化过程中执行, -#### 加载器 +#### 对象访问 -类加载器是 Java 的核心组件,用于加载字节码到 JVM 内存,得到 Class 类的对象 +JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: -从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器: +* 句柄访问:Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 + + 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改 -- 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分 -- 自定义类加载器(User-Defined ClassLoader):Java 虚拟机规范**将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器**,使用 Java 语言实现,独立于虚拟机 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-句柄访问.png) -从 Java 开发人员的角度看: +* 直接指针(HotSpot 采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址 + + 优点:速度更快,**节省了一次指针定位的时间开销** -* 启动类加载器(Bootstrap ClassLoader): - * 处于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类 - * 类加载器负责加载在 `JAVA_HOME/jre/lib `或 `sun.boot.class.path` 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中 - * 仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在 lib 目录中也不会被加载 - * 启动类加载器无法被 Java 程序直接引用,编写自定义类加载器时,如果要把加载请求委派给启动类加载器,直接使用 null 代替 -* 扩展类加载器(Extension ClassLoader): - * 由 ExtClassLoader (sun.misc.Launcher$ExtClassLoader) 实现,上级为 Bootstrap,显示为 null - * 将 `JAVA_HOME/jre/lib/ext `或者被 `java.ext.dir` 系统变量所指定路径中的所有类库加载到内存中 - * 开发者可以使用扩展类加载器,创建的 JAR 放在此目录下,会由扩展类加载器自动加载 -* 应用程序类加载器(Application ClassLoader): - * 由 AppClassLoader(sun.misc.Launcher$AppClassLoader) 实现,上级为 Extension - * 负责加载环境变量 classpath 或系统属性 `java.class.path` 指定路径下的类库 - * 这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此称为系统类加载器 - * 可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,这个就是程序中默认的类加载器 -* 自定义类加载器:由开发人员自定义的类加载器,上级是 Application + 缺点:对象被移动时(如进行GC后的内存重新排列),对象的 reference 也需要同步更新 + + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-直接指针.png) -```java -public static void main(String[] args) { - //获取系统类加载器 - ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); - System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 - //获取其上层 扩展类加载器 - ClassLoader extClassLoader = systemClassLoader.getParent(); - System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6 - //获取其上层 获取不到引导类加载器 - ClassLoader bootStrapClassLoader = extClassLoader.getParent(); - System.out.println(bootStrapClassLoader);//null +参考文章:https://www.cnblogs.com/afraidToForget/p/12584866.html - //对于用户自定义类来说:使用系统类加载器进行加载 - ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); - System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 - //String 类使用引导类加载器进行加载的 --> java核心类库都是使用启动类加载器加载的 - ClassLoader classLoader1 = String.class.getClassLoader(); - System.out.println(classLoader1);//null -} -``` - -补充两个类加载器: +*** -* SecureClassLoader 扩展了 ClassLoader,新增了几个与使用相关的代码源和权限定义类验证(对 class 源码的访问权限)的方法,一般不会直接跟这个类打交道,更多是与它的子类 URLClassLoader 有所关联 -* ClassLoader 是一个抽象类,很多方法是空的没有实现,而 URLClassLoader 这个实现类为这些方法提供了具体的实现,并新增了 URLClassPath 类协助取得 Class 字节流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁 +### 对象创建 -*** +#### 生命周期 +在 Java 中,对象的生命周期包括以下几个阶段: +1. 创建阶段 (Created): +2. 应用阶段 (In Use):对象至少被一个强引用持有着 +3. 不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 +4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括 GC Root 的强引用 +5. 收集阶段 (Collected):垃圾回收器对该对象的内存空间重新分配做好准备,该对象如果重写了 finalize() 方法,则会去执行该方法 +6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完 finalize()方 法后仍然处于不可达状态时进入该阶段 +7. 对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 -#### 常用API -ClassLoader 类,是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器) -获取 ClassLoader 的途径: +参考文章:https://blog.csdn.net/sodino/article/details/38387049 -* 获取当前类的 ClassLoader:`clazz.getClassLoader()` -* 获取当前线程上下文的 ClassLoader:`Thread.currentThread.getContextClassLoader()` -* 获取系统的 ClassLoader:`ClassLoader.getSystemClassLoader()` -* 获取调用者的 ClassLoader:`DriverManager.getCallerClassLoader()` -ClassLoader 类常用方法: -* `getParent()`:返回该类加载器的超类加载器 -* `loadclass(String name)`:加载名为 name 的类,返回结果为 Class 类的实例,**该方法就是双亲委派模式** -* `findclass(String name)`:查找二进制名称为 name 的类,返回结果为 Class 类的实例,该方法会在检查完父类加载器之后被 loadClass() 方法调用 -* `findLoadedClass(String name)`:查找名称为 name 的已经被加载过的类,final 修饰无法重写 -* `defineClass(String name, byte[] b, int off, int len)`:将**字节流**解析成 JVM 能够识别的类对象 -* `resolveclass(Class c)`:链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 -* `InputStream getResourceAsStream(String name)`:指定资源名称获取输入流 +*** -*** +#### 创建时机 +类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 +Java 对象创建时机: -#### 加载模型 +1. 使用 new 关键字创建对象:由执行类实例创建表达式而引起的对象创建 -##### 加载机制 +2. 使用 Class 类的 newInstance 方法(反射机制) -在 JVM 中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 +3. 使用 Constructor 类的 newInstance 方法(反射机制) -- **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 + ```java + public class Student { + private int id; + public Student(Integer id) { + this.id = id; + } + public static void main(String[] args) throws Exception { + Constructor c = Student.class.getConstructor(Integer.class); + Student stu = c.newInstance(123); + } + } + ``` -- **双亲委派:**先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 + 使用 newInstance 方法的这两种方式创建对象使用的就是 Java 的反射机制,事实上 Class 的 newInstance 方法内部调用的也是 Constructor 的 newInstance 方法 -- **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区中 - - 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因 +4. 使用 Clone 方法创建对象:用 clone 方法创建对象的过程中并不会调用任何构造函数,要想使用 clone 方法,我们就必须先实现 Cloneable 接口并实现其定义的 clone 方法 +5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM 会创建一个**单独的对象**,在此过程中,JVM 并不会调用任何构造函数,为了反序列化一个对象,需要让类实现 Serializable 接口 +从 Java 虚拟机层面看,除了使用 new 关键字创建对象的方式外,其他方式全部都是通过转变为 invokevirtual 指令直接创建对象的 @@ -12285,343 +12460,310 @@ ClassLoader 类常用方法: -##### 双亲委派 - -双亲委派模型(Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它类加载器都要有父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) +#### 创建过程 -工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载 +创建对象的过程: -双亲委派机制的优点: +1. 判断对象对应的类是否加载、链接、初始化 -* 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证全局唯一性 +2. 为对象分配内存:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量,即使从**隐藏变量**也会被分配空间(继承部分解释了为什么会隐藏) -* Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一 +3. 处理并发安全问题: -* 保护程序安全,防止类库的核心 API 被随意篡改 + * 采用 CAS 配上自旋保证更新的原子性 + * 每个线程预先分配一块 TLAB - 例如:在工程中新建 java.lang 包,接着在该包下新建 String 类,并定义 main 函数 +4. 初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 - ```java - public class String { - public static void main(String[] args) { - System.out.println("demo info"); - } - } - ``` - - 此时执行 main 函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库。出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 +5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的 HashCode、对象的 GC 信息、锁信息等数据存储在对象头中 -双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类**(可见性) +6. 执行 init 方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化 - + * 实例变量初始化与实例代码块初始化: + 对实例变量直接赋值或者使用实例代码块赋值,**编译器会将其中的代码放到类的构造函数中去**,并且这些代码会被放在对超类构造函数的调用语句之后 (Java 要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前 + * 构造函数初始化: -*** + **Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到 Object 类。然后从 Object 类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 -##### 源码分析 +*** -```java -protected Class loadClass(String name, boolean resolve) - throws ClassNotFoundException { - synchronized (getClassLoadingLock(name)) { - // 调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类 - Class c = findLoadedClass(name); - - // 当前类加载器如果没有加载过 - if (c == null) { - long t0 = System.nanoTime(); - try { - // 判断当前类加载器是否有父类加载器 - if (parent != null) { - // 如果当前类加载器有父类加载器,则调用父类加载器的 loadClass(name,false) -          // 父类加载器的 loadClass 方法,又会检查自己是否已经加载过 - c = parent.loadClass(name, false); - } else { - // 当前类加载器没有父类加载器,说明当前类加载器是 BootStrapClassLoader -           // 则调用 BootStrap ClassLoader 的方法加载类 - c = findBootstrapClassOrNull(name); - } - } catch (ClassNotFoundException e) { } - if (c == null) { - // 如果调用父类的类加载器无法对类进行加载,则用自己的 findClass() 方法进行加载 - // 可以自定义 findClass() 方法 - long t1 = System.nanoTime(); - c = findClass(name); - // this is the defining class loader; record the stats - sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); - sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); - sun.misc.PerfCounter.getFindClasses().increment(); - } - } - if (resolve) { - // 链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 - resolveClass(c); - } - return c; - } -} -``` +#### 承上启下 +1. 一个实例变量在对象初始化的过程中会被赋值几次? + JVM 在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值 + 在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值 + 在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值 + 在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 + 在 Java 的对象初始化过程中,一个实例变量最多可以被初始化 4 次 -**** +2. 类的初始化过程与类的实例化过程的异同? + 类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程 + 类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化,先初始化才能实例化) +3. 假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(**经典案例**) -##### 破坏委派 + ```java + public class StaticTest { + public static void main(String[] args) { + staticFunction();//调用静态方法,触发初始化 + } + + static StaticTest st = new StaticTest(); + + static { //静态代码块 + System.out.println("1"); + } + + { // 实例代码块 + System.out.println("2"); + } + + StaticTest() { // 实例构造器 + System.out.println("3"); + System.out.println("a=" + a + ",b=" + b); + } + + public static void staticFunction() { // 静态方法 + System.out.println("4"); + } + + int a = 110; // 实例变量 + static int b = 112; // 静态变量 + }/* Output: + 2 + 3 + a=110,b=0 + 1 + 4 + *///:~ + ``` -双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者的类加载器实现方式 + `static StaticTest st = new StaticTest();`: -破坏双亲委派模型的方式: + * 实例实例化不一定要在类初始化结束之后才开始 -* 自定义 ClassLoader + * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此在实例化上述程序中的 st 变量时,**实际上是把实例化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致 a 为 110,b 为 0 的原因 - * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 - * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 + 代码等价于: -* 引入线程**上下文类加载器** + ```java + public class StaticTest { + (){ + a = 110; // 实例变量 + System.out.println("2"); // 实例代码块 + System.out.println("3"); // 实例构造器中代码的执行 + System.out.println("a=" + a + ",b=" + b); // 实例构造器中代码的执行 + 类变量st被初始化 + System.out.println("1"); //静态代码块 + 类变量b被初始化为112 + } + } + ``` - Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类: + - * SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的 - * SPI 的实现类是由系统类加载器来加载的,引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader 无法委派 AppClassLoader 来加载类 - JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置线程上下文类加载器,在执行线程中抛弃双亲委派加载模式,使程序可以逆向使用类加载器,使 Bootstrap 加载器拿到了 Application 加载器加载的类,破坏了双亲委派模型 - -* 实现程序的动态性,如代码热替换(Hot Swap)、模块热部署(Hot Deployment) - IBM 公司主导的 JSR一291(OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换,在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构 +*** - 当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索: - 1. 将以 java.* 开头的类,委派给父类加载器加载 - 2. 否则,将委派列表名单内的类,委派给父类加载器加载 - 3. 否则,将 Import 列表中的类,委派给 Export 这个类的 Bundle 的类加载器加载 - 4. 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载 - 5. 否则,查找类是否在自己的 Fragment Bundle 中,如果在就委派给 Fragment Bundle 类加载器加载 - 6. 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载 - 7. 否则,类查找失败 - - 热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,**热替换的关键需求在于服务不能中断**,修改必须立即表现正在运行的系统之中 - +### 加载过程 +#### 生命周期 +类是在运行期间**第一次使用时动态加载**的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间 -*** +![](https://gitee.com/seazean/images/raw/master/Java/JVM-类的生命周期.png) +包括 7 个阶段: +* 加载(Loading) +* 链接:验证(Verification)、准备(Preparation)、解析(Resolution) +* 初始化(Initialization) +* 使用(Using) +* 卸载(Unloading) -#### 沙箱机制 -沙箱机制(Sandbox):将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 -沙箱**限制系统资源访问**,包括 CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 +*** -* JDK1.0:Java 中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码被看作是不受信的。对于授信的本地代码,可以访问一切本地资源,而对于非授信的远程代码不可以访问本地资源,其实依赖于沙箱机制。如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现 -* JDK1.1:针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限 -* JDK1.2:改进了安全机制,增加了代码签名,不论本地代码或是远程代码都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制 -* JDK1.6:当前最新的安全机制,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,不同的保护域对应不一样的权限。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问 - +#### 加载阶段 +加载是类加载的其中一个阶段,注意不要混淆 -*** +加载过程完成以下三件事: +- 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) +- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(Java 类模型) +- 在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口 +其中二进制字节流可以从以下方式中获取: -#### 自定义 +- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 +- 从网络中获取,最典型的应用是 Applet +- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 +- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码 -对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可 +将字节码文件加载至方法区后,会**在堆中**创建一个 java.lang.Class 对象,用来引用位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象 -作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏 +方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构: -```java -//自定义类加载器,读取指定的类路径classPath下的class文件 -public class MyClassLoader extends ClassLoader{ - private String classPath; +* `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 java 使用 +* `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 - public MyClassLoader(String classPath) { - this.classPath = classPath; - } - - public MyClassLoader(ClassLoader parent, String byteCodePath) { - super(parent); - this.classPath = classPath; - } +加载过程: - @Override - protected Class findClass(String name) throws ClassNotFoundException { - BufferedInputStream bis = null; - ByteArrayOutputStream baos = null; - try { - // 获取字节码文件的完整路径 - String fileName = classPath + className + ".class"; - // 获取一个输入流 - bis = new BufferedInputStream(new FileInputStream(fileName)); - // 获取一个输出流 - baos = new ByteArrayOutputStream(); - // 具体读入数据并写出的过程 - int len; - byte[] data = new byte[1024]; - while ((len = bis.read(data)) != -1) { - baos.write(data, 0, len); - } - // 获取内存中的完整的字节数组的数据 - byte[] byteCodes = baos.toByteArray(); - // 调用 defineClass(),将字节数组的数据转换为 Class 的实例。 - Class clazz = defineClass(null, byteCodes, 0, byteCodes.length); - return clazz; - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - if (baos != null) - baos.close(); - } catch (IOException e) { - e.printStackTrace(); - } - try { - if (bis != null) - bis.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - return null; - } -} -``` +* 如果这个类还有父类没有加载,先加载父类 +* 加载和链接可能是交替运行的 +* Class 对象和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 -```java -public static void main(String[] args) { - MyClassLoader loader = new MyClassLoader("D:\Workspace\Project\JVM_study\src\java1\"); + - try { - Class clazz = loader.loadClass("Demo1"); - System.out.println("加载此类的类的加载器为:" + clazz.getClassLoader().getClass().getName());//MyClassLoader +创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程: - System.out.println("加载当前类的类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());//sun.misc.Launcher$AppClassLoader - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } -} -``` +- 如果数组的元素类型是引用类型,那么遵循定义的加载过程递归加载和创建数组的元素类型 +- JVM 使用指定的元素类型和数组维度来创建新的数组类 +- 基本数据类型由启动类加载器加载 -**** +*** -#### JDK9 +#### 链接阶段 -为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动: +##### 验证 -* 扩展机制被移除,扩展类加载器由于**向后兼容性**的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取 +确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全 -* JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件),其中 Java 类库就满足了可扩展的需求,那就无须再保留 `\lib\ext` 目录,此前使用这个目录或者 `java.ext.dirs` 系统变量来扩展 JDK 功能的机制就不需要再存在 +主要包括**四种验证**: -* 启动类加载器、平台类加载器、应用程序类加载器全都继承于 `jdk.internal.loader.BuiltinClassLoader` +* 文件格式验证 - +* 语义检查,但凡在语义上不符合规范的,虚拟机不会给予验证通过 + * 是否所有的类都有父类的存在(除了 Object 外,其他类都应该有父类) + * 是否一些被定义为 final 的方法或者类被重写或继承了 -*** + * 非抽象类是否实现了所有抽象方法或者接口方法 + * 是否存在不兼容的方法 +* 字节码验证,试图通过对字节码流的分析,判断字节码是否可以被正确地执行 + * 在字节码的执行过程中,是否会跳转到一条不存在的指令 + * 函数的调用是否传递了正确类型的参数 + * 变量的赋值是不是给了正确的数据类型 + * 栈映射帧(StackMapTable)在这个阶段用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型 +* 符号引用验证,Class 文件在其常量池会通过字符串记录将要使用的其他类或者方法 -## 运行机制 -### 执行过程 - Java 文件编译执行的过程: +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java文件编译执行的过程.png) -- 类加载器:用于装载字节码文件(.class文件) -- 运行时数据区:用于分配存储空间 -- 执行引擎:执行字节码文件或本地方法 -- 垃圾回收器:用于对 JVM 中的垃圾内容进行回收 +##### 准备 +准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: -**** +* 类变量也叫静态变量,就是是被 static 修饰的变量 +* 实例变量也叫对象变量,即没加 static 的变量 +说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 +类变量初始化: -### 字节码 +* static 变量分配空间和赋值是两个步骤:**分配空间在准备阶段完成,赋值在初始化阶段完成** +* 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值(方法区)就确定了,准备阶段会显式初始化 +* 如果 static 变量是 final 的,但属于引用类型或者构造器方法的字符串,赋值在初始化阶段完成 -#### 跨平台性 +实例: -Java 语言:跨平台的语言(write once ,run anywhere) +* 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123: -* 当 Java 源代码成功编译成字节码后,在不同的平台上面运行**无须再次编译** -* 让一个 Java 程序正确地运行在 JVM 中,Java 源码就必须要被编译为符合 JVM 规范的字节码 + ```java + public static int value = 123; + ``` -编译过程中的编译器: +* 常量 value 被初始化为 123 而不是 0: -* 前端编译器: Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ,把源代码编译为字节码文件 .class + ```java + public static final int value = 123; + ``` - * IntelliJ IDEA 使用 javac 编译器 - * Eclipse 中,当开发人员编写完代码后保存时,ECJ 编译器就会把未编译部分的源码逐行进行编译,而非每次都全量编译,因此 ECJ 的编译效率会比 javac 更加迅速和高效 - * 前端编译器并不会直接涉及编译优化等方面的技术,具体优化细节移交给 HotSpot 的 JIT 编译器负责 +* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故 boolean 的默认值就是 false -* 后端运行期编译器:HotSpot VM 的 C1、C2 编译器,也就是 JIT 编译器,Graal 编译器 - * JIT编译器:执行引擎部分详解 - * Graal 编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追平了 C2 -* 静态提前编译器:AOT (Ahead Of Time Compiler)编译器,直接把源代码编译成本地机器代码, +*** - * JDK 9 引入,是与即时编译相对立的一个概念,即时编译指的是在程序的运行过程中将字节码转换为机器码,AOT 是程序运行之前便将字节码转换为机器码 - * 优点:JVM 加载已经预编译成二进制库,可以直接执行,不必等待即时编译器的预热,减少 Java 应用第一次运行慢的现象 - * 缺点: - * 破坏了 Java **一次编译,到处运行**,必须为每个不同硬件编译对应的发行包 - * 降低了 Java 链接过程的动态性,加载的代码在编译期就必须全部已知 +##### 解析 +将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: +* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** +* 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中 +例如:在 `com.demo.Solution` 类中引用了 `com.test.Quest`,把 `com.test.Quest` 作为符号引用存进类常量池,在类加载完后,**用这个符号引用去方法区找这个类的内存地址** -*** +解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 +* 在类加载阶段解析的是非虚方法,静态绑定 +* 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** +* 通过解析操作,符号引用就可以转变为目标方法在类的虚方法表中的位置,从而使得方法被成功调用 +```java +public class Load2 { + public static void main(String[] args) throws Exception{ + ClassLoader classloader = Load2.class.getClassLoader(); + // cloadClass 加载类方法不会导致类的解析和初始化,也不会加载D + Class c = classloader.loadClass("cn.jvm.t3.load.C"); + + // new C();会导致类的解析和初始化,从而解析初始化D + System.in.read(); + } +} +class C { + D d = new D(); +} +class D { +} +``` -#### 语言发展 -机器码:各种用二进制编码方式表示的指令,与 CPU 紧密相关,所以不同种类的 CPU 对应的机器指令不同 -指令:指令就是把机器码中特定的 0 和 1 序列,简化成对应的指令,例如 mov,inc 等,可读性稍好,但是不同的硬件平台的同一种指令(比如 mov),对应的机器码也可能不同 +**** -指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 -- x86 指令集,对应的是 x86 架构的平台 -- ARM 指令集,对应的是 ARM 架构的平台 -汇编语言:用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址 +#### 初始化 -* 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令 -* 计算机只认识指令码,汇编语言编写的程序也必须翻译成机器指令码,计算机才能识别和执行 +##### 介绍 -高级语言:为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言 +初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行 -字节码:是一种中间状态(中间码)的二进制代码,比机器码更抽象,需要直译器转译后才能成为机器码 +在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 clinit,另一个是实例的初始化方法 init -* 字节码为了实现特定软件运行和软件环境,与硬件环境无关 -* 通过编译器和虚拟机器实现,编译器将源码编译成字节码,虚拟机器将字节码转译为可以直接执行的指令 +类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机**调用一次**,而实例构造器则会被虚拟机调用多次,只要程序员创建对象 - +类在第一次实例化加载一次,把 class 读入内存,后续实例化不再加载,引用第一次加载的类 @@ -12629,108 +12771,83 @@ Java 语言:跨平台的语言(write once ,run anywhere) +##### clinit +():类构造器,由编译器自动收集类中**所有类变量的赋值动作和静态语句块**中的语句合并产生的 -#### 类结构 - -##### 文件结构 +作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 -字节码是一种二进制的类文件,是编译之后供虚拟机解释执行的二进制字节码文件,**一个 class 文件对应一个 public 类型的类或接口** +* 如果类中没有静态变量或静态代码块,那么 clinit 方法将不会被生成 +* clinit 方法只执行一次,在执行 clinit 方法时,必须先执行父类的clinit方法 +* static 变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定 +* static 不加 final 的变量都在初始化环节赋值 -字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以执行效率比 Java 高 +**线程安全**问题: -JVM 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html +* 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都阻塞等待,直到活动线程执行 () 方法完毕 +* 如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽 -根据 JVM 规范,类文件结构如下: +特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问 ```java -ClassFile { - u4 magic; - u2 minor_version; - u2 major_version; - u2 constant_pool_count; - cp_info constant_pool[constant_pool_count-1]; - u2 access_flags; - u2 this_class; - u2 super_class; - u2 interfaces_count; - u2 interfaces[interfaces_count]; - u2 fields_count; - field_info fields[fields_count]; - u2 methods_count; - method_info methods[methods_count]; - u2 attributes_count; - attribute_info attributes[attributes_count]; +public class Test { + static { + //i = 0; // 给变量赋值可以正常编译通过 + System.out.print(i); // 这句编译器会提示“非法向前引用” + } + static int i = 1; } ``` -| 类型 | 名称 | 说明 | 长度 | 数量 | -| -------------- | ------------------- | -------------------- | ------- | --------------------- | -| u4 | magic | 魔数,识别类文件格式 | 4个字节 | 1 | -| u2 | minor_version | 副版本号(小版本) | 2个字节 | 1 | -| u2 | major_version | 主版本号(大版本) | 2个字节 | 1 | -| u2 | constant_pool_count | 常量池计数器 | 2个字节 | 1 | -| cp_info | constant_pool | 常量池表 | n个字节 | constant_pool_count-1 | -| u2 | access_flags | 访问标识 | 2个字节 | 1 | -| u2 | this_class | 类索引 | 2个字节 | 1 | -| u2 | super_class | 父类索引 | 2个字节 | 1 | -| u2 | interfaces_count | 接口计数 | 2个字节 | 1 | -| u2 | interfaces | 接口索引集合 | 2个字节 | interfaces_count | -| u2 | fields_count | 字段计数器 | 2个字节 | 1 | -| field_info | fields | 字段表 | n个字节 | fields_count | -| u2 | methods_count | 方法计数器 | 2个字节 | 1 | -| method_info | methods | 方法表 | n个字节 | methods_count | -| u2 | attributes_count | 属性计数器 | 2个字节 | 1 | -| attribute_info | attributes | 属性表 | n个字节 | attributes_count | +接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法,两者不同的是: -Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表 +* 在初始化一个接口时,并不会先初始化它的父接口,所以执行接口的 () 方法不需要先执行父接口的 () 方法 +* 在初始化一个类时,不会先初始化所实现的接口,所以接口的实现类在初始化时不会执行接口的 () 方法 +* 只有当父接口中定义的变量使用时,父接口才会初始化 -* 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串 -* 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,表都以 `_info` 结尾,用于描述有层次关系的数据,整个 Class 文件本质上就是一张表,由于表没有固定长度,所以通常会在其前面加上个数说明 -获取方式: -* HelloWorld.java 执行 `javac -parameters -d . HellowWorld.java`指令 -* 写入文件指令 `javap -v xxx.class >xxx.txt` -* IDEA 插件 jclasslib +**** -*** +##### 时机 +类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化 +**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会发生): -##### 魔数版本 +* 当创建一个类的实例时,使用 new 关键字,或者通过反射、克隆、反序列化(前文讲述的对象的创建时机) +* 当调用类的静态方法或访问静态字段时,遇到 getstatic、putstatic、invokestatic 这三条字节码指令,如果类没有进行过初始化,则必须先触发其初始化 + * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) + * putstatic:程序给类的静态变量赋值 + * invokestatic :调用一个类的静态方法 +* 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行初始化,则需要先触发其初始化 +* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,但这条规则并**不适用于接口** +* 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 +* MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这两个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类 +* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 -魔数:每个 Class 文件开头的 4 个字节的无符号整数称为魔数(Magic Number),是 Class 文件的标识符,代表这是一个能被虚拟机接受的有效合法的 Class 文件, +**被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 -* 魔数值固定为 0xCAFEBABE,不符合则会抛出错误 +* 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 +* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法 +* 常量(final 修饰)在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 +* 调用 ClassLoader 类的 loadClass() 方法加载一个类,并不是对类的主动使用,不会导致类的初始化 -* 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动 -版本:4 个 字节,5 6两个字节代表的是编译的副版本号 minor_version,而 7 8 两个字节是编译的主版本号 major_version -* 不同版本的 Java 编译器编译的 Class 文件对应的版本是不一样的,高版本的 Java 虚拟机可以执行由低版本编译器生成的 Class 文件,反之 JVM 会抛出异常 `java.lang.UnsupportedClassVersionError` +*** -| 主版本(十进制) | 副版本(十进制) | 编译器版本 | -| ---------------- | ---------------- | ---------- | -| 45 | 3 | 1.1 | -| 46 | 0 | 1.2 | -| 47 | 0 | 1.3 | -| 48 | 0 | 1.4 | -| 49 | 0 | 1.5 | -| 50 | 0 | 1.6 | -| 51 | 0 | 1.7 | -| 52 | 0 | 1.8 | -| 53 | 0 | 1.9 | -| 54 | 0 | 1.10 | -| 55 | 0 | 1.11 | -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构.png) +##### init + +init 指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 +实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行 -图片来源:https://www.bilibili.com/video/BV1PJ411n7xZ +类实例化过程:**父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数** @@ -12738,81 +12855,53 @@ Class 文件格式采用一种类似于 C 语言结构体的方式进行数据 -##### 常量池 - -常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的无符号数,代表常量池计数器(constant_pool_count),这个容量计数是从 1 而不是 0 开始,是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目,这种情况可用索引值 0 来表示 +#### 卸载阶段 -constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表明有多少个常量池表项。表项中存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池 +时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止 -* 字面量(Literal) :基本数据类型、字符串类型常量、声明为 final 的常量值等 +卸载类即该类的 **Class 对象被 GC**,卸载类需要满足3个要求: -* 符号引用(Symbolic References):类和接口的全限定名、字段的名称和描述符、方法的名称和描述符 +1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象 +2. 该类没有在其他任何地方被引用 +3. 该类的类加载器的实例已被 GC,一般是可替换类加载器的场景,如 OSGi、JSP 的重加载等,很难达成 - * 全限定名:com/test/Demo 这个就是类的全限定名,仅仅是把包名的 `.` 替换成 `/`,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个 `;` 表示全限定名结束 +在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,这些类始终是可及的 - * 简单名称:指没有类型和参数修饰的方法或者字段名称,比如字段 x 的简单名称就是 x - * 描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值 - | 标志符 | 含义 | - | ------ | --------------------------------------------------------- | - | B | 基本数据类型 byte | - | C | 基本数据类型 char | - | D | 基本数据类型 double | - | F | 基本数据类型 float | - | I | 基本数据类型 int | - | J | 基本数据类型 long | - | S | 基本数据类型 short | - | Z | 基本数据类型 boolean | - | V | 代表 void 类型 | - | L | 对象类型,比如:`Ljava/lang/Object;`,不同方法间用`;`隔开 | - | [ | 数组类型,代表一维数组。比如:`double[][][] is [[[D` | +**** -常量类型和结构: -| 类型 | 标志(或标识) | 描述 | -| -------------------------------- | ------------ | ---------------------- | -| CONSTANT_utf8_info | 1 | UTF-8编码的字符串 | -| CONSTANT_Integer_info | 3 | 整型字面量 | -| CONSTANT_Float_info | 4 | 浮点型字面量 | -| CONSTANT_Long_info | 5 | 长整型字面量 | -| CONSTANT_Double_info | 6 | 双精度浮点型字面量 | -| CONSTANT_Class_info | 7 | 类或接口的符号引用 | -| CONSTANT_String_info | 8 | 字符串类型字面量 | -| CONSTANT_Fieldref_info | 9 | 字段的符号引用 | -| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 | -| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 | -| CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 | -| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | -| CONSTANT_MethodType_info | 16 | 标志方法类型 | -| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | -18 种常量没有出现 byte、short、char,boolean 的原因:编译之后都可以理解为 Integer +### 类加载器 +#### 类加载 +类加载方式: -**** +* 隐式加载:不直接在代码中调用 ClassLoader 的方法加载类对象 + * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 + * 在 JVM 启动时,通过三大类加载器加载 class +* 显式加载: + * ClassLoader.loadClass(className):只加载和连接,**不会进行初始化** + * Class.forName(String name, boolean initialize, ClassLoader loader):使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 +类的唯一性: +* 在 JVM 中表示两个 class 对象判断为同一个类存在的两个必要条件: + - 类的完整类名必须一致,包括包名 + - 加载这个类的 ClassLoader(指 ClassLoader 实例对象)必须相同 +* 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true -##### 访问标识 +命名空间: -访问标识(access_flag),又叫访问标志、访问标记,该标识用两个字节表示,用于识别一些类或者接口层次的访问信息,包括这个 Class 是类还是接口,是否定义为 public类型,是否定义为 abstract类型等 +- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成 +- 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类 -* 类的访问权限通常为 ACC_ 开头的常量 -* 每一种类型的表示都是通过设置访问标记的 32 位中的特定位来实现的,比如若是 public final 的类,则该标记为 `ACC_PUBLIC | ACC_FINAL` -* 使用 `ACC_SUPER` 可以让类更准确地定位到父类的方法,确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义,现代编译器都会设置并且使用这个标记 +基本特征: -| 标志名称 | 标志值 | 含义 | -| -------------- | ------ | ------------------------------------------------------------ | -| ACC_PUBLIC | 0x0001 | 标志为 public 类型 | -| ACC_FINAL | 0x0010 | 标志被声明为 final,只有类可以设置 | -| ACC_SUPER | 0x0020 | 标志允许使用 invokespecial 字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真,使用增强的方法调用父类方法 | -| ACC_INTERFACE | 0x0200 | 标志这是一个接口 | -| ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 | -| ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(由编译器产生的类,没有源码对应) | -| ACC_ANNOTATION | 0x2000 | 标志这是一个注解 | -| ACC_ENUM | 0x4000 | 标志这是一个枚举 | +* **可见性**,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的 +* **单一性**,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,不会在子加载器中重复加载 @@ -12820,84 +12909,89 @@ constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表 -##### 索引集合 +#### 加载器 -类索引、父类索引、接口索引集合 +类加载器是 Java 的核心组件,用于加载字节码到 JVM 内存,得到 Class 类的对象 -* 类索引用于确定这个类的全限定名 +从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器: -* 父类索引用于确定这个类的父类的全限定名,Java 语言不允许多重继承,所以父类索引只有一个,除了Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为0 +- 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分 +- 自定义类加载器(User-Defined ClassLoader):Java 虚拟机规范**将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器**,使用 Java 语言实现,独立于虚拟机 -* 接口索引集合就用来描述这个类实现了哪些接口 - * interfaces_count 项的值表示当前类或接口的直接超接口数量 - * interfaces[] 接口索引集合,被实现的接口将按 implements 语句后的接口顺序从左到右排列在接口索引集合中 +从 Java 开发人员的角度看: -| 长度 | 含义 | -| ---- | ---------------------------- | -| u2 | this_class | -| u2 | super_class | -| u2 | interfaces_count | -| u2 | interfaces[interfaces_count] | +* 启动类加载器(Bootstrap ClassLoader): + * 处于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类 + * 类加载器负责加载在 `JAVA_HOME/jre/lib `或 `sun.boot.class.path` 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中 + * 仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在 lib 目录中也不会被加载 + * 启动类加载器无法被 Java 程序直接引用,编写自定义类加载器时,如果要把加载请求委派给启动类加载器,直接使用 null 代替 +* 扩展类加载器(Extension ClassLoader): + * 由 ExtClassLoader (sun.misc.Launcher$ExtClassLoader) 实现,上级为 Bootstrap,显示为 null + * 将 `JAVA_HOME/jre/lib/ext `或者被 `java.ext.dir` 系统变量所指定路径中的所有类库加载到内存中 + * 开发者可以使用扩展类加载器,创建的 JAR 放在此目录下,会由扩展类加载器自动加载 +* 应用程序类加载器(Application ClassLoader): + * 由 AppClassLoader(sun.misc.Launcher$AppClassLoader) 实现,上级为 Extension + * 负责加载环境变量 classpath 或系统属性 `java.class.path` 指定路径下的类库 + * 这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此称为系统类加载器 + * 可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,这个就是程序中默认的类加载器 +* 自定义类加载器:由开发人员自定义的类加载器,上级是 Application +```java +public static void main(String[] args) { + //获取系统类加载器 + ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); + System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 + //获取其上层 扩展类加载器 + ClassLoader extClassLoader = systemClassLoader.getParent(); + System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6 -*** + //获取其上层 获取不到引导类加载器 + ClassLoader bootStrapClassLoader = extClassLoader.getParent(); + System.out.println(bootStrapClassLoader);//null + //对于用户自定义类来说:使用系统类加载器进行加载 + ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); + System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 + //String 类使用引导类加载器进行加载的 --> java核心类库都是使用启动类加载器加载的 + ClassLoader classLoader1 = String.class.getClassLoader(); + System.out.println(classLoader1);//null -##### 字段表 +} +``` -字段 fields 用于描述接口或类中声明的变量,包括类变量以及实例变量,但不包括方法内部、代码块内部声明的局部变量以及从父类或父接口继承。字段叫什么名字、被定义为什么数据类型,都是无法固定的,只能引用常量池中的常量来描述 +补充两个类加载器: -fields_count(字段计数器),表示当前 class 文件 fields 表的成员个数,用两个字节来表示 +* SecureClassLoader 扩展了 ClassLoader,新增了几个与使用相关的代码源和权限定义类验证(对 class 源码的访问权限)的方法,一般不会直接跟这个类打交道,更多是与它的子类 URLClassLoader 有所关联 +* ClassLoader 是一个抽象类,很多方法是空的没有实现,而 URLClassLoader 这个实现类为这些方法提供了具体的实现,并新增了 URLClassPath 类协助取得 Class 字节流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁 -fields[](字段表): -* 表中的每个成员都是一个 fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述 -* 字段访问标识: +*** - | 标志名称 | 标志值 | 含义 | - | ------------- | ------ | -------------------------- | - | ACC_PUBLIC | 0x0001 | 字段是否为public | - | ACC_PRIVATE | 0x0002 | 字段是否为private | - | ACC_PROTECTED | 0x0004 | 字段是否为protected | - | ACC_STATIC | 0x0008 | 字段是否为static | - | ACC_FINAL | 0x0010 | 字段是否为final | - | ACC_VOLATILE | 0x0040 | 字段是否为volatile | - | ACC_TRANSTENT | 0x0080 | 字段是否为transient | - | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | - | ACC_ENUM | 0x4000 | 字段是否为enum | -* 字段名索引:根据该值查询常量池中的指定索引项即可 -* 描述符索引:用来描述字段的数据类型、方法的参数列表和返回值 +#### 常用API - | 字符 | 类型 | 含义 | - | ----------- | --------- | ----------------------- | - | B | byte | 有符号字节型树 | - | C | char | Unicode字符,UTF-16编码 | - | D | double | 双精度浮点数 | - | F | float | 单精度浮点数 | - | I | int | 整型数 | - | J | long | 长整数 | - | S | short | 有符号短整数 | - | Z | boolean | 布尔值true/false | - | V | void | 代表void类型 | - | L Classname | reference | 一个名为Classname的实例 | - | [ | reference | 一个一维数组 | +ClassLoader 类,是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器) -* 属性表集合:属性个数存放在 attribute_count 中,属性具体内容存放在 attribute 数组中,一个字段还可能拥有一些属性,用于存储更多的额外信息,比如初始化值、一些注释信息等 +获取 ClassLoader 的途径: - ```java - ConstantValue_attribute{ - u2 attribute_name_index; - u4 attribute_length; - u2 constantvalue_index; - } - ``` +* 获取当前类的 ClassLoader:`clazz.getClassLoader()` +* 获取当前线程上下文的 ClassLoader:`Thread.currentThread.getContextClassLoader()` +* 获取系统的 ClassLoader:`ClassLoader.getSystemClassLoader()` +* 获取调用者的 ClassLoader:`DriverManager.getCallerClassLoader()` - 对于常量属性而言,attribute_length 值恒为2 +ClassLoader 类常用方法: + +* `getParent()`:返回该类加载器的超类加载器 +* `loadclass(String name)`:加载名为 name 的类,返回结果为 Class 类的实例,**该方法就是双亲委派模式** +* `findclass(String name)`:查找二进制名称为 name 的类,返回结果为 Class 类的实例,该方法会在检查完父类加载器之后被 loadClass() 方法调用 +* `findLoadedClass(String name)`:查找名称为 name 的已经被加载过的类,final 修饰无法重写 +* `defineClass(String name, byte[] b, int off, int len)`:将**字节流**解析成 JVM 能够识别的类对象 +* `resolveclass(Class c)`:链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 +* `InputStream getResourceAsStream(String name)`:指定资源名称获取输入流 @@ -12905,108 +12999,108 @@ fields[](字段表): -##### 方法表 +#### 加载模型 -方法表是 methods 指向常量池索引集合,其中每一个 method_info 项都对应着一个类或者接口中的方法信息,完整描述了每个方法的签名 +##### 加载机制 -* 如果这个方法不是抽象的或者不是 native 的,字节码中就会体现出来 -* methods 表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法 -* methods 表可能会出现由编译器自动添加的方法,比如初始化方法 和实例化方法 +在 JVM 中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 -**重载(Overload)**一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,因为返回值不会包含在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存 +- **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 -methods_count(方法计数器):表示 class 文件 methods 表的成员个数,使用两个字节来表示 +- **双亲委派:**先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 -methods[](方法表):每个表项都是一个 method_info 结构,表示当前类或接口中某个方法的完整描述 +- **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区中 + - 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因 -* 方法表结构如下: - | 类型 | 名称 | 含义 | 数量 | - | -------------- | ---------------- | ---------- | ---------------- | - | u2 | access_flags | 访问标志 | 1 | - | u2 | name_index | 字段名索引 | 1 | - | u2 | descriptor_index | 描述符索引 | 1 | - | u2 | attrubutes_count | 属性计数器 | 1 | - | attribute_info | attributes | 属性集合 | attributes_count | -* 方法表访问标志: - | 标志名称 | 标志值 | 含义 | - | ------------- | ------ | -------------------------- | - | ACC_PUBLIC | 0x0001 | 字段是否为 public | - | ACC_PRIVATE | 0x0002 | 字段是否为 private | - | ACC_PROTECTED | 0x0004 | 字段是否为 protected | - | ACC_STATIC | 0x0008 | 字段是否为 static | - | ACC_FINAL | 0x0010 | 字段是否为 final | - | ACC_VOLATILE | 0x0040 | 字段是否为 volatile | - | ACC_TRANSTENT | 0x0080 | 字段是否为 transient | - | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | - | ACC_ENUM | 0x4000 | 字段是否为 enum | +*** -*** +##### 双亲委派 +双亲委派模型(Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它类加载器都要有父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) -##### 属性表 +工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载 -属性表集合,指的是 Class 文件所携带的辅助信息,比如该 Class 文件的源文件的名称,以及任何带有 `RetentionPolicy.CLASS` 或者 `RetentionPolicy.RUNTIME` 的注解,这类信息通常被用于 Java 虚拟机的验证和运行,以及 Java 程序的调试。字段表、方法表都可以有自己的属性表,用于描述某些场景专有的信息 +双亲委派机制的优点: -attributes_ count(属性计数器):表示当前文件属性表的成员个数 +* 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证全局唯一性 -attributes[](属性表):属性表的每个项的值必须是 attribute_info 结构 +* Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一 -* 属性的通用格式: +* 保护程序安全,防止类库的核心 API 被随意篡改 + + 例如:在工程中新建 java.lang 包,接着在该包下新建 String 类,并定义 main 函数 ```java - ConstantValue_attribute{ - u2 attribute_name_index; //属性名索引 - u4 attribute_length; //属性长度 - u2 attribute_info; //属性表 + public class String { + public static void main(String[] args) { + System.out.println("demo info"); + } } ``` + + 此时执行 main 函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库。出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 -* 属性类型: - - | 属性名称 | 使用位置 | 含义 | - | ------------------------------------- | ------------------ | ------------------------------------------------------------ | - | Code | 方法表 | Java 代码编译成的字节码指令 | - | ConstantValue | 字段表 | final 关键字定义的常量池 | - | Deprecated | 类、方法、字段表 | 被声明为 deprecated 的方法和字段 | - | Exceptions | 方法表 | 方法抛出的异常 | - | EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 | - | InnerClass | 类文件 | 内部类列表 | - | LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 | - | LocalVariableTable | Code 属性 | 方法的局部变量描述 | - | StackMapTable | Code 属性 | JDK1.6 中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 | - | Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 | - | SourceFile | 类文件 | 记录源文件名称 | - | SourceDebugExtension | 类文件 | 用于存储额外的调试信息 | - | Syothetic | 类,方法表,字段表 | 标志方法或字段为编泽器自动生成的 | - | LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 | - | RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 | - | RuntimelnvisibleAnnotations | 类,方法表,字段表 | 用于指明哪些注解是运行时不可见的 | - | RuntimeVisibleParameterAnnotation | 方法表 | 作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法 | - | RuntirmelnvisibleParameterAnniotation | 方法表 | 作用与 RuntimelnvisibleAnnotations 属性类似,作用对象哪个为方法参数 | - | AnnotationDefauit | 方法表 | 用于记录注解类元素的默认值 | - | BootstrapMethods | 类文件 | 用于保存 invokeddynanic 指令引用的引导方式限定符 | - +双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类**(可见性) + -**** +*** -#### 编译指令 +##### 源码分析 -##### javac +```java +protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + // 调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类 + Class c = findLoadedClass(name); + + // 当前类加载器如果没有加载过 + if (c == null) { + long t0 = System.nanoTime(); + try { + // 判断当前类加载器是否有父类加载器 + if (parent != null) { + // 如果当前类加载器有父类加载器,则调用父类加载器的 loadClass(name,false) +          // 父类加载器的 loadClass 方法,又会检查自己是否已经加载过 + c = parent.loadClass(name, false); + } else { + // 当前类加载器没有父类加载器,说明当前类加载器是 BootStrapClassLoader +           // 则调用 BootStrap ClassLoader 的方法加载类 + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) { } -javac:编译命令,将 java 源文件编译成 class 字节码文件 + if (c == null) { + // 如果调用父类的类加载器无法对类进行加载,则用自己的 findClass() 方法进行加载 + // 可以自定义 findClass() 方法 + long t1 = System.nanoTime(); + c = findClass(name); -`javac xx.java` 不会在生成对应的局部变量表等信息,使用 `javac -g xx.java` 可以生成所有相关信息 + // this is the defining class loader; record the stats + sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); + sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); + sun.misc.PerfCounter.getFindClasses().increment(); + } + } + if (resolve) { + // 链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 + resolveClass(c); + } + return c; + } +} +``` @@ -13014,62 +13108,43 @@ javac:编译命令,将 java 源文件编译成 class 字节码文件 -##### javap - -javap 反编译生成的字节码文件,根据 class 字节码文件,反解析出当前类对应的 code 区 (字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息 - -用法:javap - -```sh --help --help -? 输出此用法消息 --version 版本信息 --public 仅显示公共类和成员 --protected 显示受保护的/公共类和成员 --package 显示程序包/受保护的/公共类和成员 (默认) --p -private 显示所有类和成员 - #常用的以下三个 --v -verbose 输出附加信息 --l 输出行号和本地变量表 --c 对代码进行反汇编 #反编译 - --s 输出内部类型签名 --sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) --constants 显示最终常量 --classpath 指定查找用户类文件的位置 --cp 指定查找用户类文件的位置 --bootclasspath 覆盖引导类文件的位置 -``` - - +##### 破坏委派 -*** +双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者的类加载器实现方式 +破坏双亲委派模型的方式: +* 自定义 ClassLoader -#### 指令集 + * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 + * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 -##### 执行指令 +* 引入线程**上下文类加载器** -Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某种操作的操作码(opcode)以及零至多个代表此操作所需参数的操作数(operand)所构成,虚拟机中许多指令并不包含操作数,只有一个操作码(零地址指令) + Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类: -由于限制了 Java 虚拟机操作码的长度为一个字节(0~255),所以指令集的操作码总数不可能超过 256 条 + * SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的 + * SPI 的实现类是由系统类加载器来加载的,引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader 无法委派 AppClassLoader 来加载类 -在 JVM 的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据 + JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置线程上下文类加载器,在执行线程中抛弃双亲委派加载模式,使程序可以逆向使用类加载器,使 Bootstrap 加载器拿到了 Application 加载器加载的类,破坏了双亲委派模型 + +* 实现程序的动态性,如代码热替换(Hot Swap)、模块热部署(Hot Deployment) -* i 代表对 int 类型的数据操作 -* l 代表 long -* s 代表 short -* b 代表 byte -* c 代表 char -* f 代表 float -* d 代表 double + IBM 公司主导的 JSR一291(OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换,在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构 -大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend-)为相应的 int 类型数据 + 当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索: -在做值相关操作时: + 1. 将以 java.* 开头的类,委派给父类加载器加载 + 2. 否则,将委派列表名单内的类,委派给父类加载器加载 + 3. 否则,将 Import 列表中的类,委派给 Export 这个类的 Bundle 的类加载器加载 + 4. 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载 + 5. 否则,查找类是否在自己的 Fragment Bundle 中,如果在就委派给 Fragment Bundle 类加载器加载 + 6. 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载 + 7. 否则,类查找失败 + + 热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,**热替换的关键需求在于服务不能中断**,修改必须立即表现正在运行的系统之中 -- 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,也可能是对象的引用)被压入操作数栈 -- 一个指令,也可以从操作数栈中取出一到多个值(pop 多次),完成赋值、加减乘除、方法传参、系统调用等等操作 + @@ -13077,195 +13152,180 @@ Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某 -##### 加载存储 - -加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递 - -局部变量压栈指令:将给定的局部变量表中的数据压入操作数栈 +#### 沙箱机制 -* xload、xload_n,x 表示取值数据类型,为 i、l、f、d、a, n 为 0 到 3 -* 指令 xload_n 表示将第 n 个局部变量压入操作数栈,aload_n 表示将一个对象引用压栈 -* 指令 xload n 通过指定参数的形式,把局部变量压入操作数栈,局部变量数量超过 4 个时使用这个命令 +沙箱机制(Sandbox):将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 -常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const、push、ldc指令 +沙箱**限制系统资源访问**,包括 CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 -* push:包括 bipush 和 sipush,区别在于接收数据类型的不同,bipush 接收 8 位整数作为参数,sipush 接收 16 位整数 -* ldc:如果以上指令不能满足需求,可以使用 ldc 指令,接收一个 8 位的参数,该参数指向常量池中的 int、 float 或者 String 的索引,将指定的内容压入堆栈。ldc_w 接收两个 8 位参数,能支持的索引范围更大,如果要压入的元素是 long 或 double 类型的,则使用 ldc2_w 指令 -* aconst_null 将 null 对象引用压入栈,iconst_m1 将 int 类型常量 -1 压入栈,iconst_0 将 int 类型常量 0 压入栈 +* JDK1.0:Java 中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码被看作是不受信的。对于授信的本地代码,可以访问一切本地资源,而对于非授信的远程代码不可以访问本地资源,其实依赖于沙箱机制。如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现 +* JDK1.1:针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限 +* JDK1.2:改进了安全机制,增加了代码签名,不论本地代码或是远程代码都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制 +* JDK1.6:当前最新的安全机制,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,不同的保护域对应不一样的权限。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问 -出栈装入局部变量表指令:将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值 + -* xstore、xstore_n,x 表示取值类型为 i、l、f、d、a, n 为 0 到 3 -* xastore 表示存入数组,x 取值为 i、l、f、d、a、b、c、s -扩充局部变量表的访问索引的指令:wide +*** -**** +#### 自定义 +对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可 -##### 算术指令 +作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏 -算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算结果重新压入操作数栈 +```java +//自定义类加载器,读取指定的类路径classPath下的class文件 +public class MyClassLoader extends ClassLoader{ + private String classPath; -没有直接支持 byte、 short、 char 和 boolean类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组 - -* 加法指令:iadd、ladd、fadd、dadd -* 减法指令:isub、lsub、fsub、dsub -* 乘法指令:imu、lmu、fmul、dmul -* 除法指令:idiv、ldiv、fdiv、ddiv -* 求余指令:irem、lrem、frem、drem(remainder 余数) -* 取反指令:ineg、lneg、fneg、dneg (negation 取反) -* 自增指令:iinc(直接**在局部变量 slot 上进行运算**,不用放入操作数栈) -* 位运算指令,又可分为: - - 位移指令:ishl、ishr、 iushr、lshl、lshr、 lushr - - 按位或指令:ior、lor - - 按位与指令:iand、land - - 按位异或指令:ixor、lxor - -* 比较指令:dcmpg、dcmpl、 fcmpg、fcmpl、lcmp - -运算模式: - -* 向最接近数舍入模式,JVM 在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示形式与该值一样接近,将优先选择最低有效位为零的 -* 向零舍入模式:将浮点数转换为整数时,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果 - -NaN 值:当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义,将使用 NaN 值来表示 - -```java -double j = i / 0.0; -System.out.println(j);//无穷大,NaN: not a number -``` - -**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc - -```java - 4 iload_1 //存入操作数栈 - 5 iinc 1 by 1 //自增i++ - 8 istore_3 //把操作数栈没有自增的数据的存入局部变量表 - 9 iinc 2 by 1 //++i -12 iload_2 //加载到操作数栈 -13 istore 4 //存入局部变量表,这个存入没有 _ 符号,_只能到3 -``` + public MyClassLoader(String classPath) { + this.classPath = classPath; + } + + public MyClassLoader(ClassLoader parent, String byteCodePath) { + super(parent); + this.classPath = classPath; + } -```java -public class Demo { - public static void main(String[] args) { - int a = 10; - int b = a++ + ++a + a--; - System.out.println(a); //11 - System.out.println(b); //34 + @Override + protected Class findClass(String name) throws ClassNotFoundException { + BufferedInputStream bis = null; + ByteArrayOutputStream baos = null; + try { + // 获取字节码文件的完整路径 + String fileName = classPath + className + ".class"; + // 获取一个输入流 + bis = new BufferedInputStream(new FileInputStream(fileName)); + // 获取一个输出流 + baos = new ByteArrayOutputStream(); + // 具体读入数据并写出的过程 + int len; + byte[] data = new byte[1024]; + while ((len = bis.read(data)) != -1) { + baos.write(data, 0, len); + } + // 获取内存中的完整的字节数组的数据 + byte[] byteCodes = baos.toByteArray(); + // 调用 defineClass(),将字节数组的数据转换为 Class 的实例。 + Class clazz = defineClass(null, byteCodes, 0, byteCodes.length); + return clazz; + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (baos != null) + baos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + try { + if (bis != null) + bis.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return null; } } ``` -判断结果: - ```java -public class Demo { - public static void main(String[] args) { - int i = 0; - int x = 0; - while (i < 10) { - x = x++; - i++; - } - System.out.println(x); // 结果是 0 +public static void main(String[] args) { + MyClassLoader loader = new MyClassLoader("D:\Workspace\Project\JVM_study\src\java1\"); + + try { + Class clazz = loader.loadClass("Demo1"); + System.out.println("加载此类的类的加载器为:" + clazz.getClassLoader().getClass().getName());//MyClassLoader + + System.out.println("加载当前类的类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());//sun.misc.Launcher$AppClassLoader + } catch (ClassNotFoundException e) { + e.printStackTrace(); } } ``` -*** +**** -##### 类型转换 +#### JDK9 -类型转换指令可以将两种不同的数值类型进行相互转换,除了 boolean 之外的七种类型 +为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动: -宽化类型转换: +* 扩展机制被移除,扩展类加载器由于**向后兼容性**的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取 -* JVM 支持以下数值的宽化类型转换(widening numeric conversion),小范围类型到大范围类型的安全转换 - * 从 int 类型到 long、float 或者 double 类型,对应的指令为 i2l、i2f、i2d - * 从 long 类型到 float、 double 类型,对应的指令为 l2f、l2d - * 从 float 类型到 double 类型,对应的指令为 f2d +* JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件),其中 Java 类库就满足了可扩展的需求,那就无须再保留 `\lib\ext` 目录,此前使用这个目录或者 `java.ext.dirs` 系统变量来扩展 JDK 功能的机制就不需要再存在 -* 精度损失问题 - * 宽化类型转换是不会因为超过目标类型最大值而丢失信息 - * 从 int 转换到 float 或者 long 类型转换到 double 时,将可能发生精度丢失 +* 启动类加载器、平台类加载器、应用程序类加载器全都继承于 `jdk.internal.loader.BuiltinClassLoader` -* 从 byte、char 和 short 类型到 int 类型的宽化类型转换实际上是不存在的,JVM 把它们当作 int 处理 + -窄化类型转换: -* Java 虚拟机直接支持以下窄化类型转换: - * 从 int 类型至 byte、 short 或者 char 类型,对应的指令有 i2b、i2c、i2s - * 从 long 类型到 int 类型,对应的指令有 l2i - * 从 float 类型到 int 或者 long 类型,对应的指令有:f2i、f2l - * 从 double 类型到 int、long 或 float 者类型,对应的指令有 d2i、d2、d2f -* 精度损失问题: - * 窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,转换过程可能会导致数值丢失精度 - * 将一个浮点值窄化转换为整数类型 T(T 限于 int 或 long 类型之一)时,将遵循以下转换规则: - - 如果浮点值是 NaN,那转换结果就是 int 或 long 类型的 0 - - 如果浮点值不是无穷大的话,浮点值使用 IEEE 754 的向零舍入模式取整,获得整数值 v,如果 v 在目标类型 T 的表示范围之内,那转换结果就是 v,否则将根据 v 的符号,转换为 T 所能表示的最大或者最小正数 +*** -*** +## 运行机制 -##### 创建访问 +### 执行过程 -创建指令: + Java 文件编译执行的过程: -* 创建类实例指令:new,接收一个操作数指向常量池的索引,表示要创建的类型,执行完成后将对象的引用压入栈 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java文件编译执行的过程.png) - ```java - 0: new #2 // class com/jvm/bytecode/Demo - 3: dup - 4: invokespecial #3 // Method "":()V - ``` +- 类加载器:用于装载字节码文件(.class文件) +- 运行时数据区:用于分配存储空间 +- 执行引擎:执行字节码文件或本地方法 +- 垃圾回收器:用于对 JVM 中的垃圾内容进行回收 - **dup 是复制操作数栈栈顶的内容**,需要两份引用原因: - - 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) - - 一个要配合 astore_1 赋值给局部变量 -* 创建数组的指令:newarray、anewarray、multianewarray +**** - * newarray:创建基本类型数组 - * anewarray:创建引用类型数组 - * multianewarray:创建多维数组 -字段访问指令:对象创建后可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素 -* 访问类字段(static字段,或者称为类变量)的指令:getstatic、putstatic -* 访问类实例字段(非static字段,或者称为实例变量)的指令:getfield、 putfield +### 字节码 -类型检查指令:检查类实例或数组类型的指令 +#### 跨平台性 -* checkcast:用于检查类型强制转换是否可以进行,如果可以进行 checkcast 指令不会改变操作数栈,否则它会抛出 ClassCastException 异常 +Java 语言:跨平台的语言(write once ,run anywhere) -* instanceof:判断给定对象是否是某一个类的实例,会将判断结果压入操作数栈 +* 当 Java 源代码成功编译成字节码后,在不同的平台上面运行**无须再次编译** +* 让一个 Java 程序正确地运行在 JVM 中,Java 源码就必须要被编译为符合 JVM 规范的字节码 +编译过程中的编译器: +* 前端编译器: Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ,把源代码编译为字节码文件 .class + * IntelliJ IDEA 使用 javac 编译器 + * Eclipse 中,当开发人员编写完代码后保存时,ECJ 编译器就会把未编译部分的源码逐行进行编译,而非每次都全量编译,因此 ECJ 的编译效率会比 javac 更加迅速和高效 + * 前端编译器并不会直接涉及编译优化等方面的技术,具体优化细节移交给 HotSpot 的 JIT 编译器负责 -**** +* 后端运行期编译器:HotSpot VM 的 C1、C2 编译器,也就是 JIT 编译器,Graal 编译器 + * JIT编译器:执行引擎部分详解 + * Graal 编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追平了 C2 +* 静态提前编译器:AOT (Ahead Of Time Compiler)编译器,直接把源代码编译成本地机器代码, -##### 方法指令 + * JDK 9 引入,是与即时编译相对立的一个概念,即时编译指的是在程序的运行过程中将字节码转换为机器码,AOT 是程序运行之前便将字节码转换为机器码 + + * 优点:JVM 加载已经预编译成二进制库,可以直接执行,不必等待即时编译器的预热,减少 Java 应用第一次运行慢的现象 + * 缺点: + * 破坏了 Java **一次编译,到处运行**,必须为每个不同硬件编译对应的发行包 + * 降低了 Java 链接过程的动态性,加载的代码在编译期就必须全部已知 -方法调用指令:invokevirtual、 invokeinterface、invokespecial、invokestatic、invokedynamic -**方法调用章节详解** @@ -13273,140 +13333,101 @@ public class Demo { -##### 操作数栈 +#### 语言发展 -JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的指令 +机器码:各种用二进制编码方式表示的指令,与 CPU 紧密相关,所以不同种类的 CPU 对应的机器指令不同 -* pop、pop2:将一个或两个元素从栈顶弹出,并且直接废弃 -* dup、dup2,dup_x1、dup2_x1,dup_x2、dup2_x2:复制栈顶一个或两个数值并重新压入栈顶 +指令:指令就是把机器码中特定的 0 和 1 序列,简化成对应的指令,例如 mov,inc 等,可读性稍好,但是不同的硬件平台的同一种指令(比如 mov),对应的机器码也可能不同 -* swap:将栈最顶端的两个 slot 数值位置交换,JVM 没有提供交换两个 64 位数据类型数值的指令 +指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 -* nop:一个非常特殊的指令,字节码为 0x00,和汇编语言中的 nop 一样,表示什么都不做,一般可用于调试、占位等 +- x86 指令集,对应的是 x86 架构的平台 +- ARM 指令集,对应的是 ARM 架构的平台 +汇编语言:用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址 +* 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令 +* 计算机只认识指令码,汇编语言编写的程序也必须翻译成机器指令码,计算机才能识别和执行 -*** +高级语言:为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言 +字节码:是一种中间状态(中间码)的二进制代码,比机器码更抽象,需要直译器转译后才能成为机器码 +* 字节码为了实现特定软件运行和软件环境,与硬件环境无关 +* 通过编译器和虚拟机器实现,编译器将源码编译成字节码,虚拟机器将字节码转译为可以直接执行的指令 -##### 控制转移 + -比较指令:比较栈顶两个元素的大小,并将比较结果入栈 -* lcmp:比较两个 long 类型值 -* fcmpl:比较两个 float 类型值(当遇到NaN时,返回-1) -* fcmpg:比较两个 float 类型值(当遇到NaN时,返回1) -* dcmpl:比较两个 double 类型值(当遇到NaN时,返回-1) -* dcmpg:比较两个 double 类型值(当遇到NaN时,返回1) +*** -条件跳转指令: -| 指令 | 说明 | -| --------- | -------------------------------------------------- | -| ifeq | equals,当栈顶int类型数值等于0时跳转 | -| ifne | not equals,当栈顶in类型数值不等于0时跳转 | -| iflt | lower than,当栈顶in类型数值小于0时跳转 | -| ifle | lower or equals,当栈顶in类型数值小于等于0时跳转 | -| ifgt | greater than,当栈顶int类型数组大于0时跳转 | -| ifge | greater or equals,当栈顶in类型数值大于等于0时跳转 | -| ifnull | 为 null 时跳转 | -| ifnonnull | 不为 null 时跳转 | -比较条件跳转指令: -| 指令 | 说明 | -| --------- | --------------------------------------------------------- | -| if_icmpeq | 比较栈顶两 int 类型数值大小(下同),当前者等于后者时跳转 | -| if_icmpne | 当前者不等于后者时跳转 | -| if_icmplt | 当前者小于后者时跳转 | -| if_icmple | 当前者小于等于后者时跳转 | -| if_icmpgt | 当前者大于后者时跳转 | -| if_icmpge | 当前者大于等于后者时跳转 | -| if_acmpeq | 当结果相等时跳转 | -| if_acmpne | 当结果不相等时跳转 | -多条件分支跳转指令: +#### 类结构 -* tableswitch:用于 switch 条件跳转,case 值连续 -* lookupswitch:用于 switch 条件跳转,case 值不连续 - -无条件跳转指令: - -* goto:用来进行跳转到指定行号的字节码 - -* goto_w:无条件跳转(宽索引) - - - - - -*** +##### 文件结构 +字节码是一种二进制的类文件,是编译之后供虚拟机解释执行的二进制字节码文件,**一个 class 文件对应一个 public 类型的类或接口** +字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以执行效率比 Java 高 -##### 异常处理 +JVM 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html -###### 处理机制 +根据 JVM 规范,类文件结构如下: -抛出异常指令:athrow 指令 +```java +ClassFile { + u4 magic; + u2 minor_version; + u2 major_version; + u2 constant_pool_count; + cp_info constant_pool[constant_pool_count-1]; + u2 access_flags; + u2 this_class; + u2 super_class; + u2 interfaces_count; + u2 interfaces[interfaces_count]; + u2 fields_count; + field_info fields[fields_count]; + u2 methods_count; + method_info methods[methods_count]; + u2 attributes_count; + attribute_info attributes[attributes_count]; +} +``` -JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是**采用异常表来完成**的 +| 类型 | 名称 | 说明 | 长度 | 数量 | +| -------------- | ------------------- | -------------------- | ------- | --------------------- | +| u4 | magic | 魔数,识别类文件格式 | 4个字节 | 1 | +| u2 | minor_version | 副版本号(小版本) | 2个字节 | 1 | +| u2 | major_version | 主版本号(大版本) | 2个字节 | 1 | +| u2 | constant_pool_count | 常量池计数器 | 2个字节 | 1 | +| cp_info | constant_pool | 常量池表 | n个字节 | constant_pool_count-1 | +| u2 | access_flags | 访问标识 | 2个字节 | 1 | +| u2 | this_class | 类索引 | 2个字节 | 1 | +| u2 | super_class | 父类索引 | 2个字节 | 1 | +| u2 | interfaces_count | 接口计数 | 2个字节 | 1 | +| u2 | interfaces | 接口索引集合 | 2个字节 | interfaces_count | +| u2 | fields_count | 字段计数器 | 2个字节 | 1 | +| field_info | fields | 字段表 | n个字节 | fields_count | +| u2 | methods_count | 方法计数器 | 2个字节 | 1 | +| method_info | methods | 方法表 | n个字节 | methods_count | +| u2 | attributes_count | 属性计数器 | 2个字节 | 1 | +| attribute_info | attributes | 属性表 | n个字节 | attributes_count | -* 代码: +Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表 - ```java - public static void main(String[] args) { - int i = 0; - try { - i = 10; - } catch (Exception e) { - i = 20; - } finally { - i = 30; - } - } - ``` - -* 字节码: +* 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串 +* 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,表都以 `_info` 结尾,用于描述有层次关系的数据,整个 Class 文件本质上就是一张表,由于表没有固定长度,所以通常会在其前面加上个数说明 - * 多出一个 **Exception table** 的结构,**[from, to) 是前闭后开的检测范围**,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 - * 11 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 +获取方式: - ```java - 0: iconst_0 - 1: istore_1 // 0 -> i ->赋值 - 2: bipush 10 // try 10 放入操作数栈顶 - 4: istore_1 // 10 -> i 将操作数栈顶数据弹出,存入局部变量表的 slot1 - 5: bipush 30 // 【finally】 - 7: istore_1 // 30 -> i - 8: goto 27 // return ----------------------------------- - 11: astore_2 // catch Exceptin -> e ---------------------- - 12: bipush 20 // - 14: istore_1 // 20 -> i - 15: bipush 30 // 【finally】 - 17: istore_1 // 30 -> i - 18: goto 27 // return ----------------------------------- - 21: astore_3 // catch any -> slot 3 ---------------------- - 22: bipush 30 // 【finally】 - 24: istore_1 // 30 -> i - 25: aload_3 // 将局部变量表的slot 3数据弹出,放入操作数栈栈顶 - 26: athrow // throw 抛出异常 - 27: return - Exception table: - // 任何阶段出现任务异常都会执行 finally - from to target type - 2 5 11 Class java/lang/Exception - 2 5 21 any // 剩余的异常类型,比如 Error - 11 15 21 any // 剩余的异常类型,比如 Error - LineNumberTable: ... - LocalVariableTable: - Start Length Slot Name Signature - 12 3 2 e Ljava/lang/Exception; - 0 28 0 args [Ljava/lang/String; - 2 26 1 i I - ``` +* HelloWorld.java 执行 `javac -parameters -d . HellowWorld.java`指令 +* 写入文件指令 `javap -v xxx.class >xxx.txt` +* IDEA 插件 jclasslib @@ -13414,418 +13435,482 @@ JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是* -###### finally - -finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程(上节案例) - -* 代码: - - ```java - public static int test() { - try { - return 10; - } finally { - return 20; - } - } - ``` +##### 魔数版本 -* 字节码: +魔数:每个 Class 文件开头的 4 个字节的无符号整数称为魔数(Magic Number),是 Class 文件的标识符,代表这是一个能被虚拟机接受的有效合法的 Class 文件, - ```java - 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 - 3: bipush 20 // 20 放入栈顶 - 5: ireturn // 返回栈顶 int(20) - 6: astore_1 // catch any 存入局部变量表的 slot1 - 7: bipush 20 // 20 放入栈顶 - 9: ireturn // 返回栈顶 int(20) - Exception table: - from to target type - 0 3 6 any - ``` +* 魔数值固定为 0xCAFEBABE,不符合则会抛出错误 +* 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动 +版本:4 个 字节,5 6两个字节代表的是编译的副版本号 minor_version,而 7 8 两个字节是编译的主版本号 major_version -*** +* 不同版本的 Java 编译器编译的 Class 文件对应的版本是不一样的,高版本的 Java 虚拟机可以执行由低版本编译器生成的 Class 文件,反之 JVM 会抛出异常 `java.lang.UnsupportedClassVersionError` +| 主版本(十进制) | 副版本(十进制) | 编译器版本 | +| ---------------- | ---------------- | ---------- | +| 45 | 3 | 1.1 | +| 46 | 0 | 1.2 | +| 47 | 0 | 1.3 | +| 48 | 0 | 1.4 | +| 49 | 0 | 1.5 | +| 50 | 0 | 1.6 | +| 51 | 0 | 1.7 | +| 52 | 0 | 1.8 | +| 53 | 0 | 1.9 | +| 54 | 0 | 1.10 | +| 55 | 0 | 1.11 | +![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构.png) -###### return -* 吞异常 - ```java - public static int test() { - try { - return 10; - } finally { - return 20; - } - } - ``` +图片来源:https://www.bilibili.com/video/BV1PJ411n7xZ - ```java - 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 - 3: bipush 20 // 20 放入栈顶 - 5: ireturn // 返回栈顶 int(20) - 6: astore_1 // catch any 存入局部变量表的 slot1 - 7: bipush 20 // 20 放入栈顶 - 9: ireturn // 返回栈顶 int(20) - Exception table: - from to target type - 0 3 6 any - ``` - * 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果以 finally 的为准 - * 字节码中没有 **athrow** ,表明如果在 finally 中出现了 return,会**吞掉异常** -* 不吞异常 +*** - ```java - public class Demo { - public static void main(String[] args) { - int result = test(); - System.out.println(result);//10 - } - public static int test() { - int i = 10; - try { - return i;//返回10 - } finally { - i = 20; - } - } - } - ``` - - ```java - 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 赋值给i,放入slot 0 - 3: iload_0 // i(10)加载至操作数栈 - 4: istore_1 // 10 -> slot 1,【暂存至 slot 1,目的是为了固定返回值】 - 5: bipush 20 // 20 放入栈顶 - 7: istore_0 // 20 slot 0 - 8: iload_1 // slot 1(10) 载入 slot 1 暂存的值 - 9: ireturn // 返回栈顶的 int(10) - 10: astore_2 // catch any -> slot 2 存入局部变量表的 slot2 - 11: bipush 20 - 13: istore_0 - 14: aload_2 - 15: athrow // 不会吞掉异常 - Exception table: - from to target type - 3 5 10 any - ``` +##### 常量池 -*** +常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的无符号数,代表常量池计数器(constant_pool_count),这个容量计数是从 1 而不是 0 开始,是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目,这种情况可用索引值 0 来表示 +constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表明有多少个常量池表项。表项中存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池 +* 字面量(Literal) :基本数据类型、字符串类型常量、声明为 final 的常量值等 -##### 同步控制 +* 符号引用(Symbolic References):类和接口的全限定名、字段的名称和描述符、方法的名称和描述符 -方法级的同步:是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法 + * 全限定名:com/test/Demo 这个就是类的全限定名,仅仅是把包名的 `.` 替换成 `/`,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个 `;` 表示全限定名结束 -方法内指定指令序列的同步:有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义 + * 简单名称:指没有类型和参数修饰的方法或者字段名称,比如字段 x 的简单名称就是 x -* montiorenter:进入并获取对象监视器,即为栈顶对象加锁 -* monitorexit:释放并退出对象监视器,即为栈顶对象解锁 + * 描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值 - + | 标志符 | 含义 | + | ------ | --------------------------------------------------------- | + | B | 基本数据类型 byte | + | C | 基本数据类型 char | + | D | 基本数据类型 double | + | F | 基本数据类型 float | + | I | 基本数据类型 int | + | J | 基本数据类型 long | + | S | 基本数据类型 short | + | Z | 基本数据类型 boolean | + | V | 代表 void 类型 | + | L | 对象类型,比如:`Ljava/lang/Object;`,不同方法间用`;`隔开 | + | [ | 数组类型,代表一维数组。比如:`double[][][] is [[[D` | +常量类型和结构: +| 类型 | 标志(或标识) | 描述 | +| -------------------------------- | ------------ | ---------------------- | +| CONSTANT_utf8_info | 1 | UTF-8编码的字符串 | +| CONSTANT_Integer_info | 3 | 整型字面量 | +| CONSTANT_Float_info | 4 | 浮点型字面量 | +| CONSTANT_Long_info | 5 | 长整型字面量 | +| CONSTANT_Double_info | 6 | 双精度浮点型字面量 | +| CONSTANT_Class_info | 7 | 类或接口的符号引用 | +| CONSTANT_String_info | 8 | 字符串类型字面量 | +| CONSTANT_Fieldref_info | 9 | 字段的符号引用 | +| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 | +| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 | +| CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 | +| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | +| CONSTANT_MethodType_info | 16 | 标志方法类型 | +| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | +18 种常量没有出现 byte、short、char,boolean 的原因:编译之后都可以理解为 Integer -*** +**** -#### 执行流程 -原始 Java 代码: +##### 访问标识 -```java -public class Demo { - public static void main(String[] args) { - int a = 10; - int b = Short.MAX_VALUE + 1; - int c = a + b; - System.out.println(c); - } -} -``` +访问标识(access_flag),又叫访问标志、访问标记,该标识用两个字节表示,用于识别一些类或者接口层次的访问信息,包括这个 Class 是类还是接口,是否定义为 public类型,是否定义为 abstract类型等 -javap -v Demo.class:省略 +* 类的访问权限通常为 ACC_ 开头的常量 +* 每一种类型的表示都是通过设置访问标记的 32 位中的特定位来实现的,比如若是 public final 的类,则该标记为 `ACC_PUBLIC | ACC_FINAL` +* 使用 `ACC_SUPER` 可以让类更准确地定位到父类的方法,确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义,现代编译器都会设置并且使用这个标记 -* 常量池载入运行时常量池 +| 标志名称 | 标志值 | 含义 | +| -------------- | ------ | ------------------------------------------------------------ | +| ACC_PUBLIC | 0x0001 | 标志为 public 类型 | +| ACC_FINAL | 0x0010 | 标志被声明为 final,只有类可以设置 | +| ACC_SUPER | 0x0020 | 标志允许使用 invokespecial 字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真,使用增强的方法调用父类方法 | +| ACC_INTERFACE | 0x0200 | 标志这是一个接口 | +| ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 | +| ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(由编译器产生的类,没有源码对应) | +| ACC_ANNOTATION | 0x2000 | 标志这是一个注解 | +| ACC_ENUM | 0x4000 | 标志这是一个枚举 | -* 方法区字节码载入方法区 -* main 线程开始运行,分配栈帧内存:(操作数栈stack=2,局部变量表locals=4) -* **执行引擎**开始执行字节码 +*** - `bipush 10`:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令 - * sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节) - * ldc 将一个 int 压入操作数栈 - * ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节) - * 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池 - `istore_1`:将操作数栈顶数据弹出,存入局部变量表的 slot 1 +##### 索引集合 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程1.png) +类索引、父类索引、接口索引集合 - `ldc #3`:从常量池加载 #3 数据到操作数栈 - Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算完成 +* 类索引用于确定这个类的全限定名 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程2.png) +* 父类索引用于确定这个类的父类的全限定名,Java 语言不允许多重继承,所以父类索引只有一个,除了Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为0 - `istore_2`:将操作数栈顶数据弹出,存入局部变量表的 slot 2 +* 接口索引集合就用来描述这个类实现了哪些接口 + * interfaces_count 项的值表示当前类或接口的直接超接口数量 + * interfaces[] 接口索引集合,被实现的接口将按 implements 语句后的接口顺序从左到右排列在接口索引集合中 - `iload_1`:将局部变量表的 slot 1数据弹出,放入操作数栈栈顶 +| 长度 | 含义 | +| ---- | ---------------------------- | +| u2 | this_class | +| u2 | super_class | +| u2 | interfaces_count | +| u2 | interfaces[interfaces_count] | - `iload_2`:将局部变量表的 slot 2数据弹出,放入操作数栈栈顶 - `iadd`:执行相加操作 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程3.png) +*** - `istore_3`:将操作数栈顶数据弹出,存入局部变量表的 slot 3 - `getstatic #4`:获取静态字段 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程4.png) +##### 字段表 - `iload_3`: +字段 fields 用于描述接口或类中声明的变量,包括类变量以及实例变量,但不包括方法内部、代码块内部声明的局部变量以及从父类或父接口继承。字段叫什么名字、被定义为什么数据类型,都是无法固定的,只能引用常量池中的常量来描述 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程5.png) +fields_count(字段计数器),表示当前 class 文件 fields 表的成员个数,用两个字节来表示 - `invokevirtual #5`: +fields[](字段表): - * 找到常量池 #5 项 - * 定位到方法区 java/io/PrintStream.println:(I)V 方法 - * **生成新的栈帧**(分配 locals、stack等) - * 传递参数,执行新栈帧中的字节码 - * 执行完毕,弹出栈帧 - * 清除 main 操作数栈内容 +* 表中的每个成员都是一个 fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程6.png) +* 字段访问标识: - return:完成 main 方法调用,弹出 main 栈帧,程序结束 + | 标志名称 | 标志值 | 含义 | + | ------------- | ------ | -------------------------- | + | ACC_PUBLIC | 0x0001 | 字段是否为public | + | ACC_PRIVATE | 0x0002 | 字段是否为private | + | ACC_PROTECTED | 0x0004 | 字段是否为protected | + | ACC_STATIC | 0x0008 | 字段是否为static | + | ACC_FINAL | 0x0010 | 字段是否为final | + | ACC_VOLATILE | 0x0040 | 字段是否为volatile | + | ACC_TRANSTENT | 0x0080 | 字段是否为transient | + | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | + | ACC_ENUM | 0x4000 | 字段是否为enum | - +* 字段名索引:根据该值查询常量池中的指定索引项即可 +* 描述符索引:用来描述字段的数据类型、方法的参数列表和返回值 + | 字符 | 类型 | 含义 | + | ----------- | --------- | ----------------------- | + | B | byte | 有符号字节型树 | + | C | char | Unicode字符,UTF-16编码 | + | D | double | 双精度浮点数 | + | F | float | 单精度浮点数 | + | I | int | 整型数 | + | J | long | 长整数 | + | S | short | 有符号短整数 | + | Z | boolean | 布尔值true/false | + | V | void | 代表void类型 | + | L Classname | reference | 一个名为Classname的实例 | + | [ | reference | 一个一维数组 | -*** +* 属性表集合:属性个数存放在 attribute_count 中,属性具体内容存放在 attribute 数组中,一个字段还可能拥有一些属性,用于存储更多的额外信息,比如初始化值、一些注释信息等 + ```java + ConstantValue_attribute{ + u2 attribute_name_index; + u4 attribute_length; + u2 constantvalue_index; + } + ``` + 对于常量属性而言,attribute_length 值恒为2 -### 执行引擎 -#### 基本介绍 -执行引擎:Java 虚拟机的核心组成部分之一,类加载主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将字节码指令解释/编译为对应平台上的本地机器指令 +*** -虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力: -* 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上 -* 虚拟机的执行引擎是由软件自行实现的,可以不受物理条件制约地定制指令集与执行引擎的结构体系 -Java 是**半编译半解释型语言**,将解释执行与编译执行二者结合起来进行: +##### 方法表 -* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行 -* 即时编译器(JIT : Just In Time Compiler):虚拟机运行时将源代码直接编译成**和本地机器平台相关的机器码**后再执行,并存入 Code Cache,下次遇到相同的代码直接执行,效率高 +方法表是 methods 指向常量池索引集合,其中每一个 method_info 项都对应着一个类或者接口中的方法信息,完整描述了每个方法的签名 +* 如果这个方法不是抽象的或者不是 native 的,字节码中就会体现出来 +* methods 表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法 +* methods 表可能会出现由编译器自动添加的方法,比如初始化方法 和实例化方法 +**重载(Overload)**一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,因为返回值不会包含在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存 -*** +methods_count(方法计数器):表示 class 文件 methods 表的成员个数,使用两个字节来表示 +methods[](方法表):每个表项都是一个 method_info 结构,表示当前类或接口中某个方法的完整描述 +* 方法表结构如下: -#### 执行方式 + | 类型 | 名称 | 含义 | 数量 | + | -------------- | ---------------- | ---------- | ---------------- | + | u2 | access_flags | 访问标志 | 1 | + | u2 | name_index | 字段名索引 | 1 | + | u2 | descriptor_index | 描述符索引 | 1 | + | u2 | attrubutes_count | 属性计数器 | 1 | + | attribute_info | attributes | 属性集合 | attributes_count | -HotSpot VM 采用**解释器与即时编译器并存的架构**,解释器和即时编译器能够相互协作,去选择最合适的方式来权衡编译本地代码和直接解释执行代码的时间 +* 方法表访问标志: -HostSpot JVM 的默认执行方式: + | 标志名称 | 标志值 | 含义 | + | ------------- | ------ | -------------------------- | + | ACC_PUBLIC | 0x0001 | 字段是否为 public | + | ACC_PRIVATE | 0x0002 | 字段是否为 private | + | ACC_PROTECTED | 0x0004 | 字段是否为 protected | + | ACC_STATIC | 0x0008 | 字段是否为 static | + | ACC_FINAL | 0x0010 | 字段是否为 final | + | ACC_VOLATILE | 0x0040 | 字段是否为 volatile | + | ACC_TRANSTENT | 0x0080 | 字段是否为 transient | + | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | + | ACC_ENUM | 0x4000 | 字段是否为 enum | -* 当程序启动后,解释器可以马上发挥作用立即执行,省去编译器编译的时间(解释器存在的**必要性**) -* 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率 -HotSpot VM 可以通过 VM 参数设置程序执行方式: -- -Xint:完全采用解释器模式执行程序 -- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行 -- -Xmixed:采用解释器 + 即时编译器的混合模式共同执行程序 +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-执行引擎工作流程.png) +##### 属性表 -*** +属性表集合,指的是 Class 文件所携带的辅助信息,比如该 Class 文件的源文件的名称,以及任何带有 `RetentionPolicy.CLASS` 或者 `RetentionPolicy.RUNTIME` 的注解,这类信息通常被用于 Java 虚拟机的验证和运行,以及 Java 程序的调试。字段表、方法表都可以有自己的属性表,用于描述某些场景专有的信息 +attributes_ count(属性计数器):表示当前文件属性表的成员个数 +attributes[](属性表):属性表的每个项的值必须是 attribute_info 结构 -#### 热点探测 +* 属性的通用格式: -热点代码:被 JIT 编译器编译的字节码,根据代码被调用执行的频率而定,一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 + ```java + ConstantValue_attribute{ + u2 attribute_name_index; //属性名索引 + u4 attribute_length; //属性长度 + u2 attribute_info; //属性表 + } + ``` -热点探测:JIT 编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令进行缓存,以提升程序执行性能 +* 属性类型: -JIT 编译在默认情况是异步进行的,当触发某方法或某代码块的优化时,先将其放入编译队列,然后由编译线程进行编译,编译之后的代码放在 CodeCache 中,通过 `-XX:-BackgroundCompilation` 参数可以关闭异步编译 + | 属性名称 | 使用位置 | 含义 | + | ------------------------------------- | ------------------ | ------------------------------------------------------------ | + | Code | 方法表 | Java 代码编译成的字节码指令 | + | ConstantValue | 字段表 | final 关键字定义的常量池 | + | Deprecated | 类、方法、字段表 | 被声明为 deprecated 的方法和字段 | + | Exceptions | 方法表 | 方法抛出的异常 | + | EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 | + | InnerClass | 类文件 | 内部类列表 | + | LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 | + | LocalVariableTable | Code 属性 | 方法的局部变量描述 | + | StackMapTable | Code 属性 | JDK1.6 中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 | + | Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 | + | SourceFile | 类文件 | 记录源文件名称 | + | SourceDebugExtension | 类文件 | 用于存储额外的调试信息 | + | Syothetic | 类,方法表,字段表 | 标志方法或字段为编泽器自动生成的 | + | LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 | + | RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 | + | RuntimelnvisibleAnnotations | 类,方法表,字段表 | 用于指明哪些注解是运行时不可见的 | + | RuntimeVisibleParameterAnnotation | 方法表 | 作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法 | + | RuntirmelnvisibleParameterAnniotation | 方法表 | 作用与 RuntimelnvisibleAnnotations 属性类似,作用对象哪个为方法参数 | + | AnnotationDefauit | 方法表 | 用于记录注解类元素的默认值 | + | BootstrapMethods | 类文件 | 用于保存 invokeddynanic 指令引用的引导方式限定符 | -* CodeCache 用于缓存编译后的机器码、动态生成的代码和本地方法代码 JNI -* 如果 CodeCache 区域被占满,编译器被停用,字节码将不会编译为机器码,应用程序继续运行,但运行性能会降低很多 -HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立 2 个不同类型的计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter) -* 方法调用计数器:用于统计方法被调用的次数,默认阈值在 Client 模式 下是 1500 次,在 Server 模式下是 10000 次(需要进行激进的优化),超过这个阈值,就会触发 JIT 编译,阈值可以通过虚拟机参数 `-XX:CompileThreshold` 设置 - 工作流程:当一个方法被调用时, 会先检查该方法是否存在被 JIT 编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器**提交一个该方法的代码编译请求** -* 回边计数器:统计一个方法中循环体代码执行的次数,在字节码中控制流向后跳转的指令称为回边 +**** - 如果一个方法中的循环体需要执行多次,可以优化为为栈上替换,简称 OSR (On StackReplacement) 编译,**OSR 替换循环代码体的入口,C1、C2 替换的是方法调用的入口**,OSR 编译后会出现方法的整段代码被编译了,但是只有循环体部分才执行编译后的机器码,其他部分仍是解释执行 +#### 编译指令 -*** +##### javac +javac:编译命令,将 java 源文件编译成 class 字节码文件 +`javac xx.java` 不会在生成对应的局部变量表等信息,使用 `javac -g xx.java` 可以生成所有相关信息 -#### 分层编译 -HotSpot VM 内嵌两个 JIT 编译器,分别为 Client Compiler 和 Server Compiler,简称 C1 编译器和 C2 编译器 -C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1 编译器的优化方法: +**** -* 方法内联:**将调用的函数代码编译到调用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 - 方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 - ```java - private static int square(final int i) { - return i * i; - } - System.out.println(square(9)); - ``` +##### javap - square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置: +javap 反编译生成的字节码文件,根据 class 字节码文件,反解析出当前类对应的 code 区 (字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息 - ```java - System.out.println(9 * 9); - ``` +用法:javap - 还能够进行常量折叠(constant folding)的优化: +```sh +-help --help -? 输出此用法消息 +-version 版本信息 +-public 仅显示公共类和成员 +-protected 显示受保护的/公共类和成员 +-package 显示程序包/受保护的/公共类和成员 (默认) +-p -private 显示所有类和成员 + #常用的以下三个 +-v -verbose 输出附加信息 +-l 输出行号和本地变量表 +-c 对代码进行反汇编 #反编译 - ```java - System.out.println(81); - ``` +-s 输出内部类型签名 +-sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) +-constants 显示最终常量 +-classpath 指定查找用户类文件的位置 +-cp 指定查找用户类文件的位置 +-bootclasspath 覆盖引导类文件的位置 +``` -* 冗余消除:根据运行时状况进行代码折叠或削除 -* 内联缓存:是一种加快动态绑定的优化技术(方法调用部分详解) -C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译,这也是使用分层编译的原因 +*** -C2 的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 -VM 参数设置: -- -client:指定 Java 虚拟机运行在 Client 模式下,并使用 C1 编译器 -- -server:指定 Java 虚拟机运行在 Server 模式下,并使用 C2 编译器 -- `-server -XX:+TieredCompilation`:在 1.8 之前,分层编译默认是关闭的,可以添加该参数开启 +#### 指令集 -分层编译策略 (Tiered Compilation):程序解释执行可以触发 C1 编译,将字节码编译成机器码,加上性能监控,C2 编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: +##### 执行指令 -* 0 层,解释执行(Interpreter) +Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某种操作的操作码(opcode)以及零至多个代表此操作所需参数的操作数(operand)所构成,虚拟机中许多指令并不包含操作数,只有一个操作码(零地址指令) -* 1 层,使用 C1 即时编译器编译执行(不带 profiling) +由于限制了 Java 虚拟机操作码的长度为一个字节(0~255),所以指令集的操作码总数不可能超过 256 条 -* 2 层,使用 C1 即时编译器编译执行(带基本的 profiling) +在 JVM 的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据 -* 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) +* i 代表对 int 类型的数据操作 +* l 代表 long +* s 代表 short +* b 代表 byte +* c 代表 char +* f 代表 float +* d 代表 double -* 4 层,使用 C2 即时编译器编译执行(C1 和 C2 协作运行) +大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend-)为相应的 int 类型数据 - 说明:profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等 +在做值相关操作时: +- 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,也可能是对象的引用)被压入操作数栈 +- 一个指令,也可以从操作数栈中取出一到多个值(pop 多次),完成赋值、加减乘除、方法传参、系统调用等等操作 -参考文章:https://www.jianshu.com/p/20bd2e9b1f03 +*** -*** +##### 加载存储 +加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递 -### 方法调用 +局部变量压栈指令:将给定的局部变量表中的数据压入操作数栈 -#### 方法识别 +* xload、xload_n,x 表示取值数据类型,为 i、l、f、d、a, n 为 0 到 3 +* 指令 xload_n 表示将第 n 个局部变量压入操作数栈,aload_n 表示将一个对象引用压栈 +* 指令 xload n 通过指定参数的形式,把局部变量压入操作数栈,局部变量数量超过 4 个时使用这个命令 -Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor) +常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const、push、ldc指令 -* **方法描述符是由方法的参数类型以及返回类型所构成**,Java 层面叫方法特征签名 -* 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 +* push:包括 bipush 和 sipush,区别在于接收数据类型的不同,bipush 接收 8 位整数作为参数,sipush 接收 16 位整数 +* ldc:如果以上指令不能满足需求,可以使用 ldc 指令,接收一个 8 位的参数,该参数指向常量池中的 int、 float 或者 String 的索引,将指定的内容压入堆栈。ldc_w 接收两个 8 位参数,能支持的索引范围更大,如果要压入的元素是 long 或 double 类型的,则使用 ldc2_w 指令 +* aconst_null 将 null 对象引用压入栈,iconst_m1 将 int 类型常量 -1 压入栈,iconst_0 将 int 类型常量 0 压入栈 -JVM 根据名字和描述符来判断的,只要返回值不一样(方法描述符不一样),其它完全一样,在 JVM 中是允许的,但 Java 语言不允许 +出栈装入局部变量表指令:将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值 -```java -// 返回值类型不同,编译阶段直接报错 -public static Integer invoke(Object... args) { - return 1; -} -public static int invoke(Object... args) { - return 2; -} -``` +* xstore、xstore_n,x 表示取值类型为 i、l、f、d、a, n 为 0 到 3 +* xastore 表示存入数组,x 取值为 i、l、f、d、a、b、c、s +扩充局部变量表的访问索引的指令:wide -*** +**** -#### 调用机制 -方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,不是方法的具体运行过程 +##### 算术指令 -在 JVM 中,将符号引用转换为直接引用有两种机制: +算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算结果重新压入操作数栈 -- 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) -- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) +没有直接支持 byte、 short、 char 和 boolean类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组 -对应方法的绑定(分配)机制:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次: +* 加法指令:iadd、ladd、fadd、dadd +* 减法指令:isub、lsub、fsub、dsub +* 乘法指令:imu、lmu、fmul、dmul +* 除法指令:idiv、ldiv、fdiv、ddiv +* 求余指令:irem、lrem、frem、drem(remainder 余数) +* 取反指令:ineg、lneg、fneg、dneg (negation 取反) +* 自增指令:iinc(直接**在局部变量 slot 上进行运算**,不用放入操作数栈) +* 位运算指令,又可分为: + - 位移指令:ishl、ishr、 iushr、lshl、lshr、 lushr + - 按位或指令:ior、lor + - 按位与指令:iand、land + - 按位异或指令:ixor、lxor -- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 -- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 +* 比较指令:dcmpg、dcmpl、 fcmpg、fcmpl、lcmp -* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 +运算模式: -非虚方法: +* 向最接近数舍入模式,JVM 在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示形式与该值一样接近,将优先选择最低有效位为零的 +* 向零舍入模式:将浮点数转换为整数时,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果 -- 非虚方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的 -- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法 -- 所有普通成员方法、实例方法、被重写的方法都是虚方法 +NaN 值:当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义,将使用 NaN 值来表示 -动态类型语言和静态类型语言: +```java +double j = i / 0.0; +System.out.println(j);//无穷大,NaN: not a number +``` -- 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 +**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc -- 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 +```java + 4 iload_1 //存入操作数栈 + 5 iinc 1 by 1 //自增i++ + 8 istore_3 //把操作数栈没有自增的数据的存入局部变量表 + 9 iinc 2 by 1 //++i +12 iload_2 //加载到操作数栈 +13 istore 4 //存入局部变量表,这个存入没有 _ 符号,_只能到3 +``` -- **Java 是静态类型语言**(尽管 Lambda 表达式为其增加了动态特性),JS,Python 是动态类型语言 +```java +public class Demo { + public static void main(String[] args) { + int a = 10; + int b = a++ + ++a + a--; + System.out.println(a); //11 + System.out.println(b); //34 + } +} +``` - ```java - String s = "abc"; //Java - info = "abc"; //Python - ``` +判断结果: + +```java +public class Demo { + public static void main(String[] args) { + int i = 0; + int x = 0; + while (i < 10) { + x = x++; + i++; + } + System.out.println(x); // 结果是 0 + } +} +``` @@ -13833,34 +13918,36 @@ public static int invoke(Object... args) { -#### 调用指令 - -##### 五种指令 +##### 类型转换 -普通调用指令: +类型转换指令可以将两种不同的数值类型进行相互转换,除了 boolean 之外的七种类型 -- invokestatic:调用静态方法 -- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 -- invokevirtual:调用所有虚方法(虚方法分派) -- invokeinterface:调用接口方法 +宽化类型转换: -动态调用指令: +* JVM 支持以下数值的宽化类型转换(widening numeric conversion),小范围类型到大范围类型的安全转换 + * 从 int 类型到 long、float 或者 double 类型,对应的指令为 i2l、i2f、i2d + * 从 long 类型到 float、 double 类型,对应的指令为 l2f、l2d + * 从 float 类型到 double 类型,对应的指令为 f2d -- invokedynamic:动态解析出需要调用的方法 - - Java7 为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令 - - Java8 的 lambda 表达式的出现,invokedynamic 指令在 Java 中才有了直接生成方式 +* 精度损失问题 + * 宽化类型转换是不会因为超过目标类型最大值而丢失信息 + * 从 int 转换到 float 或者 long 类型转换到 double 时,将可能发生精度丢失 -指令对比: +* 从 byte、char 和 short 类型到 int 类型的宽化类型转换实际上是不存在的,JVM 把它们当作 int 处理 -- 普通调用指令固化在虚拟机内部,方法的调用执行不可干预,根据方法的符号引用链接到具体的目标方法 -- 动态调用指令支持用户确定方法 -- invokestatic 和 invokespecial 指令调用的方法称为非虚方法,虚拟机能够直接识别具体的目标方法 -- invokevirtual 和 invokeinterface 指令调用的方法称为虚方法,虚拟机需要在执行过程中根据调用者的动态类型来确定目标方法 +窄化类型转换: -指令说明: +* Java 虚拟机直接支持以下窄化类型转换: + * 从 int 类型至 byte、 short 或者 char 类型,对应的指令有 i2b、i2c、i2s + * 从 long 类型到 int 类型,对应的指令有 l2i + * 从 float 类型到 int 或者 long 类型,对应的指令有:f2i、f2l + * 从 double 类型到 int、long 或 float 者类型,对应的指令有 d2i、d2、d2f -- 如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final,那么可以不通过动态绑定,直接确定目标方法 -- 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 +* 精度损失问题: + * 窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,转换过程可能会导致数值丢失精度 + * 将一个浮点值窄化转换为整数类型 T(T 限于 int 或 long 类型之一)时,将遵循以下转换规则: + - 如果浮点值是 NaN,那转换结果就是 int 或 long 类型的 0 + - 如果浮点值不是无穷大的话,浮点值使用 IEEE 754 的向零舍入模式取整,获得整数值 v,如果 v 在目标类型 T 的表示范围之内,那转换结果就是 v,否则将根据 v 的符号,转换为 T 所能表示的最大或者最小正数 @@ -13868,89 +13955,52 @@ public static int invoke(Object... args) { -##### 符号引用 +##### 创建访问 -在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 +创建指令: -* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 -* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引 +* 创建类实例指令:new,接收一个操作数指向常量池的索引,表示要创建的类型,执行完成后将对象的引用压入栈 -符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: + ```java + 0: new #2 // class com/jvm/bytecode/Demo + 3: dup + 4: invokespecial #3 // Method "":()V + ``` -```java -Constant pool: -... - #16 = InterfaceMethodref #27.#29 // 接口 -... - #22 = Methodref #1.#33 // 非接口 -... -``` + **dup 是复制操作数栈栈顶的内容**,需要两份引用原因: -对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找: + - 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) + - 一个要配合 astore_1 赋值给局部变量 -1. 在 C 中查找符合名字及描述符的方法 -2. 如果没有找到,在 C 的父类中继续搜索,直至 Object 类 -3. 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。如果有多个符合条件的目标方法,则任意返回其中一个 +* 创建数组的指令:newarray、anewarray、multianewarray -对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找: + * newarray:创建基本类型数组 + * anewarray:创建引用类型数组 + * multianewarray:创建多维数组 -1. 在 I 中查找符合名字及描述符的方法 -2. 如果没有找到,在 Object 类中的公有实例方法中搜索 -3. 如果没有找到,则在 I 的超接口中搜索,这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致 +字段访问指令:对象创建后可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素 +* 访问类字段(static字段,或者称为类变量)的指令:getstatic、putstatic +* 访问类实例字段(非static字段,或者称为实例变量)的指令:getfield、 putfield +类型检查指令:检查类实例或数组类型的指令 -*** +* checkcast:用于检查类型强制转换是否可以进行,如果可以进行 checkcast 指令不会改变操作数栈,否则它会抛出 ClassCastException 异常 +* instanceof:判断给定对象是否是某一个类的实例,会将判断结果压入操作数栈 -##### 执行流程 -```java -public class Demo { - public Demo() { } - private void test1() { } - private final void test2() { } - public void test3() { } - public static void test4() { } +**** - public static void main(String[] args) { - Demo3_9 d = new Demo3_9(); - d.test1(); - d.test2(); - d.test3(); - d.test4(); - Demo.test4(); - } -} -``` -几种不同的方法调用对应的字节码指令: -```java -0: new #2 // class cn/jvm/t3/bytecode/Demo -3: dup -4: invokespecial #3 // Method "":()V -7: astore_1 -8: aload_1 -9: invokespecial #4 // Method test1:()V -12: aload_1 -13: invokespecial #5 // Method test2:()V -16: aload_1 -17: invokevirtual #6 // Method test3:()V -20: aload_1 -21: pop -22: invokestatic #7 // Method test4:()V -25: invokestatic #7 // Method test4:()V -28: return -``` +##### 方法指令 -- invokespecial 调用该对象的构造方法 :()V +方法调用指令:invokevirtual、 invokeinterface、invokespecial、invokestatic、invokedynamic -- `d.test4()` 是通过**对象引用**调用一个静态方法,在调用 invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 - - 不建议使用 `对象.静态方法()` 的方式调用静态方法,多了aload 和 pop 指令 - - 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 +**方法调用章节详解** @@ -13958,129 +14008,177 @@ public class Demo { -#### 多态原理 - -##### 执行原理 - -Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类定义了与父类中非私有、非静态方法同名的方法,只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。 - -理解多态: +##### 操作数栈 -- 多态有编译时多态和运行时多态,即静态绑定和动态绑定 -- 前者是通过方法重载实现,后者是通过重写实现(子类覆盖父类方法,虚方法表) -- 虚方法:运行时动态绑定的方法,对比静态绑定的非虚方法调用来说,虚方法调用更加耗时 +JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的指令 -方法重写的本质: +* pop、pop2:将一个或两个元素从栈顶弹出,并且直接废弃 +* dup、dup2,dup_x1、dup2_x1,dup_x2、dup2_x2:复制栈顶一个或两个数值并重新压入栈顶 -1. 找到操作数栈的第一个元素**所执行的对象的实际类型**,记作 C +* swap:将栈最顶端的两个 slot 数值位置交换,JVM 没有提供交换两个 64 位数据类型数值的指令 -2. 如果在类型 C 中找到与描述符和名称都相符的方法,则进行访问**权限校验**(私有的),如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 +* nop:一个非常特殊的指令,字节码为 0x00,和汇编语言中的 nop 一样,表示什么都不做,一般可用于调试、占位等 - IllegalAccessError:表示程序试图访问或修改一个属性或调用一个方法,这个属性或方法没有权限访问,一般会引起编译器异常。如果这个错误发生在运行时,就说明一个类发生了不兼容的改变 -3. 找不到,就会按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程 -4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常 +*** -*** +##### 控制转移 +比较指令:比较栈顶两个元素的大小,并将比较结果入栈 -##### 虚方法表 +* lcmp:比较两个 long 类型值 +* fcmpl:比较两个 float 类型值(当遇到NaN时,返回-1) +* fcmpg:比较两个 float 类型值(当遇到NaN时,返回1) +* dcmpl:比较两个 double 类型值(当遇到NaN时,返回-1) +* dcmpg:比较两个 double 类型值(当遇到NaN时,返回1) -在虚拟机工作过程中会频繁使用到动态绑定,每次动态绑定的过程中都要重新在类的元数据中搜索合适目标,影响到执行效率。为了提高性能,JVM 采取了一种用**空间换取时间**的策略来实现动态绑定,在每个类的方法区建立一个虚方法表(virtual method table),实现使用索引表来代替查找,可以快速定位目标方法 +条件跳转指令: -* invokevirtual 所使用的虚方法表(virtual method table,vtable),执行流程 - 1. 先通过栈帧中的对象引用找到对象,分析对象头,找到对象的实际 Class - 2. Class 结构中有 vtable,查表得到方法的具体地址,执行方法的字节码 -* invokeinterface 所使用的接口方法表(interface method table,itable) +| 指令 | 说明 | +| --------- | -------------------------------------------------- | +| ifeq | equals,当栈顶int类型数值等于0时跳转 | +| ifne | not equals,当栈顶in类型数值不等于0时跳转 | +| iflt | lower than,当栈顶in类型数值小于0时跳转 | +| ifle | lower or equals,当栈顶in类型数值小于等于0时跳转 | +| ifgt | greater than,当栈顶int类型数组大于0时跳转 | +| ifge | greater or equals,当栈顶in类型数值大于等于0时跳转 | +| ifnull | 为 null 时跳转 | +| ifnonnull | 不为 null 时跳转 | -虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法表也初始化完毕 +比较条件跳转指令: -虚方法表的执行过程: +| 指令 | 说明 | +| --------- | --------------------------------------------------------- | +| if_icmpeq | 比较栈顶两 int 类型数值大小(下同),当前者等于后者时跳转 | +| if_icmpne | 当前者不等于后者时跳转 | +| if_icmplt | 当前者小于后者时跳转 | +| if_icmple | 当前者小于等于后者时跳转 | +| if_icmpgt | 当前者大于后者时跳转 | +| if_icmpge | 当前者大于等于后者时跳转 | +| if_acmpeq | 当结果相等时跳转 | +| if_acmpne | 当结果不相等时跳转 | -* 对于静态绑定的方法调用而言,实际引用将指向具体的目标方法 -* 对于动态绑定的方法调用而言,实际引用则是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针) +多条件分支跳转指令: -为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表。每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法 +* tableswitch:用于 switch 条件跳转,case 值连续 +* lookupswitch:用于 switch 条件跳转,case 值不连续 -方法表满足以下的特质: +无条件跳转指令: -* 其一,子类方法表中包含父类方法表中的**所有方法**,并且在方法表中的索引值与父类方法表种的索引值相同 -* 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**。所以这就是为什么多态情况下可以访问父类的方法。 +* goto:用来进行跳转到指定行号的字节码 - +* goto_w:无条件跳转(宽索引) -Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方法表调换了 toString 方法和 passThroughImmigration 方法的位置,是因为 toString 方法的索引值需要与 Object 类中同名方法的索引值一致,为了保持简洁,这里不考虑 Object 类中的其他方法。 -虚方法表对性能的影响: -* 使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者、读取调用者的动态类型、读取该类型的方法表、读取方法表中某个索引值所对应的目标方法,但是相对于创建并初始化 Java 栈帧这操作的开销可以忽略不计 -* 上述优化的效果看上去不错,但实际上**仅存在于解释执行**中,或者即时编译代码的最坏情况。因为即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining) -```java -class Person { - public String toString() { - return "I'm a person."; - } - public void eat() {} - public void speak() {} -} -class Boy extends Person { - public String toString() { - return "I'm a boy"; - } - public void speak() {} - public void fight() {} -} +*** -class Girl extends Person { - public String toString() { - return "I'm a girl"; - } - public void speak() {} - public void sing() {} -} -``` -![](https://gitee.com/seazean/images/raw/master/Java/JVM-虚方法表指向.png) +##### 异常处理 +###### 处理机制 -参考文档:https://www.cnblogs.com/kaleidoscope/p/9790766.html +抛出异常指令:athrow 指令 +JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是**采用异常表来完成**的 +* 代码: -*** + ```java + public static void main(String[] args) { + int i = 0; + try { + i = 10; + } catch (Exception e) { + i = 20; + } finally { + i = 30; + } + } + ``` + +* 字节码: + * 多出一个 **Exception table** 的结构,**[from, to) 是前闭后开的检测范围**,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 + * 11 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 + ```java + 0: iconst_0 + 1: istore_1 // 0 -> i ->赋值 + 2: bipush 10 // try 10 放入操作数栈顶 + 4: istore_1 // 10 -> i 将操作数栈顶数据弹出,存入局部变量表的 slot1 + 5: bipush 30 // 【finally】 + 7: istore_1 // 30 -> i + 8: goto 27 // return ----------------------------------- + 11: astore_2 // catch Exceptin -> e ---------------------- + 12: bipush 20 // + 14: istore_1 // 20 -> i + 15: bipush 30 // 【finally】 + 17: istore_1 // 30 -> i + 18: goto 27 // return ----------------------------------- + 21: astore_3 // catch any -> slot 3 ---------------------- + 22: bipush 30 // 【finally】 + 24: istore_1 // 30 -> i + 25: aload_3 // 将局部变量表的slot 3数据弹出,放入操作数栈栈顶 + 26: athrow // throw 抛出异常 + 27: return + Exception table: + // 任何阶段出现任务异常都会执行 finally + from to target type + 2 5 11 Class java/lang/Exception + 2 5 21 any // 剩余的异常类型,比如 Error + 11 15 21 any // 剩余的异常类型,比如 Error + LineNumberTable: ... + LocalVariableTable: + Start Length Slot Name Signature + 12 3 2 e Ljava/lang/Exception; + 0 28 0 args [Ljava/lang/String; + 2 26 1 i I + ``` -##### 内联缓存 -内联缓存:是一种加快动态绑定的优化技术,能够缓存虚方法调用中**调用者的动态类型以及该类型所对应的目标方法**。在之后的执行过程中,如果碰到已缓存的类型,便会直接调用该类型所对应的目标方法;反之内联缓存则会退化至使用基于方法表的动态绑定 -多态的三个术语: +*** -* 单态 (monomorphic):指的是仅有一种状态的情况 -* 多态 (polymorphic):指的是有限数量种状态的情况,二态(bimorphic)是多态的其中一种 -* 超多态 (megamorphic):指的是更多种状态的情况,通常用一个具体数值来区分多态和超多态,在这个数值之下,称之为多态,否则称之为超多态 -对于内联缓存来说,有对应的单态内联缓存、多态内联缓存: -* 单态内联缓存:只缓存了一种动态类型以及所对应的目标方法,实现简单,比较所缓存的动态类型,如果命中,则直接调用对应的目标方法。 -* 多态内联缓存:缓存了多个动态类型及其目标方法,需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法 +###### finally -为了节省内存空间,**Java 虚拟机只采用单态内联缓存**,没有命中的处理方法: +finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程(上节案例) -* 替换单态内联缓存中的纪录,类似于 CPU 中的数据缓存,对数据的局部性有要求,即在替换内联缓存之后的一段时间内,方法调用的调用者的动态类型应当保持一致,从而能够有效地利用内联缓存 -* 劣化为超多态状态,这也是 Java 虚拟机的具体实现方式,这种状态实际上放弃了优化的机会,将直接访问方法表来动态绑定目标方法,但是与替换内联缓存纪录相比节省了写缓存的额外开销 +* 代码: -虽然内联缓存附带内联二字,但是并没有内联目标方法 + ```java + public static int test() { + try { + return 10; + } finally { + return 20; + } + } + ``` +* 字节码: + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 + 3: bipush 20 // 20 放入栈顶 + 5: ireturn // 返回栈顶 int(20) + 6: astore_1 // catch any 存入局部变量表的 slot1 + 7: bipush 20 // 20 放入栈顶 + 9: ireturn // 返回栈顶 int(20) + Exception table: + from to target type + 0 3 6 any + ``` @@ -14088,30 +14186,73 @@ class Girl extends Person { -### 代码优化 +###### return -#### 语法糖 - -语法糖:指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担 +* 吞异常 + ```java + public static int test() { + try { + return 10; + } finally { + return 20; + } + } + ``` + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 + 3: bipush 20 // 20 放入栈顶 + 5: ireturn // 返回栈顶 int(20) + 6: astore_1 // catch any 存入局部变量表的 slot1 + 7: bipush 20 // 20 放入栈顶 + 9: ireturn // 返回栈顶 int(20) + Exception table: + from to target type + 0 3 6 any + ``` -#### 构造器 + * 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果以 finally 的为准 + * 字节码中没有 **athrow** ,表明如果在 finally 中出现了 return,会**吞掉异常** -```java -public class Candy1 { -} -``` +* 不吞异常 -```java -public class Candy1 { - // 这个无参构造是编译器帮助我们加上的 - public Candy1() { - super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." - ":()V - } -} -``` + ```java + public class Demo { + public static void main(String[] args) { + int result = test(); + System.out.println(result);//10 + } + public static int test() { + int i = 10; + try { + return i;//返回10 + } finally { + i = 20; + } + } + } + ``` + + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 赋值给i,放入slot 0 + 3: iload_0 // i(10)加载至操作数栈 + 4: istore_1 // 10 -> slot 1,【暂存至 slot 1,目的是为了固定返回值】 + 5: bipush 20 // 20 放入栈顶 + 7: istore_0 // 20 slot 0 + 8: iload_1 // slot 1(10) 载入 slot 1 暂存的值 + 9: ireturn // 返回栈顶的 int(10) + 10: astore_2 // catch any -> slot 2 存入局部变量表的 slot2 + 11: bipush 20 + 13: istore_0 + 14: aload_2 + 15: athrow // 不会吞掉异常 + Exception table: + from to target type + 3 5 10 any + ``` @@ -14119,130 +14260,100 @@ public class Candy1 { -#### 拆装箱 +##### 同步控制 -```java -Integer x = 1; -int y = x; -``` +方法级的同步:是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法 -这段代码在 JDK 5 之前是无法编译通过的,必须改写为代码片段2: +方法内指定指令序列的同步:有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义 -```java -Integer x = Integer.valueOf(1); -int y = x.intValue(); -``` +* montiorenter:进入并获取对象监视器,即为栈顶对象加锁 +* monitorexit:释放并退出对象监视器,即为栈顶对象解锁 -JDK5 以后编译阶段自动转换成上述片段 + -*** +*** -#### 泛型擦除 -泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息 -在编译为字节码之后就丢失了,实际的类型都**当做了 Object 类型**来处理: -```java -List list = new ArrayList<>(); -list.add(10); // 实际调用的是 List.add(Object e) -Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index); -``` +#### 执行流程 -编译器真正生成的字节码中,还要额外做一个类型转换的操作: +原始 Java 代码: ```java -// 需要将 Object 转为 Integer -Integer x = (Integer)list.get(0); +public class Demo { + public static void main(String[] args) { + int a = 10; + int b = Short.MAX_VALUE + 1; + int c = a + b; + System.out.println(c); + } +} ``` -如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是: - -```java -// 需要将 Object 转为 Integer, 并执行拆箱操作 -int x = ((Integer)list.get(0)).intValue(); -``` +javap -v Demo.class:省略 +* 常量池载入运行时常量池 +* 方法区字节码载入方法区 -*** +* main 线程开始运行,分配栈帧内存:(操作数栈stack=2,局部变量表locals=4) +* **执行引擎**开始执行字节码 + `bipush 10`:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令 -#### 可变参数 + * sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节) + * ldc 将一个 int 压入操作数栈 + * ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节) + * 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池 -```java -public class Candy4 { - public static void foo(String... args) { - String[] array = args; // 直接赋值 - System.out.println(array); - } - public static void main(String[] args) { - foo("hello", "world"); - } -} -``` + `istore_1`:将操作数栈顶数据弹出,存入局部变量表的 slot 1 -可变参数 `String... args` 其实是 `String[] args` , java 编译器会在编译期间将上述代码变换为: + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程1.png) -```java -public static void main(String[] args) { - foo(new String[]{"hello", "world"}); -} -``` + `ldc #3`:从常量池加载 #3 数据到操作数栈 + Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算完成 -注意:如果调用了 `foo()` 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程2.png) + `istore_2`:将操作数栈顶数据弹出,存入局部变量表的 slot 2 + `iload_1`:将局部变量表的 slot 1数据弹出,放入操作数栈栈顶 -**** + `iload_2`:将局部变量表的 slot 2数据弹出,放入操作数栈栈顶 + `iadd`:执行相加操作 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程3.png) -#### foreach + `istore_3`:将操作数栈顶数据弹出,存入局部变量表的 slot 3 -**数组的循环:** + `getstatic #4`:获取静态字段 -```java -int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖 -for (int e : array) { - System.out.println(e); -} -``` + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程4.png) -编译后为循环取数: + `iload_3`: -```java -for(int i = 0; i < array.length; ++i) { - int e = array[i]; - System.out.println(e); -} -``` + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程5.png) -**集合的循环:** + `invokevirtual #5`: -```java -List list = Arrays.asList(1,2,3,4,5); -for (Integer i : list) { - System.out.println(i); -} -``` + * 找到常量池 #5 项 + * 定位到方法区 java/io/PrintStream.println:(I)V 方法 + * **生成新的栈帧**(分配 locals、stack等) + * 传递参数,执行新栈帧中的字节码 + * 执行完毕,弹出栈帧 + * 清除 main 操作数栈内容 -编译后转换为对迭代器的调用: + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程6.png) -```java -List list = Arrays.asList(1, 2, 3, 4, 5); -Iterator iter = list.iterator(); -while(iter.hasNext()) { - Integer e = (Integer)iter.next(); - System.out.println(e); -} -``` + return:完成 main 方法调用,弹出 main 栈帧,程序结束 -注意:foreach 循环写法,能够配合数组以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器 + @@ -14250,56 +14361,21 @@ while(iter.hasNext()) { -#### switch - -##### 字符串 - -从 JDK 开始,switch 可以作用于字符串和枚举类: +### 执行引擎 -```java -switch (str) { - case "hello": { - System.out.println("h"); - break; - } - case "world": { - System.out.println("w"); - break; - } -} -``` +#### 基本介绍 -注意:**switch 配合 String 和枚举使用时,变量不能为null** +执行引擎:Java 虚拟机的核心组成部分之一,类加载主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将字节码指令解释/编译为对应平台上的本地机器指令 -会被编译器转换为: +虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力: -```java -byte x = -1; -switch(str.hashCode()) { - case 99162322: // hello 的 hashCode - if (str.equals("hello")) { - x = 0; - } - break; - case 113318802: // world 的 hashCode - if (str.equals("world")) { - x = 1; - } -} -switch(x) { - case 0: - System.out.println("h"); - break; - case 1: - System.out.println("w"); - break; -} -``` +* 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上 +* 虚拟机的执行引擎是由软件自行实现的,可以不受物理条件制约地定制指令集与执行引擎的结构体系 -总结: +Java 是**半编译半解释型语言**,将解释执行与编译执行二者结合起来进行: -* 执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较 -* hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突 +* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行 +* 即时编译器(JIT : Just In Time Compiler):虚拟机运行时将源代码直接编译成**和本地机器平台相关的机器码**后再执行,并存入 Code Cache,下次遇到相同的代码直接执行,效率高 @@ -14307,249 +14383,116 @@ switch(x) { -##### 枚举 +#### 执行方式 -switch 枚举的例子,原始代码: +HotSpot VM 采用**解释器与即时编译器并存的架构**,解释器和即时编译器能够相互协作,去选择最合适的方式来权衡编译本地代码和直接解释执行代码的时间 -```java -enum Sex { - MALE, FEMALE -} -public class Candy7 { - public static void foo(Sex sex) { - switch (sex) { - case MALE: - System.out.println("男"); - break; - case FEMALE: - System.out.println("女"); - break; - } - } -} -``` +HostSpot JVM 的默认执行方式: -编译转换后的代码: +* 当程序启动后,解释器可以马上发挥作用立即执行,省去编译器编译的时间(解释器存在的**必要性**) +* 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率 -```java -/** -* 定义一个合成类(仅 jvm 使用,对我们不可见) -* 用来映射枚举的 ordinal 与数组元素的关系 -* 枚举的 ordinal 表示枚举对象的序号,从 0 开始 -* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1 -*/ -static class $MAP { - // 数组大小即为枚举元素个数,里面存储 case 用来对比的数字 - static int[] map = new int[2]; - static { - map[Sex.MALE.ordinal()] = 1; - map[Sex.FEMALE.ordinal()] = 2; - } -} -public static void foo(Sex sex) { - int x = $MAP.map[sex.ordinal()]; - switch (x) { - case 1: - System.out.println("男"); - break; - case 2: - System.out.println("女"); - break; - } -} -``` +HotSpot VM 可以通过 VM 参数设置程序执行方式: +- -Xint:完全采用解释器模式执行程序 +- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行 +- -Xmixed:采用解释器 + 即时编译器的混合模式共同执行程序 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-执行引擎工作流程.png) -*** +*** -#### 枚举类 -JDK 7 新增了枚举类: -```java -enum Sex { - MALE, FEMALE -} -``` +#### 热点探测 -编译转换后: +热点代码:被 JIT 编译器编译的字节码,根据代码被调用执行的频率而定,一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 -```java -public final class Sex extends Enum { - public static final Sex MALE; - public static final Sex FEMALE; - private static final Sex[] $VALUES; - static { - MALE = new Sex("MALE", 0); - FEMALE = new Sex("FEMALE", 1); - $VALUES = new Sex[]{MALE, FEMALE}; - } - private Sex(String name, int ordinal) { - super(name, ordinal); - } - public static Sex[] values() { - return $VALUES.clone(); - } - public static Sex valueOf(String name) { - return Enum.valueOf(Sex.class, name); - } -} -``` +热点探测:JIT 编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令进行缓存,以提升程序执行性能 +JIT 编译在默认情况是异步进行的,当触发某方法或某代码块的优化时,先将其放入编译队列,然后由编译线程进行编译,编译之后的代码放在 CodeCache 中,通过 `-XX:-BackgroundCompilation` 参数可以关闭异步编译 +* CodeCache 用于缓存编译后的机器码、动态生成的代码和本地方法代码 JNI +* 如果 CodeCache 区域被占满,编译器被停用,字节码将不会编译为机器码,应用程序继续运行,但运行性能会降低很多 +HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立 2 个不同类型的计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter) +* 方法调用计数器:用于统计方法被调用的次数,默认阈值在 Client 模式 下是 1500 次,在 Server 模式下是 10000 次(需要进行激进的优化),超过这个阈值,就会触发 JIT 编译,阈值可以通过虚拟机参数 `-XX:CompileThreshold` 设置 -*** + 工作流程:当一个方法被调用时, 会先检查该方法是否存在被 JIT 编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器**提交一个该方法的代码编译请求** +* 回边计数器:统计一个方法中循环体代码执行的次数,在字节码中控制流向后跳转的指令称为回边 + 如果一个方法中的循环体需要执行多次,可以优化为为栈上替换,简称 OSR (On StackReplacement) 编译,**OSR 替换循环代码体的入口,C1、C2 替换的是方法调用的入口**,OSR 编译后会出现方法的整段代码被编译了,但是只有循环体部分才执行编译后的机器码,其他部分仍是解释执行 -#### try-w-r -JDK 7 开始新增了对需要关闭的资源处理的特殊语法 `try-with-resources`,格式: -```java -try(资源变量 = 创建资源对象){ -} catch( ) { -} -``` +*** -其中资源对象需要实现 **AutoCloseable**接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码: -```java -try(InputStream is = new FileInputStream("d:\\1.txt")) { - System.out.println(is); -} catch (IOException e) { - e.printStackTrace(); -} -``` -转换成: +#### 分层编译 -`addSuppressed(Throwable e)`:添加被压制异常,是为了防止异常信息的丢失(**fianlly 中如果抛出了异常**) +HotSpot VM 内嵌两个 JIT 编译器,分别为 Client Compiler 和 Server Compiler,简称 C1 编译器和 C2 编译器 -```java -try { - InputStream is = new FileInputStream("d:\\1.txt"); - Throwable t = null; - try { - System.out.println(is); - } catch (Throwable e1) { - // t 是我们代码出现的异常 - t = e1; - throw e1; - } finally { - // 判断了资源不为空 - if (is != null) { - // 如果我们代码有异常 - if (t != null) { - try { - is.close(); - } catch (Throwable e2) { - // 如果 close 出现异常,作为被压制异常添加 - t.addSuppressed(e2); - } - } else { - // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e - is.close(); - } - } - } -} catch (IOException e) { - e.printStackTrace(); -} -``` +C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1 编译器的优化方法: +* 方法内联:**将调用的函数代码编译到调用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 + 方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 -*** + ```java + private static int square(final int i) { + return i * i; + } + System.out.println(square(9)); + ``` + square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置: + ```java + System.out.println(9 * 9); + ``` -#### 方法重写 + 还能够进行常量折叠(constant folding)的优化: -方法重写时对返回值分两种情况: + ```java + System.out.println(81); + ``` -* 父子类的返回值完全一致 -* 子类返回值可以是父类返回值的子类 +* 冗余消除:根据运行时状况进行代码折叠或削除 -```java -class A { - public Number m() { - return 1; - } -} -class B extends A { - @Override - // 子类m方法的返回值是Integer是父类m方法返回值Number的子类 - public Integer m() { - return 2; - } -} -``` +* 内联缓存:是一种加快动态绑定的优化技术(方法调用部分详解) -对于子类,java 编译器会做如下处理: +C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译,这也是使用分层编译的原因 -```java -class B extends A { - public Integer m() { - return 2; - } - // 此方法才是真正重写了父类 public Number m() 方法 - public synthetic bridge Number m() { - // 调用 public Integer m() - return m(); - } -} -``` +C2 的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 -其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突 +VM 参数设置: +- -client:指定 Java 虚拟机运行在 Client 模式下,并使用 C1 编译器 +- -server:指定 Java 虚拟机运行在 Server 模式下,并使用 C2 编译器 +- `-server -XX:+TieredCompilation`:在 1.8 之前,分层编译默认是关闭的,可以添加该参数开启 +分层编译策略 (Tiered Compilation):程序解释执行可以触发 C1 编译,将字节码编译成机器码,加上性能监控,C2 编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: -*** +* 0 层,解释执行(Interpreter) +* 1 层,使用 C1 即时编译器编译执行(不带 profiling) +* 2 层,使用 C1 即时编译器编译执行(带基本的 profiling) -#### 匿名内部类 +* 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) -##### 无参优化 +* 4 层,使用 C2 即时编译器编译执行(C1 和 C2 协作运行) -源代码: + 说明:profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等 -```java -public class Candy11 { - public static void main(String[] args) { - Runnable runnable = new Runnable() { - @Override - public void run() { - System.out.println("ok"); - } - }; - } -} -``` -转化后代码: -```java -// 额外生成的类 -final class Candy11$1 implements Runnable { - Candy11$1() { - } - public void run() { - System.out.println("ok"); - } -} -public class Candy11 { - public static void main(String[] args) { - Runnable runnable = new Candy11$1(); - } -} -``` +参考文章:https://www.jianshu.com/p/20bd2e9b1f03 @@ -14557,181 +14500,138 @@ public class Candy11 { -##### 带参优化 +### 方法调用 -引用局部变量的匿名内部类,源代码: +#### 方法识别 + +Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor) + +* **方法描述符是由方法的参数类型以及返回类型所构成**,Java 层面叫方法特征签名 +* 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 + +JVM 根据名字和描述符来判断的,只要返回值不一样(方法描述符不一样),其它完全一样,在 JVM 中是允许的,但 Java 语言不允许 ```java -public class Candy11 { - public static void test(final int x) { - Runnable runnable = new Runnable() { - @Override - public void run() { - System.out.println("ok:" + x); - } - }; - } -} -``` - -转换后代码: - -```java -final class Candy11$1 implements Runnable { - int val$x; - Candy11$1(int x) { - this.val$x = x; - } - public void run() { - System.out.println("ok:" + this.val$x); - } +// 返回值类型不同,编译阶段直接报错 +public static Integer invoke(Object... args) { + return 1; } -public class Candy11 { - public static void test(final int x) { - Runnable runnable = new Candy11$1(x); - } +public static int invoke(Object... args) { + return 2; } ``` -局部变量在底层创建为内部类的成员变量,必须是 final 的原因: - -* 在 Java 中方法调用是值传递的,在匿名内部类中对变量的操作都是基于原变量的副本,不会影响到原变量的值,所以原变量的值的改变也无法同步到副本中 - -* 外部变量为 final 是在编译期以强制手段确保用户不会在内部类中做修改原变量值的操作,也是**防止外部操作修改了变量而内部类无法随之变化**出现的影响 - - 在创建 `Candy11$1 ` 对象时,将 x 的值赋值给了 `Candy11$1` 对象的 val 属性,x 不应该再发生变化了,因为发生变化,this.val$x 属性没有机会再跟着变化 - *** -#### 反射优化 +#### 调用机制 -```java -public class Reflect1 { - public static void foo() { - System.out.println("foo..."); - } - public static void main(String[] args) throws Exception { - Method foo = Reflect1.class.getMethod("foo"); - for (int i = 0; i <= 16; i++) { - System.out.printf("%d\t", i); - foo.invoke(null); - } - System.in.read(); - } -} -``` +方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,不是方法的具体运行过程 -foo.invoke 0 ~ 15次调用的是 MethodAccessor 的实现类 `NativeMethodAccessorImpl.invoke0()`,本地方法执行速度慢;当调用到第 16 次时,会采用运行时生成的类 `sun.reflect.GeneratedMethodAccessor1` 代替 +在 JVM 中,将符号引用转换为直接引用有两种机制: -```java -public Object invoke(Object obj, Object[] args)throws Exception { - // inflationThreshold 膨胀阈值,默认 15 - if (++numInvocations > ReflectionFactory.inflationThreshold() - && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) { - MethodAccessorImpl acc = (MethodAccessorImpl) - new MethodAccessorGenerator(). - generateMethod(method.getDeclaringClass(), - method.getName(), - method.getParameterTypes(), - method.getReturnType(), - method.getExceptionTypes(), - method.getModifiers()); - parent.setDelegate(acc); - } - // 【调用本地方法实现】 - return invoke0(method, obj, args); -} -private static native Object invoke0(Method m, Object obj, Object[] args); -``` +- 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) +- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) -```java -public class GeneratedMethodAccessor1 extends MethodAccessorImpl { - // 如果有参数,那么抛非法参数异常 - block4 : { - if (arrobject == null || arrobject.length == 0) break block4; - throw new IllegalArgumentException(); - } - try { - // 【可以看到,已经是直接调用方法】 - Reflect1.foo(); - // 因为没有返回值 - return null; - } - //.... -} -``` +对应方法的绑定(分配)机制:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次: -通过查看 ReflectionFactory 源码可知: +- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 +- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 -* sun.reflect.noInflation 可以用来禁用膨胀,直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算 -* sun.reflect.inflationThreshold 可以修改膨胀阈值 +* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 +非虚方法: +- 非虚方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的 +- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法 +- 所有普通成员方法、实例方法、被重写的方法都是虚方法 +动态类型语言和静态类型语言: +- 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 -*** +- 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 +- **Java 是静态类型语言**(尽管 Lambda 表达式为其增加了动态特性),JS,Python 是动态类型语言 + ```java + String s = "abc"; //Java + info = "abc"; //Python + ``` -## 系统优化 +*** -### 性能调优 -#### 性能指标 -性能指标主要是吞吐量、响应时间、QPS、TPS 等、并发用户数等,而这些性能指标又依赖于系统服务器的资源,如 CPU、内存、磁盘 IO、网络 IO 等,对于这些指标数据的收集,通常可以根据Java本身的工具或指令进行查询 +#### 调用指令 -几个重要的指标: +##### 五种指令 -1. 停顿时间(响应时间):提交请求和返回该请求的响应之间使用的时间,比如垃圾回收中 STW 的时间 -2. 吞吐量:对单位时间内完成的工作量(请求)的量度(可以对比 GC 的性能指标) -3. 并发数:同一时刻,对服务器有实际交互的请求数 -4. QPS:Queries Per Second,每秒处理的查询量 -5. TPS:Transactions Per Second,每秒产生的事务数 -6. 内存占用:Java 堆区所占的内存大小 +普通调用指令: +- invokestatic:调用静态方法 +- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 +- invokevirtual:调用所有虚方法(虚方法分派) +- invokeinterface:调用接口方法 +动态调用指令: -*** +- invokedynamic:动态解析出需要调用的方法 + - Java7 为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令 + - Java8 的 lambda 表达式的出现,invokedynamic 指令在 Java 中才有了直接生成方式 +指令对比: +- 普通调用指令固化在虚拟机内部,方法的调用执行不可干预,根据方法的符号引用链接到具体的目标方法 +- 动态调用指令支持用户确定方法 +- invokestatic 和 invokespecial 指令调用的方法称为非虚方法,虚拟机能够直接识别具体的目标方法 +- invokevirtual 和 invokeinterface 指令调用的方法称为虚方法,虚拟机需要在执行过程中根据调用者的动态类型来确定目标方法 -#### 优化步骤 +指令说明: -对于一个系统要部署上线时,则一定会对 JVM 进行调整,不经过任何调整直接上线,容易出现线上系统频繁 FullGC 造成系统卡顿、CPU 使用频率过高、系统无反应等问题 +- 如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final,那么可以不通过动态绑定,直接确定目标方法 +- 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 -1. 性能监控:通过运行日志、堆栈信息、线程快照等信息监控是否出现 GC 频繁、OOM、内存泄漏、死锁、响应时间过长等情况 -2. 性能分析: - * 打印 GC 日志,通过 GCviewe r或者 http://gceasy.io 来分析异常信息 +*** - - 运用命令行工具、jstack、jmap、jinfo 等 - - dump 出堆文件,使用内存分析工具分析文件 - - 使用阿里 Arthas、jconsole、JVisualVM 来**实时查看 JVM 状态** +##### 符号引用 - - jstack 查看堆栈信息 +在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 -3. 性能调优: +* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 +* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引 - * 适当增加内存,根据业务背景选择垃圾回收器 +符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: - - 优化代码,控制内存使用 +```java +Constant pool: +... + #16 = InterfaceMethodref #27.#29 // 接口 +... + #22 = Methodref #1.#33 // 非接口 +... +``` - - 增加机器,分散节点压力 +对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找: - - 合理设置线程池线程数量 +1. 在 C 中查找符合名字及描述符的方法 +2. 如果没有找到,在 C 的父类中继续搜索,直至 Object 类 +3. 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。如果有多个符合条件的目标方法,则任意返回其中一个 - - 使用中间件提高程序效率,比如缓存、消息队列等 +对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找: + +1. 在 I 中查找符合名字及描述符的方法 +2. 如果没有找到,在 Object 类中的公有实例方法中搜索 +3. 如果没有找到,则在 I 的超接口中搜索,这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致 @@ -14739,3061 +14639,83 @@ public class GeneratedMethodAccessor1 extends MethodAccessorImpl { -#### 参数调优 +##### 执行流程 -对于 JVM 调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型 +```java +public class Demo { + public Demo() { } + private void test1() { } + private final void test2() { } -* 设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值 + public void test3() { } + public static void test4() { } - ```sh - -Xms:设置堆的初始化大小 - -Xmx:设置堆的最大大小 - ``` + public static void main(String[] args) { + Demo3_9 d = new Demo3_9(); + d.test1(); + d.test2(); + d.test3(); + d.test4(); + Demo.test4(); + } +} +``` -* 设置年轻代中 Eden 区和两个 Survivor 区的大小比例。该值如果不设置,则默认比例为 8:1:1。Java 官方通过增大 Eden 区的大小,来减少 YGC 发生的次数,虽然次数减少了,但 Eden 区满的时候,由于占用的空间较大,导致释放缓慢,此时 STW 的时间较长,因此需要按照程序情况去调优 +几种不同的方法调用对应的字节码指令: - ```sh - -XX:SurvivorRatio - ``` +```java +0: new #2 // class cn/jvm/t3/bytecode/Demo +3: dup +4: invokespecial #3 // Method "":()V +7: astore_1 +8: aload_1 +9: invokespecial #4 // Method test1:()V +12: aload_1 +13: invokespecial #5 // Method test2:()V +16: aload_1 +17: invokevirtual #6 // Method test3:()V +20: aload_1 +21: pop +22: invokestatic #7 // Method test4:()V +25: invokestatic #7 // Method test4:()V +28: return +``` -* 年轻代和老年代默认比例为 1:2,可以通过调整二者空间大小比率来设置两者的大小。 +- invokespecial 调用该对象的构造方法 :()V - ```sh - -XX:newSize 设置年轻代的初始大小 - -XX:MaxNewSize 设置年轻代的最大大小, 初始大小和最大大小两个值通常相同 - ``` +- `d.test4()` 是通过**对象引用**调用一个静态方法,在调用 invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 + - 不建议使用 `对象.静态方法()` 的方式调用静态方法,多了aload 和 pop 指令 + - 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 -* 线程堆栈的设置:**每个线程默认会开启 1M 的堆栈**,用于存放栈帧、调用参数、局部变量等,但一般 256K 就够用,通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统 - ```sh - -Xss 对每个线程stack大小的调整,-Xss128k - ``` -* 一般一天超过一次 FullGC 就是有问题,首先通过工具查看是否出现内存泄露,如果出现内存泄露则调整代码,没有的话则调整 JVM 参数 +*** -* 系统 CPU 持续飙高的话,首先先排查代码问题,如果代码没问题,则咨询运维或者云服务器供应商,通常服务器重启或者服务器迁移即可解决 -* 如果数据查询性能很低下的话,如果系统并发量并没有多少,则应更加关注数据库的相关问题 -* 如果服务器配置还不错,JDK8 开始尽量使用 G1 或者新生代和老年代组合使用并行垃圾回收器 +#### 多态原理 +##### 执行原理 +Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类定义了与父类中非私有、非静态方法同名的方法,只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。 +理解多态: +- 多态有编译时多态和运行时多态,即静态绑定和动态绑定 +- 前者是通过方法重载实现,后者是通过重写实现(子类覆盖父类方法,虚方法表) +- 虚方法:运行时动态绑定的方法,对比静态绑定的非虚方法调用来说,虚方法调用更加耗时 -**** - - - - - -### 命令行篇 - -#### jps - -jps(Java Process Statu):显示指定系统内所有的 HotSpot 虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程,进程的本地虚拟机 ID 与操作系统的进程 ID 是一致的,是唯一的 - -使用语法:`jps [options] [hostid]` - -options 参数: - -- -q:仅仅显示 LVMID(local virtual machine id),即本地虚拟机唯一 id,不显示主类的名称等 - -- -l:输出应用程序主类的全类名或如果进程执行的是 jar 包,则输出 jar 完整路径 - -- -m:输出虚拟机进程启动时传递给主类 main()的参数 - -- -v:列出虚拟机进程启动时的JVM参数,比如 -Xms20m -Xmx50m是启动程序指定的 jvm 参数 - -ostid 参数:RMI注册表中注册的主机名,如果想要远程监控主机上的 java 程序,需要安装 jstatd - - - -**** - - - -#### jstat - -jstat(JVM Statistics Monitoring Tool):用于监视 JVM 各种运行状态信息的命令行工具,可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有 GUI 的图形界面,只提供了纯文本控制台环境的服务器上,它是运行期定位虚拟机性能问题的首选工具,常用于检测垃圾回收问题以及内存泄漏问题 - -使用语法:`jstat -