
Java 源文件 (. java): 包含高级的 Java 编程语言代码,由程序员编写。这是人类可读的源代码
Java – javac --> class 运行 --> JIT 同时编译 -->机器码 --> 硬件运行

字节码文件 (.class): 由Java编译器将Java源文件编译生成的二进制文件,其中包含了Java虚拟机可以执行的字节码。字节码是一种由高级语言(比如Java)源代码编译生成的、介于源代码和机器代码之间的中间代码形式。
JVM 的整体结构

Java 代码执行流程

JVM 架构模型
java 编辑器输入的指令流基于两种架构
栈/寄存器的指令集架构
Java 编译器输入的指令流是指在编译 Java 源代码时,Java 编译器生成的字节码 指令序列,用于描述源代码的 执行逻辑
java 编译器输入的 指令流 基于 栈结构 转化为字节码文件

为何不更换寄存器架构

JVM 的生命周期
- 虚拟机的启动

- 虚拟机的执行

- 虚拟机的退出

JVM 发展历程 (了解)
title:JVM 发展历程 (了解)
- `Sun Classic VM`

只用解释器,会一行一行执行,即使 for 循环, 而 JIT 编译器可以寻找热点代码,也就是执行频率高的代码,会将其编成本地机器指令,然后缓存起来,下次反复执行就不会先解释器逐行再去翻译,但所有代码都会被编译为机器码需要时间长

- `Exact VM`

- `HotSpot` 目前称霸

- `JRockit`

- `IBM的J9`

- `KVM和CDC/CLDC Hotspot`

- `Azul VM`

- `Liquid VM`

- `Apache Harmony`

- `Microsoft JVM`

- `TaobaoJVM`



类加载子系统
javap -v xxx.class
解析:反汇编(将字节码文件内容转换为人类可读的形式)
idea中叫反编译,class文件和java文件看起来一样
类加载器与类的加载过程
ClassLoader 对 class 文件进行加载 (不管能否运行, 看Execution Engine),需要加载类的信息存放于方法区
title:补充加载. class 文件的方式

title:类加载器与类的加载过程

特定文件标识 : 二进制开头 `ca fe ba be` , 所有能被 `Java` 虚拟机识别的都有这个标识


物理磁盘上 `car.class` 文件以`二进制流`的方式加载到内存中
实例化对应: 内存中调用 car 的构造器或构造方法

