JVM
JVM
0 引言
什么是 JVM ?
定义:
Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)
好处:
一次编写,到处运行
自动内存管理,垃圾回收功能
数组下标越界检查
多态
比较:
jvm jre jdk
常见的 JVM
路线
1 内存结构
1.1 程序计数器
- Program Counter Register 程序计数器(寄存器)
- 作用:是记住下一条jvm指令的执行地址
- 特点
- 是线程私有的
- 不会存在内存溢出
1 | 0: getstatic #20 // PrintStream out = System.out; |
1.2 虚拟机栈
Java Virtual Machine Stacks (Java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
栈内存溢出
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
线程诊断
1.3 本地方法栈
1.4 堆
- Heap 堆
- 通过 new 关键字,创建对象都会使用堆内存
- 特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
- 堆内存溢出
- 堆内存诊断
- jps 工具 查看当前系统中有哪些 java 进程
- jmap 工具 查看堆内存占用情况 jmap - heap 进程id (某一时刻,要连续得使用jconsole)
- jconsole 工具 图形界面的,多功能的监测工具,可以连续监测
1.5 方法区
- 方法区内存溢出
- 1.8以前会导致永久代内存溢出
- 1.8 之后会导致元空间内存溢出
- 运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量 等信息
- 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
- StringTable
- 特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
- 性能调优
- 调整 -XX:StringTableSize=桶个数
- 字符常量种类多则调整大一些
- 考虑将字符串对象是否入池
- 调整 -XX:StringTableSize=桶个数
- 特性
1.6 直接内存
- Direct Memory
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
- 分配和回收原理
- 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
- ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调 用 freeMemory 来释放直接内存
2 垃圾回收
2.1 如何判断对象可以回收
引用计数法
- A引用B B引用A会造成循环引用,出问题(故Java不使用)
- 可达性分析算法
- Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收
- 哪些对象可以作为 GC Root ?
四种引用
- 强引用
- 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
- 软引用(SoftReference)
- 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
- 可以配合引用队列来释放软引用自身
- 弱引用(WeakReference)
- 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
- 可以配合引用队列来释放弱引用自身
- 虚引用(PhantomReference)
- 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存
- 终结器引用(FinalReference)
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象
- 强引用
2.2 垃圾回收算法
- 标记清除算法
- 速度快但是会造成内存碎片
- 标记整理算法
- 没有内存碎片但是速度慢
- 复制算法
- 不会有内存碎片但是需要双倍的空间
2.3 分代垃圾回收
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的 对象年龄加 1并且交换 from to
- minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
- 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
表头 | 表头 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
2.4 垃圾回收器
串行
-XX:+UseSerialGC = Serial + SerialOld
- 单线程
- 堆内存较小,适合个人电脑
吞吐量优先
多线程
堆内存较大,多核 cpu
让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高
```java
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- ![image-20230321154900987](https://myl-mdimg.oss-cn-beijing.aliyuncs.com/TyporaImg/JVM.assets/image-20230321154900987.png)
- 响应时间优先
- 多线程
- 堆内存较大,多核 cpu
- 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.
- ```java
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark
2.5 G1垃圾回收器
定义:Garbage First
- 2004 论文发布
- 2009 JDK 6u14 体验
- 2012 JDK 7u4 官方支持
- 2017 JDK 9 默认
适用场景
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
- 超大堆内存,会将堆划分为多个大小相等的 Region
- 整体上是
标记+整理
算法,两个区域之间是复制
算法
相关 JVM 参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
垃圾回收阶段
Young Collection
会stw(stop the work)
-
新生代
-
新生代到幸存区
-
进入老年代
-
Young Collection + CM
- 在 Young GC 时会进行 GC Root 的初始标记
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定
-XX:InitiatingHeapOccupancyPercent=percent
(默认45%)
Mixed Collection(优先收集垃圾最多的以达到暂停时间最短的)
会对 E、S、O 进行全面垃圾回收
- 最终标记(Remark)会 STW
- 拷贝存活(Evacuation)会 STW
-XX:MaxGCPauseMillis=ms
2.6 垃圾回收调优
- 调优领域
- 内存
- 锁竞争
- cpu
- 占用 io
- 确定目标
- 【低延迟】还是【高吞吐量】,选择合适的回收器
- CMS,G1,ZGC
- ParallelGC
- Zing
- 最快的GC
- 查看 FullGC 前后的内存占用,考虑下面几个问题
- 数据是不是太多?
- resultSet = statement.executeQuery(“select * from 大表 limit n”)
- 数据表示是否太臃肿?
- 对象图
- 对象大小 16 Integer 24 int 4
- 是否存在内存泄漏?
- static Map map =
- 软
- 弱
- 第三方缓存实现
- 数据是不是太多?
- 查看 FullGC 前后的内存占用,考虑下面几个问题
- 新生代调优
- 新生代的特点
- 所有的 new 操作的内存分配非常廉价
- TLAB thread-local allocation buffer
- 死亡对象的回收代价是零
- 大部分对象用过即死
- Minor GC 的时间远远低于 Full GC
- 所有的 new 操作的内存分配非常廉价
- 新生代的特点
- 老年代调优
- CMS 的老年代内存越大越好
- 先尝试不做调优,如果没有 Full GC 那么已经…,否则先尝试调优新生代
- 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
-XX:CMSInitiatingOccupancyFraction=percent
3 类加载与字节码技术
3.1 类文件结构
根据jvm规范,类文件结构
```java
ClassFile {u4 magic;//前四个字节表示:魔数(ca fe ba be) 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 信息,表示成员变量的数量 field_info fields[fields_count]; u2 methods_count;//方法信息 method_info methods[methods_count]; u2 attributes_count;//附加属性 attribute_info attributes[attributes_count];
}
1
2
3
4
5
#### 3.2 字节码指令
- javap工具[root@localhost ~]# javap -v HelloWorld.class
Classfile /root/HelloWorld.classLast modified Jul 7, 2019; size 597 bytes MD5 checksum 361dca1c3f4ae38644a9cd5060ac6dbc Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#21 // java/lang/Object."<init>":()V #2 = Fieldref #22.#23 //
java/lang/System.out:Ljava/io/PrintStream;
#3 = String #24 // hello world #4 = Methodref #25.#26 // java/io/PrintStream.println:
(Ljava/lang/String;)V
#5 = Class #27 // cn/itcast/jvm/t5/HelloWorld #6 = Class #28 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcn/itcast/jvm/t5/HelloWorld; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 MethodParameters #19 = Utf8 SourceFile #20 = Utf8 HelloWorld.java #21 = NameAndType #7:#8 // "<init>":()V #22 = Class #29 // java/lang/System #23 = NameAndType #30:#31 // out:Ljava/io/PrintStream; #24 = Utf8 hello world #25 = Class #32 // java/io/PrintStream #26 = NameAndType #33:#34 // println:(Ljava/lang/String;)V #27 = Utf8 cn/itcast/jvm/t5/HelloWorld #28 = Utf8 java/lang/Object #29 = Utf8 java/lang/System #30 = Utf8 out #31 = Utf8 Ljava/io/PrintStream; #32 = Utf8 java/io/PrintStream #33 = Utf8 println #34 = Utf8 (Ljava/lang/String;)V
{
public cn.itcast.jvm.t5.HelloWorld(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."
“:()V 4: return LineNumberTable: line 4: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcn/itcast/jvm/t5/HelloWorld; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String hello world 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 6: 0 line 7: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; MethodParameters: Name Flags args
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- 过程
- 原始java代码
- 编译后的字节码文件
- 常量池载入运行时常量池
![image-20230322104419079](https://myl-mdimg.oss-cn-beijing.aliyuncs.com/TyporaImg/JVM.assets/image-20230322104419079.png)
- 方法字节码载入方法区
![image-20230322104440580](https://myl-mdimg.oss-cn-beijing.aliyuncs.com/TyporaImg/JVM.assets/image-20230322104440580.png)
- main线程开始运行,分配栈帧内存
![image-20230322104528061](https://myl-mdimg.oss-cn-beijing.aliyuncs.com/TyporaImg/JVM.assets/image-20230322104528061.png)
- 执行引擎开始执行字节码
#### 3.3 编译期处理
- 所谓的 `语法糖` ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成 和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利
- 默认的构造器
- ```java
public class Candy1 {
}编译成.class后的代码
```java
public class Candy1 {// 这个无参构造是编译器帮助我们加上的 public Candy1() { super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V }
}
1
2
3
4
5
6
7
8
9
10
11
12
- 自动拆装箱
- JDK5以后引用
- ```java
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}```java
public class Candy2 {public static void main(String[] args) { Integer x = Integer.valueOf(1); int y = x.intValue(); }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- 泛型集合取值
- 泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息
在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理
- ```java
public class Candy3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10); // 实际调用的是 List.add(Object e)
Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
}
}所以取值的时候,编译器真正生成的字节码中,还要额外做一个类型转换的操作
```java
// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);
// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 可变参数
- JDK5引入新特性
- ```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");
}
}转为了
```java
public class Candy4 {public static void foo(String[] args) { String[] array = args; // 直接赋值 System.out.println(array); } public static void main(String[] args) { foo(new String[]{"hello", "world"}); }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- foreach循环
- jdk5引入
- ```java
public class Candy5_1 {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦
for (int e : array) {
System.out.println(e);
}
}
}
```java
public class Candy5_1 {public Candy5_1() { } public static void main(String[] args) { int[] array = new int[]{1, 2, 3, 4, 5}; for(int i = 0; i < array.length; ++i) { int e = array[i]; System.out.println(e); } }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- switch字符串
- jdk7开始switch 可以作用于字符串和枚举类,这个功能其实也是语法糖
- ```java
public class Candy6_1 {
public static void choose(String str) {
switch (str) {
case "hello": {
System.out.println("h");
break;
}
case "world": {
System.out.println("w");
break;
}
}
}
}变编译器转换为
```java
public class Candy6_1 {public Candy6_1() { } public static void choose(String str) { 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"); } }
}
1
2
3
4
5
6
7
8
9
10
11
- switch枚举
- 枚举类
- jdk7引入
- ```java
enum Sex {
MALE, FEMALE
}```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}; } /** * Sole constructor. Programmers cannot invoke this constructor. * It is for use by code emitted by the compiler in response to * enum type declarations. * * @param name - The name of this enum constant, which is the identifier * used to declare it. * @param ordinal - The ordinal of this enumeration constant (its position * in the enum declaration, where the initial constant is assigned */ 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); }
}
1
2
3
4
5
6
7
8
9
10
11
- try-with-resources
- JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources`
- ```java
try(资源变量 = 创建资源对象){
} catch( ) {
}略
方法重写时的桥接方法
- ```java
class A {
}public Number m() { return 1; }
class B extends A {
}@Override // 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类 public Integer m() { return 2; }
1
2
3
4
5
6
7
8
9
10
11
12
- ```java
class B extends A {
public Integer m() {
return 2;
}
// 此方法才是真正重写了父类 public Number m() 方法
public synthetic bridge Number m() {
// 调用 public Integer m()
return m();
}
}
- ```java
匿名内部类
3.4 类加载阶段
- 加载
- 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
- _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴 露给 java 使用
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
- 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
- 链接
- 验证:验证类是否符合JVM规范,安全性检查
- 准备:为 static 变量分配空间,设置默认值
- 解析:将常量池中的符号引用解析为直接引用
- 初始化
- 初始化即调用
<cinit>()V
,虚拟机会保证这个类的『构造方法』的线程安全
- 初始化即调用
3.5 类加载器
启动类加载器
用 Bootstrap 类加载器加载类:
```java
package cn.itcast.jvm.t3.load;public class F { static { System.out.println("bootstrap F init"); }
}
1
2
3
4
5
6
7
8
9
10
11
- 执行
- ```java
package cn.itcast.jvm.t3.load;
public class Load5_1 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
System.out.println(aClass.getClassLoader());
}
}输出
E:/git/jvm/out/production/jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5 bootstrap F init null
- -Xbootclasspath 表示设置 bootclasspath
- 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
- 可以用这个办法替换核心类
java -Xbootclasspath:<new bootclasspath>
java -Xbootclasspath/a:<追加路径>
java -Xbootclasspath/p:<追加路径>
扩展类加载器
双亲委派模式
- 所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则
线程上下文类加载器
自定义类加载器