加载 Loading
通过类的 全限定名 获取该类的 二进制字节流 (类是懒加载: 按需加载 [主动使用]),将字节流所代表的 静态储存结构 转换为 方法区的运行时数据,然后会在内存中生成这个类的 class 对象
静态方法 属于类本身,而不是类的实例。它们在类被加载完成就后可以使用。
[[#^ne4cv6|JVM/上内存与垃圾回收 > ^ne4cv6)
title: 加载 `Loading`

链接 linking (验证 -->准备–>解析)

验证如 ca fe ba be
[[#^j81ym3|虚方法表在此阶段创建)
初始化 Initialization

- 对象的创建
- 执行类的
构造方法(constructor)和静态变量的赋值
负责完成对象的初始化工作,包括分配实例变量的空间,并进行默认赋值。也可以显式地为实例变量赋值
<clinit>方法
title:`<clinit>`方法

<clinit>方法内容如下

如果没有类变量(静态变量)的赋值动作和静态代码块的语句合并,就不会有`<clinit>`方法,如


也就说有静态变量的`赋值`才会有`<clinit>`方法,不赋值也没有
title:虚拟机保证一个类的`<clinit>`方法在多线程时被同步加锁


只有一个线程执行,虚拟机保证了类在被加载时只会执行一次`<clinit>`方法,static中的方法是`<clinit>`方法的体现,static只执行了一次说明
`<clinit>`方法只执行了一次
类加载器分类
启动类加载器: 这个类加载使用 c/C++语言实现的,嵌套在 JVM 内部
拓展类 ext: 负责加载 Java 拓展库,这些库通常是一些可选的,不是Java核心但常用的功能扩展。
系统类 appclassLoader 程序默认加载器: 一般来说,Java 应用的类都是由它来完成加载
ext 与 app 并没有继承关系,但 ext 是 app 的上层,只是有一个引用
ext 与 app 都和 classLoader(抽象类) 有继承关系
title: 类加载器分类

- 引导类加载器 : Java的核心类库都是使用引导类加载器进行加载的


- 拓展类加载器 : `ExtClassLoade`

- 系统类加载器 : `AppClassLoade` (引用程序类加载器)(用户默认加载器)

title:关系



title: 加载器
```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@1540e19d
//获取其上层:获取不到引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
//对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classLoader.getParent());//sun.misc.Launcher$ExtClassLoader@1b6d3586
//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
}
title: 验证对应路径的类是对应加载器加载的
```java
public static void main(String[] args) {
System.out.println("**********启动类加载器**************");
//获取BootstrapClassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);//null
System.out.println("***********扩展类加载器*************");
//System.getProperty():获取指定的Java系统属性
//"java.ext.dirs":这个属性键表示Java扩展目录属性
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
ClassLoader classLoader1 = SunEC.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1b6d3586
}
title: 用户自定义类加载器
new UserClassLoader(加载路径)



获取 ClassLoader 抽象类
ClassLoader抽象类
除了引导类加载器,所有类加载器都继承于 ClassLoader
java 中可以分为两类加载器,一类引导类加载器,一类继承 ClassLoader 类的

- 获取
classLoader途经

//1.
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
//2.
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1); //sun.misc.Launcher$AppClassLoader@18b4aac2
//3.
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader();//sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader classLoader3 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
System.out.println(classLoader3);
双亲委派机制
类是按需加载的,同时是双亲委派机制,类会委托给父类加载器,不断向上委托,顶层父类判断自己能否加载,不能则委托给子类
title:简介


title:证明


结果自己创建的 `String` 没有自身系统类加载器使用,而是委托最上层父类加载器去加载,最上层加载器会看所属包,如果不属于自己的管理范畴,则会继续向下委托给拓展类加载器,拓展类加载器看如果也不属于自己的管理范畴,则也不管,才委托给系统类加载器

委托给引导类加载器后,发现java中真正的String中根本没有main方法,就直接忽略掉了自己写的string如果“官方版本”存在,就会忽略“自定义版本”

反向委派
接口是核心 API,使用引导类加载器加载,但具体实现类是第三方 Jar 包,就会通过 线程上下文加载器 ,反向委派 到 系统类加载器

优势
- 避免了类的重复加载,每个加载器都有自己的加载范畴
- 保护程序安全,防止核心 API 被篡,假如传过来的自定义
String不会被加载,还是引导类加载器加载核心API
title:沙箱安全机制
会没有权限,直接报错,禁止了自定义类使用核心API的包名


其他
一个类对象必须来源于同一个 class 文件和同一个 classLoader 才相等
用户类加载器加载的类会将这个类加载器的一个引用作为类型信息的一部分保存在方法区
Java 程序对类的主动使用会导致初始化,被动使用不会导致类的初始化 ^ne4cv6
title:其他



运行时数据区
title:例图

红色一个进程一份
灰色一个线程一份

^dugjvf

一个 JVM 只有一个 Runtime 实例,即运行时环境
线程
在 Hotspot JVM 里,每个线程都与操作系统的本地线程直接映射。当一个 Java 线程准备好执行以后,此时一个操作系统的本地线程, 也同时创建。Java 线程执行终止后,本地线程也会回收。
title:线程
在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。 当一个Java线程准备好执行以后,此时一个操作系统的本地线程 也同时创建。Java线程执行终止后,本地线程也会回收。 操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本 地线程初始化成功,它就会调用Java线程中的run ()方法

程序计数器 (PC 寄存器)
为了能够准确地记录各个线程正在执行的当前字节码指令地址,
每一个线程都分配一个 PC 寄存器
存储执行指向执行下一条指令的地址,地址由执行引擎读取,可以理解成游标或者集合的迭代器, 是程序控制流的指示器(分支,跳转,循环,异常)
不存在 error,速度最快
既没有 GC(不需要垃圾回收,不断被替换为下一个地址)
唯一没有 OOM(没有溢出)
title: 程序计数器 (PC 寄存器)



title:简介


指定地址(偏移地址)PC寄存器存储的数据

title:PC寄存器存储字节码指令地址的作用

为了准确记录各个线程当前执行的字节码指令地址,各个线程这样不会互相干扰
title:PC寄存器为什么被设定为线程私有

看着像是并发,实际是一个一个执行
虚拟机栈

一个线程对应一个 Java 虚拟机栈,有 Error,不存在 GC,存在 OOM,速度仅次于程序计数器
栈是运行时单位,堆是存储的单位

title:简介



食材明细 --> 变量
步骤 -->字节码指令
食材 -- > 堆

- 优点

- 栈中可能出现的异常
栈的大小可设置动态或固定
固定时,超过 Java 虚拟机栈允许的最大容量会抛出StackOverflowError
若动态拓展申请不到足够内存会抛出OutOfMemoryError
title:-Xss 栈的内存大小设置





只调用了1000多次
栈的内存决定栈帧总大小,单个栈帧大小决定栈帧数量
栈的储存单位: 栈帧
每个线程都有自己的栈,栈中的数据都是以 栈帧(Stack Frame) 的格式存在,线程上的每个方法都各自对应一个 栈帧(Stack Frame) ,栈帧是一个内存区域,是一个数据集,维护方法执行过程中的各种数据信息

寄存器存的也是当前栈帧


以此类推

方法如果没处理异常会抛给 main, 没处理都会抛出异常的方式结束
- 栈帧内部结构

栈帧内部结构: 局部变量表
栈帧为一个 数字数组(大小编译期确定),也称局部变量数组或本地变量表,主要用于存储 调用方法的参数 (方法执行时,虚拟机使用局部变量表完成方法的传递) 和定义在方法体内的 局部变量和值, 只在当前栈帧中有效
栈帧中与性能调优关系最密切的就是局部变量表
栈越大,方法嵌套调用次数越多
局部变量表中的变量是垃圾回收的根节点 (只要被局部变量表中直接或间接引用的对象都不会被回收)
title:局部变量表


title:解析

局部变量表长度 locals=3


L代表引用类型
局部变量表的容量大小在编译器就确认下来了


最后还有一个收尾的大括号代表一个
title:简介


title:细节理解
变量个数

该java代码所对应的字节码长度

start (字节码指令行号) 和 line Number (代码行号)的对应

title:
字节码指令对应的就是java指令内容
LineNumberTable : 字节码行和代码行对应表

局部变量的描述

根据索引使用相应变量
start pc是字节码指令行数
length描述当前变量作用域的范围(即pc开始往后的字节码范围)
如pc 8, length8 ,即字节码行号为`8-8+8`的作用范围
start pc其实就是起始的范围
index 是索引
如num --> pc11 --> line 16:即作用域的开始是声明的下一行


`start pc + length = code length `



局部变量表单位 slot
局部变量表
int i=1
杜宇基本数据类型储存 i 和对应的值 1
对于引用类型局部变量表储存地址值
局部变量表的最基本的单位是 Slot
byte 1,short 2,int 4,long 8,float 4 double 8,char 2 字节,大小对应slot
引用类型32位 占用一个 slot
title: 关于 `slot` 的理解

访问占用 64 位的局部变量值,只需要使用前一个索引即可



如果当前帧是由构造方法或者实例方法创建的,那么该对象引用 `this` 将会存放在 `index` 为 `0` 的 `slot` 处,其余的参数按照参数表顺序继续排列
对于 `static 方法` : `this` 变量则不存在于当前方法的局部变量表中,由于 `static` 方法是 `属于类` 而不是实例的,它不需要通过 `this` 关键字引用当前对象。在 `static` 方法中,不存在当前对象的引用,因此局部变量表中不会分配 `this` 变量
title:slot演示


weight占据两个3,4
- 构造器


栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的 (因为开辟的数组不能变, 所以需要重复利用)(槽中的数据不再使用且需要被其他对象使用时,需要被回收,如果只是不再使用没有被回收)
title:槽位重用
b过了作用域还占据着slot,目前还没有被重复利用


c重复利用了b的index空间,
变量的分类:按照数据类型分:
① 基本数据类型
② 引用数据类型
按照在类中声明的位置分:
① 成员变量:在使用前,都经历过默认初始化赋值
类变量: linking的prepare阶段:给类变量默认赋值 ---> initial阶段:给类变量显式赋值即静态代码块赋值
实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
② 局部变量:在使用前,必须要进行显式赋值的!因为局部变量不会被默认赋值,编译不通过
栈帧内部结构: 操作数栈
每一个独立的栈帧包含一个后进先出 (last-In-First-Out) 的操作数栈,也可以称之为表达式栈 (Expression Stack)
操作数栈 (Operand Stack) 的作用就是辅助完成局部变量的数据的各种,主要用于保存计算的临时结果,作为变量临时存储空间,所需最大深度在编译期间就确定好了
调用方法的返回值会被压入操作数栈,并更新程序计数器下一条指令的地址
[[#方法返回地址|JVM/上内存与垃圾回收 > 方法返回地址)
操作数栈 并非采用访问索引 的方式来进行数据访问的,而是只能通过入栈 (push) 和出栈 (pop) 操作来完成一次数据访问
title:操作数栈

既有栈的特点,也具有数组或链表的特点


代码追踪
[[…/…/IT/Utility/Excalidraw/Drawing 2023-12-14 20.45.04.excalidraw|…/…/Utility/Excalidraw/Drawing 2023-12-14 20.45.04.excalidraw)

title:简介
` 0 bipush 15 //bipush(push byte)`将一个字节的常量值推送到`操作数栈`上(`bipush`byte-->int ;`sipush`是short-->int)
` 2 istore_1 `//Integer STORE_ X表示将`操作数栈`上数据存储到`局部变量表`的索引X的位置
` 3 bipush 8`
` 5 istore_2`
`6 iload_1 ` // `iload_X`将`局部变量表`的数据X放到`操作数栈`
`7 iload_2`
` 8 iadd` //将`操作数栈`的数据相加
` 9 istore_3` //
`10 return`
- 局部变量表中,byte会以int形式存储
- 操作数栈中,bipush指令压入的是byte类型
为什么字节码指令之间的编号似乎不连续

`aload_0` 的含义是将当前对象引用加载到操作数栈上
title:i++ 和++i 的字节码指令
对于左边没有赋值操作的i++和++i字节码指令一样
对于左边有赋值操作的
i++先`load`出来再`incre by`
++i先`incre by` 再`load`出来
```java
public void add(){
//第1类问题:
int i1 = 10;
i1++;
// 0 bipush 10
// 2 istore_1
// 3 iinc 1 by 1
int i2 = 10;
++i2;
// 6 bipush 10
// 8 istore_2
// 9 iinc 2 by 1
//第2类问题:
int i3 = 10;
int i4 = i3++;
// 12 bipush 10
// 14 istore_3
// 15 iload_3
// 16 iinc 3 by 1
// 19 istore 4
int i5 = 10;
int i6 = ++i5;
// 21 bipush 10
// 23 istore 5
// 25 iinc 5 by 1
// 28 iload 5
// 30 istore 6
//第3类问题:
int i7 = 10;
i7 = i7++;
// 32 bipush 10
// 34 istore 7
// 36 iload 7
// 38 iinc 7 by 1
// 41 istore 7
int i8 = 10;
i8 = ++i8;
// 43 bipush 10
// 45 istore 8
// 47 iinc 8 by 1
// 50 iload 8
// 52 istore 8
//第4类问题:
int i9 = 10;
int i10 = i9++ + ++i9;
// 54 bipush 10
// 56 istore 9
// 58 iload 9
// 60 iinc 9 by 1
// 63 iinc 9 by 1
// 66 iload 9
// 68 iadd
// 69 istore 10
// 71 return
}
栈顶缓存技术 (Top-of-Stack Cashing)
将栈顶元素全部缓存在物理 CPU 寄存器 中,以降低对内存读写次数,提升执行引擎的执行效率

动态链接 (或指向运行时[[#3631常量池|常量池) 的方法引用)
动态链接: 将符号引用转换为调用方法的直接引用
- 符号引用: 用符号来描述被引用目标,不直接指向内存地址。
- 直接引用: 直接指向内存地址或偏移量,可以被JVM直接使用。
每一个栈帧内部都包含一个指向 运行时常量池中 该 栈帧所属方法的引用 包含这个引用的目的就是为了支持当前方法的代码能够实现 动态链接 (Dynamic Linking), 动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
比如 : invokedynamic 指令会携带一个符号引用, 到方法真正被调用时 JVM 会将这些符号引用转换为调用方法的直接引用
也就是需要将符号引用转换为调用方法的直接引用就是动态链接,就是 invokedynamic
有时动态链接和方法返回地址和一些附加信息会被叫做帧数据区
方法引用:myClass.myMethod () 调用时,JVM 会使用运行时常量池中的方法符号引用来解析 myMethod 的实际内存地址
字段引用:如果 MyClass 中有字段访问,JVM 会使用运行时常量池中的字段符号引用来解析字段的实际内存地址
title:动态链接


方法的调用
在 JVM 中,将符号引用转换为调用方法的直接引用与方法的绑定机制有关
title:绑定




invokestatic : 调用静态
invokespecial : 调用 <init> 方法,私有,父类,唯一方法
invokevirtual 调用虚方法
invokeinterface 调用接口
invokedynamic 动态解析调用方法
“虚方法” (Virtual Method) 指的是可以被子类重写 (Override) 的实例方法。也就是说,当调用一个对象的方法时,具体执行哪个方法版本 (父类还是子类) 要在运行时才能确定。这就是 “虚” 的含义,因为它不是在编译时就确定下来的。
- 特殊
调用自己类独有方法但为public的也是invokevirtual
父类中方法声明为 final 的方法,不能被子类重写,可以被子类直接调用,直接调用会显示 invokevirtual,但此方法是非虚方法 , 也可以通过 super.,会显示为非虚方法
如果直接不通过 super. 调用父类的方法,编译期间不能确定子类是否重写父类方法,会显示为 invokevirtual

title: 非虚方法




Java 是静态, lamda 的引用使 Java 在一定程度上具有动态
js, python 都是动态
title:动态与静态类型语言



调用一个对象的方法时,对象首先会压入操作数栈,然后根据字节码指令如 invokevirtual 找到这个对象的实际类型,然后找到常量池中和调用方法名一致的,进行访问权限校验 (不通过 IllegalAccessError),通过后会按照继承关系依次验证,抛出 AbstactMethodError 说明找到最上面的接口发现是抽象方法,没有被重写
title:方法重写的本质

title: `IllegalAccessError`无权限访问
`maven` : `jar` 包冲突可能出现

- 虚方法表
字节码中包含了方法的符号引用,但并没有包含方法的具体内存地址,设计出了虚方法表
虚方法表它包含了类中所有的虚方法及其实际的 内存地址
虚方法表为了提高性能,避免代码中动态分配导致 JVM 执行时频繁搜索对应方法 ^j81ym3
记录了对于某个类中的方法是指向父类还是自己类的,不需要再进行查找是属于哪个类的
如果没有虚方法表,JVM 就必须在运行时进行复杂的判断来确定具体调用哪个方法版本,这将大大降低程序的性能。
例如:
- 每次调用方法时,都需要遍历整个继承链,找到最合适的方法,这个过程效率很低。
- 当继承层次很深时,性能损失会更严重。
title: 虚方法表





方法返回地址
A 方法中调用完 B 方法
正常退出 :
B 无返回值 : 执行引擎接受返回地址 (即 A 的地址), 根据这个地址执行下一个栈帧
B 有返回值 : 执行引擎接收到方法 B 的返回值,该返回值会被传递给方法 A,方法 A 根据这个返回值进行后续处理
异常退出: 由于异常退出并没有产生正常的返回值,所以方法 A 不会收到来自方法 B 的有效返回值,只是异常表存储了当异常发生时,应该 target 到哪里 (异常表返回地址, 返回地址通过异常表来确定) , 进行解决
title:方法返回地址


返回情况


字节码行号
一些附加信息
比如对程序调试提供支持的信息
本地方法栈
Java 虚拟机栈用于管理 Java 方法的调用 (线程私有)
本地方法栈用于管理[[#本地方法库/接口|本地方法)的调用
title:简介

本地方法会压入本地方法栈,由动态链接的方式调用 C 中的库,由执行引擎执行

堆
一个进程由一个 JVM 实例只有一个堆空间,在启动时被创建,多个线程公用堆空间 (共享意味着线程安全问题), 堆也是 Java 内存管理的核心区域
JVM 启动,空间大小已经确定 (物理内存空间虽然不连续,但逻辑视为连续空间)
所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区
Thread Local Allocation Buffer, TLAB
title:堆简介


title:`栈-堆-方法区` 关系

方法结束后,指向堆中的地址消失了, 堆空间中实例继续存在 (为了避免方法多 GC 频率过高,所以堆空间满后再执行,避免影响用户线程),当堆空间满后,需要 GC 判断,发现没有引用就回收
title:分代


现在说的区没有包括元空间/永久区
- 新生代

Survivor 只会用一个
- 养老代
- 
-

设置堆的大小与 OOM
-
设置堆空间大小的参数
-Xms用来设置堆空间(年轻代+老年代)的初始内存大小
-X是jvm的运行参数
ms是memory start
-Xmx用来设置堆空间(年轻代+老年代)的最大内存大小 -
默认堆空间的大小
初始内存大小:物理电脑内存大小 /64
最大内存大小:物理电脑内存大小 /4 -
手动设置:
-Xms600m-Xmx600m
开发中建议将初始堆内存和最大的堆内存设置成相同的值 , 因为需要扩容和释放,避免调整造成系统额外的压力 -
查看设置的参数
方式一:jps/jstat-gc进程id
方式二:-XX:+PrintGCDetails^ikhhqe

参数的设置只对 新生代, 养老代 生效

title: 查看堆大小代码
```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");
System.out.println("-Xmx : " + maxMemory + "M");
System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
年轻代与老年代
新生代 80%的对象都是朝生夕死
title:年轻代与老年代比例

配置新生代老年代比例


使用时发现不是 8 比 1, 需要手动指定
-Xms600m -Xmx600m
-XX:NewRatio : 设置新生代与老年代的比例。默认值是
-XX:SurvivorRatio :设置新生代中Eden区与Survivor区的比例。默认值是8 ,实际需要手动指定8
-XX:-UseAdaptiveSizePolicy :关闭自适应的内存分配策略(暂时用不到)自适应两个sur区可能不一样大
-Xmn:设置新生代的空间的大小(一般不设置)(优先高于比例)
图解对象分配过程
由于内存分配算法和垃圾回收密切相关
Minor GC,Major GC ,Full GC
title:GC

伊甸园区满后会触发 `GC ` 回收回收,顺便回收 `survival`, 但 `survival` 满时不会触发 `GC ` 回收

年龄计数器: 数值
`S0`, `S1` 哪个空哪个是 to, 另一个 from, to 代表下一次放的地方


title:新对象放入流程



- 年轻代
GC(Minor GC)触发机制
年轻代空间不足就会触发 minor GC
交替使用 Survivor 区可以减少内存碎片。每次 GC 后,存活的对象被紧凑地复制到另一个 Survivor 区,避免了内存碎片的产生。
title:年轻代 `GC(Minor GC)` 触发机制


- 老年代
Major GC/Full GC触发机制
老年代空间不足时,触发 Major GC (根据收集器不同, 应该至少触发一次 Minor Gc)
Major GC 一般比 Minor Gc 要慢 10 倍以上 , Major GC 后,内存还不足会调用 FULL GC ,不足就爆 OOM
title:老年代 `Major GC/Full GC` 触发机制

Full GC触发机制

title:OOM测试
老年代空间不足时会进行一次垃圾回收,回收后仍然不足才会爆OOM,爆OOM前必然会有Full GC
`-Xms9m -Xmx9m -XX:+PrintGCDetails`
```java
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "atguigu.com";
while (true) {
list.add(a);
a = a + a;
i++;
}
} catch (Throwable t) {
t.printStackTrace();
System.out.println("遍历次数为:" + i);
}
}
[GC (Allocation Failure) [PSYoungGen: 2027K->504K(回收后新生代大小)(2560K)总空间大小] 2027K堆空间回收前(和前面一样因为这时候堆空间老年代没数据)->926K堆空间回收后,不一样了,这时说明有数据放到老年代了(9728K)堆空间总大小, 0.0007621 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2210K->512K(2560K)] 2632K->1610K(9728K), 0.0006082 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2310K->496K(2560K)] 3408K->2346K(9728K), 0.0007793 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1238K->512K(2560K)] 7313K->6602K(9728K), 0.0005005 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 512K->496K(2560K)] 6602K->6602K(9728K)最后 新生代放到老年代,回收后没变或者更大, 0.0003862 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 6106K->5014K(7168K)] 6602K->5014K(9728K), [Metaspace: 3439K->3439K(1056768K)], 0.0031555 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 5014K->5014K(8704K), 0.0003299 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 5014K->4912K(7168K)] 5014K->4912K(8704K), [Metaspace: 3439K->3439K(1056768K)], 0.0055557 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
遍历次数为:16
Heap
PSYoungGen total 1536K, used 59K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 1024K, 5% used [0x00000000ffd00000,0x00000000ffd0ef18,0x00000000ffe00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 7168K, used 4912K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 68% used [0x00000000ff600000,0x00000000ffacc2f8,0x00000000ffd00000)
Metaspace used 3473K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 371K, capacity 388K, committed 512K, reserved 1048576K
堆空间分代思想
分代就是为了 优化 GC 性能,对朝生夕死的对象及逆行回收,就能腾出很大的空间
title:分代思想

内存分配策略
Minor GC 存活–> age=1 -->每一次 Minor GC -->age+1 --> >设定值(15) -->晋升老年代
设定值 -XX:MaxTenuringThreshold
特殊: 若 s 区中相同年龄的对象大小总和大于 S 区空间的一半,着大于等于该年龄的对象可直接进入老年代
大对象直接分配到老年代
title:内存分配策略


为对象分配内存: TLAB
TLAB 是 JVM 中的一个内存分配机制。它为每个线程分配一个小的内存区域,用于对象的快速分配,每个线程都在自己的 TLAB 中分配对象。这片区域是私有的,其他线程不能访问,这样可以减少线程之间的竞争,提高内存分配的效率。但一旦对象分配完成并且有其他线程持有该对象的引用,其他线程是可以访问和使用这个对象的。

TLAB 是 线程本地分配缓冲区, 每个线程都有一个自己的缓冲区,每个线程都有独自的 TLAB 可以用来分配对象空间 (并不是所有对象都能分配到 TLAB[仅占 eden 1%], 但优先 TLAB),可以 减少线程之间的竞争,避免了部分线程安全问题


title:TLAB (默认开启)




new 对应着在堆空间开辟新的空间,在 TLAB 开辟新的空间
堆空间的参数设置
测试堆空间常用的jvm参数:
-XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
-XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)
具体查看某个参数的指令:jps:查看当前运行中的进程
jinfo -flag SurvivorRatio 进程id
-Xms:初始堆空间内存 (默认为物理内存的1/64)
-Xmx:最大堆空间内存(默认为物理内存的1/4)
-Xmn:设置新生代的大小 (初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比 (默认2)
-XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例(S0/S1空间如果过小过大就失去了Minor GC就失去了意义)
-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails:输出详细的GC处理日志
打印gc简要信息:① -XX:+PrintGC ② -verbose:gc
-XX:HandlePromotionFailure:是否设置空间分配担保
title:`XX:HandlePromotionFailure`

堆是分配对象的唯一选择吗
title:堆是分配对象的唯一选择吗

title:逃逸分析(6 后默认开启的): 如果发现一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配
`关闭-DoEscapeAnalysis`,不开意味着都在堆空间开辟空间
`-XX: PrintEscapeAnalysis` 查看逃逸分析的筛选结果




title: 对象在外部有调用的可能`-->`发生逃逸
```java
如何快速的判断是否发生了逃逸分析,大家就看new的对象实体是否有可能在方法外被调用
public EscapeAnalysis obj;
/*
方法返回EscapeAnalysis对象,发生逃逸
*/
public EscapeAnalysis getInstance(){
return obj == null? new EscapeAnalysis() : obj;
}
/*
为成员属性赋值,发生逃逸
*/
public void setObj(){
this.obj = new EscapeAnalysis();
}
//思考:如果当前的obj引用声明为static的?仍然会发生逃逸。
/*
对象的作用域仅在当前方法中有效,没有发生逃逸
*/
public void useEscapeAnalysis(){
EscapeAnalysis e = new EscapeAnalysis();
}
/*
引用成员变量的值,发生逃逸
*/
public void useEscapeAnalysis1(){
EscapeAnalysis e = getInstance();
//getInstance().xxx()同样会发生逃逸
}
- 结论
开发中能用局部变量的,就不要使用在方法外定义(不要声明为属性)
使用逃逸分析编译器所作的优化
- 栈上分配
- 同步省略 (锁消除)
- 分离对象或者标量替换
title:使用逃逸分析编译器所作的优化
不能做到100%优化,总会有一些没有优化到栈上



title: 栈上分配测试
关闭逃逸分析:`-DoEscapeAnalysis`,未发生逃逸时,会有1千万个`User`对象,且发生GC
开启后,不会发生GC,只有10万User对象
```java
-Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
public class StackAllocation {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
// 查看执行时间
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start) + " ms");
// 为了方便查看堆内存中对象个数,线程sleep
try {
Thread.sleep(1000000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
User user = new User();
//关闭逃逸分析:-DoEscapeAnalysis,未发生逃逸时,会有1千万个User对象,且发生GC
//开启后,不会发生GC,只有10万User对象
}
static class User {
}
}

标量替换
^ktyad4
title: 标量替换

未发生逃逸的 `聚合量` 经过 JIT 分析, 会被替换为 `标量`,不再需要创建对象,大大减少堆内存的占用




title:逃逸分析设置

方法区 ( 元空间 )
方法区(堆外空间)
方法区包括类型信息,常量,静态变量,运行时常量池,字符串常量, 编译后的代码缓存
字节码文件中的常量池会被加载到方法区中,被称为运行时常量池
title:方法区 ( 元空间 )



到对象类型数据的指针指明了是哪个类 new 的,因此多态的数据类型运行时才知道 ^9djjlm
方法区的理解
方法区看作是一块独立于 Java 堆的内存空间,也可以固定大小或者拓展,逻辑连续物理不连续,各个线程共享的空间
存在 OOM, 可以不 GC, 不去压缩处理碎片问题
title:方法区的理解



title:Hotspot 中方法区的演进

设置方法区大小与 OOM
-XX:MetaspaceSize
-XX:MaxMetaspaceSize (-1 没有限制)
title:设置方法区大小与 OOM


方法区的内部结构
title:方法区的内部结构



title:各种信息

当前类的完整信息





字节码文件中的常量池会被加载到方法区中,被称为运行时常量池
title:运行时常量池

class文件信息

class常量池
常量池是字节码文件中的
运行时常量池是方法区中的
常量池
存放编译期间生成的各种字面量和符号引用
数量值,字符串值,类引用,字段引用,方法引用,(字符串, final 常量,基本数据类型,其他)和符号引用,加载后放到运行时常量池中
运行时常量池 是当 class 文件被加载完成后,JVM会将 class 常量池 里的内容转移到 运行时常量池 里,在 class 常量池的符号引用有一部分是会被转变为直接引用,比如说类的静态方法、私有方法、实例构造方法、父类方法。这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的
title:常量池







title:细节说明





都算是符号引用,真正执行符号引用会转换为直接引用
title:class文件信息,class文件常量池,运行时常量池

方法区的演进细节

title:p97(没听)



title:StrinaTable为什么调整

静态变量放在哪?

三个对象都放在堆中,变量本身有的在堆中,有的在局部变量表中
第一个变量名: 堆空间,虽然规定了要放在方法区中,但 JDK 7 后虚拟机自己选择放到了堆空间中
第二个变量名: 是成员变量,放到了堆空间中
第三个变量名: 栈帧中的局部变量表

方法区的垃圾回收
方法区的垃圾回收通常为运行时常量池中废弃的常量和不再使用的类型


总结
对象实例化,内存布局与访问定位


对象的实例化
title:字节码角度


判断类是否加载
new方法区把类加载,堆开辟空间,此时类已经确定
对象创建执行步骤 ^2kosms
title:对象创建执行步骤
① 加载类元信息
② 为对象分配内存(和清除算法有关)
③ 处理并发问题
④ 属性的默认初始化(零值初始化)
⑤ 设置对象头的信息
⑥ 属性的显式初始化、代码块中初始化、构造器中初始化

1. 
2. 
规整,指针碰撞:有个指针指向空闲和使用的分界处

不规整:空闲列表法




id,name,account 都是在init中执行的
[[Java SE/4.面对对象编程/3.面向对象下/4.代码块#1 3 程序中成员变量赋值的执行顺序)
^2wz2dk
对象的内存布局

title: java
```java
public class Customer{
int id = 1001;
String name;
Account acct;
{
name = "匿名客户";
}
public Customer(){
acct = new Account();
}
}
class Account{
}
public class CustomerTest {
public static void main(String[] args) {
Customer cust = new Customer();
}
}

对象的访问定位
大多数现代JVM(例如HotSpot JVM)默认使用直接指针(Direct Pointer)访问方式,因为它提供了更高的访问效率
- 使用
-XX:+UseCompressedOops(默认启用)表示使用压缩指针(这通常意味着直接指针)。 - 使用
-XX:-UseCompressedOops表示禁用压缩指针。
这些参数主要是针对指针压缩的配置,但是不同的JVM实现可能会有不同的参数来选择对象访问方式。如果需要更改具体的对象访问方式,可能需要查阅特定JVM实现的文档。

局部变量表 中存储 句柄访问 和 直接访问 的值
局部变量表
存储 : 句柄池中句柄对象的引用
存储 : 对象的直接引用
句柄访问
句柄池: 储存到对象实例和对象类型的指针

好处:对象移动时,到对象类型的数据的指针不需要修改(标记整理算法,新生区 s 交换需要移动)

直接指针

效率比上面的高
直接内存


title:简介



由于看不到本地内存,但也会出现 OutOfMemoryError,出现类似错误需要考虑是否可能出现这中错误

执行引擎
Java 核心组成部分之一,将字节码指令解释/后端编译成机器指令
虚拟机的执行引擎是由软件自行实现的,因此能够执行不被硬件直接支持的指令集格式
执行引擎的输入输出都是字节流,但输入的是字节码二进制流,处理过程是字节码解释指令的等效过程,输出的是执行结果
title:执行引擎





Java 代码编译和执行过程

绿色代表解释器执行过程
蓝色代表编译器执行过程




title:例图



title:简介
- 机器码



机器码-->指令-->指令集-->汇编语言---> 高级语言

title:字节码文件



JIT 编译器

title:解释器对比JIT 编译器

`HotSpot` 会自己选择使用解释器还是使用编译器

Jit 将代码编译且缓存起来了,比解释器快,而解释器响应速度快


title:热机冷机案例
由于还没有进行热点代码统计和JIT动态编译


title:热点代码



超过这个阈值的才会被cache

- 热度衰减

- HotSpot VM中JIT分类




- AOT编译器(-->.so)(linux64支持)





本地方法库/接口
title:简介



为什么需要 Native Method
title:简介



[[…/…/IT/Java SE/4.面对对象编程/3.应用程序开发/String和StringBuffer和StringBuilder#1 String 类的理解 jdk 8|StringTable)
垃圾回收机制
title:什么是垃圾



title:早期了解


title: Java 垃圾回收机制
[官网](<https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html>)



垃圾回收相关算法
垃圾标记阶段
引用计数器算法(不用) / 可达性分析算法 , 可区分已经死亡需要释放的对象
标记阶段:引用计数器算法
- 引用计数器算法 (不用)
title:引用计数器算法

存在循环引用问题

导致 3 个没有被 GC, 其实已经不需要了

标记阶段:可达性分析算法
局部变量表中的变量是垃圾回收的根节点
(只要被局部变量表中直接或间接引用的对象都不会被回收)
通过可达性分析算法 , 根据 根对象集合 (GC Roots) 为初始起点,搜索被根对象集合所连接的目标对象是否可达,搜索过的路径被称为 引用链,如果对象没有被任何引用链相连接,则是不可达的, 就意味对象死亡 (也有可能之后复活)
[[#^0oij7m|JVM/上内存与垃圾回收 > ^0oij7m)
- 可以构成
根对象集合 (GC Roots)的- 虚拟机栈引用的对象
Object obj1 = new Object(); - 本地方法栈内本地方法引用的对象
public native void nativeMethod(); - 方法区中类静态属性引用对象
public static Object obj2 = new Object(); - 方法区常量引用的对象
public static final String STR = "Hello"; - 运行时常量池中引用的对象 ``
- 虚拟机栈引用的对象
title:可达性分析算法






一致性 : 不在对象引用动态变化进行判断,必须 `Stop the world`
对象的 finalization 机制
GC回收会自动调用 finalize 方法
object 类中的 finalize 是空白,子类重写了这个方法,如 close(),永远不要主动调用 finalize 方法
finalize 没有被重写则会直接被判定为不可触及
如果被重写了也只能调用一次,会将该类插入到一个 F-Queue 队列中,有虚拟机调用 finalize 方法, 调用后的对象没有复活就会变成不可触及状态 (被标记为垃圾),永远无法复活 (具体流程为图四) / 对象没有重写 finalize 方法调用 finalize 也会被视为不可触及
title:对象的 finalization 机制



- 两次标记

^0oij7m
Gc 只会被调用一次,(使用 finalize 只能复活一次,第二次不会复活)
title:复活场景
手动重写的finalize方法,与引用链上的任意一个对象建立了联系
```java
public class CanReliveObj {
public static CanReliveObj obj;//类变量,属于 GC Root
//此方法只能被调用一次
@Override
protected void finalize() throws Throwable {
System.out.println("调用当前类重写的finalize()方法");
obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
}
public static void main(String[] args) {
try {
obj = new CanReliveObj();
// 对象第一次成功拯救自己
obj = null;
System.gc();//调用垃圾回收器,调用了重写的finalize, obj = this,建立了联系
System.out.println("第1次 gc");
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
System.out.println("第2次 gc");
// 下面这段代码与上面的完全相同,但是这次自救却失败了
obj = null;
System.gc();
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- MAT 与
JProfiler的 GC Roots 溯源

清除阶段:标记-清除算法
空间满后进入 STW ,进入标记和清除两个阶段
标记清除两个环节
标记 : 标记可达对象 (非垃圾对象)
清除 : 对堆内存从头到尾进行遍历, 没有被标记的会被清除
效率不高, 产生碎片
需要维护空闲列表
title:简介

绿色是可达到的对象,黑色是发现没有被标记的

stop the world

空闲列表(放入新的如果不够可以放入老年代空闲空间)

清除阶段:复制算法
复制算法适合存活对象少,垃圾多时复制成本过高
将活着的内存空间分成了两块,每次只使用其中一块 (需要两倍空间)
高效没有碎片,但是浪费空间, GC 需要维护 region 之间对象引用关系
因为内存规则,创建对象使用指针碰撞就可以
删除并不是真正的删除
title:复制算法



如果大部分都是存活对象时效率会很差,但一般新生代一次都可以回收70%-99%的内存空间

清除阶段:标记-压缩算法 (标记-清除-压缩)
标记-压缩 算法: 先标记清除,然后把对象 移动 到一端 (移动过程会 STW),创建的新对象需要 指针碰撞
而 标记-清除 没有移动,只是清除,需要维护空闲列表
指针碰撞 : 只需要修改指针的偏移量就可以将对象分配到一个空闲内存位置上
title:标记-压缩算法





指针碰撞


分代收集算法
不同生命周期的对象采用不同的收集方式,以便提高收集效率
年轻代: 复制算法,复制算法效率只与对象大小有关,而且年轻代对象存活少,需要复制的对象就少
老年代: 标记-清除,标记-压缩,或者两者结合
Mark 阶段 消耗与 存活对象数量 成 正比
Sweep 阶段消耗与 管理区域大小 成 正相关
Compact 阶段消耗与 存活对象数据 成 正比

title:分代收集算法




增量收集算法、分区算法
增量收集算法
由于 STW 导致长时间停顿,就可以让垃圾收集线程和应用程序线程交替执行,每次垃圾线程回收一小片区域
分区算法
将整个空间分为若小块
title:简介
- 增量收集算法


- 分区算法




垃圾回收相关概念
System.gc() 的理解
与Runtime.getRuntime().gc();的作用一样
触发 Full GC,仅仅是提醒 JVM 进行 GC,不保证 GC 执行
System.runFinalization();//强制调用使用引用的对象的finalize()方法
title:简介

title: GC案例
```java
//不会GC
public void localvarGC1() {
byte[] buffer = new byte[10 * 1024 * 1024];//10MB
System.gc();
}
//buffer被回收
public void localvarGC2() {
byte[] buffer = new byte[10 * 1024 * 1024];
buffer = null;
System.gc();
}
//buffer没有被回收,因为没有对象需要占用buffer所在的slot槽,不需要被回收,但字节码也不会显示

public void localvarGC3() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
System.gc();
}
//此时buffer会被回收,value会占据之前buffer的slot槽,会导致原来的引用不存在,原来的数据就被GC掉了
public void localvarGC4() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
int value = 10;
System.gc();
}
public void localvarGC5() {
localvarGC1();
System.gc();
}
// 方法调用完毕再GC可以被回收
public static void main(String[] args) {
LocalVarGC local = new LocalVarGC();
local.localvarGC5();
}
内存溢出与内存泄漏
内存溢出
OutOfMemoryError
没有足够的空闲空间,并且垃圾回收器无法提供更多内存
title:内存溢出


内存泄漏(GC 不能回收)
内存泄漏可能导致内存溢出
由于设计无法清理或者忘记关闭 .close 导致的内存泄露
title:内存泄漏


忘记断开一个引用导致存在内存泄漏

Stop The World
进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程
STW 过程中整个引用程序的线程都会被暂停
任何垃圾回收器都会有这个操作


垃圾回收的并行与并发
title:并行并发复习
- 并行

- 并发


title:垃圾回收器的并行并发


OopMap
OopMap 是 HotSpot 虚拟机中的一种数据结构,用于记录对象的布局信息。一旦类加载完成,HotSpot 会根据对象的内存布局,计算并记录在对象内存中的哪些偏移量上存储着什么类型的数据。同时,在即时编译过程中,HotSpot 也会在特定位置生成 OopMap,用于记录栈上和寄存器中的引用位置。
OopMap 使得 JVM 可以实现精确式 GC


安全点与安全区域
安全点与安全区域是为了保证 GC 的效率性能
并不是随时可以停下来进行 GC, 只有特定位置停下来进行 GC
根据长执行的特征作为标准
而在安全区域 (代码片段)内则都可以进行 GC
如果堆空间满了,还没到安全点,JVM 可能会触发一些紧急的垃圾回收操作,以腾出一些空间。这通常被称为“GC 主动触发”或“GC 主动处理”(Garbage Collection induced by Application Threads)
title:简介




再谈引用
4 种引用强度依次减弱,都是可达的
我们为了对对象回收的优先级进行区分,区分出哪些要优先回收,对应用类型进行区分
title:再谈引用


强引用 (主)
宁可抛出异常也不回收
title: 强引用


软引用 - 不足即回收

软引用 - 不足即回收,回收后空间还不足就会爆 OOM 异常
当爆 OOM 时和软引用无关
爆 OOM 时软引用已经被回收
并不是 GC 后软引用就不在,而是满时前的 GC 才会清除,内存充足时不会回收
内存充足不会回收,不足才会回收
title:软引用



此时有一个软引用,一个强引用
弱引用 - GC发现即回收
只要 GC 发现了就回收,无论内存充足与否
title:弱引用



虚引用 - 对象回收跟踪
虚引用可以 跟踪 对象的回收时间,可以将一些资源释放的操作放置在虚引用种执行和记录, 只是为了在对象被垃圾回收时收到通知。
虚引用不能单独使用,它必须与引用队列(ReferenceQueue)联合使用。
GC 具有不确定性,只是通知 JVM, 且 GC 只会被调用一次,(使用 finalize 只能复活一次)
设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
title: java
```java
public class PhantomReferenceExample {
public static void main(String[] args) throws InterruptedException {
// 创建一个引用队列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
// 创建一个对象并将其包装在虚引用中
Object obj = new Object();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, referenceQueue);
// 清空强引用,允许对象被垃圾回收
obj = null;
// 强制进行垃圾回收
System.gc();
// 等待一段时间,确保垃圾回收器运行
Thread.sleep(1000);
// 检查引用队列中是否有虚引用
if (referenceQueue.poll() != null) {
System.out.println("对象已被垃圾回收");
} else {
System.out.println("对象尚未被垃圾回收");
}
}
}
title:虚引用




title: java
```java
public class PhantomReferenceTest {
public static PhantomReferenceTest obj;//当前类对象的声明
static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;//引用队列
public static class CheckRefQueue extends Thread {
@Override
public void run() {
while (true) {
if (phantomQueue != null) {
PhantomReference<PhantomReferenceTest> objt = null;
try {
objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (objt != null) {
System.out.println("追踪垃圾回收过程:PhantomReferenceTest实例被GC了");
}
}
}
}
}
@Override
protected void finalize() throws Throwable { //finalize()方法只能被调用一次!
super.finalize();
System.out.println("调用当前类的finalize()方法");
obj = this;
}
public static void main(String[] args) {
Thread t = new CheckRefQueue();
t.setDaemon(true);//设置为守护线程:当程序中没有非守护线程时,守护线程也就执行结束。
t.start();
phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
obj = new PhantomReferenceTest();
//构造了 PhantomReferenceTest 对象的虚引用,并指定了引用队列
PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);
try {
//不可获取虚引用中的对象
System.out.println(phantomRef.get());
//将强引用去除
obj = null;
//第一次进行GC,由于对象可复活,GC无法回收该对象
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用");
}
System.out.println("第 2 次 gc");
obj = null;
System.gc(); //一旦将obj对象回收,就会将此虚引用存放到引用队列中。
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
终结器引用

垃圾回收器
垃圾收集器没有在规范中进行过多的规定, 可以由不同的厂商、不同版本的JVM来实现,可以分为很逗类型
GC 分类与性能指标
title:GC 分类
- 按照线程分


- 按照工作模式分


性能重点注意吞吐量(运行时间比代码运行时间)与暂停时间
低延迟 : 运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间)
吞吐量 : 后台运算
目前标准: 在最大吞吐量优先的情况下,降低停顿时间
title:与性能指标







不同的垃圾回收器概述

title:7种经典的垃圾回收器


JDK14关系图

paralel scavenge 底层不太一样不能和CMS GC搭配

- 查看默认垃圾回收器
-XX:+PrintCommadLineFlags 查看命令行相关参数(包括使用的垃圾收集器)
命令行指令 jinfo -flag 相关垃圾回收期参数 进程ID
Serial 回收器:串行回收 (了解)
title:`Serial` 回收器:串行回收




ParNew 回收器:并行回收 (失宠)
-XX:+UseParNewGC 手动指定PerNew
-XX:ParalleGCThreads 限制线程数量
title: `ParNew` 回收器:并行回收



Parallel 回收器:吞吐量优先 (JDK 8默认)
低延迟 : 需要和用户交互的
吞吐量 : 后台运算
title:Parallel Scavenge




-XX:+UserParallelGC
-XX:+UserParal1elOldGC
-XX:ParallelGCThreads
-XX:MaxGCPauseMillis
-XX:GCTimeRatio
-XX:+UseAdaptiveSizePolicy
title:配置



CMS 回收器: 低延迟 (并发)
title:`CMS` 回收器: 低延迟







为了保证用户对象能够继续执行,不能改变对象的地址,因此不能使用compact

title:设置


总结


G1 回收器
G1收集器则不需要区分新生代和老年代,
G1 采用分区算法
将整个堆内存划分成多个大小相等的 Region 区域, 并根据 Region 中的对象存活时间和占用空间来决定回收优先级。
title: `G1` 回收器



- 特点
region- 可预测停顿模型
soft real-time
title:G1的特点优势






G1使用压缩技术来整理堆空间。在垃圾回收过程中,G1会将存活对象移动到堆中的其他区域,并且尽量减少内存碎片化,从而实现内存的压缩。这一步骤类似于标记-清除算法中的压缩过程

title:G1参数设置



title:适用场景

Region
一个 region 只能是一个角色, 清空后可以转换角色, 大于 1.5倍region 的对象会放到 humongous (避免短期大对象放到老年代堆垃圾收集器造成影响,确保老年代都是长期对象)
title:Region
- 化整为零

G1垃圾回收器,新生代老年代在逻辑上不再是连续的了


- region内部细节

指针在`allocated`/`unallocate`分界处

G1 回收过程
混合回收 :
由于低延迟: 只挑选价值比较高的进行回收
title: G1 回收过程



给每个 region 配置了一个 Rset,记录了该 region 被哪个 region 指向 ^atdrli
title:Remembered Set
解决了一个对象被不同区域引用的问题
回收年轻代也不得不同时扫描老年代,因为老年代的对象可能引用了新生代的对象
`Remembered Set` 避免了全局扫描,而回收老年代则不用担心年轻代引用了老年代

如果引用的对象是同一个region内的就会不记录

- 年轻代GC
- 扫描根
- 更新
Rset: 会根据dirty card queue更新Rset(dirty card queue会在引用赋值语句前后保存对象的引用信息)(由于Rset需要线程同步, 没有直接记录到Rset中, 而是记录到dirty car queue中) - 处理
Rset - 复制对象
- 处理引用
- 并发标记过程
- 初始标记阶段
- 根区域扫描
- 并发标记 : 只会回收一部分,判断活性看值不值得回收
- 再次标记 : 修正上一次标记结果
snap-at-the-beginning - 独占清理: 计算存活对象和 GC 回收比例,没有真正清理
- 并发清理
title:回收具体过程(了解)


1. 年轻代GC


2. 并发标记过程

3. 混合回收

复制算法.避免了碎片

4. Full GC(可选过程四)

title:补充


总结
title:总结














ZGC






GC 日志分析
title:GC 日志分析
参数列表





Young GC

Full GC

title:案例

对象后不下后先进行一次GC,发现s区放不下年轻代中的内容,直接到老年代,4MB的大对象在年轻代能放下了


-Xloggc:./logs/gc.log 先要创建对应目录
title:日志分析工具

快捷
ctrl+shift+n 查类
其他
title:不加报错,加后删除不报错


面试

title: java
```java
int num = 10;
//s1的声明方式是线程安全的
//每个栈帧之间的数据不共享
public static void method1(){
//StringBuilder:线程不安全
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
//sBuilder的操作过程:是线程不安全的
//sBuilder可能被多个线程使用
public static void method2(StringBuilder sBuilder){
sBuilder.append("a");
sBuilder.append("b");
//...
}
//s1的操作:是线程不安全的
//有返回,返回可能被其他多个线程使用
public static StringBuilder method3(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1;
}
//s1的操作:是线程安全的
public static String method4(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1.toString();
}
总结:当有内部数据的作用域不只在方法内时,就是方法不安全的

