赞
踩
java文件–>字节码文件—>jvm虚拟机
jvm是java的虚拟机 用于运行java编译后的字节码文件 也就是.class文件
jvm也是java能够跨越平台的原因 因为jvm他是负责运行字节码的 只要有对应的编译器 编译为符合java标准的字节码文件 就可以运行
该虚拟机是Oracle公司提供的虚拟机 也就是java的默认的虚拟机
基于栈的指令集架构
1 设计和实现更简单 使用在资源受限的系统中
2 避开了寄存器的分配难题 使用零地址指令方式分配
3 指令流中的指令大部分是零地址指令 其执行过程依赖与操作栈 指令集更小
编译器更容易实现
4 不需要硬件支持 可移植性更好 更好的跨平台
基于寄存器的指令集架构
1 典型的应用是x86的二进制的指令集 比如传统的pc以及安卓的Davlik虚拟机
2 指令集架构 完全依赖硬件 可移植性差
3 性能优秀并且执行更加高效
4 花费更少的指令去完成一项操作
5 在大部分的情况下 基于寄存器架构的指令集往往都一地址指令 二地址指令
和三地址指令为主 而基于栈架构的指令集却是以0地址为主
虚拟机的启动
Java的虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类
来完成的 这个类是由虚拟机的具体实现指定的
虚拟机的运行
一个运行中的java的虚拟机有着一个清晰的任务 --执行java程序
程序开始时他才运行 结束时他就停止
执行一个所谓的java程序的时候 真真正正的是执行力一个叫做java虚拟机的一个进程
{
运行ing 运行java虚拟机进程
41364 RemoteMavenServer36
41780 Stack --java程序运行时占用的进程
45592 Launcher
34060 Jps
44780
结束 自动回收进程
12804 Jps
41364 RemoteMavenServer36
45592 Launcher
44780
}
虚拟机的退出
java虚拟机退出的几种情况
1 程序正常执行结束
2 程序在执行过程中遇到了异常或者错误而异常终止
3 由于操作系统出现了错误而导致java虚拟机进程终止
4 某线程调用Runtime类或者System类的exit方法 或Runtime的halt方法
并且 java安全管理器也允许这次的exit或者halt操作
HotSpot VM
默认的虚拟机
J9 VM
IBM自己的虚拟机 也比较快 定位于HotSpot接近 服务器端 嵌入式开发 桌面应用都有涉及
Jrockit VM
专注于服务器的开发 没有解析器(因为不用在意服务启动速度) 其所有命令都靠即使编译器来运行
所有虚拟机的原则 一次编译 到处运行(one compiler,run everywhere)
Azul Vm
前面三大"高性能Jvm’ 使用在通用硬件平台上
这里AZUL VM 个 BEA Liquid VM 是于特定硬件平台绑定 软硬件配合专有虚拟机
2018.04月 发布的新一代虚拟机 号称 ‘Run Programs Faster AnyWhere’ 于java的’write once,run everywhere’ 一致
.class文件(字节码文件) --> 加载阶段 --> 链接阶段 —> 初始化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sxzR2oEE-1678259480100)(img_1.png)]
1 通过一个类的全限定名称获取定义此类的二进制字节流
2 在这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3 在内存中生成一个代表这个类的java.lag.Class对象(对象.class) 作为方法区的各种数据的访问入口
验证
目的在于确保class文件的字节流中包含信息符合当前虚拟机的要求
主要包含四种特征 文件格式验证 元数据验证 字节码验证 符号引用验证
准备
为类变量 分配内存 并且设置该变量的默认初始值 即0
这里不包含有final(常量)修饰的static 因为final在编译时后就会分配了 准备阶段会显示初始化
这里不会为实例变量分配初始化 类变量会在分配方法区中 而实例变量会随着对象一起被分配到堆内存中
解析
将常量池的符号引用改为直接引用的过程
直接引用就是直接指向目标的指针
引用要使用的类
例如代码中含有 System.out.print(“”) 那么就会在解析阶段引入 com.java.System类 java.lang.String类…
-[ ****此方法不需定义 是javac编译器自动手机类中的所有类的变量的赋值动作和静态代码块中的语句合并而来]
[构造器方法 中指令按照语句在源文件中出现的顺序执行]
//父类
public class Father {
public static int num = 1;
static {
Father.num = 2;
}
}
//子类
public class Son extends Father {
// 获取父类变量 测试启动类有父类的情况下 父类是否会先调用clinit方法来初始化
public static void main(String[] args) {
System.out.println(num); //结果为2 说明父类已经在内部执行了一次 因为调用了静态代码块
}
}
-[ 需必须保证一个类的 方法在多线程下被同步加锁]
{
[创建一个多线程的类]
public class ThreadTest {
// 多线程
public static void main(String[] args) {
Runnable r = () -> {
// if (Thread.currentThread().getName().equals(“Thread-0”)) {
// try {
// Thread.sleep(5000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
System.out.println(Thread.currentThread().getName() + “开始”);
System.out.println(1);
// 创建对象 证明多线程环境下 对象只会被初始化一次 不会初始化第二次 因为cinit有同步锁
Dead dead = new Dead();
System.out.println(Thread.currentThread().getName() + “结束”);
}; Thread t1 = new Thread(r); Thread t2 = new Thread(r); Thread t3 = new Thread(() -> { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t3"); }); t3.start(); t1.start(); t2.start(); }
}
class Dead{
static {
System.out.println(Thread.currentThread().getName()+“加载”);
}
}
}
0 iconst_1 //创建一个变量 值为1
1 putstatic #3 <com/misaka/java/Init.a : I> //将1给一个静态变量作为值 静态变量的地址为#3
4 iconst_2 //创建一个量 值为2 也可以写 bipush 值
5 putstatic #3 <com/misaka/java/Init.a : I>//将2给一个静态变量作为值 静态变量的地址为#3
8 return
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PTt2hO0V-1678259480101)(img_2.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-alnru4Tf-1678259480101)(img_3.png)]
类加载器对象直接的关系 并非传统的继承 而是层级的关系
(默认类加载器 )最低层级 系统类加载器也叫应用类加载器 systemClassLoader(AppClassLoader) 该类为classloader的最低层
上一层 扩展类加载器 ExtClassLoader对象 通过systemClassLoader.parent()方法获取
[查看指定对象所使用的类加载器 xxx.class.getClassLoader()]
最高层 引导类加载器 bootstrapClassLoader 对象 Java的核心类库都是使用的该类加载器加载的 如 java.lang java.util
这个类使用的时c/c++ 语言实现的 嵌套在jvm内部
它用来加载java的核心类库(JAVA_HOME/jre/lib…)用于提供JVM自身需要的类
并不继承自java.lang.ClassLoader 没有父加载器
扩展类加载器和系统加载器 的父类加载器
出于安全考虑 该加载器只加载包名为java,javax,sun等开头的类
再Java的日常应用开发中 类的加载几乎是由上述三种类加载器相互配合运行的
在必要时 我们可以自定义类加载器 来定制类的加载方法
为什么要自定义
隔离加载类
修改类的加载方法
扩展加载原
防止源码泄漏
CLassLoader类 他是一个抽象列 其后所有的类加载器都继承字ClassLoader
(不包括bootstrapClassLoader 因为是c/c++语言的)
1 获取当前类的ClassLoader
xx.class.getClassLoder()
Class.forname(全限定名称).getClassLoader
2 根据线程上下文对象来获取类加载器
Thread.currentThread().getContextLoader()
3 CLassLoader.getSystemLoader() 获取
例如
我们也创建一个对象 并且包名为java.lang
类名为String 当我们创建对象时会发生什么事情呢?
答案是 还是会使用java.lang.String(原)
当一个类加载器收到了一个类的加载请求时(new xx())
那么 该请求首先会向上委派 也就是如果是AppClassLoader收到的请求
首先会给他的.parent()对象 如果还有.parent()对象则继续委派
如果父类加载器可以完成委派任务 那么就返回 倘若不能 则尝试使用子类加载器来初始化
这就是双亲委派机制
代码解释
public Class getClass(ClassLoader cl){
//判断是否为顶层
if(cl.parent()!=null){
//还没有达到顶层
//继续递归
getClass(cl.parent());
// 这下面的代码是递归返回时调用的
{判断该子类加载器可以类加载吗}
}else{
// 到达顶层
{这里代码判断引用类加载器是否可以实现类加载}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6TRK12rU-1678259480102)(img_4.png)]
就是一个请求由低到高层传递 如果高层不能解决则再往下传递
为了避免 自定义的类冲突了java底层的类
并且避免了 类的重复加载
并且防止核心api的源代码被修改
该机制就是防止包名为java,javax开头的类 使用main方法
如果使用 那么就会直接报错提示
1 全限定名称必须一致
2 ClassLoader的种类也必须一致
例如 应用类加载器 引导类加载器 扩展类加载器
例如 1 2
name java.lang.String java.lang.String
Loader BootStrapClassLoader AppClassLoader
上面两种因为Loader不同 所以不是同一个类 并且第二个类会因为沙箱安全机制而被限制使用Main方法
一个jvm只有一个运行时数据区
运行时数据区的布局
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jzRvlGtG-1678259480102)(img_5.png)]
详细布局
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nSIcWRnP-1678259480102)(img_6.png)]
在运行时区中 方法区(元空间) 和 堆区 都是一个进程一份
其他的如 PC寄存器 本地方法栈 虚拟机栈 都是一个线程一份
一个进程可以有多个线程
JVM允许多个线程并发
在 hotspot中 每一个线程都与本地操作系统的本地线程直接映射
JVM中的程序计数寄存器(Program Counter Register)
PC寄存器用来存储指向下一条指令的指令地址 也就是即将要执行的指令
代码 由执行引擎读取并且执行下一条指令
`
— 在上面的结构中 PC寄存器在哪里呢?
-复习PC寄存器概念
[ PC寄存器用来存储指向下一条指令的指令地址 也就是即将要执行的指令 代码 由执行引擎读取并且执行下一条指令]
假设 我们的程序刚好运行到了 下面的代码
指令地址 指令
5 istore_2
6 iload_1
那么PC寄存器就会保存改指令的指令地址[5] 并且执行引擎会到PC寄存器中读取该值 并且通过该值作为一个指针获取对应的指令[istore_2]
当改行代码运行结束时 PC寄存器会再次获取下一行指令的指令地址[6] 并且重复上面的操作 直到程序结束
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JJT2MUsY-1678259480102)(img_7.png)]
使用PC寄存器存储字节码指令地址有什么用吗?
为什么使用PC寄存器记录当前线程的执行地址呢?
无线程私有场景
例如
运行了一个java程序进程里面有三个线程
首先线程一运行到了第5个指令 指令地址为7 在这时Cpu开始运行第二个线程
第二个线程运行到了第一个指令 指令地址为1 由于没有线程私有 所有的线程公共使用一个PC寄存器
所以线程一的值被覆盖 当PC寄存器回到线程1时 就无法找到上一次运行的位置
而线程私有就可以做到每一个线程都有一个PC计数器 所以就不会出现指令地址覆盖的问题
初步印象
以前对内存区理解只有 java堆栈结构
栈和堆
栈是运行时单位
堆是存储的单位
是什么
java虚拟机栈 在java线程中会自动生成一个 是[线程私有]的
其内部包含一个个栈帧
生命周期
生命周期与线程一致
作用
主管java程序的运行 他保存的方法的局部变量 部分结果 并参与方法的
调用与返回
虚拟机栈的栈帧
一个栈帧对应一个方法 栈顶的方法为当前方法 与js的方法栈一致
栈内存允许java栈大小是动态or固定
OutOfMemoryError 内存溢出异常 java虚拟机尝试动态扩展 但是内存不足无法创建虚拟机栈 就报错(栈内存动态)
StackOverFlowError 栈溢出 线程请求超出方法栈容量 也就是死递归 (栈内存固定)
每一个线程都有一个虚拟机栈 里面的数据都是以[栈帧]的格式存在
每一个栈帧就是一个方法
栈帧是一个内存区块 是一个数据集 维系着方法的执行过程中的任何数据信息
虚拟机栈和普通栈一致 都要遵循 先进后出的原则
在一条线程上 一个时间点上 只有一个活动的栈帧 即只有当前正在执行的方法的栈帧(栈顶)
这个栈帧被称为 [当前栈帧] 栈帧对应的方法叫 [当前方法] 定义该方法的类叫做 [当前类]
局部变量表
是一个数字数组 主要用于存储方法参数和定义在方法体内的局部变量
这些数据包括基本数据类型 对象引用 以及返回值类型
由于局部变量表是建立在线程的栈上 所以没有安全问题
局部变量表生命周期就是线程结束 就销毁
[变量槽]
变量槽就是局部变量表的容器 是一个数组的结构
32位占一个槽 int object…
64位占两个 long double
变量槽的先后依照变量的创建时间来决定 越早创建就越早入槽
this会第一个放在当前方法的局部变量表中 这也是为什么静态方法获取不到this的原因 因为静态方法当前的局部变量表中没有this
{ [测试 解析为什么static方法不能调用this?]
[结果 因为static方法的局部变量表中没有this!!]
`
public void Test1(){
// 测试 非静态方法的局部变量表
// 结果
// 局部变量表中有this
// Slot长度为 2 分别为 this 和 s
String s = new String();
}
public static void xx(){
// 测试 静态类局部变量表
// 结果
// 局部变量表中无 this
// Slot长度为1 T
/ // 因为当前方法的局部变量表中没有this!!
int T = 102;
}
public Slot(){
// 测试 构造器局部变量表
// 结果
// 局部变量表中有this
// SLot长度为1 this
this.c=10;
}
`
}
[变量槽还有一个功能叫做槽位回收 功能是 回收局部变量开辟的槽位 给下一个变量 就不用多开一个槽位了 例如下面的代码]
public void TestBack(){ // 测试下槽位的回收 int a=10; // 创建一个局部作用域 { int b=a; b=b+a; } int c=10; // 上面的Slot长度是多少呢? 答案是3 // 虽然`` 有 this a b c 4个局部变量 但是 其中b变量在38行之后就不能使用了 所以空出来的槽位就给了变量c // 所以最后只有 this a c 三个局部变量 }
操作数栈(或表达式栈)
该内容的底层数据结构为栈 并且该栈是通过数组实现的
作用
在方法执行的过程中 根据字节码的指令 往栈中写入数据或者提取数据 (即入栈与出栈的操作)
解析字节码
public static void main(String[] args) {
byte a =10;
int b =5;
int c =a+b;
/* 解析字节码
* i 代表int
PC 指令地址 指令 解析
PC寄存器值: 0 * 0 bipush 10 存储一个int类型的数为10到操作数栈中
*
PC寄存器值: 2 2 istore_1 存储该值并且放在局部变量表的变量槽1中 变量槽0为this or args
*
PC寄存器值: 3 3 iconst_5 存储一个int类型的数为5到操作数栈中
*
PC寄存器值: 4 4 istore_2 存储该值并且放在局部变量表的变量槽2中
*
PC寄存器值: 5 5 iload_1 取出局部变量表中槽位为1的值 并且放在操作数栈中
*
PC寄存器值: 6 6 iload_2 取出局部变量表中槽位为2的值 并且放在操作数栈中
*
PC寄存器值: 7 7 iadd 对所有操作数栈的值进行加法运算 并且将结果压入操作数栈中
*
PC寄存器值: 8 8 istore_3 将结果保存到局部变量表的第三个槽位中
*
PC寄存器值: 9 9 return 结束函数
*
* */
[指令介绍]
bipush : byte类型转为int类型 存入操作数栈
iload_局部变量表槽位 : 取出对应槽位的值到操作数栈中
iadd : int 类型相加
istore_槽位: 将操作数栈的栈顶数据以int类型存储到指定的槽位中
}
[下面三部分 也叫帧数据区]
动态链接(或指向运行时常量池的方法引用)
- 字节码
- 10 getstatic #2 <java/lang/System.out
上面字节码中 #2就是一种动态链接 也就是会去常量池中 找到#2的引用并且获取
方法返回地址(或方法正常退出或者异常退出的定义)
例如A方法中13行调用了B方法
然后B方法执行完毕后 将PC寄存器(B方法执行完后 下一个执行的是A方法)的值给执行引擎
然后就能回到A方法的13行继续执行 这个过程叫方法返回地址
在字节码指令中 返回指令包含 ireturn (int,Bool,Byte,Char都是这样一个 )
lreturn long
dreturn Double
Freturn float
areturn Object(引用类型)
return返回void
附加消息
1 按照类型分类
基本数据类型 引用数据类型
2 按照声明的位置分
成员变量(在类中声明) 会有默认值 可以不进行显式赋值
-类变量 无static
- 在使用前 都经历过类加载器的第二个阶段(连接阶段)的准备环节 进行默认赋值
-实例变量 有static
- 随着对象的创建 会在堆空间中会分配实例变量空间 并且进行默认赋值
局部变量(在方法中声明) 没有默认值 必须进行显式赋值 否则报错
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PZdL3dpC-1678259480103)(img_11.png)]
早期绑定(静态链接) 不是多态 方法唯一
目标方法如果在编译期间就可知 且运行时期保持不变 即可将这个方法与所属的类型进行绑定
这样一来 由于明确了被调用的目标方法究竟是哪一个所以就可以使用静态链接的方法将符号引用转为直接引用
晚期绑定(动态链接) 多态方法 例如一个类有多个实现类重写了父类方法 当要调用父类的方法时 就要通过子类来确定要调用哪一个子类的方法
如果被调用的方法再编译器无法确定下来 只能在运行阶段根据实际的类型绑定相关的方法
这种绑定方法就是晚期绑定
虚方法与晚期绑定是对应的
非虚方法与早期绑定是对应的
非虚方法
如果方法在编译的阶段就确定了具体的调用版本 这个版本在运行是不可变的
这样的方法叫非虚方法
静态方法(static) 私有方法(private) final方法(不可修改) 实例构造器 父类方法 都是非虚方法
其他方法为虚方法
普通调用指令
invokevirtual 调用所有的虚方法
invokeinterface 调用所有的接口方法
动态调用指令
invokedynamic 动态解析出要调用的方法 然后执行 (Lambda表达式)
执行后去常量池中寻找
1 类的继承关系
2 方法的重写
静态类型语言 (强语言)
对类型的检查在编译期
动态类型语言 (弱语言)
对类型的检查在运行期
java属于静态类型语言
String a= “abc”;//需要定义类型
js属于动态类型语言
var a=‘abc’; //不需要定义类型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yyr0iRso-1678259480104)(img_12.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SHFtiFUm-1678259480104)(img_13.png)]
举例栈溢出的情况?
StackOverFlowError 静态溢出报错
通过 -Xss调整 动态溢出 OOM报错
调整栈的大小 就可以保证栈不溢出吗
不能 只能将阈值变高
分配的栈空间越大越好吗
不是 !
垃圾回收是否涉及到虚拟机栈
不会 虚拟机栈只涉及Error
方法中定义的局部变量表 是否线程安全?
线程安全?
如果只有一个线程才可以操作这个数据 则是线程安全的
如果有多个线程操作此数据 则数据是共享数据 如果不考虑同步机制的话 则存在线程安全问题
图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yVmikZBb-1678259480104)(img_14.png)]
管理本地方法的调用
也是线程私有的
也可以像虚拟机栈一样设置长度 -Xss
也会有StackOverFlowError 和 OOM报错
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zXdPem9e-1678259480104)(img_15.png)]
堆的核心描述
堆是进程唯一的 也就是一个jvm只有一个堆内存
也就是jvm所有的线程公用一个堆空间
Java的堆内存在jvm启动时就被创建 其空间大小也被确定了 是Jvm管理的最大的内存空间
堆关于对象
"几乎"所有的对象实例 以及数组 都应该运行时分配在堆内存上
方法结束后 对象不会马上被移除 会等到GC回收来移出
GC垃圾回收
Java7以前的堆内存逻辑分为 新生区 养老区 永久区
Java8以后堆内存逻辑为 新生区 养老区 元空间
-Xms 用于标识堆区的最小内存
-Xmx 用于标识堆区的最大内存
【 -X : 操作虚拟机的标识
ms : memory start
mx : memory max
一旦堆空间内存超过了-Xmx设置的值 则会抛出OOM错误
通常会将 -Xms -Xmx 设置为同一个值 其目的是为能在java垃圾回收机制 清理完堆区后
不需要重新计算堆区的大小 从而提升性能
默认大小 初始 物理电脑内存/64
最大 物理电脑内存/4
方法1 jps获取进程id —> jstat -gs 进程id
方法2 -XX:+PrintGcDetails
对象多次创建 导致报错
存储在JVM中的java对象 可以被划分为两大类
一种是生命周期较短的瞬时对象 这类对象的创建和消亡都非常的迅速
另外类的对象的生命周期却非常的长 在某一些极端情况下 可以和JVm的生命周期一样
Java堆区所以被分为 两大区域 - 新生代 和 老年代
图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d0MI1oKi-1678259480105)(img_16.png)]
600M的堆空间 默认新生代为200M 老年代为400M
默认 -XX:NewRatio=2 标识新生代占1 老年代占2 也就是 新生代占堆空间的1/3
也可以设置为 -XX:NewRatio=4 标识新生代占1 老年代占4 也就是 新生代的占堆空间的1/5
Eden空间 和 from和to区的比例为
8 : 1 : 1
可以通过 -XX:SurvivorRatio=比例 来调整
几乎所有的java对象都是在Eden区被New出来的
绝大部分的对象销毁都在新生代执行
可以使用 -Xmn来设置新生代的最大内存大小 [一般用默认值]
因为jvm有一个自适应 要将其关闭才是8:1:1
-XX:-UseAdaptiveSizePolicy
From区 : 也叫Survivor区 用于存储Eden满时 活下来的对象
to区 : 也叫Survivor区 作为准备存储Eden满时From区中活下来的对象 这时to区就转为from区 from区转为to区
总结 : [谁空 谁是to区]
From区和to区会互相变换 例如上一次Survivor0是From区 那么下一次Survivor1区为From区 Survivor0是to区
每一次Eden区满了就会启动一次GC 因为是新生代所以也叫YGC
如果Eden的对象活下来进入了From区 并且在From区和to区中传递15次[阈值] 那么就会被送入老年代(元数据)
如果Survivor区直接占满 那么就会直接跳级到老年代存储
老年区如果也放不下 那么就会执行FGC(Full GC) 如果还是放不下 那么就会报错OOM(OutOfMemory)
阈值的修改
-XX:MaxTenuringThreshold= 进行设置
new 对象时将其放在Eden区中
如果 new对象时Eden区方法 则开启GC开始垃圾回收 检查哪些对象已经没有引用 并且将其删除
有引用的对象则从Eden区到From区
– 图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tU3qfppM-1678259480105)(img_17.png)]
JDK命令行
Eclipse
Jconsole
VisualVm
Jprofiler
Java Flight Recorder
GCViewer
GC Easy
-图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-16b1Bky2-1678259480105)(img_18.png)]
MinorGC触发机制
当Eden空间不足时触发MinorGC该GC是最快的
该GC会引发STM 暂停用户线程 等垃圾回收结束 用户线程才继续进行
Majorgc的图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DrnSagHy-1678259480105)(img_19.png)]
经研究 不同的对象的生命周期不同 70-90的对象都是临时对象
新生代
里面包含 Eden区 From区 to区
老年代
存放在From区和to区经历多次GC依然存货的对象
如果对象在Eden区出生 并且经过了第一次GC 依然存活 并且能够被from区收容的话
那将被移入 并且年龄设置为1 后续每一次GC都会增加年龄 直到增大到15(默认值)
就会进入老年代
对象晋升老年代的阈值 可以通过选项 -XX:MaxTenuringThreshold来设置
如果对象太大导致Eden区放不下 那么就直接放到老年区 所以要避免创建过大的对象
TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。
如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。
TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UVskcUWH-1678259480105)(img_20.png)]
TLAB 也是一块内存区域 用于存放一些线程本身的对象
结论!!!
[开发中 能使用局部变量就不要在方法外面定义]
栈上分配 同步省略 标量替换
JIT 编译器在编译阶段时根据逃逸分析的结果 发现如果一个对象
没有逃出方法的话 就会被优化为栈上分配 分配完毕后 继续调用栈内执行 最后线程结束 栈空间被回收 局部变量对象也被回收
(也就是直接放在虚拟机栈的栈帧的局部变量表中 生命周期与方法一致 所以就不需要GC来进行回收了)
这样就无需进行垃圾回收了
JDK8自动开启逃逸分析
开启GC日志 设置堆内存大小 开启逃逸分析
-XX:+PrintGC -Xms500M -Xmn500M -XX:+DoEscapeAnalysis
逃逸分析可以大幅度的降低运行时长 提升运行效率 所以能使用局部变量就使用局部变量
原因
线程同步的代价时非常高的 同步的后果时降低并发性能
功能
在动态编译同步块的时候 JIT编译器就可以借助逃逸分析来判断同步块所使用的锁对象是否只能被一个线程访问而没有被发布到其他线程
如没有 娜美就会取消这部分代码的同步 这和过程就叫做同步省略 也叫同步锁消除
有的对象可能不需要作为一个连续的内存结构存储也可以被访问到 那么对象的部分(或者全部)
可以不存储在内存中 而是存储在CPU寄存器中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pg126EhZ-1678259480106)(img_22.png)]
方法区也是线程共享的区域
方法区在jvm创建时创建
方法区的大小是可以调节的
方法区的大小决定了可以存储多少个类 如果类太多 则会此处报错 OOM
关闭JVM就会释放内存
jdk7设置
-XX:PermSize=size
-XX:MaxPermSize=size
jdk8
-XX:MetaSpaceSizea
-XX:MaxMetaSpaceSize
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t1DdN8VX-1678259480106)(img_24.png)]
1 通过各种工具先判断是那种情况引起的错误
方法区主要存储的是虚拟机加载的类型信息(Person,User…) 常量(final) 静态变量(static) 即使编译器(JIT)编译后的代码缓存
[类信息] : 类 , 接口,枚举,注解
方法区会存储类型信息的
- 这个类型的完整全限定名称
- 这个类型的直接的父类的全限定名称(接口和java.lang.Object都没有父类)
- 这个类型的修饰符(public,static,final的其中一个)
- 这个类型直接接口的一个有序列表
[域信息]: JVM必须再方法区中保存类型的所有域的相关信息以及域的声明顺序
- 域的相关信息包括 域名称 域类型 域修饰符 (public,protected,static,private,final…)
[方法信息]:JVM必须在方法区中保存以下的信息
- 方法名称
- 方法的返回类型(如void …)
- 方法的参数的数量和类型(按顺序)
- 方法的修饰符(public,private,protected,static,final.synchronized,native,abstrack)
- 方法的字节码 (bytecode) 操作数栈 局部变量表及变量槽大小(abstrack和native除外)
- 异常表(abstract和native方法除外)
每一个异常开始的位置和结束的位置 代码处理在pc计数器中的偏移地址 被捕获的异常类的常量池索引(如异常类为NullPointException 常量池索引为#2)
常量池表是Class文件的一部分 用于存放编译器生成的各种字面量和符号引用
这部分内容将在类加载后存放在方法区的运行时常量池中
1 可以看作为一张表 虚拟机指令根据这一张常量表 找到要指向的类名和方法名 参数类型 字面量等类型
运行池常量池 是方法区的一部分
jdk1.6 以前 有永久代 静态变量存放在永久代上面
jdk1.7 有永久代 但已经开始"去永久代"字符串常量池 静态变量溢出 保存在堆中
jdk1.8以后 没有永久代 类型信息 字段 方法 常量保存在本地内存中的元空间中 但字符串常量池和静态变量转移到了堆中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S0ihEQNG-1678259480107)(img_25.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-88w759uS-1678259480107)(img_26.png)]
1 静态引用的对应的对象实体永远都在堆空间中 无论jdk版本 (即new的对象)
2 如果要查看这个静态变量的存储位置 则要下载一个工具jhsdb(jdk9以上的bin目录中)
堆内存分为了三个阶段
年轻代 老年代 元数据
MirrorGC MajorGC或者FullGC
年轻代又分为了 Eden区 From区和to区
老年区存放的是在From区和to区中经历一定的GC次数后的对象
命令复习
设置堆空间大小
-Xms -Xmx
设置新生代和老年代的比例
-XX:NewRatio
设置(jdk7:永久区)jdk8:元空间大小
jdk7
-XX:PermSize -XX:MaxPermSize
jdk8
-XX:MetaspaceSize -XX:MaxMetaspaceSize(默认值为-1 则无限制 与本地内存一致)
有些人认为方法区 是没有垃圾回收欣慰的 其实不然
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YFmxvV91-1678259480107)(img_27.png)]
常量的回收u[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6CGVRJIA-1678259480107)(img_30.png)]
类的回收
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FYXTKmHy-1678259480107)(img_31.png)]
1 加载对象到堆空间的新生代的Eden区中
2 将类信息存放在方法区(元空间or永久代)中
3 如果该对象是一个局部变量 还要把该对象写入对应的方法的栈帧的局部变量表中
1 new
2 Class的newInstence() 反射的方法 只能调用空参的构造器 并且权限必须为public(jdk9中被下面的替代)
3 Constructor的newInstance()反射的方法 可以调用空参和有参的构造器 权限没有要求
4 使用clone 不使用任何构造器 当前类需要实现Cloneable接口 实现clone方法
5 反序列化 : 从文件or网络中会获取一个对象的二进制流
6 第三方库Object
一共有六步
1 判断类是否完成了 加载 链接 初始化
2 在堆内存中开辟空间
5 设置对象的对象头 对象元数据绑定 (类中创建一个指针绑定方法区的类信息)
6 显式初始化(执行Init) 代码块赋值/直接赋值 构造器赋值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OWT0HoOQ-1678259480108)(img_32.png)]
分为三部分
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mWexWAFM-1678259480109)(img_35.png)]
直接分配本地的内存空间
ByteBuffer b = ByteBuffer.allocateDirect(1024);//数值 单位为Byte
释放
b=null
直接内存也会有 OOM异常
需要通过设置 jvm虚拟机属性来解决
-XX:MaxDirectMemorySize=5m (代表最大虚拟内存为5m)
作用
执行引擎的作用就是将字节码的指令解释/编译为对应的平台上的本地机器指令才可以 简单来说 JVM的执行引擎充当了将高级语言
解释编译为汇编语言(机器语言)的翻译
执行
执行引擎 就是在运行时读取PC计时器的值 通过该值 执行对应的指令
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yTKLt7Vi-1678259480109)(img_36.png)]
解释器(Interpreter) : 当Java虚拟机 启动是会根据预定义的规范对字节码采用逐行解释的方式执行 将每条
字节码的文件中的内容 "翻译"为对应的平台的本地机器的指令执行
JIT :(Just In Time Compiler) 就是需笔记将源代码 直接翻译为和本地机器平台相关的机器语言
因为java的执行引擎中使用了 解释器和编译器两种 来处理字节码文件
hotspot采用了解释器和即时编译器共存的架构 在java虚拟机运行时
解释器和即使编译器 能够相互合作 各自取长补短 尽力取选择最合适的方法来权衡比那一本地代码的时间 和直接解释执行代码的时间
机器码
各种用二进制编码方法标识的指令叫做机器指令码
机器语言虽然能被计算机直接接收 但是和人的语言差距太大 不易理解和记忆
执行速度最快
指令
由于机器码是由0和1组成的 可读性太差 所以将有序的01机器码封装为了指令
例如 mov(写入数据)等等指令 但是在不同的系统上面有不同的指令集 所以对应的机器码也可能不同
汇编语言
由于指令的可读性还是太差 于是人们发明了汇编
在不同的硬件平台 汇编语言对应着不同的机器指令集 通过汇编过程转为机器指令
编译过程可以分为两个阶段 编译和汇编
编译过程 是指读取的源程序(字符流) 对之进行语法分析 将高级语言指令转为功能等效的汇编代码
汇编过程 实际上指汇编语言代码翻译成目标机器指令的过程
Java语言的编译器 其实是一个 "不确定"的过程 因为它可能是指一个[前端编译器]把 .java文件转为.class文件
也有可能叫[后端编译器] 就是JIT 将字节码文件 转为机器码的过程
还有可能是[静态提前编译器] (AOT编译器) 直接将.java的文件转为机器码的过程
通过一个计数器来记录一个方法调用的次数 如果次数达到一定的阈值 就使用JIT编译器来编译
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ln2lahKY-1678259480110)(img_37.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yP3IFcON-1678259480110)(img_39.png)]
sentinel的原理
1 在一定的时间内 错误的请求 如果超出了阈值 那么就会关闭链接 如何在一定的时间内错误
的请求没有达到 则在下一个周期中 错误次数会清0
JIT的热度衰减
1 在一定时间内 如果方法的调用次数达不到启动JIT的高度 那这个方法的调用计数器就会减少一半
这个过程叫做 热度的衰减 这段时间叫 半衰周期
可以通过 -XX:-UseCounterDecay 来关闭热度衰减 这样而来只要系统运行的时间足够长
那么大部分的代码都会通过JIT来编译
可以通过 -XX:CounterHalfLifeTime 来设置半衰期的时间 单位为秒s
就是一个方法循环调用时的一个计数器 在编译方法判断时 要将回边计数器的值加上普通计数器的值 来判断是否达到阈值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xwsn6Heb-1678259480110)(img_40.png)]
String的基本特性
使用""引起来标识
String声明为Final不可被继承
String实现了Serializable接口 标识支持序列化
实现了Comparable接口 标识可以比大小
String在jdk8及以前内部定义了final char[] value 用于存储字符串数据
jdk9后 改为byte[]
因为java公司现大部分的字符粗对象都是用于存储拉丁字符
而一个拉丁字符只占一个byte(一个字节) 而不是一个char(两个字节) 从而导致了资源浪费了一半
而且字符串对象在堆空间中占比很大
String a1 = “abc”; //这里是把"abc"这三个字符存储到堆空间的字符串常量池中 并且返回一个内存地址给a1
String a2 = “abc”; //判断堆空间中字符串常量池是否有"abc"值 有的话直接使用 并且返回一个地址给a2
a1==a2 ; //答案为true 这里是比较地址 由于a1和a2的常量池地址一致 所以相等
并且如果进行字符串修改 也是重新开辟一个字符串常量池空间存储修改后的值
而不是在原有的字符串上面修改 这就是字符串的[不可变性];
public class StringTest {
// 测试String的不可变性
String str = “hello”;
char[] c = {‘t’,‘e’,‘s’,‘t’};
public void Exchange(String str,char[] c){
剖析
str传入后 形参是一个赋值操作 str=传入值 此时地址一致
str=“ex” 这时进行了修改 但是根据字符串不可变 这里只是在字符串常量池中添加了一个新的字符串叫做"ex"
并且只是函数体内部的变量指向它 函数外部字符串保持不变
str=“ex”;
c 是一个数组 形参传入时 内部c和外部c都指向了一个数组 这里修改了数组是会生效的
c[0]=‘b’;
}
public static void main(String[] args) {
StringTest stringTest =new StringTest();
stringTest.Exchange(stringTest.str, stringTest.c);
System.out.println(stringTest.str); //hello
System.out.println(stringTest.c); //best
}
}
[字符串常量池不会存储相同的字符串 - 也就是有一个字符串在里面 如果下一次存储
值相同于已有的字符串 则直接引用 而不是添加]
– jdk8后 -XX:StringTableSize 的最小值为1009
// 通过Stirng的intern()方法来存储字符串到常量池中 数据量为20w
// 并且查看字符串常量池大小 对运行时长的影响
// -XX:StringTableSize
// 1009大小 228ms
// 100009大小 80ms
可以看出大容量换来的提升非常大
// intern() 方法
// 将对应的字符串对象的值存储到字符串常量池中 如果存在就不存储 而是返回地址
s.intern();
常量池酒类是一个java系统级别提供的胡拿出 8中基本数据类型的常量池都是系统协调的
String的比较特殊 它的主要的方法有两种
直接使用双引号声明出来的String对象 会直接存储到对象常量池中
比如 String s1 =“atguigu.com”;
如果不是用双引号申明的String对象 可以使用String提供的intern()方法
这个后面重点
`
public class StringConnectTest {
// 测试不同情况下的字符串拼接操作
static final String a=“a”;
static final String b=“b”;
public static void main(String[] args) { // 测试常量************************************************************************* FinalConnect(a,b); String S1="abc";//这里的操作是 将"abc"放入字符串常量池中 并且返回地址给S1 String S2="a"+"b"+"c"; // 这里就是要判断是否也是使用的字符串常量池的值 /* 字节码文件的编译结果显而易见 String S1 = "abc"; String S2 = "abc"; System.out.println(S1 == S2); System.out.println(S1.equals(S2)); * */ System.out.println(a+b);// 常量池的字符串拼接都是放在字符串常量池中的 这个叫编译期优化 System.out.println(S1==S2); //true System.out.println(S1.equals(S2));//true // 测试有变量的场景******************************************************** String t ="hellox"; String t1 = "hello"; String t2 = t1+"x"; // 将t2的值放入字符串常量池中 并且赋值地址给t3 String t3 = t2.intern(); System.out.println(t==t2); //false 因为字符串拼接时有变量 那底层会将其放在堆空间中 而不是字符串常量池中 System.out.println(t==t3); //true 这这里已经将t3的值放入了字符串常量池中 根据字符长常量池中字符不能重复的规则 所以直接引用已有的地址 所以为true System.out.println(t.equals(t2)); //true //*****************************测试变量字符串拼接的比较 String c1 = "hello"; String c2 ="x"; String c3 =c1+c2; String c4 =c1+c2; System.out.println(c3==c4); //false 因为变量字符串拼接 结果是存储在堆内存中的 堆内存没有对字符串做不可重复的限制(底层不是hashTable) 所以有一个新的 就开辟一个新的空间 } public static void FinalConnect(String a,String b){ System.out.println(a+b); //使用的是StringBuilder.append(a) StringBuilder.append(b) } }
`
1 常量字符串拼接 都是将结果存储到字符串常量池中 有不可重复的性质
例如
String b=“abc”;
String a =“a”+“b”+“c”; //这两个相等
final String t ="c";
final String t2 ="b";
String t3 = "cb";
String a =t+t2; //这两个也相当 因为该变量由final修饰
2 变量字符串拼接 都是将结果放入堆空间中 (因为底层是将拼接的字符串用 new String()进行处理) 可以重复 (可以通过调用intern()方法来做到显式的存储字符串到字符串常量池中)
例如
String d = "ab";
String a ="a";
String b="b";
String c=a+b;
c==d? // false 因为在堆空间中 字符串可以重复 每一个都是单独的对象
String e = c.intern();
e==d? //true 这里是显式的将c的值 放入了字符串常量池中 而常量池中是不可重复的 有重复的直接引用 所以为true
例如 执行一个 s1+s2
字节码运行的
1 StringBuilder s = new StringBuilder();
2 s.append(s1);
3 s.append(s2);
4 s.toString() //这里的toString类似于 new String(s1+s2)
`
public class StringAddTest { public static void main(String[] args) { Add(); // append(); } public static void Add(){ String str = ""; long start= System.currentTimeMillis(); for (int j = 0; j < 1000000; j++) { String s= str+j; } long end = System.currentTimeMillis(); System.out.println("Add"+(end-start)+"ms"); //50ms } public static void append(){ StringBuilder str = new StringBuilder(); long start= System.currentTimeMillis(); for (int j = 0; j < 1000000; j++) { str.append(j); } long end = System.currentTimeMillis(); System.out.println("ap"+(end-start)+"ms"); //38ms } }
`
上面的结果标识 +耗时50ms append耗时38ms
append的效率更高
因为 + 在底层运行时 每一次都会产生一个StringBuilder来用于拼接 String用于存储结果值
而 append() 就没有创建任何对象
append()也可以优化 StringBuilder的底层是一个char[16]的数组
如果数组满了 就会进行扩容操作(新建数组 复制数组 抛弃老数组[暂用堆空间的eden区位置 容易发生mirrorGC])
所以我们可以使用一个确定不会溢出的长度 并且通过构造形参来初始化
StringBuilder str = new StringBuilder(10000); 10000指长度的最大值 实测100000个数据 能减少4ms 38ms->34ms
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MJ1nofwS-1678259480111)(img_42.png)]
Intern()方法是确保整个堆空间中 对应的字符串只在字符串常量池中有一份拷贝
其他地方就不会出现任何关于字符串的对象 这样就可以节约内存空间 加快字符串的执行速度
[调用Intern()方法的字符串 都会放在字符串常量池中]
[any String invoke Intern() method it will be put in String Intern Pool]
new String(“ab”);
public static void main(String[] args) {
new String(“ab”);
// 会创建两个
/*
* new 类型 时会创建一个对应类型的对象在堆空间中
* “ab” 在字节码中代码为 ldc ab 就是在常量池中存储一个"ab"对象
*
* */
}
new String(“1”)+new String(“1”);
创建五个对象
两个new出来的String对象 在堆空间中
一个"1"字符串 在字符串常量池中 因为字符串常量池不能重复 所以第二个"1"是直接引用的
由于是两个变量 所以要使用StringBuilder对象
并且最后StringBuilder会调用toString方法 该方法也是调用new String() 所以这里也有一个 [但是StringBuilder.toString的字符串不会放在字符串常量池中 而是在堆空间中]
String s3 = new String(“1”)+new String(“1”);
s3.intern(); // intern方法 是检查是否常量池有对应的字符串 有则返回地址 没有则添加 [不会影响到调用者本身的地址]
String s4 = “11”;// 使用的是上一行 生成的字符串的地址
s4==s3? jdk6:false jdk7:true
jdk6的intern() 方法 : 首先 因为s3是一个变量字符串相加 所以字符串使用的是StringBuilder 但是StringBuilder的toString方法不会将字符串存入字符串常量区中
[如果不满足以上条件 例如只有 new String(“1”) 而没有拼接操作 那intern()方法就会没有任何用处 因为"1"已经在字符串常量池中申明了!!(相当于是字面量创建) ]
[那么下面的s4==s3 就会变成 s4指向的是字符串常量池中的地址 对比 s3指向堆空间String对象的地址 那么就肯定为false]
所以intern() 在字符串常量池中生成一个"11" 并且是一个全新的地址 所以肯定不和s3的newString()的地址一致
jdk7的intern() 方法 : 在jdk7中由于字符串常量池已经移入到了堆空间中 java为了节省空间 如果字符串调用intern()
不是像以前一样生成一个全新的对象在字符串常量池中 而是直接在字符串常量池中记录当前在堆空间的new String的地址
所以s4指向的就是s3的new String的地址
String s1 = new String(“1”)+new String(“1”); //这里拼接字符串 因为是变量拼接 那么会使用StringBUilder.toString 该方法不会添加字符串到字符串常量池中
String s2 =“11”; // 添加一个"11"到字符串常量池中 并且s2指向它
s1.intern(); // 判断字符串常量池中 是否有 11 有就直接返回地址 没有就新加 明显常量池中已经有了 所以不会有任何操作
System.out.println(s1==s2); //s1 指向的是堆内存中的String对象 值为11
//s2 指向的是字符串常量池中的"11" 所以为false
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M8WEe2TI-1678259480111)(img_43.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B6YLMfHr-1678259480111)(img_44.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8zx2g9Ic-1678259480111)(img_45.png)]
调用intern()之前 注意看字符串数据 是否已经在字符串常量池中创建
jdk7调用intern()方法为 如果常量池没有 将调用者的地址 作为对应的字符串常量池的值
jdk6调用intern()方法为 如果常量池没有 直接在字符串常量池中开辟空间 生成一个字符串
变量字符串的底层使用的是StringBuilder.toString来拼接 该方法不会存储字符串在字符串常量池中!!!
字面量初始的字符串 “” 在底层是直接调用的ldc指令 来直接存储数据到字符串常量池中!!!
1 使用Intern() 更加节省内存空间 因为intern是将堆内存的对象 放入字符串常量池中存储 堆内存就没有对象了 只有字符串常量池中有一个对象
而 直接new String(“xx”) 会产生两个对象 一个是String对象在堆内存中 一个是"xx"在字符串常量池中 所以会更花空间
如果是拼接字符串 则更加花费空间 所以最好以后拼接使用 StringBuilder.append(); 然后再使用 StringBuilder.toString().intern(); 将其放入常量池 减少堆空间中的字符串对象数量
2 总结
如何项目中存在大量的字符串 最好每一个字符串对象 都调用intern()方法
-XX:+PrintStringTableStatistics
StringTable的GC也是使用的堆空间的GC处理方法
1 年轻代 mirrorGC
2 老年代 majorGC FullGC
当字符串常量池被装满时 就会使用GC垃圾回收
调整字符串常量池的大小
-XX:StringTableSize
1 背景
默认情况创建的对象
两个堆空间对象 str1 str2
一个字符串常量 ‘hello’
问题?
上面两个堆空间的value都是指向的"hello"这一个字符串常量
能否去除一个堆空间对象 只留下一个呢?
2 重复的意义
String1.equals(String2)==true 就说明这两个字符串堆内存的value指向的字符串常量是一样的 也就是重复了
3 处理
通过G1垃圾回收器 实现自动持续对重复的String对象进行去重 这样就可以避免浪费内存
4 实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PPa36dFj-1678259480112)(img_46.png)]
5 命令行实现
UseStringDeduplication true 开启String去重 默认为false
什么是GC 为什么需要GC?
对于高级语言来说 一个基本的认知就是如果不进行垃圾回收 内存迟早都要被消耗完
因为不断的分配内存会导致空间不足
随着应用体积的加大 用户的数量变多 没有GC就无法保证程序能够正常的运行
但是GC回收时又会导致STW(Stop The World) 所以我们要对其进行优化
GC除了可以释放垃圾对象 还可以整理内存的记录碎片 碎片整理将占用的堆内存
移到堆的一段 以便JVM将其整理出的内存分配给新的对象
垃圾回收的经典问题
1 哪些内存需要回收?
2 什么时候回收?
3 如何回收?
2 什么是垃圾
垃圾是指运行程序中 没有任何指针指向的对象 这个对象就是一个垃圾对象
频繁收集年轻代
较少收集老年代
基本不收集永久代
标记阶段 引用计数算法 可达性分析算法
对象的 finalization机制
清除阶段 标记-清除算法
如何判断一个对象是否需要回收
在堆里面存放着几乎所有的Java对象的实例 在GC执行垃圾回收之前 首先需要区分出内存中 哪些是存活的对象
哪些是死亡的对象 只有被标记已经死亡的对象 才会被回收 这个过程叫做[垃圾标记阶段]
一般有两种算法 [引用计数算法 ] [可达性分析算法]
`
public class JavaGcMethodTest { // 测试java垃圾回收算法 public byte[] mem = new byte[1024 * 1024 * 50]; //50m public JavaGcMethodTest refrence; public static void main(String[] args) { JavaGcMethodTest j1 = new JavaGcMethodTest(); JavaGcMethodTest j2 = new JavaGcMethodTest(); j1.refrence=j2; //互相调用 j2.refrence=j1; j1=null; j2=null; // 垃圾回收 System.gc(); /* * [GC (Allocation Failure) [DefNew: 1533K->512K(4928K), 0.0010260 secs][Tenured: 31K->542K(10944K), 0.0016745 secs] 1533K->542K(15872K), [Metaspace: 2086K->2086K(4480K)], 0.0030201 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [DefNew: 202K->35K(4992K), 0.0004279 secs][Tenured: 51742K->51777K(62148K), 0.0010058 secs] 51945K->51777K(67140K), [Metaspace: 2188K->2188K(4480K)], 0.0016173 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] * 这里进行了垃圾回收 如果是计数器算法的话 就不会 因为即使j1 和 j2 都不指向堆空间中的对象 但是堆空间的对象中的refrence还有引用 计数器就不为0 所以不会回收 */
`
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yrPZAf77-1678259480112)(img_47.png)]
相对于引用数据算法而言 可达性分析算法不仅也具备实现简单和执行高效
并且可以解决循环引用的问题 防止内存泄漏
Java C# 就是使用的这个算法
要有一个根对象 叫做GC Roots 必须为活跃的引用
1 可达性分析算法就是以GC Roots作为起点 按照从上到下的顺序搜索被根对象链接的目标对象是否可达
2 使用可达性分析算法后 内存中的存活对象 都会被[根对象]集合直接或者间接的链接搜索所走过的路径叫做 引用链
3 如果目标对象没有任何引用链那么对象就是不可达的 意味着对象已经死亡 就是垃圾对象
4 在可达性分析算法中 只有能够被对象集合直接或者间接链接的才是存活对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XMYrk8f2-1678259480112)(img_48.png)]
1 虚拟机栈中的对象
比如 每一个线程中的方法中使用的参数 局部变量等(方法体内部声明的变量)
2 本地方法栈中JNI(本地方法)引用的对象
3 方法区中静态属性引用的对象
比如 Java类的引用类型静态变量
4 方法区中常量的引用对象
比如 StringTable(字符串常量池) 里面的引用
如果一个对象变量不在堆空间中 但是它指向堆空间中的数据 他就是一个GC root对象
例如 虚拟机栈中局部变量表的变量
字符串变量 like String s = new String(“xx”);
s 指向的是堆空间的对象
但是s本身在虚拟机栈中
方法区的静态变量和常量
static Student a =new Student();
final Student a = new Student();
这些 a都在方法区中 而 Student()在堆内存中
如果要使用可达性分析算法 来判断内存是否可以被回收 那么分析工作必须在一个能保障一致性
的快照中进行 这点不满足的话 分析结果的准确性就无法保证
也就是在GC时[会暂停线程 保存快照 在快照中操作完毕后 同步状态给线程]
这就是STW的重要原因 即使是(几乎)不会停顿的CMS收集器中 枚举根节点时 也是必须要停顿的[保证一致性]
在对象回收时 GC就会调用该方法 通常都是在其内部 使用[资源释放] 也就是对象的生命周期方法
因为 调用该函数时 可能导致对象复活 (xx.finalize() xx会被加载)
finalize()方法的执行时间时是没有保证的 他完全有GC决定 极端情况下 甚至不会调用(即对象没有死亡)
1 可触及的 从根节点开始 可以到达该对象
2 可复活的 对象的所有引用都会释放 但是对象可能在finalize中复活[复活后的对象 再次==null时 则不会再调用finalize()方法]
3 不可触及的 对象的finalize被调用 并且没有复活 娜美就会进入不可触及的状态
所以就是垃圾对象了 [finalize()方法只会被调用一次!!!]
1 如果一个对象 没有引用链 根据可达性算法 则将其标记一次
2 进行筛选 判断对象是否有必要执行finalize()
1 如果对象没有重写方法 或者已经调用过一次 则虚拟机视为[没必要执行] 直接设置状态为不可触及
2 如果重写了方法 且未执行 就会将其计入一个队列中 一次执行 finalize()方法
3 finalize()方法 是对象最后逃离回收的机会 GC会在这里第二次标记 如果ObjA在Finalize方法中于引用(GC ROOTS)上面任意一个对象
产生了链接 那么会在第二次标记时会被移除"名单" 如果该对象再次出现没有引用的情况 则直接判断为[不可达状态] 也就是说
[一个对象的finalize()方法 只会触发一次]
this就是要被回收的对象
Mat
他是一个强大Java堆内存分析器 用于查找内存泄漏 内存消耗情况
-XX:+HeapDumpOnOutOfMemoryError
在OOM时 生成Dump文件
当成功的区分出死亡和存活的对象后 GC接下来就是要垃圾回收 释放掉内存空间 以便于可用的空间为新对象分配内存
目前JVM使用的有三种算法 [标记清除算法 复制算法 标记压缩算法]
在堆内存消耗完毕后 就会执行 STW 暂停线程 然后进行两项工作
1 标记 : 从引用根节点开始便利 标记所有被引用的对象 一般时在对象的对象头信息中标记是否为可达对象
2 消除 : 从堆内存从头到位进行线性便利 如果发现对象头信息不是可达对象 就将其回收
优点
简单
缺点
速度慢 效率低 因为都是循环
产生的空间不连续 需要维护一片内存空间 会出现碎片问题
该算法很类似堆内存中的新生代中的 from 区 和 to区原理
只不过是将堆内存 分为了内存from区 和内存to区
jvm先在堆内存from区 做一个判断 判断对象是否存活 存活就放入to区
然后在清除from区所有数据 然后to区变from区 from区变to区 这样子来做到垃圾回收
优点
效率好 简单实现 没有标记和清除的过程
保证空间的连续性 不会出现碎片问题 不需要以后创建对象 需要维护一个对象内存表(详情见 对象创建过程的第二个判断对象内存是否连续)
[如果一个应用 死亡的对象特别多 存活的很少 那么复制算法是最好的 例如新生代的 From区和To区 就是使用的复制算法
From区和To区 死亡的对象特别多 活下来的很少 所以复制算法的复制动作也会很少 死亡的就直接删除 所以效率很高]
缺点
内存消耗大 需要两倍的空间
会修改对象地址 引用也需要修改对象的引用地址非常消耗时间
[复习 对象指向方法
1 直接指向 即 变量直接指向堆空间内存 对象内部一个指针指向方法对应的对象元数据 java使用 对象改变位置 引用也要改变
2 句柄指向 即 变量指向一个对象 该对象中有两个指针 一个指向堆内存对象 一个指向方法区对象数据 对象改变位置 修改句柄数据]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OmQPs4cp-1678259480112)(img_51.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KeDe08cy-1678259480113)(img_52.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YfGtusqL-1678259480113)(img_49.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xZhGsYkr-1678259480113)(img_50.png)]
[在堆空间的老年代中 存活的对象非常多 复制算法在这种环境下 速度非常慢 因为执行复制次数多
所以有了标记压缩算法]
标记压缩算法过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BXgZa5UR-1678259480113)(img_53.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mS0HDKWJ-1678259480113)(img_54.png)]
上面的三种算法 标记清除 标记压缩 复制算法 都有适合直接的处理情况
所以jvm选择了 分代收集的思想 (年轻代 老年代 永久代)
什么是分代收集?
分代收集算法 是基于这样一个事实 不同的对象的生命周期是不一样的
因为[不同的生命周期对象可以采取不同的收集方法 以便提升效率] 一般是把Java堆
分为新生代和老年代 这样就可以根据各个年代的特点 使用不同的收集算法
在java运行时 会产生大量的对象 其中有些对象时与业务信息相关 比如http请求的session对象
线程 socket链接 这类对象跟业务直接挂钩 因此生命周期较长 但是还有一些对象 只要是程序运行过程中 生成的临时变量
这些对象生命周期比较短 例如String 由于其不变性的原因 系统会产生大量的这些对象 有些对象甚至只用一次就回收
年轻代
年轻代的特点 区域比老年区小 对象生命周期短 回收频繁 大部分对象都会死亡
综上述述 最适合的算法一定是复制算法
- 回收频繁 那就一定要效率高 并且大部分的对象都会死亡 所以复制算法的复制动作也会变得很少
并且由于复制算法特性 占用空间大
所以JVM新生代底层中 用S0 S1区作为复制算法区(作为对象的计数) 他们的比例只有20%的新生代 多数的对象还是放在伊甸园区中(8:1:1)
老年代
老年代的特点 区域大 生命周期长 存活率高 回收不频繁
这种情况存在大量的存活对象 复制算法明显不合适 一般是由标记压缩 标记清除 两种算法 混合运行
标记压缩 标记清除
标记阶段 开销与存活对象成正比
压缩阶段 开销与存活对象的数据成正比
清除阶段 开销与管理空间的大小成正比
老年代的落地GC回收器
以HtoSpot的为例 其中老年代使用的是[CMS回收器]
CMS就是基于Mark-Sweep实现的 对于对象回收效率高 但是碎片化问题依然存在
所以 CMS采用基于Mark-Compact算法的[Serial Old回收器]作为补偿措施 [Serial Old是CMS回收器的串行回收器]
如果内存回收不佳(碎片导致回收失败时) 将采用[Serial Old回收器] 执行FullGC达到堆老年代的内存整理
上述的所有的算法 在垃圾回收的过程中 应用软件将处于一种 STW的状态 在STW状态下 应用程序的所有线程都会挂起 暂停主线程 等待垃圾收集
完成 如果垃圾回收时间过长 应用程序就会暂停很久 将严重的影响用户的体验或者系统的稳定性 为了解决这个问题
即对实时垃圾收集算法的研究 就产生了[增量收集算法]
实现
如果一次性收集所有的垃圾 会非常的慢 增量收集算法就是 每次回收 只回收一小部分 然后就转到主线程执行 然后再回收
交替执行 直到回收完毕
实现算法
增量收集算法 是使用的[标记清除 和 复制算法 ]
增量收集算法主要解决了 线程的冲突问题的妥善处理 允许垃圾回收线程 以分阶段的方法完成标记 清理 复制的等工作
缺点
使用这种方法 由于再垃圾回收的过程中 间断性的执行力主线程的代码 虽然减少了停顿时间
但是因为线程的切换 会使得垃圾回收成本上升 [造成系统吞吐量的下降]
分代算法 按照对象的生命周期长短划为两个部分 分区算法将整个堆空间划分为连续的不同的小区间
每一个区间都独立使用 独立回收 这种算法的好处是可以控制一次回收多少个小区间
分代算法(堆空间分代)
底层
新生代 : 复制算法
老年代 : CMS垃圾回收器 (标记清除 标记压缩)
分区算法(堆空间分区)
增量收集算法(减少STW时间 线程交替运行)
标记清除 复制 算法
System.gc()方法的理解
调用时会显式的触发FullGC 同时对老年区和新生区的对象进行回收
尝试释放被丢弃的对象占用的内存
然而System.gc() 无法保证对垃圾收集器的调用(不能确保能够马上调用)
JVM实现者可以通过System,gc()调用来决定JVM的GC行为 而一般的情况下 垃圾回收应该是自动进行的
无需手动触发 否则就太过于麻烦了 在一些特殊的情况下 如我们正在编写一个性能基准 我们就可以运行之间调用System.gc()
[补充:System.runFinalization() 会强制的调用失去引用的对象的finalize()方法]
-System.gc()的测试
`
package com.misaka.java; public class SystemGcTest { // 测试回收 public void GCTest1(){ byte[] bytes =new byte[1024*1024*10]; System.gc(); //不会回收 因为对象还有引用 } public void GCTest2(){ byte[] bytes =new byte[1024*1024*10]; bytes=null; System.gc(); //回收 } public void GCTest3(){ { byte[] bytes =new byte[1024*1024*10]; } System.gc(); //不会回收 因为bytes虽然已经脱离了作用域 但是局部变量表中的变量槽1依然是bytes(只是标记为可覆盖 等待下一个变量声明覆盖 不会删除 也就是说局部变量表中bytes变量依然存在) 所以对象还是有指向 } public void GCTest4(){ { byte[] bytes =new byte[1024*1024*10]; } int value =10; System.gc();//回收 因为bytes脱离作用域 然后又声明了value 所以jvm将value值存入了bytes变量原先的局部变量槽中 从而byte对象失去引用 所以回收 } public void GCTest5(){ GCTest1(); System.gc(); //会回收GCTest1里面的对象 因为GCTest1方法已经出栈了(从虚拟机栈中脱出) 其内部的局部变量表也会销毁 从而对象失去引用 所以回收 } public static void main(String[] args) { SystemGcTest s= new SystemGcTest(); s.GCTest5(); } }
`
OOM(OutOfMemory) java中对该错误的解释是 没有空闲内存 并且垃圾收集器也无法提供更多内存
OOM会在抛出前 调用FullGC 来回收老年区和新生代中的死亡对象 调用完毕后再放入 放入成功就ok 失败就抛出错误
但是如果对象的大小超过了-Xmx的值 则就直接报错 不需要再去调用FullGC
2 代码中大量的生命周期过长的大对象 并且长时间不能被GC所收集(存在引用)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tE77nMJn-1678259480114)(img_57.png)]
对象不会被程序所使用了 但是GC又没有回收 才叫内存泄漏 内存泄漏会间接的导致OOM
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eFYO2LOJ-1678259480114)(img_58.png)]
图例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QQAWewfY-1678259480115)(img_59.png)]
举例
1 有些对象的生命周期是和JVM一样长的 如果有外部的引用关联了它
那么这个外部引用根据可达性分析算法 就无法回收 从而导致了内存泄漏
[ 补充 引用计数算法的内存泄漏 是指对象本体即使销毁了(赋值null) 但是如果对象的属性
有被其他对象所引用 即使对象属性已经无意义了 但是属性的值也无法销毁 因为引用计数器不为0 这就是引用计数算法的内存泄漏]
2 一些提供close()方法的对象例如io对象 database对象 socket对象
这些对象如果不调用close()方法 则也无法回收 从而导致 OOM
指的是 GC事件调用时 会产生应用程序的停顿 这就是STW
可达性分析算法 必须要一个确保一致性的快照中进行
如果分析过程没有STW 则分析结果的准确性无法得到保证(万一 一个对象的值刚好为null 过一会就要重新赋值 就在这是GC回收了该对象 就会导致程序报NullPointError)
该事件 只能控制时长和频率 而不能避免
开发中不要使用System.gc();会导致STW的发生
1 测试用例 两个线程 一个每1s输出一次 一个每当list容量>100000 时清空并且调用GC
结果
`
public class StopTheWorldDemo { // 测试STW // 设置两个线程 一个为输出 一个为触发System.gc() public static void main(String[] args) { PrintThread printThread = new PrintThread(); WorkThread workThread = new WorkThread(); //通过lamba 表达式来实现java的函数式编程 复习 // java c c++ 为编译阶段语言 所以要声明数据类型 通过数据类型判断存储类型 // js php python为运行阶段语言 所以不用设置数据类型 通过变量值来判断存储类型 Thread thread = new Thread(() -> { System.out.println("hello" + Thread.currentThread().getName()); }); workThread.start(); printThread.start(); thread.start(); }
// 测试结果
// 1 正常模式 无GC调用时 输出清空
/* 可以看到ms在10左右波动
1008ms
1006ms
1010ms
1004ms
1006ms
1006ms
1005ms
1001ms
1002ms
1009ms
1005ms
1005ms
1004ms
1006ms
/
// 2 开启GC 查看STW情况
/ 几乎全都大于10ms 只有一个没有STW清况 最大停顿为29ms
*1014ms
1017ms
1014ms
1016ms
1013ms
1024ms
1012ms
1027ms
1017ms
1029ms
1021ms
1003ms
*/
static class WorkThread extends Thread {
@Override
public void run() {
super.run();
List list = new ArrayList();
while (true) {
list.add(10000);
if (list.size() > 1000000) {
list.clear(); //断开数据
System.gc();//调用FullGC
}
}
}
}
static class PrintThread extends Thread {
@Override
public void run() {
super.run();
while (true) {
long start = System.currentTimeMillis();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() - start + “ms”);
}
}
}
}
并发
并发不是真正意义的"同时执行" 只是CPU把一个事件短划分为几个时间区间
然后在几个时间区间之间来回切换 由于CPU处理的速度非常快 只要把时间间隔处理的得当
即可让用户感觉到是在"同时运行"
并发图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zr6l6cKj-1678259480115)(img_61.png)]
并行
如果CPU为多核心 当一个CPU执行一个进程时 另一个可以执行另一个进程 两个进程互不抢占资源
可以同时执行 这就是并行
适合科学计算 后台处理 等弱交互环境
并行
在同一个时间点内 多个事情同时发生
多个任务之间不会抢占资源
并发
在同一个时间段内 多个事情同时发生
多个任务之间会抢占资源
并行
指多条垃圾收集线程并行工作 但此时主线程STW等待 回收完毕后继续执行
串行
相较于并行的概念 单线程执行(效率较低 因为只有一条线程处理垃圾回收)
如果内存不够 则程序暂停 启动JVM的垃圾收集器回收 回收完毕后 再启动程序的线程
-图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kDucztxW-1678259480116)(img_63.png)]
必须为多核心
用户线程继续运行 垃圾回收线程在另外一个cpu核心上面
使用的垃圾回收器
CMS G1
并发执行图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HkLoAHr6-1678259480116)(img_64.png)]
安全点
程序执行时 不是在所有的地方都能停顿GC的 只有在特定的位置才可以开始GC 这个点就叫安全点
安全点的选择很重要 如果太少可能导致GC的等待时间过长
如果太多 也会GC的太频繁 大部分指令的执行时间都非常短暂 通常会根据
“是否具有让程序长时间执行的特征” 为标准 比如: 选择一些执行时间较长的指令 作为Safe Point 如方法调用 循环跳转 和 异常跳转等
安全区域
是指在一个代码片段中 对象的引用关系不会发生变化 在这个区域中的任何位置 都可以开始GC 这个就是安全区域
我们希望能描述一类对象 当内存空间还够用时 则保留在内存中 如果内存空间在进行垃圾回收后 还是很紧张 则可以抛弃对象
这是就要用到 [强引用(99%的对象) 软引用 弱引用 虚引用]
最传统的引用的定义 是指程序代码中普通存在的引用负责制 及类似 Object a = new Object() 这种关系
无论什么情况下 只要强引用的引用关系还在 就永远不会回收
内存足够时 不会回收
在系统将要OOM之前 才会将这些对象 列入回收
范围之中的第二次回收 如果这次回收后还没有足够的内存则就抛出OOM
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yIzKEv3n-1678259480117)(img_67.png)]
被弱引用关联的对象只能生存到下一次GC之前 当GC工作无论空间是否足够 都直接回收
一个对象是否有虚引用的存在 完全不会对其生存时间构成影响 也无法通过虚引用来获取一个对象的实例
为一个对象设置虚引用关联的唯一目的 就是能在这个对象被收集器收集时收到一个系统通知
上面三种引用 在某一个条件下都是可以被回收的 所以
[强引用是造成内存泄漏的主要原因]
只要有引用可达堆对象(可触及 [可达性算法]) 那么就永远不会回收
Object a = new Object();
JVM即将抛出OOM时 直接回收 不管是否为死亡对象
Object a= new Object(); //创建强引用
SoftReference s = new SoftReference(a); //创建软引用 并且变量为s 指向堆内存中的Object
a=null; //断开强引用索引
测试用例
import java.lang.ref.SoftReference; public class RefrenceSofttest { // 测试用例 设置堆空间大小为1g -XX:NewRatio=1 也就是老年代500m 新生代500m // 如果softReference对象没被回收 那创建第二个对象时 一定会报OOM报错 public static void main(String[] args) { byte[] bytes = new byte[1024*1024*500]; //500m // 设置为软引用 SoftReference<byte[]> softReference = new SoftReference<>(bytes); // 删除强引用关联 bytes=null; // 再放入一个对像 查看是否会GC清除 软引用对象来避免OOM /* * [GC (Allocation Failure) [DefNew: 16778K->579K(471872K), 0.0014024 secs][Tenured: 512000K->512578K(524288K), 0.0015513 secs] 528778K->512578K(996160K), [Metaspace: 2188K->2188K(4480K)], 0.0030337 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Allocation Failure) [Tenured: 512578K->562K(524288K), 0.0010438 secs] 512578K->562K(996160K), [Metaspace: 2188K->2188K(4480K)], 0.0010584 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] * * */ // 测试结果成功 就算软引用对象还有引用指向它 在OOM即将发生时 就会直接回收 来避免OOM byte[] bytes2 = new byte[1024*1024*500]; //500m try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }
`
[软引用的使用一般用于存储缓存 在内存空间充足时存在 OOM即将发生时 直接清除]
弱引用实例对象.get(); 该方法会返回你包装的对象
在GC执行时 直接回收 不管是否为死亡对象
弱引用也是用来保存一些缓存数据 如果内存不足时 那么就会删除
内存充足时 可以用来作到加速系统的目的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rlrRG15x-1678259480117)(img_68.png)]
弱引用和软引用的不同是
软引用是在OOM即将发生时才被清除
而软引用 在第一次GC时 就直接清除
`
import java.lang.ref.WeakReference; public class WeakReferenceTest { // 弱引用test public static void main(String[] args) { // 查看是否GC时直接回收 不管是否达到内存不足和无引用 byte[] bytes = new byte[1]; //包装为弱对象 WeakReference weakReference = new WeakReference(bytes); // 删除强引用 bytes=null; // 查看是否有值 System.out.println(weakReference.get()); //[B@16d3586 // 调用GC System.gc(); // 查看是否已经回收 System.out.println(weakReference.get()); //null // 证明弱引用 只要GC 直接回收 } }
`
与弱引用一样 也有.get() 来获取包装的对象
WeakHashMap可以避免 OOM的出现
因为底层的数据都是弱引用 GC时就直接清除
该引用时所有中最弱的一个
一个对象如果只有虚引用 就几乎和没有引用一样 随时可能被回收
他不能单独使用 也无法通过get() 获取对象
[他唯一的作用 目的是追踪垃圾回收过长 比如能在这个对象回收时 收到一个系统通知]
因为当虚引用被回收掉的时候 JVM会将其对应的强引用对象 放在队列中
`
public class PhtomReferenceTest { //虚引用的作用 就是追踪他所包裹的对象的销毁状态 如果销毁 就将其虚引用对象数据类型为(PhantomReference<所包裹的对象>) 放入队列中 // 创建一个实例对象 public static PhtomReferenceTest phtomReferenceTestPhantomReference; static ReferenceQueue<PhtomReferenceTest> referenceQueue; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("复活咯"); //复活对象 phtomReferenceTestPhantomReference = this; } // 该线程用于检查对象状态 static class CheckState extends Thread { @Override public void run() { // 检查队列是否为空 while (true) { if (!(referenceQueue == null)) { // 不为空 PhantomReference<PhtomReferenceTest> phtomReferenceTest = null; try { // 获取 死亡后放入队列的虚引用对象 phtomReferenceTest = (PhantomReference<PhtomReferenceTest>) referenceQueue.remove(); System.out.println(phtomReferenceTest + "死亡"); //这就是追踪操作 } catch (InterruptedException e) { e.printStackTrace(); } } } } } //虚引用的使用 public static void main(String[] args) throws Exception { CheckState checkState = new CheckState(); checkState.setDaemon(true);//设置为守护线程 意思就是 如果程序中只有守护线程在运行了 那么就结束该线程 checkState.start(); // 创建对象 phtomReferenceTestPhantomReference = new PhtomReferenceTest(); // 创建引用队列 用于存放被销毁的虚引用 referenceQueue = new ReferenceQueue<>(); // 创建虚引用对象 PhantomReference phantomReference = new PhantomReference(phtomReferenceTestPhantomReference, referenceQueue); // 获取虚引用的值 System.out.println(phantomReference.get()); //null // 删除强引用 phtomReferenceTestPhantomReference = null; // 调用GC 虚引用会被 直接回收 但是finalize方法 中 会复活对象 System.gc(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } if (phtomReferenceTestPhantomReference == null) { System.out.println("死亡咯"); } else { System.out.println("没死"); } // 再次调用GC 因为Finalize方法 只能调用一次所以死亡 phtomReferenceTestPhantomReference = null; // 调用GC 虚引用会被 直接回收 但是finalize方法 中 会复活对象 System.gc(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } if (phtomReferenceTestPhantomReference == null) { System.out.println("死亡咯"); } else { System.out.println("没死"); } } }
`
checkState.setDaemon(true);//设置为守护线程 意思就是 如果程序中只有守护线程在运行了 那么就结束该线程
强引用 是最常见的引用 GC回收时 如果强引用对象为可达状态(有引用指向) 就永远不会回收 [可达不回收]
软引用(SoftReference(对象)) 在内存充足时不会被GC回收 内存不足(OOM之前) 即使对象可达 也会被回收 通过get()获取所包裹的对象 [可达 内存不足回收 其他不回收]
弱引用(WeakReference(对象)) 遇到即回收 也就是GC执行一次后 即使可达也马上回收 通过get()获取所包裹的对象 [可达 遇到GC就回收]
虚引用(new PhantomReference(对象,引用队列)) 被虚引用包裹的对象相当于无引用 也就是GC遇到就马上回收 通过get()获取不到包裹的对象为null [可达 遇到GC就回收 并且存储回收的虚对象到引用队列中]
与弱引用不同的是 他的构造函数不仅要传入要包裹的对象 还要传入引用队列(ReferenceQueue)
该队列用于存储被回收的虚引用包裹对象 数据类型为(PhantomReference<包裹对象的类型>)
可以通过一个线程来循环判断队列是否为空 并且不为空就取出对象 这样可以监视对象的回收时间
对象的创建分为6步 例如Object a= new Object()
1 判断是否类加载完毕
-类加载包括
自定义类加载器 ApplicationClassLoader(自定义类) ExtClassLoader BootStrapClassLoader(系统类)
算法有 双亲委派机制
2 开辟堆空间
java的运行
[类加载阶段]
0 .java文件 通过编译器 编译为对应的.class文件
1 通过类加载器三步骤
[运行时]
1 堆内存 进程私有 (-Xmx -Xmx 调整大小) 有GC 有保错 OOM
堆内存分为了三部分
-老年代(与新生代比例通过 -XX;NewRatio=比例 调整) FullGC MajorGC
当对象在新生代中的From区to区切换次数达到阈值(默认15)
就转入老年代
或者对象太大 也会直接放入老年代
2 方法区 进程私有 有GC(要设置) 有保错
主要存储类信息 方法信息 类中的静态变量和常量 常量池 JIT缓存
3 PC计数器 线程私有
用于存储程序的执行地址 执行引擎就是通过PC计数器 来执行对应的代码
一个线程一个 防止执行错误
4 本地方法栈 线程私有 有错误 无CG
用于调用本地的方法 用native标识 本地方法一般为C语言
5 虚拟机栈 线程私有 有错误 无CG
与js方法栈一致
栈帧
一个栈帧就是一个方法的调用 方法开始 栈帧入栈 方法结束 栈帧出栈
栈帧分为了五部分
1 局部变量池[方法中没有静态变量和常量]
存储方法中的所有变量 static方法没有this 普通方法有this
底层为数组 存储的单位叫做变量槽
方法中的局部变量即使失效 也不会删除变量槽 下一个变量声明时 会使用失效数据的变量槽 来节省空间
2 操作数栈
主要用于赋值 加减 取值 等等
3 方法返回PC
方法返回时 回到主线程 会返回一个PC计数器的值 该值就是方法在主线程中的PC位置 用于主线程继续执行下面的代码
4 动态链接
引用常量池的链接 例如 调用方法 invokevictual(普通方法)[invokespecial(父类方法) invokestatic(静态方法) invokeinterface(接口方法) invokedynamic(动态引用)] #3 这个#3就是动态链接
5 附加信息
执行引擎
java是一个解释编译型语言
执行引擎作用就是将JVM的指令 通过解释编译成2进制机器码 交给cpu运行
解释器的效率慢 响应速度快(逐行编译)
JIT编译器 效率高 响应慢(整体编译)
-JIT的热度衰减
sentinel的原理
1 在一定的时间内 错误的请求 如果超出了阈值 那么就会关闭链接 如何在一定的时间内错误
的请求没有达到 则在下一个周期中 错误次数会清0
JIT的热度衰减
1 在一定时间内 如果方法的调用次数达不到启动JIT的高度 那这个方法的调用计数器就会减少一半
这个过程叫做 热度的衰减 这段时间叫 半衰周期
可以通过 -XX:-UseCounterDecay 来关闭热度衰减 这样而来只要系统运行的时间足够长
那么大部分的代码都会通过JIT来编译
可以通过 -XX:CounterHalfLifeTime 来设置半衰期的时间 单位为秒s
[GC的算法]
1 三大基本算法
标记清除
速度较快 碎片化问题
标记压缩
速度最慢 无碎片问题
复制算法
速度最快 无碎片问题 空间要求大一般是存活对象内存的两倍
分区算法 STW
将堆空间对象按照生命周期分成区域 每次回收一个区域
增量收集算法 无STW
该算法就是主线程和垃圾回收线程并发交替执行
你执行一些 我执行一下 没有阻塞 但是线程交互成本高
3 安全点 安全区域
安全点 : 可以安全暂停主线程 执行GC的点
安全区域 :可以安全暂停主线程 执行GC的区域
4 引用
强引用
软引用
弱引用
虚引用
[上面就是引用的复习]
[String的不变性]
0 为什么String不能改变??
因为StringPool的底层为StringHashTable (哈希表结构)
哈希表结构中 key值不能重复 String重复声明 后面重复的值 都是一个内存地址([复用性])
1 String底层为什么从char[] 变为byte[]?
因为java公司发现大家存储的大部分都是英文字符 只占一个byte 用char(占两个byte)太浪费
2 String的intern()方法
[前提]
jdk6 : 字符串放在永久代 通过 -XX:[Max]PermSize 来调整
jdk7+ : 字符串放在堆空间的字符串常量池中 通过 -XX:[Max]MetaspaceSize 来调整
[方法]
intern()方法 检查调用方法的字符串如果字符串常量池中没有 将调用该方法的字符串放入字符串常量池中
有的话就返回字符串常量池中对应的字符串的地址
jdk6存储形式 : 在永久区中new一个String放在字符串常量池中
后续创建该字符串都是指向该new String()的地址 [这里等于有两个对象 一个堆空间中的String对象 一个永久区的字符串常量]
jdk7+ 存储形式 : 在堆空间的字符串常量池中 存储调用该方法的[对象]的地址 作为对应的字符串地址
后续创建该字符串都是指向调用该方法对象的地址 这里比jdk要省下一个字符串常量的内存 [这里只有一个对象 堆空间中的String对象 ]
[所以所有不会加入字符串常量池的字符串对象 最好都调用一次intern()来节省空间]
例如 new String(“xx”)+new Stirng(“yy”)
不调用清况
[ 6个对象]
两个String对象
两个字符串常量
一个StringBuilder对象
一个StringBuilder.toString()的对象(toString()值不会加入字符串常量池)
调用后 new String(“xx”)+new Stirng(“yy”).intern()
[ 4个对象]
两个Stirng对象
[两个字符串常量 被两个String对象替代 ]
一个StringBuilder对象
一个StringBuilder.toString()的对象(toString()值不会加入字符串常量池)
[所以说 真的非常节省堆空间内存!!!]
– jvm上篇的最后一章
1 按线程分
串行回收器 和并行回收器 都有STW
串行为只有1条线程 并行为多条线程处理
平台配置较低 单核cpu 比较适合
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v812XqM1-1678259480117)(img_70.png)]
2 按工作模式分
独占式垃圾回收 并发式垃圾回收
独占式就是并行 会阻碍主线程运行 在安全点or安全区域开始执行GC STW
并发式就是并发 不会阻碍主线程 线程交替执行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9HkvtLFD-1678259480118)(img_71.png)]
3 按碎片处理
压缩式和非压缩式
压缩式会进行碎片整理
非压缩式就不会
[压缩式使用 指针碰撞来分配对象 非压缩式通过空闲列表分配]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IL8lqgjq-1678259480118)(img_72.png)]
4 按工作的内存区间 又可分为年轻代和老年代回收器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e9cogO5m-1678259480118)(img_73.png)]
[*吞吐量 ]
运行用户代码的时间 占总运行时间的比例
(总运行时间 程序的运行时间 + 内存回收的时间)
[垃圾收集开销 ]
垃圾回收时间与总运行时长的比例
[**暂停时间]
执行垃圾回收时 程序的工作线程暂停的时间
[收集频率]
相对于程序的执行 发生GC的频率
[内存占用]
Java堆区 所占空间的大小
[快速]
一个对象从诞生到回收所经历的时间
[其中上面标注的为重点指标 其中暂停时间是最重要的]
[并且这三点垃圾回收器不能全部达到 也就是最多有两个]
内存大 (内存大导致GC不频繁)暂停时间就久 吞吐量大(因为GC次数少 所以总GC时长短 吞吐量大) (高延迟因为STW长)
内存小 (内存小导致GC频繁)暂停时间短 吞吐量小(因为GC次数多 所以总GC时长长 吞吐量低) (低延迟因为STW短)
也就是 用户代码运行时间 /(用户代码时间+垃圾回收时间[jvm运行总时长])
例如 java虚拟机运行了100s 用户代码运行花费了99s 垃圾回收花了1s 那么吞吐量就是(99/(99+1)) 就是0.99 也就是99%
(higher is better)
这种高吞吐量 应用能够容忍较高的暂停时间 因此高吞吐量的应用程序有更长的时间基准 快速响应是不必考虑的
吞吐量好 说明暂停时间(STW)短(因为吞吐量是与GC时长挂钩的)
图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jvDjcJTT-1678259480118)(img_74.png)]
是指一个时间段内主线程暂停 让GC运行的状态
内存大 (内存大导致GC不频繁)暂停时间就久 吞吐量大(因为GC次数少 所以总GC时长短 吞吐量大) (高延迟因为STW长)
内存小 (内存小导致GC频繁)暂停时间短 吞吐量小(因为GC次数多 所以总GC时长长 吞吐量低) (低延迟因为STW短)
高吞吐量
如果注重高吞吐量 这回让应用程序的最终用户感觉只有用户线程在运行 直觉上 高吞吐=运行速度快
低吞吐
低吞吐的优点是 STW时间较短 因为执行频繁 在一些交互性引用上面
200ms的STW可能都是非常影响用户感知的 所以低延迟适合"交互性应用"
不幸的是 吞吐量和暂停时间互相矛盾
1 因为高吞吐必然会带来较少的GC次数 从而增加每一次的GC暂停时间
2 低暂停时间 又会带来较多的GC次数 从而使吞吐量下降
7种[经典]垃圾回收器
串行垃圾回收器 Serial ,Serial Old
并行垃圾回收 ParNew, Parallel Scavenge ,Parallel Old
并发垃圾回收 CMS.G1
分代 年轻代 老年代
年轻代 Serial , ParNew , Parallel Scavenge
老年代 Serial Old,Parallel Old,CMS
整个堆内存
G1
图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-not76vQP-1678259480119)(img_77.png)]
-XX:+PrintCommandLineFlags
最老的回收器
串行的回收 适合单核的cpu
[Serial收集底层为复制算法 串行回收 和 STW机制 实现的回收]
除了年轻代以外
还有一个 Serial Old 垃圾收集器 作为老年代收集器
同样使用了[串行回收] STW 和只不过使用的是[标记压缩算法]
Serial Old 还可以和Parallel Scavenge
还可以作为CMS的备选方案
图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ajvgBjfv-1678259480119)(img_79.png)]
优势
简单并且高效 (与其他收集器的单线程比)
对于限定单个CPU的环境来说 Serial由于没有线程交互的开销
可以获得最高的单线程性能
在用户场景中 可以使用的内存一半不大 (几十MB到100-200mb) 可以在较短的时间中
完成垃圾收集(几十ms到几百ms) 只要不频繁的收集 使用串行是可以接收的
在HotSpotVM中 通过 -XX:+UseSerialGC 来开启
上面的指令一旦执行 那么默认
新生代为 SerialGC
老年代为 Serial Old GC
如果SerialGC是年轻代的单线程版本 那么ParNew就是SerialGC的多线程版本
这两个垃圾回收器除了线程数量的差别其他几乎无差别
算法也是使用的复制算法 和 STW机制
如果使用ParNew 就是用 +UseParNewGC
年轻代为 ParNew
老年代为 Serial Old 或者 CMS+(Serial Old)
优势
如果有多核心的CPU那么ParNew的处理速度很快
劣势
如果只是单线程CPU 就是用SerialGC更快 因为SerialGC不会考虑线程切换的问题
[限制垃圾回收线程的数量 -XX:ParallelThreads 默认为CPU数据相同的线程数]
//Java HotSpot™ Client VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release
[ParnewGC 在JDK8环境中 显示将被移出 因为后期单线程用SerialGC效率高
多线程又有 ParallelScavenge 和 G1 甚至 ZGC 所以就被取消了]
前面的可以知道 吞吐量高 延迟高 STW暂停时间长
吞吐量低 延迟低 STW时间短
Parallel Scavenge GC 的底层同样使用的是[ 复制算法 并行回收 和 STW机制]
那么和ParNew的区别在哪里?
1 和ParNew不同的是 Parallel Scavenge的目标是达到一个可控制的吞吐量 它也被称为吞吐量优先的GC
2 自适应调节也是Parallel Scavenge的特色 也是和 ParNew的一个重要的区别
[高吞吐量的GC 注定不适合做 用户交互频繁的环境 因为延迟非常的高 所以例如
订单处理 科学计算 批量处理 都不适合]
Parallel Scavenge的老年代GC
所以 组合为 Parallel Scavenge + Parallel Old
并且作为Java8的默认GC
[jdk8默认开启]
通过 -XX:+UseParallelGC 来开启组合
-XX:ParallelThreads 值 来限制线程
常见的GC
新生代 SerialGC(单线程) ParNewGC(多线程) Parallel Scavenge GC(并发专注吞吐量)
老年代 SerialOld , ParallelOld , CMS
整堆 G1
常见的GC组合
SerialGC+SerialOld (单单)
ParNewGC+SerialOld (多单)
Parallel Scavenge GC+ParallelOld (多多)
调整线程数量
[-XX:ParallelGCThreads] 值
[jinfo -flag ParallelGCThreads 74888 查看对应的线程数 默认为cpu线程数量]
[-XX:MaxGCPauseMillis] 最大暂停时间(STW时间)
为了尽可能的达到设置的时间 GC会自动调正堆内存大小 和其他参数
对于用户来讲 当然单次STW越短越好(低吞吐量) 但是在Server端
我们注重高并发场景 整体的吞吐量 所以服务器适合 Parallel Scavenge GC(高延迟 高吞吐量)
[该属性要小心使用]
[-XX:GCTimeRatio]:垃圾收集器的运行时长占时间的比例 也就是设置吞吐量
取值范围(0-100) 默认为99 也就是例总时长为100s 垃圾回收器只运行1s 用户线程运行99s
[该属性与MaxGCPauseMillis又有矛盾性 暂停时间越久 那么GCTimeRatio容易超过比例]
[-XX:+UseAdaptiveSizePolicy ]设置自适应策略 默认为开启状态
1 在这种模式下 年轻代的大小 会和Eden区 和 Survivor比例 晋升老年区时对象的年龄
都会被调整 回达到在堆大小和吞吐量和STW的平衡点
2 在手动调优较为困难的情况下 可以直接使用这种自适应的方法 只需要指定虚拟机最大的堆(-Xmx) GC垃圾回收时长占比(GCTimeRatio)和 停顿时间(MaxCGPauseTimeMillis)三个参数
就能直接自动调整
与 Parallel Scavenge不同的是
Para 是注重 高吞吐量 所以延迟高 用户交互体验不好
CMS 是注重 低延迟 并且是一款 并发处理的垃圾回收(开辟一条GC线程 与主线程同时执行)
-CMS 的垃圾回收算法为 [标记-清除算法] 并且也会Stop-The-World(CMS也有停顿时间 只不过会尽量缩短停顿时间)
图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pFpuwsWY-1678259480120)(img_80.png)]
四个步骤
1 初始标记(STW)
在这个阶段中 程序的所有的工作线程 将会因为STW暂停
这个阶段主要内容 [就是标记出 GC Roots 直接关联的对象]
一旦标记完成 就会恢复之前的暂停的所有应用线程 由于直接关联的对象比较小 所以速度很快(STW时间短)
2 并发标记
从GC Root的直接关联对象开始遍历整个对象图的过程(也就是遍历子类 和父类) 这个过程较长 但是不停顿用户线程
和主线程同时(并发)执行
3 重新标记((STW))
由于在并发标记阶段中 程序的工作线程和垃圾线程 一起运行 因此为了修正并发标记期间 因为用户线程运行而导致的标记变动的那一部分对象的标记记录
这个阶段的STW时间比初始标记稍微慢一点 但是也比并发标记时间短
4 并发回收
此阶段清理标记阶段以及确认死亡的对象 释放空间 该阶段也是并发执行
[这里由于清除算法使用的是 标记清除算法 所以会有碎片化的问题 后续可以通过其他的手段来处理碎片化的问题]
所以流程
初始标记 --> 并发标记 --> 重新标记 --> 并发清理
1 由于并发标记时 主线程还在运行所以[要保证内存足够在垃圾线程并发时 还足够使用 而不会OOM]
因此CMS不能像其他的GC一样老年区几乎要被填满的时候才开始会输 而是在堆内存的使用率到一定的阈值后 就开始回收
以确保应用程序在CMS工作的时候不会OOM 要是CMS运行时内存不足 就会出现一次 [Concurrent Mode Fail]失败
这个时候虚拟机将启用预备的方案 临时的启用Serial Old收集器(单线程)来避免 但是这样的代价是STW时间会很长
[这就是为什么CMS需要配合SerialOld 来使用]
2 CMS收集器的垃圾收集算法 还是采用的标记-清除的算法 这意味着每次执行后 分配新内存时 无法使用指针碰撞
而是要使用一个稀疏数组(空闲列表)
3 CMS为什么不使用 标记压缩算法呢?
1 因为是并发执行 如果使用压缩算法 标记压缩会与一个整理内存的动作
但是在清除时 主线程还在运行 主线程中调用对象的引用就会找不到对应的对象
会抛出空指针异常 复制算法同理 所以只有使用标记清除算法
1 会产生内存碎片
因为内存碎片 会导致用户线程的空闲空间不足(空间浪费)
如果这是来了一个大对象 就会触发 FullGC
2 CMS对CPU非常敏感 因为要占用CPU线程资源 而且如果性能不好 会导致主线程运行缓慢 吞吐量也会降低
3 CMS无法处理浮动垃圾
即 在并发标记阶段时 由于主线程还在run 主线程新废弃的对象就不会被标记
(因为只有初始标记时才会生成要回收的对象 后续的例如并发标记 重新标记 都是基于最开始的初始标记的结果 所以后期新增的对象 无法第一时间回收 这也是内存泄漏)
最终这些对象可能会因为没有即使的回收 导致 FullGC
1 -XX:+UseConcMarkSweepGC (启动CMS)
手动指定使用CMS收集器 执行内存回收的任务
开启该参数后 会自动将-XX:UseParNewGC打开 即ParNew(Young区)+CMS(Old区)+SerialOld(备用)的组合
2 -XX:CMSInitiatingOccupancyFraction 设置堆内存的使用率阈值 一旦达到 就执行CMS回收
JDK5 默认为68 也就是堆内存占用达到68%时 启动CMS
JDK6 默认为92
如果内存增长缓慢 则可以试着设置一个较大的阈值 防止GC次数过多
减少老年区的回收 能够明显的提升性能
如果内存增长很快 那么就要设置一个较低的阈值防止出现"Concurrent Fail"也就是
防止调用Serial Old GC 因此较低的阈值 可以避免 Serial Old GC 或者Full GC
3 -XX:+UseCMSCompactAtFullCollection
使用内存碎片管理 在FullGC后 以避免碎片化 但是STW时间会很长
4 -XX:CMSFullGCsBeforeCompaction
设置 多少次FullGC后 执行碎片管理(压缩)
5 -XX:ParallelCMSThreads 设置CMS线程数量
CMS默认的启动数量为 (ParallelGCThreads+3)/4
ParallelGCThreads : 为设置Parallel Scavenge的线程数的属性 默认值为cpu核心数量
1 如果想要内存消耗小 SerialGC
2 如果想要最大化吞吐量(Server端) Parallel Scavenge GC
3 如果想要最小化延迟 (多交互场景) CMS GC
jdk9中被标记为即将废除
jdk14中 CMS被废弃
分区算法 区域(Region)
1 业务庞大复杂 用户越来越多
2 为了适应不断扩大的内存和处理器数量 进一步的降低STW 同时兼顾吞吐量
3 官方给G1的定义是 在延迟可控的情况下 获取较高的吞吐量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OR7dTlg2-1678259480120)(img_81.png)]
1 G1是一款面向Server应用的垃圾回收器 主要针对
配置多核CPU以及大容量内存的机器 以极高的概率满足设置的STW时间 以及兼顾高吞吐量
2 在JDK9后 Parallel + ParallelOld组合 被G1取代 被成为"全功能的垃圾回收器"
3 通过-XX:+UseG1GC 来启用
与其他的GC来相比 G1使用了全新的分区算法
1 并行与并发
并行 G1在回收期间 可以有多个GC线程同时工作 有效利用多核计算能力
此时用户线程STW
并发 G1拥有队应用程序交替执行的能力 部分工作可以与应用程序同时执行
延迟一般来说 不会出现完全阻塞的情况发生
2 分代收集
从分代上来看 G1依然有分代的特性 他也会区分年轻代和老年代 年轻代依然有Eden区和Survivor区
但从堆结构上看 他不要求整个Eden区 年轻代 或者老年区 都是连续的内存空间 也不再固定大小 和固定的数量
将堆空间分为若干个的区域 这些区域包含了对逻辑上的年轻代和老年代
和之前的垃圾回收器不同 他同事兼顾两代 而不像以前的GC 要么老年代 要么年轻代 也就是"淡化"了分代的概念
通过分区替代
图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rHGNJDs3-1678259480120)(img_82.png)]
CMS 可以设置 在多少次FullGC后执行一次碎片整理
-XX:CMSFullGCsBeforeCompaction
G1将内存划分为一个一个的区域 内存的回收单位是区
区和区之间都是使用的[复制算法] 但是整体利用看作
是[标记-压缩] 两种方法都可以避免内存碎片 这种特性
使得程序可以长时间稳定运行[无内存泄漏因素]
分配超大对象时不会因为无法找到连续空间而触发下一次GC
尤其当Java堆非常大时 G1优势更大 *[Java堆内存越大 GC时长越久 如果GC执行还频繁 则吞吐量会很低]
G1除了追求低STW之外 还能建立可预测的STW时长模型
线程跑了了M秒的时长 那么GC时长就不得超过N秒(例如程序总时长100s 设置GC不得超过1s)
CMS在小应用的应用大概率优于G1 分界点大概在 6-8g之间是平衡点
-XX:+UseG1GC
-XX:G1HeapRegionSize 设置每一个的Region的大小 值为2的次方 因为是复制算法
范围为 1-32mb 默认值为1/2000的堆内存大小
-XX:MaxCGPauseMillis 设置期望的STW时长 默认200ms
-XX:ParallelGCThreads 设置STW时的线程数 最大8
-XX:ConcGCThreads 设置并发的线程数 为ParallelGCThreads的1/4左右
-XX:InitiatingHeapOccupancyPercent 设置占用多少堆内存的阈值 触发GC
默认为45
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IqbORj0B-1678259480121)(img_86.png)]
在G1收集器中 他将堆空间分为2048个大小相等区域
每一个区域根据堆空间的大小确定 整体被控制在2的次方 最大为32mb
图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oqnZXXEr-1678259480121)(img_87.png)]
[如果对象的大小超过了Region的大小怎么办? *(存储大对象)]
G1添加了一种新的分区 不同于 Eden Survivor Old
叫做[humongous] 简称h区
h区用于存储大小超过1.5个Region大小的大对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q9RbXkfd-1678259480121)(img_88.png)]
1 指针碰撞
对象在Region中 依次有顺序的存放
2 TLAB
每一个线程独立一份区域
一个线程在Region块中分一小份的区域存放对象 避免同步问题
1 年轻代GC
当伊甸园区的内存用尽时 开始年轻代的回收 G1的年轻代回收 是一个并行的独占式收集
在年轻代回收区 G1会暂停所有的app线程 启动多线程回收 然后从年轻代区间移动存活的对象 到老年区也可能是两个区间都会涉及
2 堆空间阈值超过后–>老年代并发标记
当堆内存的阈值超过设定的阈值时 老年区开始标记要回收的Region
3 老年代并发标记过后–>混合回收(年轻代和老年代同时回收 与FullGC不同的是 老年代不会全部回收 只会回收标记的对象)
标记完成后 马上就会进行混合回收 对于一个混合回收起 G1从老年区移动存活对象到空闲区
这些空闲区也会成为老年区的一部分 G1垃圾回收器与其他的GC不同的点在于 G1的老年区回收 不需要
回收整个老年区 而是只需要回收标记的一部分(标记的规则 就是垃圾价值算法) 并且老年代Region和年轻代会一并回收
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N61kyFYS-1678259480122)(img_90.png)]
解决区域之间的对象调用
比如YGC的时候 如果要判断老年代中是不是有引用新生代 就得去扫描老年代
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YkmB9jhZ-1678259480122)(img_91.png)]
年轻代GC–>(年轻代GC+并发标记)–>混合回收
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UwM9PlBt-1678259480122)(img_93.png)]
YGC 年轻代GC
JVM启动时 G1会先准备好Eden区 当Eden内存耗尽时 G1会启动YGC
[YGC只回收Eden 和 S0 S1]
该GC发生时 会先STW G1创建一个回收列表 回收列表 是指需要被回收的内存分段的列表
年轻代在回收过程中 包含[年轻代整个空间 和其他的GC一致](Eden Survivor区)
GC后 会将Eden Survivor存活的对象 放在一个空闲的Region中 通过复制算法 并将其标记为Survivor区
并且由于是复制算法 所以原先的空间被清空了 变为空闲的Region
图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kWO5dGXO-1678259480123)(img_94.png)]
[YGC阶段]
1 扫描根节点关联的所有对象
根是指static变量 指向的对象 正在执行的方法调用执行链上的局部变量等
上述的扫描的引用和记忆集 会作为后续操作的入口(类似CMS的初始扫描)
2 更新Rset(记忆集)
处理 dirty card Queue 中的数据 更新Rset 此阶段完成后 Rset可以准确的反应老年代对所在Region的对象的引用关系
[dirty card Queue : 是java在运行时 如果在代码中 出现Object a =Object 的对象引用 就会将该引用的信息保存在dirty card queue
中 在年轻代GC时 就会取出数据 更新Rset 保证为正确的引用关系]
[为什么不在运行时直接更新Rset呢? 因为Rset是线程同步的 会造成阻塞 用队列要好很多]
3 处理Rset(查看是否为空引用的垃圾对象)
识别被老年代对象引用的年轻代对象 这些为存活的对象 其他为死亡的对象
4 复制对象
此阶段 对象树被遍历年轻代中存活的对象 会被Copy到Survivor区中 并且年龄加一 如果
Survivor区中的对象年龄到达阈值 就会被复制到Old区的内存中 如果Survivor不够 Eden区的部分数据
会直接晋升到老年代中
5 处理引用
处理 Soft(软引用-内存不足GC) Weak(弱引用-发现就GC) Phantom(虚引用-发现就GC 跟踪对象回收)
最终Eden区空间数据为空 设置为空闲区 GC停止 而且目标内存为连续的 没有碎片 因为是[复制算法]
[并发标记过程- 老年区]
1 初始化标记
将根节点中 直接关联的对象标记 STW
2 根区域扫描
G1 GC扫描Survivor区直接可以达到的老年代区域对象 并且标记被引用的对象 这一过程必须在YGC之前[因为YGC会改变Survivor区]
3 并发标记
在整个堆内存中进行并发标记(与主线程同步执行) 此过程可能被[YGC]打断(Eden区满)
在并发期间 若[发现区域中对象都是垃圾 则直接回收] 同时在标记过程中 会计算每一个区域的对象活性
(区域中存活对象的比例)
4 重新标记(与CMS一致)
修正并发带来的可能数据不一致
5 独占清理 (该阶段 不会清理对象)
计算每一个区域的存活对象和回收的比例 并进行排序 识别为可以混合回收的区域 STW
6 并发清理
识别并清理空闲区域
[混合回收]
当越来越多的对象晋升到了老年代 为了避免内存耗尽 虚拟机会触发一个混合的垃圾收集
即MixedGC 该算法不是一个OldGC 除了回收整个年轻代 还会回收部分老年代[在独占清理中]
图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jzVbx06M-1678259480123)(img_95.png)]
老年代中 内存默认为8次回收 垃圾比例越高的区域 就优先回收 并且有一个阈值 决定是否回收 默认为65%
因为这存活对象占比高的话 在复制时会花费更多的时间
如果堆内存天笑 G1复制存活的对象的时候没有空闲的空间
则会FullGC 清理堆内存 通过调整堆内存大小解决
发生的情况
1 回收时to空间没有足够的空闲的空间
2 并发处理完成之前 主线程内存耗尽[如果暂停时间较短(吞吐量小 堆内存小) 说明每一次的GC垃圾数量回收的少 就有可能运行时内存溢出]
##RSET 记忆集图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-isoNZwY0-1678259480123)(img_97.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qbfSKYE4-1678259480123)(img_98.png)]
2 暂停时间限制不要太苛刻
1 G1的吞吐量是默认90% 也就是 100s中90s运行 10s垃圾回收
2 评估GC的吞吐量时 STW不要太苛刻 目标苛刻 表示你愿承受更大垃圾回收开销[STW短 回收频率高 吞吐量低]
1 Serial 串行 响应速度优先 复制算法
2 ParNew 并行 响应速度优先 复制算法
3 Parallel Scavenge并行 吞吐量优先 复制算法
4 CMS 并发 响应速度优先 标记清除算法
5 G1 并发 响应速度优先 复制算法 标记压缩算法
老年代
1 Serival Old 串行 响应速度优先 标记压缩算法
2 Parallel Old 并行 吞吐量优先 标记压缩
[组合 ]
Serial + SerialOld 场景单线程CPU 不吃设备性能 效率较低 单线程效率高
ParNew + (CMS+SeriOld)
Parallel+ ParallelOld
注重高吞吐量 适合服务器端
G1+G1
注重高吞吐量的情况下 兼顾一下STW时长
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2664xhC1-1678259480124)(img_100.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6pxRba53-1678259480124)(img_99.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IorrzpHO-1678259480124)(img_101.png)]
Serial(串行)>ParNew,Parallel Scavenge(并行)>CMS(并发)==>G1(并发 分区分代算法)
垃圾收集的算法哪些 如何判断一个对象是否可以回收?
ParallelGC
GC原因 GC前占用 GC后占用 总共的年轻代空间 GC前占用 GC后占 堆空间总大小 耗时
[GC (Allocation Failure) [PSYoungGen: 1515K-> 248K(1792K)] 1515K->672K(5888K), 0.0011278 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
-XX:+PrintGC 输出GC日志
[图解]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3w6FmYOh-1678259480124)(img_102.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UQDl2Zuj-1678259480124)(img_103.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZPYklV9K-1678259480125)(img_104.png)]
##GCDetail YGC的具体
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JlV4dHOc-1678259480125)(img_105.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D5vZeyG4-1678259480125)(img_106.png)]
Heap
PSYoungGen total 1536K[这里年轻区内存只会有eden加from区的内存 ], used 64K [0x04cc0000, 0x05000000, 0x05000000)
eden space 1280K, 5% used [0x04cc0000,0x04cd0048,0x04e00000)
from space 256K, 0% used [0x04fc0000,0x04fc0000,0x05000000)
to space 1024K, 0% used [0x04e00000,0x04e00000,0x04f00000)
ParOldGen total 6912K[老年区的总内存], used 5168K [0x04600000, 0x04cc0000, 0x04cc0000)
object space 6912K, 74% used [0x04600000,0x04b0c368,0x04cc0000)
Metaspace used 154K, capacity 2280K, committed 2368K, reserved 4480K
-Xloggc:绝对路径or相对路径
GC Easy
ZGC 主打低延迟
ZGC几乎都是并发执行的 分为四个阶段
1 并发标记
2 并发预备重分配
3 并发重分配
4 并发重映射
1 前端编译器的职责就是将Java文件根据jvm的规范将转为Class文件
Javac 就是一个前端编译器
Javac的将字节码文件的编译经历了4个过程
前端编译器是指将java文件解析为字节码文件的 处理引擎
后端编译器就是将字节码文件转为机器指令
后端编译器包含解释器 和 JIT(即时编译器) java是半解释半编译型语言
前端编译不会之直接涉及到编译优化
而是在后端编译器中优化 如热度衰减机制
看底层的方法调用 invoke…
类文件结构有几个部分?
1 默认初始化
2 显式初始化
3 构造器初始化
4 对象创建后 可以 对象.属性 or 对象.set()…
里面是什么?
源代码经过了编译机器编译之后 生成一个字节码文件 字节码文件是一个二进制文件 它的内容是JVM指令
而不是像C语言直接生成机器码
字节码指令?
Java虚拟机的指令有一个字节长度的 代表某种特定操作含义的操作码 以及随后的多个操作数 所构成
虚拟机中许多指令没有操作数 只有操作码
还有特殊的操作指令
astore_0: 底层有缓存 所以有一个_链接地址 因为0 1 2 3 比较常用
astore_1: 底层有缓存 所以有一个_链接地址
astore_2: 底层有缓存 所以有一个_链接地址
astore_3: 底层有缓存 所以有一个_链接地址
astore 4 底层无缓存
astore 5 底层无缓存
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nWwzkDgH-1678259480125)(img_107.png)]
1 二进制读取
一个一个二进制的看
2 Javap -v -p 文件 反编译
3 Jclasslib插件
任何一个CLass文件 都对应一个类or 接口 但反过来说 Class文件不一定是一个磁盘上的文件也可以通过网络传输二进制流来实例化类
Class为文件是一组以8为字节为基础的 [二进制流]
34(十六进制) --> (3*16)+4—>52(十进制)
魔数(cafe baby这几个字母 代表当前文件是一个Class文件)
大多数的文件都是通过这个来保护通过 修改后缀来让JVM运行
.mp3也有这种保护机制
Class文件版本
经过了魔数的4个字节之后 后面的4个字节的就是Class文本
前两个字节是副版本
后两个字节为主版本
34(十六进制) --> (3*16)+4—>52(十进制)
52就对应的是1.8的jdk版本
[jdk编译时 比如jdk1.6编译jdk1.8的字节码文件就会失败 但是高版本编译低版本是可以的]
常量池[存放所有的常量]
常量池对于Class文件的字段解析有着至关重要的作用
该区域是Class文件的基石
常量池内部标识符图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gMKDw7WG-1678259480126)(img_109.png)]
常量池的主要存储的内容
首先每一个常量占用两个字节 第一个字节 为标识符为1-16的值
比如 0A 转为10进制为10
对于的类型为 CONSTANT_Methodref_info类型(类中的方法的符号引用)
1 字面量
也就是文本字符串"1000" 和声明为final的常量值final int a =10;
2 符号引用
[复习]
常量池加载完毕后 运行时是放在方法区的运行时常量池中
并且引用也都是直接引用[因为在ClassLoader的类加载的链接阶段的解析环节 会将动态链接转为有真实内存地址的直接引用]
栈帧通过动态链接 获取所需的常量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CHJDBef5-1678259480126)(img_110.png)]
符号引用转为直接引用的图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AJAA9NJP-1678259480126)(img_111.png)]
Pass!!
[常量池小结]
二进制解析常量池要配合标识符图表
后期还是使用jclasslib 和 javap -v 来解析
1 这14中常量类型 都是以Tag标识符开头 占用大小为u1(1 byte) 代表表类型
Day21 完成 魔数 + Class版本信息 + 常量池
访问标志
在常量池后面 紧跟访问标记 访问标记使用的两个字节标识 用于设别一些类或者接口的访问信息
包括Class是类还是接口 是否为public 是否为abstract类型 如果是类的化 是否为final等
类索引 父类索引 接口索引集合
在访问标记后 就是类的类型 父类的类型 以及实现的接口
结构为 当前类u2字节 父类u2 接口计数器u2 【接口列表u2】
计数器如果为0 那么就没有接口列表结构
每一个都是两个字节 一共八个字节
字段表集合(属性 filed)
方法表集合
指向常量池的集合 他完整的描述了每一个方法的签名
在字节码文件中 每一个方法表元素 都包含方法的返回值 形参 等
如果方法不是抽象或者native 会在字节码中标识
方法表只描述当前的类方法 不包括父类方法 或者接口重写的方法 但是也在方法表中
1 方法计数器
大小u2
2 方法表
结构
访问标志 u2
方法名常量池 u2
描述符索引(方法的形参信息和返回值) u2
属性计数器 u2
属性表集合
方法表之后的属性表集合 指的就是class文件携带的辅助信息 比如Class的原文件.java名称
属性表计数器u2
属性名索引
属性的长度u4
源码(指向源java文件)文件索引u2
SourceFile文件解读
图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-85CBioTc-1678259480128)(img_108.png)]
总结
魔数+Class版本+ 常量池 + 访问标志 +父类引用 类引用 接口列表 + 字段表 + 方法表+属性表
魔数+Class文件版本+常量池+访问标志+类引用父类引用接口引用+字段表+方法表+属性表
javap是java的自带的反编译文件
javap
javap -help 查看所有的指令
javap -version class 查看class文件的jdk编译版本
javap -public class 查看所有公共的成员
javap -protected class 查看所有的protected成员
javap -p 查看大于private的成员(所有的类成员)
javap -constants class 显示常量的值
javap -s class 查看所有的字段和方法的描述符信息
javap -l class 输出行号和本地变量表
javap -c class 对代码进行反编译
javap -v class 输出附加信息(行号 局部变量表 反编译等)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-41faOuLZ-1678259480128)(img_122.png)]
java字节码对于虚拟机就好像汇编语言一样属于基本执行指令
java虚拟机的指令为一个字符长度的数值(2A,b7…) 一种操作的操作数 以及跟随在后面的
0至多个代表操作所需的参数 这样的一个整体叫做操作码
在jvm中 java将2A b7 等操作改为了助记符例如 aload_0 invokespecial #7 等等
字节码指令 对于java程序员是必备的基本功
java执行字节码的伪代码
do{
PC++;
根据PC指定的位置 取出操作码
if(字节码有操作数) 从常量池中取出
执行操作码的操作
}while(字节码长度>0);
字节码中的数据类型缩写
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-knnP5Dz2-1678259480129)(img_123.png)]
加载和存储指令 的作用 主要是将栈帧的局部变量表中的数据[或者常量池] 传递到栈帧的操作栈中来回传递
常用的指令
局部变量压到操作数栈指令 iload, iload_ [其中i可以为其他任意类型 n的取值范围为0-3]
常量入栈的指令
将一个常量添加到操作栈中 bipush sipush ldc ldc_w ldc2_w
aconst_null iconst_m1 icons_ Iconst_ fconst_ dconst_
3 出栈将数据放入局部变量表指令
istore isotre_(i可以为任意类型 n为0-3)
4 扩充局部变量表的指令
wide
我们知道 java方法分配栈帧时 java栈帧中往往需要开辟一个内存空间为操作数栈 来存放计算的操作数 和 返回结果
将指定的局部变量表槽位上的数据 压入操作数栈 进行操作
iload_ n==0-3 :
fload
const指令
对于特定的一些常量值 入栈的常量就在指令本身 指令有
iconst_(i的值为-1-5)
Iconst_(I的值为0-1)
fconst_(f的值为0-2)
dconst_(d的值为0-1)
比如
iconst_1 将1这个值压栈 而不是索引
-push指令
主要包括bipush和sipush 他们的区别在于接收的数据类型不同
bipush接收的是8为整数作为参数 而sipush接收16位整数 他们都将参数入栈
ldc指令
如果以上的指令都不能满足 那么就是用万能的ldc 该参数指向常量池中的Int float String的索引 将指定的内容压入堆栈
long或者double类型的数据 要使用ldc2指令
xstore_n(x是数据类型 n为0-3)
其中 istore_n 就是从操作数栈中栈顶的int元素弹出 并将其赋值给局部变量索引n位置
其中xstore [byte数据]由于没有隐藏参数信息 故需要一个byte类型的参数 指定局部变量表的槽位
让两个操作数栈的数据 进行特定的运算 然后将结果压入到栈顶
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Eqe1d8Ja-1678259480129)(img_125.png)]
dcmpg dcmpl fcmpg fcmpl lcmp
d为double f为float l为long
对比栈顶的两个元素 栈顶和栈顶顺位第二个】
如果为栈顶==栈顶2 = 则压入0为结果
如果为栈顶>栈顶2 > 则压入1为结果
如果为栈顶<栈顶2< 则压入-1为结果
小转大 不需要转换符
int a = 10;
long b =a;
将两种基本数据类型(不包括布尔类型)进行转换
大转小 需要转换符
long a =1000l;
int b=(int)a;
代码
int i=10;
int c=i++; //c为10[递增完毕后没有进行将递增后的值压入操作数栈的操作] i为11
int b=10;
int d=++b; //d为11[递增完毕后 进行了将递增后的数据压入操作数栈的操作] b也为11
字节码
0 bipush 10 // 将常量值10压入到操作数栈中
2 istore_1 // 将栈顶的元素弹栈 并且将值存入到变量槽1中
3 iload_1 // 取出变量槽1的值 并且压栈
4 iinc 1 by 1 // 变量槽1的值递增 [这时i的值为11]
7 istore_2 // 将栈顶的元素弹栈 存储到变量槽2中[c=i++; 的值为i之前的值 因为递增值没有进行压栈的操作 所以c值还是10]
8 bipush 10 //将常量值10压入到操作数栈中
10 istore_3 // 将栈顶的元素弹栈 并且将值存入到变量槽3中
11 iinc 3 by 1 //变量槽3的值递增
14 iload_3 //[重点的一步 这一步将变量槽3的值重新压栈 也就是已经递增完后的值 所以d值为11]
15 istore 4 // 将栈顶的元素弹栈 并且将值存入到变量槽4中
17 return
压栈指令
常量池
iconst 数值 ipush 数值 ldc常量池地址 ldc2ldc常量池地址 ldc_wldc常量池地址 ldc2_wldc常量池地址
局部变量表
iload_n n:0-3
iload n 取出对于槽位的数据压入操作数栈中
存储
istore_n n:0-3 将栈顶数据压入对于的变量槽中 并且清空栈顶数据
加减
iadd isub imul idiv 加减乘除
转换
宽型(小转大)
i2l i2f i2d
窄型(大转小)
l2i d2i f2i …
i++ ++i
代码
int i=10;
int c=i++; //c为10[递增完毕后没有进行将递增后的值压入操作数栈的操作] i为11
int b=10;
int d=++b; //d为11[递增完毕后 进行了将递增后的数据压入操作数栈的操作] b也为11
创建类的指令:new
他接受一个操作数 为常量池的索引 标识要创建的类型 执行完成后 将对象的引用压入栈中
创建数组的指令
1 newarray anewarray mutinewarray
newarray 创建基本类型数组
anewarray 创建引用类的数组
mutianewarray 创建一个多维数组
dup指令
将操作数栈的栈顶元素复制一份 并且压入到栈中
[主要用于 在调用方法之前 会使用一个元素 防止元素用尽]
##类的字段(属性)的操作
访问类字段(为static的字段) 使用getstatic putstatic
访问类字段(非static字段) 使用getfield putfield
1 数组也有
iastore 和 iaload 指令
1 arraylength指令
1 检查类实例或者数组类型的指令
instanceof,checkcast
- checkcast指令 [检查对象是否可以转换]
该指令用于检查类型强制转换是否可以进行 如果可以进行 那么该指令不会改变操作数栈
否则抛出ClassCastException
- instanceof指令[判断指定的对象 是否为某一个类的实例 例如 obj instanceof String]
该指令用来判断给定对象是否是某一个类的实例 他会将判断结果压入操作数栈
invokevirtual 调用实例方法 支持多态(重写的父类方法)
invokeinterface 调用接口方法 他会运行实现的方法
invokespecial 包括实例的构造器 私有方法 父类方法 这些方法都是静态绑定的 不会在调用时 进行动态派发
invokestatic 调用static方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6RgtOxxB-1678259480130)(img_128.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xkyjMRPH-1678259480130)(img_129.png)]
1 如同操作一个普通数据结构中的那样也有压栈和出栈的指令
pop[将栈顶的一个元素弹出栈 例如一个int元素]
pop2[将栈顶的两个元素弹栈 例如一个double元素]
2 复制栈顶的1个or多个元素
dup 将栈顶的一个元素[int …]复制一份 然后重新入栈
dup2 将栈的两个数值[一个 duable Long int+short]复制一份 并且重新压入
[带_x的操作为 复制栈顶数据 并且插入栈顶的以下的指定位置 位置公式 dup的系数+_x值 就是下标]
dup2_x2 将栈顶的数值 复制1个 并且压入到 2+2=4 第四个槽位下面
dupz_xy 将栈顶数值 复制1份 并且压入到 z+y的槽位的下面
3 栈顶的两个槽位数值 位置交换 swap
4 指令nop 是一条特殊指令 字节码为0x00 标识什么都不做
只是一个占位符 和调试符
例如
Object obj = new Object();
obj.toString(); // 这里底层就是pop 因为toString调用后 压入操作数栈的返回值没有其他的应用 所以就直接pop将数据弹栈 [如果要弹出的数据长度为2个操作数栈的槽位 就用pop2]
避免pop
Object obj = new Object();
String str= obj.toString(); //toString的返回值 有引用所以就是 astore指令 来将操作数栈的数据存储到局部变量表中
比较指令
条件跳转指令
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IdrPdfSG-1678259480130)(img_131.png)]
比较跳转指令
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BjWKAAHe-1678259480131)(img_132.png)]
[比较的规律 结尾的单词判断 eq等于 ne不等于 lt小于 gt大于 le小于等于 ge大于等于 ]
上方的指令 都是 比较操作数栈中 前两个值 栈顶为v2 栈顶顺位1个为v1 并且进行对比 并且顺便执行跳转的指令
多条件分支指令
无条件跳转指令
1 开辟堆空间 并且将对应的地址值 赋值给虚拟机栈的栈帧的操作数栈中
多条件分支跳转的指令 是专为switch-case 指令设计的 主要有 tableswitch ookupswitch
他们的区别
1 原理
`
0 iconst_0 //将常量0压入栈
1 istore_1 // 保存到局部变量表的槽位1中
2 iload_1 // 读取局部变量表的槽位1值 并且压栈
3 bipush 100 // 将常量100压入栈
5 if_icmpge 14 (+9) // 对比栈顶和栈顶顺位1位的值 如果顺位1位的值>=栈顶值 就跳转到14 0>=100 false
8 iinc 1 by 1 // 局部变量表的槽位1的值 +1 [这里是int类型的++ 如果位double类型 就是先加载局部变量表中的数据 再dconst_1 创建一个常量值1 再dadd做加法]
11 goto 2 (-9) // 跳转到PC 2
14 return
`
1 异常和异常的处理
过程1 : 异常对象的生成过程 --> throw[手动抛出 自动抛出]
过程2 : 异常的处理 --> try()catch{}finally{} —> 使用异常表
2 图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6SYESHDY-1678259480131)(img_133.png)]
1 如果一个方法中 定义了try catch 的异常处理 就会创建一个异常表 他包含了每个异常处理或者
finally的信息 异常表包括了每个异常的处理信息
异常表会包含4个信息
方法的同步
方法内部的指令序列的同步 都是使用monitor指令实现
[当线程退出同步代码块时[遇到错误等等] 需要通过monitorExit来退出同步锁]
概述
在java中 数据类型 分为基本数据类型和引用数据类型 基本数据类型有jvm预先定义 引用数据类型则需要进行类的加载1
按照jvm的定义 从Class文件中加载的类 到类卸载出内存为止 它的整个生命周期包括7个阶段
类的生命周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AIAW2ypE-1678259480132)(img_135.png)]
类加载(ClassLoader)过程 就是java文件转为class文件的过程 分为三部分
加载阶段[Loading]
1 通过类的全名 获取类的二进制数据流
- 获取二进制流的方法
- 通过class文件获取[最常见]
- 通过网络获取
- 通过jar zip等压缩文件 提取
- 存放在数据库中的类二进制流
- 在运行时生成一段Class的二进制信息等
[在获取了二进制流后 jvm就会处理这些数据 最后转换为java.lang.Class的实例]
2 解析类的二进制流 为方法区内的数据结构(Java模型)
3 通过二进制流创建java.lang.Class实例 标识该类型 作为方法区的各种数据的访问入口
链接阶段[Linking]
- 验证阶段
当类加载到系统后 就开始链接操作
[验证阶段的目的 是为了保证字节码数据为合法 合理 且符合规范的]
[验证的步骤比较复杂 实际的项目也很多 如图所示]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nn6FsF2C-1678259480132)(img_140.png)]
- [其中格式检查与加载阶段同步运行]
- 准备阶段
为类的静态变量分配内存(非静态变量在运行时分配到堆内存or局部变量表中 常量在编译阶段就已经赋值了) 并将其初始化为默认值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KE5cHYu7-1678259480132)(img_141.png)]
- 注意
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6goJz4oZ-1678259480133)(img_142.png)]
[也就是基本数据类型的 非final的变量 在解析阶段进行默认初始化赋值
final static修饰的基本数据类型 会在解析阶段进行显式赋值(在编译阶段就会确认要赋值的值)]
- 解析阶段
将类 接口 字段和方法的符号引用转为直接引用
- 符号引用的概述
符号引用就是一些字面量的引用 和虚拟机的内部数据结构和内存布局无关 比较容易理解的就是在Class文件中
通过常量池进行了大量的符号引用 但是在程序实际运行时 只有符号以引用是不够的
所以要换为引用对应的真实的地址
- 例如
System.out.println()方法
对应的符号引用
invokevirtual #24 ][其中#24就是符号引用]
- 图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KHBBxaaY-1678259480133)(img_143.png)]
初始化阶段
简言之 初始化阶段就是给静态的变量赋予正确的初始值(准备阶段 是赋值一个0 or null) [clinit的执行过程]
[初始化阶段 才真正的开哦是执行类中定义的Java程序方法]
初始化阶段的重要工作就是执行类的初始化方法()方法
该方法只能由java编译器生成并由jvm调用 程序开发这无法自定义一个同名的方法更无法直接在java程序中
调用该方法 虽然该方法也是由字节码指令构成
它是由静态成员的赋值语句以及static语句块合并而成的
[在加载一个类之前 必须加载类的父类 因此父类的 比子类的先调用
所以父类的static块优先于子类]
clinit方法执行
0 bipush 10 //压入常量10
2 putstatic #3 <com/misaka/java/中篇/CLinitTest.num : I> //从常量池的字段表中拿出static字段num
5 return //结束
[由父及子 父类先行]
不会出现clint的场景
总结
1 在链接阶段的准备阶段赋值的情况
总结结论
使用static final修饰 且显式赋值中不涉及到方法或构造器调用[new xx()] 就是在准备环节进行
不加final必然在clinit中执行 static final 如果显式赋值有构造器执行 就会在clinit
不加static or final 就是在init执行
clinit()方法会在多线程环节中 被正确的加载 如果多个线程同时加载一个类 那么只有
一个线程去执行这个类的方法 其他线程都需要等待 直到线程执行clinit完毕
由于clinit是线程安全的 所以如果一个类的初始化时间过于长 会导致伪-死锁
如果之前的线程成功加载了类 则后续的线程 没有机会执行()的方法了 那么 当需要使用这个类时
虚拟机会直接返回已经准备好的信息
[类的初始化情况 主动使用 被动使用]
类的使用分为主动使用和被动使用
[主动使用 意味着会调用类的clinit() 也就是执行了类的初始化阶段(给静态的变量显式赋值)]
1 当创建一个类的实例时 比如使用new关键字 或者通过反射 克隆 序列化
2 当调用类的静态方法时 即当使用了字节码invokestatic指令
3 当使用类 接口的静态字段时(final static 要看显式赋值是否是new对象) 比如使用 getstatic putstatic 指令
4 当使用Class.forName()
5 当初始化子类 如果父类还没有进行初始化 则要触发父类初始化[接口不适用 因为子接口加载不会先初始化父接口 ]
6 如果一个接口定义了default方法 那么直接实现或者间接实现该接口的类的初始化 该接口要在之前被初始化[也就是如果一个接口定义了default修饰的方法 实现类初始化时 接口会在实现类之前初始化]
7 当jvm启动时 用户需要指定一个要执行的主类 也就是main方法 虚拟机会初始化该主类
8 当初次调用MethodHandle实例时 初始化该类指向的方法所在类
[被动使用 不会触发clinit的场景]]
/*
测试不会触发初始化阶段的字节码
1 使用的是类的 静态常量字段 因为静态常量不需要再clinit方法中赋值 它已经在链接阶段的解析时就已经显式的赋值为10了
[static final复习]
如果该修饰符 修饰的变量 赋值的是一个字面量"xxx" 或者是一个基本数据类型 就是在链接阶段赋值 就不会调用clinit方法
但是如果修饰的变量 是一个引用数据类型的值 就要在初始化阶段进行赋值 就会触发init
2 通过子类获取父类的静态变量 子类不会初始化
3 调用一个类的静态常量[显式赋值非引用类型] 不会初始化
4 通过classLoader创建对象 不会初始化
*/
[-XX:+TraceClassLoading :打印类加载信息]
[总结 主动使用和被动使用 ]
1 首先的区别就是 一个在使用类时会触发clinit[初始化阶段] 一个在使用时不会触发
- 主动使用判断[会触发clinit]
- static final修饰的静态常量显式的值为引用数据类型
- new 序列化 反射机制 创建类
- 使用了static修饰的任何变量
- Class.forname 创建对象
- 被动使用[不会触发clinit]
- static final修饰的静态常量显式的值为基本数据类型或者字面量
- 子类使用的父类的常量 只会初始化父类
- CLassLoader创建对象
[static final的复习]
{static final复习}
如果该修饰符 修饰的常量 赋值的是一个字面量"xxx" 或者是一个基本数据类型 就是在链接阶段的准备阶段赋值 就不会调用clinit方法
但是如果修饰的常量 显式赋值的是一个引用数据类型的值 就要在初始化阶段进行赋值 就会触发clinit方法
*/
[类的使用 程序员的使用阶段]
调用和访问它的成员信息 或者new关键字创建对象实例
[类的卸载阶段]
方法区的垃圾回收
方法区的垃圾收集主要回收两部分内容[常量池中废弃的常量] 和 [不在使用类型]
Hotspot虚拟机对常量池的回收策略是明确的 只要常量池中的常量没有任何地方引用 就可以被回收
判断一个常量是否 “废弃” 还是比较简单的 而要判断一个类型 是否属于"不在使用的类"的条件 就比较苛刻了 需要同时满足三个条件
该类的所有的实例 都已经回收 也就是java堆空间中 不在存在该类的派生类实例
加载该类的类加载器被回收 这个条件除非是精心设计果的可替换GC的场景 如OSGI JSP的重加载 否则很难达成
该类对应的java.lang.Class对象 没有载任何地方被引用 无法在任何地方通过反射该类的方法
Java虚拟机被允许对满足上述三个条件的无用类进行回收 这里仅仅是"被允许" 而并不是和对象一样 没有引用了就必然回收
[补充内容]
- 启动类加载器加载的类 在整个jvm运行周期不可能被卸载
- 被applicationClassLoader和ExtendClassLoader加载的类 在运行时间 一般不会卸载 因为系统加载器和扩展类加载器 在整个
运行时间总是能够间接or直接的访问 其达到可回收条件的可能性很小
- 自定义的类加载器只有在很简单的上下文环境中才能卸载 而且一般还要借助于强制调用虚拟机的垃圾回收功能才可以做到
被加载的类型在运行时间也是几乎不能卸载的(至少卸载的时间是不确定的)
类与类加载器 和 Class对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DXXRjsjw-1678259480133)(img_144.png)]
当一个类被加载 链接和初始化后 它的生命周期就开始了 当代表类的CLass对象不再被引用 即不可达时
Class对象就会结束生命周期 类在方法区的数据也会被卸载 从而结束类的生命周期
[总结! : 一个类何时结束生命周期 取决于代表它的CLass对象何时结束生命周期]
类加载器是JVM执行类加载机制的前提
ClassLoader的作用
ClasssLoader是Java的和核心组件 所有的Class都是由ClassLoader加载的 类加载器通过各种方法将
Class信息的二进制数据流入JVM内容 转换为一个与目标类对应的java.lang.Class对象的实例 然后交给Java虚拟机进行链接 初始化等操作 因此
ClassLoader在整个装载阶段 只能影响类的加载 而无法通过类加载器区该改变类的链接和初始化行为 至于它是否可以运行 则由[执行引擎]来决定!
流程
显示加载和隐式加载
显式加载是在代码中调用CLassLoader加载class对象.如直接使用CLass.forName(name)或者this.getClass().getClassLoader().loadCLass()加载CLass对象.
隐式加载则不是直接在代码中 调用CLassLoader方法 加载Class对象 而是通过虚拟机自动加载到内存中 如在加载某一个类的class文件时
该类的class文件中引用了 另一个类的对象 此时额外引用的类通过jvm 自动加载到内存中
例如
显式
Class.forName(“com.misaka.xxx”);
隐式
User u = new User();
对于任意一个类 都需要由加载它的类加载器和这个类本身确认其在Java虚拟机中的唯一性 每一个个加载器
都拥有一个独立的命名空间 比较两个类是否相等 在之前直到 要全限定名称和类加载器相等才相等
,没有继承CLassloader 底层为C++
内部加载的是 Java核心类库
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-llo7d8Pv-1678259480134)(img_148.png)]
并且 系统类加载器 和 扩展类加载器都是启动类加载器加载的
继承ClassLoader类
父类加载器 为启动类加载器
从java.ext.dirs系统属性所指定的目录中加载类库 或从jre/lib/ext中加载 如果用户创建的jar就放在此
目录下 也会自动使用扩展类加载器加载
继承CLassLoader类
父类加载器 为扩展类加载器
该加载器是系统默认的加载器
自定义类加载器 都是extends ClassLoader
public CLass<?> loadClass(String name) throws CLassNotFoundException
protected Class<?> findClass(String name) throws ClassNotFoundException
protecte final CLass<?> defineCLass(String name ,byte[] b ,int off ,int len)
1 首先 如果 没有太多的业务情况 就可以直接继承URLClassLoader 这样可以避免重写findClass()方法 和 不需要重写回去字节流的方法
1 Class.forname() 方法是一个静态方法 最常用的是Class.forname(全限定名称) 根据传入的全限定
名来返回一个CLass对象 该方法在将Class文件加载到内存的同时 会执行类的初始化 [主动加载]
2 ClassLoader.loadClass() 这是实例方法需要一个ClassLoader对象来调用该方法 该方法将Class文件加载内存时 并不会执行类的
初始化 直到这个类第一次使用才进行初始化 [该方法因为需要得到一个ClassLoader 对象 所以可以根据需要指定使用那个类加载器]
如 BootstrapCLassLoader cl = cl.loadCLass(对象全限定名称) [被动加载]
1 定义
如果一个类加载器 收到一个类的加载请求时 首先不会自己尝试加载这个类 而是将这个请求任务委托给父类加载器
去完成 依次递归 如果弗列加载器 可以完成类加载任务 就成功返回 只有父类加载器无法完成此加载任务时 才自己去加载
2 本质
规定了类加载的顺序 引导类加载器加载 扩展类加载器加载 若还加载不到 才会由系统类加载器和自定义加载器加载
[每一次递归还会检查 当前类加载器 是否已经加载过对应的类 如果加载过就直接使用 不需要委派了]
3 优势
避免类的重复加载 确保一个类的全局唯一性 如果父类加载器已经加载了该类时 就没必要CLassLoader再加载一次
保护程序安全 防止API内 随意篡改
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rR0EVnpC-1678259480135)(img_151.png)]
4 代码支持
双亲委派机制 在java.lang.ClassLoader.loadClass(String,boolean)中体现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ROlbYIrt-1678259480135)(img_152.png)]
5 如果将loadClass()内部的方法体重写是不是就可以避免双亲委派机制呢[比如不通过系统类加载java核心]
这样也不行!! 因为JDK还未核心类库提供了一层保护机制 不管是自定义的类加载器 还是系统类加载器
最终都会调用 java.lang.ClassLoader.defineClass() 而该方法会执行 preDefineClass()接口 该接口中提供了对jdk核心类库的保护
第一次破坏双亲委派行为
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nsEeIEaH-1678259480136)(img_153.png)]
第二次破坏双亲委派行为
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jN0tTXmZ-1678259480136)(img_154.png)]
第三次
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t3lNsAC2-1678259480136)(img_156.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BpbTNmIE-1678259480136)(img_157.png)]
热替换是在程序的运行中 不停止服务 只通过替换程序文件来修改程序的行为
[热替换的关键 是不能中断 修改必须立即表现正在运行的系统之中] 基本上大多数脚本语言天生支持
但对Java来说 热替换并非天生就支持 如果一个类已经加载到系统中 通过修改类文件 并无法让系统再来加载
并重新定义这个类 因此 在java中 实现这一个功能 的一个可行的方法就是运用ClassLoader
注意 由不同的CLassLoader 加载的同名类属于不同的类型 不能相互转换和兼容 则两个不同的类加载器加载同一个类
在虚拟机内部 会认为这两个类是完全不同的
根据这个特点 可以用来模拟热替换的实现 基本思路如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Inaz4cSe-1678259480137)(img_158.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F1ewptCM-1678259480137)(img_159.png)]
现在重写都是重写 ClassLoader.findClass()方法
原因
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cIBLEz7p-1678259480137)(img_160.png)]
步骤
1 创建一个继承CLassLoader的类
2 获取 要解析的文件的路径 要.class后缀名
3 重写findClass方法
4 通过IO流读取对应的文件 并且将IO流的内容 存储到ByteOutPutStream对象中 然后调用其方法.toByteArr()获取byte数组
5 获取byte数组后 通过defineClass(名字,byte数组,起始下标,结束下标); like defineClass(null,byte[],0,byte[].length) [相当于类加载的加载阶段 的传入Class文件的二进制流数据]
6 调用上述的方法后 可以获取一个Class对象
7 通过Class对象创建对象
[ 通过 Class 类的 newInstance() 方法创建对象,该方法要求该 Class 对应类有无参构造方法。执行 newInstance()方法实际上就是使用对应类的无参构造方法来创建该类的实例,其代码的作用等价于Super sup = new Super()。]
1 移除了扩展类加载器的概念
2 自定义类和系统类加载器不在继承URLClassLoader 而是改为BuildinClassLoader 他还是继承SecureClassLoader
性能监控
一种以[侵入方式]收集性能数据的活动 它会影响应用的吞吐量和响应性
性能分析
性能分析是针对性能问题的答复结果 关注的范围通常比性能监控更加集中
性能分析 很少在生产环境下运行 一般是质量评估阶段 系统测试 和 开发环境下进行
性能调优
一种为改善性能或吞吐量 而更改系统参数 源代码 属性配置 的活动 性能调优 是在性能监控 和 性能分析之后的操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3ItcaDUs-1678259480138)(img_161.png)]
监控工具分为 两种
命令行工具
对于本地虚拟机来说 进程的本地虚拟机ID与操作系统的ID是一致的 也是唯一的
option
h : 获取帮助
q :只看进程ID
l : 显示全限定名称
m : 输出虚拟进程启动 是传递给main()函数的参数
v : 列出虚拟机进程启动时的JVM参数 比如(-Xms -Xmx)
上面的参数都可以一起使用 -mlv [q最好单独用]
[如果java进程配置了参数 -XX:-UsePerfData jps将无法获取该java进程]
jstat [-option] [-t] PID [-h] [] []
用于监视虚拟机各种运行状态信息的命令行工具 它可以显示本地或者远程虚拟机中的类装载 内存 垃圾收集
JIT编译等运行数据
在没有GUI的图形界面 他是最佳选择
class : 显示类加载信息 类的转载 卸载数量 总空间 类装载所消耗的时长
[GC相关的]
GC 显示与GC相关的堆信息 包括Eden去 两个Survivor去 老年代 永久代的容量 已用的空间 GC时间合计的信息
gcutil 显示GC内部的占比
gccause 显示GC的原因
[JIT相关的]
-compiler 输出JIT编译的方法 耗时等信息
-printcompilation 输出已经被JIT编译的方法
jinfo [option] PID
查看虚拟机配置参数 也可以用于调整虚拟机配置参数
jmap [option] PID
导出内存映像[Dump文件] and 内存使用情况
option
-dump 生产java堆存储快照 dump文件
-手动获取dump文件
-dump:live 只保存堆中存活的对象
[示例 jmap -dump:live,format=b,file=D:\xxxx.hprof 6432]
-dump:属性列表 每一个属性用,间隔
自动获取dump文件
HeapDumpOnOutOfMemoryError :OOM报错时 导出堆快照
HeapDumpPath :Dump文件保存位置
heap 输出整个堆空间的详细信息 包括GC的使用 堆配置信息 以及内存的使用信息
histo 输出堆中对象的统计信息 包括类 实例数量 合计容量
- histo:live只统计堆存活对象
[Jmap] 将访问堆中所有的对象 为例不受主线程干扰 jmap需要借助安全点的机制 让所有的线程留在不改变堆中数据的状态 也就是说
Jmap导出的快照必定时安全点位置的 这可能导致堆内存快照结果存在偏差
但是如果一个对象的生命周期 在两个安全点之间 那么:live选项将无法获取该对象
如果线程长时间无法到达安全点 jmap将一直等下去 与钱买你将的jstat则不同 垃圾回收器则会主动将jstat所需要的
数据保存到固定位置 而jstat只需要读取即可
jhat [option] [dumpFile]
Jdk提供了一个分析dump文件的工具 它的内部是一个微型的http服务器 生成dump文件的分析结果后 用户可以在浏览器中查看分析结果
使用jhat就启动了http服务 端口为7000
后续被 VisualVm代替
jstack
用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)
线程快照就是当前虚拟机内部指定进程的每一条线程正在执行的方法堆栈的集合
生成线程快照的作用 可以用于定位线程出现长时间停顿的原因 如线程死锁 死循环 请求外部资源导致长时间等待的问题
这些都是导致线程长时间停顿的常见原因 当线程出现停顿时 就可以同jstatck来监视每个线程的堆栈情况
在thread dump的几个状态
死锁 :DeadLock
等待资源 :Waiting on condition
等待获取监视器 Waiting on moniter entry
阻塞 Blocked
执行中 Runnable
暂停 Suspended
jcmd多功能工具
jcmd pid help 针对指定的线程列出所有的操作指令
jcmd pid 操作指令 : 执行操作指令
jstatd
远程主机的信息收集
GUI工具
Jconsole JDK 自带的JVM可视工具
运行 /bin/jconsole
Visual Vm 是一个VM工具 它提供了一个UI界面 用于产看JVM上运行的基于Java的基本信息
经常使用 完全免费 必须要会的一个工具
JMC java Misson Control 内置java Flight Recoder 能够以极低的性能开销收集java虚拟机的性能数据
eclipse MAT
是一个强大的堆栈内存分析器 可以用于查找内存泄漏和内存消耗
复习 同步锁机制
1 synchronized内部的值 作为锁 如果下一个线程的锁值[等于]当前的锁值 就会进入队列等待 不等于就可以直接进入 不需要等待 这就是防止同步锁
例如银行 通过银行卡号 来设计锁 防止同样的卡号同时操作数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xO4nypBf-1678259480138)(img_165.png)]
浅堆 和 深堆内存
浅堆永远小于深堆内存
[浅堆]
是指一个对象所消耗的内存 在32为中 一个对象引用占据4个byte 一个int会占据4个byte long占8个字节 对象头信息8个字节 对象大小会向8字节补齐
那么 String对象的底层如
int hash=0;
int hash32=0;
ref value=C:\User\Administrator;
[上述的内存占用情况
int型两个占8字节 引用型4字节 对象头8字节 一共20字节 向8位对其就是24字节]
这24字节 是String对象的浅堆大小 他与String的value值无关 无论字符串长度如何 浅堆的大小始终为24字节
[保留集]
对象A的保留级指当对象A被GC后 可以被释放的所有的对象集合(包括对象A本身)
即对象A的保留集 可以被认为是只能通过对象A被直接或间接的访问到的所有对象的集合 通俗来说 就是指仅被对象A所持有的对象的集合
[也就是A自身和A能够访问or间接访问的对象的结合]
[深堆]
深堆是指对象的保留集中所有的浅堆大小之和
[注意 浅堆是指对象本身占用的内存 不包括器内部引用对象的大小 一个对象的深堆指只能通过该对象
访问到的(直接or间接) 所有的对象的浅堆之和 即对象回收后 可以释放的真实空间]
图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X1R5x1zC-1678259480138)(img_167.png)]
支配树的概念来源于图论 通过支配树可以快速的判断出 删除对象后 哪些内存会被释放`[可达性分析算法]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jn8nc55c-1678259480139)(img_168.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i4mcvnGJ-1678259480139)(img_169.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aWMoKhIc-1678259480139)(img_166.png)]
1 内存泄漏(Memory leak)
申请了内存用完了不释放 比如一共有1024m内存 分配了212m不回收 只用的就只有800m了 仿佛泄漏了一般
2 内存溢出
申请内存时 没有足够的内存使用
所以 泄漏会导致溢出的发生
1 静态集合类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z6o3ghoS-1678259480139)(img_171.png)]
2 单例模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FzRUmwol-1678259480140)(img_172.png)]
3 内部类持有外部类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-44JvsSnJ-1678259480140)(img_173.png)]
4 各种IO流 数据库链接 网络链接
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-58WGJub5-1678259480140)(img_174.png)]
5 变量不合理的作用域
也就是 如果一个变量可以设为局部变量与虚拟机栈一起回收
就不要设置为成员变量 与类一起回收 这样容易导致内存泄漏
6 改变哈希值
在使用HashSet or HashMap 时 里面的元素 重写了hashcode方法
会导致 hashSet错乱 比如 使用remove()方法移出元素时 会出现移除失败 从而导致内存泄漏
7 缓存泄漏
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-STiWBVFf-1678259480140)(img_175.png)]
对于这个问题 一般就将缓存设置为弱引用(WeakHashMap) 就可以避免内存泄漏
8 监听器和回调
类似SQL语言
该工具是收费的
同步锁 - 死锁
线程A 线程B 其中 A调用了方法 需要在方法体中 调用B正在执行的方法 但是B里面也要调用A的方法 这时 他们的同步锁就相互无法解锁 所以两个锁都无法释放
这就导致了死锁
自定义类加载器
重写findclass方法 获取到CLass文件的二进制流后 就调用defineCLass() 函数获取CLass对象
如果每次GC后 内存图还是在稳步提升 那么就是内存泄漏的典型场景
web容器的线程最大数 比如 Tomcat的线程容量 应该略大于最大并发数
线程阻塞
死锁
基本指令
启动 java - jar arthars-boot.jar [java进程PID]
操作指令
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KENiXQVG-1678259480141)(img_178.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CHpzQy1p-1678259480141)(img_179.png)]
dashboard <-i> 毫秒 <-n> 次数
实时监控台 如果不设置-i -n 则会无限制的打印
thread [option]
线程信息
heapdump [option]
输出dump文件
–live
sc [-d] 类全限定名称[可以用正则 如 com.misaka.*] 查看jvm已加载类的信息
-d的意思是 输出详细信息
sm [-d] 类全限定名称[可以用正则 如 com.misaka.*] 查看已加载类的方法信息
jad [类的全限定名称]
反编译获取类的源码
mc 和 redefine
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D0ICDRVm-1678259480141)(img_180.png)]
ClassLoader [option]
输出所有的ClassLoader实例
monitor 方法执行监控
monitor -c 5 com.misaka.xx
上面的代码就是 监视 xx类中 init的调用情况
profiler start 启动 [开始收集]
profiler getSamples
profiler stop [结束收集]
3 -XX
这类选项属于实验性
开发和调试JVM
- 分类 {值为布尔型和基本数据类型的格式}
-XX:+ 标识启动
-XX:- 标识关闭
-XX: = value
快速的判断是否发生了逃逸分析 大家就看new的对象是否有可能在方法外部被调用 如果没有则使用栈上分配
打印的选项
-XX:+PrintCommandLineFlags 可以让程序运行前打印出用户手动设置或者JVM设置的XX选项
-XX:+PrintFlagsFinal 打印XX选项的属性生效时的值
-XX:+PrintVmOptions 打印VM参数
堆栈的选项
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8cA4nUUm-1678259480141)(img_181.png)]
栈 -Xss111k 设置每个线程的栈大小为111k
堆内存
-Xms111k 设置堆空间大小初始值为111k
-Xmx111k 设置堆空间大小最大值为111k
-Xmn2g 设置年轻代大小为2g [官方推荐3/8的堆内存大小]
-XX:NewSize=1024m 设置年轻代大小为1024m
-XX:MaxNewSize=1024m 设置年轻代最大值为1024mq
-XX:SurvivorRatio=8 设置Eden区和s0 s1区的比例 默认为8[8:1:1]
-XX:+UseAdaptiveSizePolicy 自动选择各区大小比例[如果将其关闭 新生代的比例就变回8:1:1]
-XX:NewRatio=2 年轻代和老年代的比例 默认为2 (老年区占3/2 年轻3/1)
[默认 情况下 如果-Xms -Xmx的值都是600m 那么内存中的情况为]
-XX:NewRatio=2 -XX:SurvivorRatio=8
也就是老年区为 400m
年轻代为200m [这里底层不会默认为8:1:1而是6:1:1 因为自适应默认是开启状态
如果想让它变为8:1:1的比例 就要满足两个条件
1 - 将自适应策略属性关闭(-XX:-UseAdaptiveSizePolicy)
2 - 显示的设置 SurvivorRatio值为8(-XX:SurvivorRatio=8)]
Eden区为160m s0 s1 分别为20m
永久代[方法区]的选项
jdk6
-XX:PermSize 设置永久代初始大小
-XX:MaxPermSize 设置永久代最大值
jdk7
-XX:MetaspaceSize 元空间最小的值
-XXX:MaxMetaspaceSize 元空间最大的大小
-XX:+HeapDumpOnOutOfMemory 生成dump文件 在溢出时
-XX:+HeapDumpBeforeFullGC 在FullGC之前 生成dump文件
-XX:HeapDumpPath = 路径 生成的文件的路径
-XX:OnOutOfMemoryError 指定一个exe或者脚本的路径 如果发生了OOM则执行该脚本
SerialGC[串行]
-XX:+UseSerialGC(启动GC 老年代使用SerialOldGC)
ParNewGC[并行]
-XX:+UseParNewGC
-XX:ParallelGCThread 并行的最大线程数
ParallelGC[并行 吞吐量优先]
-XX:+UseParallelGC[自动使用ParallelOldGC]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-amUXEWk7-1678259480142)(img_183.png)]
CMS[并发 低延迟]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hbey2kL5-1678259480142)(img_184.png)]
-XX:+UseConcMarkSweepGC 启动
[CMS底层使用的是标记清除算法 而不是标记压缩算法 会产生对象的碎片化的问题]
G1GC[并发 低延迟]
[底层用的是 复制算法]
分区分代算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dMIMQs6d-1678259480142)(img_185.png)]
-XX:+UseG1GC 启用G1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O0eZjIOS-1678259480142)(img_186.png)]
-XX:+PrintGC
-XX:+PrintGCDetails
下列情况会导致FUllGC
1 老年代空间不足
2 方法区空间不足
3 调用System.gc()
4 Minor GC 进入老年代的数据平均大小 大于 老年区的可用内存
5 大对象直接进入老年代 老年代空间不足
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8GoyI8WG-1678259480143)(img_188.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qF2EyBmy-1678259480143)(img_189.png)]
输出GC日志 : -Xloggc:路径
工具
堆溢出
元空间溢出
线程溢出
字节码
字节码文件是通过.java文件调用javac编译后生成的.class文件 .class文件通过类加载器就可以将其生成一个对应的CLass对象 在堆内存中
类的加载
类加载器
有4个类型的类加载器
类加载的过程
双亲委派机制
[双亲委派机制 简单来说 就是一个类的创建 类加载器的选择是从下往上先递归 然后再从上面向下递归 主要是防止类的重复加载和系统类的篡改
流程
Parent parentClassLoader = 当前加载器的父加载器;
public void 加载(){
if(当前类加载器已加载){
c= 加载当前类
}else{
if(当前类加载器.parentnull){
//null 说明当前的类的父类是bootStrapClassLoader
c= 试图使用启动类类加载 加载当前类
}else{
//当前类的父类不是bootStrap 则继续递归 直到顶层
this.parentClassLoader.加载();
}
后续c如果还是null<类加载器都没加载> 则要调用findClass进行加载<我们就是重写这个方法来制作自定义类加载器> findClass再内部将Class二进制文件读取后 传递给defineClass来生成Class对象 在堆内存中
}]
<java.lang.ClassLoader.loadClass方法就是双亲委派的算法 所以官方一般不推荐重写loadClass而是findClass 防止双亲委派失效>
所以决定类是否为同一个类 不只是判断是否为同一个全限定名称 并且还要判断是否为一个类加载器
类加载器有同步锁机制 防止多线程多次类加载[对象的头部内部信息就有对应的同步锁 还有年轻代的年龄数据]
不同的类加载器可以类加载多次 通过这个机制可以实现分代的机制
对象的创建
对象的创建分为6步
字符串的不变性
String str1 = new String("a")+new String("b"); // 这里底层拼接使用的是 StringBuilder 不会将字符串结果 存入字符串常量池 // jdk7: 调用 将ab存入字符串常量池 值为 str1的地址[省了一个"ab"的内存] jdk6 : 在字符串常量池中新建ab字符串[这样会多一个"ab"的内存] str1.intern(); String s1 = "ab"; // 这里s1指向字符串常量池的"ab" 而ab的值是str1 所以等于 s1=str1 所以true String s3 =new String("ab"); //这里s3指向的是一个堆内存地址 s3==str1 false System.out.println(s1==str1); //true 如果jdk6 就是false
GC
垃圾回收器的算法
标记清除
有内存碎片问题 但是速度很快
标记压缩
无内存碎片问题 但是速度很慢
复制算法
无内存碎片问题 但是空间需要两倍 速度最快
垃圾回收思想
可达性分析算法
垃圾回收器
新生代
SerialGC ParNewGC Parallel Scavenge G1
老年代
SerialOld CMS+SerialOld ParallelOld G1
上面的垃圾回收机制按串行并行并发
串行
SerialGC+SerialOld
并行
ParNewGC+[CMS注重低延迟 并发]
Parallel Scavenge+ParallelOld[注重吞吐量]
并发
CMS G1
-CMS的流程
[初始标记–>并发标记–>重新标记–>并发回收]
- 四个步骤
1 初始标记(STW)
在这个阶段中 程序的所有的工作线程 将会因为STW暂停
这个阶段主要内容 [就是标记出 GC Roots 直接关联的对象]
一旦标记完成 就会恢复之前的暂停的所有应用线程 由于直接关联的对象比较小 所以速度很快(STW时间短)
2 并发标记
从GC Root的直接关联对象开始遍历整个对象图的过程(也就是遍历子类 和父类) 这个过程较长 但是不停顿用户线程
和主线程同时(并发)执行
3 重新标记((STW))
由于在并发标记阶段中 程序的工作线程和垃圾线程 一起运行 因此为了修正并发标记期间 因为用户线程运行而导致的标记变动的那一部分对象的标记记录
这个阶段的STW时间比初始标记稍微慢一点 但是也比并发标记时间短
4 并发回收
此阶段清理标记阶段以及确认死亡的对象 释放空间 该阶段也是并发执行
[这里由于清除算法使用的是 标记清除算法 所以会有碎片化的问题 后续可以通过其他的手段来处理碎片化的问题]
- G1 笔记:2955行
YGC流程 :[扫描根节点 --》更新Rset–>处理Rset—>复制–>处理引用]
OGC流程 :[扫描根节点–》根区域扫描–>并发标记—>重写标记–>独占清理—>并发清理]
引用
堆内存
堆内存中分为3大区域
年轻代
老年代
元空间[方法区]
jvm设置
-Xmx -Xms -Xmn -XX:NewRatio=2 -XX:SurviorRatio=8 -XX:+UseAdaptiveSizePolicy
年轻代
java几乎90%的对象 都是在这里被回收
年轻代分为 Eden区 s0 s1 区
年轻代的算法 使用的是复制算法
老年代
如果Eden区装不下的大对象 或者 年龄达到阈值的对象 就会转入老年区
对象的过程
首先进入Eden区 如果第一次GC存活 就转入 from区 下一次依然存活继续转入to区 from --> to循环到阈值后 就可以进入老年区了
虚拟机栈
虚拟机栈 是存放方法的结构 如果方法超出 就报错 SOF
PC寄存器
线程私有的 用于给执行引擎查看需要执行的字节码
方法区
方法区内部 存放的有 类信息 执行引擎的热度衰减信息
本地方法栈
执行C语言的方法栈
执行引擎
java是一个 半解释半编译性语言
因为它同时具有 编译器(JIT) 和解释器
解释器的优点 : 延迟低 启动快
JIT的优点 : 速度快
JIT的热度衰减
1 在一定时间内 如果方法的调用次数达不到启动JIT的高度 那这个方法的调用计数器就会减少一半
这个过程叫做 热度的衰减 这段时间叫 半衰周期
可以通过 -XX:-UseCounterDecay 来关闭热度衰减 这样而来只要系统运行的时间足够长
那么大部分的代码都会通过JIT来编译
可以通过 -XX:CounterHalfLifeTime 来设置半衰期的时间 单位为秒s
为一个类加载器
类加载器有同步锁机制 防止多线程多次类加载[对象的头部内部信息就有对应的同步锁 还有年轻代的年龄数据]
不同的类加载器可以类加载多次 通过这个机制可以实现分代的机制
对象的创建
对象的创建分为6步
字符串的不变性
String str1 = new String("a")+new String("b"); // 这里底层拼接使用的是 StringBuilder 不会将字符串结果 存入字符串常量池 // jdk7: 调用 将ab存入字符串常量池 值为 str1的地址[省了一个"ab"的内存] jdk6 : 在字符串常量池中新建ab字符串[这样会多一个"ab"的内存] str1.intern(); String s1 = "ab"; // 这里s1指向字符串常量池的"ab" 而ab的值是str1 所以等于 s1=str1 所以true String s3 =new String("ab"); //这里s3指向的是一个堆内存地址 s3==str1 false System.out.println(s1==str1); //true 如果jdk6 就是false
GC
垃圾回收器的算法
标记清除
有内存碎片问题 但是速度很快
标记压缩
无内存碎片问题 但是速度很慢
复制算法
无内存碎片问题 但是空间需要两倍 速度最快
垃圾回收思想
可达性分析算法
垃圾回收器
新生代
SerialGC ParNewGC Parallel Scavenge G1
老年代
SerialOld CMS+SerialOld ParallelOld G1
上面的垃圾回收机制按串行并行并发
串行
SerialGC+SerialOld
并行
ParNewGC+[CMS注重低延迟 并发]
Parallel Scavenge+ParallelOld[注重吞吐量]
并发
CMS G1
-CMS的流程
[初始标记–>并发标记–>重新标记–>并发回收]
- 四个步骤
1 初始标记(STW)
在这个阶段中 程序的所有的工作线程 将会因为STW暂停
这个阶段主要内容 [就是标记出 GC Roots 直接关联的对象]
一旦标记完成 就会恢复之前的暂停的所有应用线程 由于直接关联的对象比较小 所以速度很快(STW时间短)
2 并发标记
从GC Root的直接关联对象开始遍历整个对象图的过程(也就是遍历子类 和父类) 这个过程较长 但是不停顿用户线程
和主线程同时(并发)执行
3 重新标记((STW))
由于在并发标记阶段中 程序的工作线程和垃圾线程 一起运行 因此为了修正并发标记期间 因为用户线程运行而导致的标记变动的那一部分对象的标记记录
这个阶段的STW时间比初始标记稍微慢一点 但是也比并发标记时间短
4 并发回收
此阶段清理标记阶段以及确认死亡的对象 释放空间 该阶段也是并发执行
[这里由于清除算法使用的是 标记清除算法 所以会有碎片化的问题 后续可以通过其他的手段来处理碎片化的问题]
- G1 笔记:2955行
YGC流程 :[扫描根节点 --》更新Rset–>处理Rset—>复制–>处理引用]
OGC流程 :[扫描根节点–》根区域扫描–>并发标记—>重写标记–>独占清理—>并发清理]
引用
堆内存
堆内存中分为3大区域
年轻代
老年代
元空间[方法区]
jvm设置
-Xmx -Xms -Xmn -XX:NewRatio=2 -XX:SurviorRatio=8 -XX:+UseAdaptiveSizePolicy
年轻代
java几乎90%的对象 都是在这里被回收
年轻代分为 Eden区 s0 s1 区
年轻代的算法 使用的是复制算法
老年代
如果Eden区装不下的大对象 或者 年龄达到阈值的对象 就会转入老年区
对象的过程
首先进入Eden区 如果第一次GC存活 就转入 from区 下一次依然存活继续转入to区 from --> to循环到阈值后 就可以进入老年区了
虚拟机栈
虚拟机栈 是存放方法的结构 如果方法超出 就报错 SOF
PC寄存器
线程私有的 用于给执行引擎查看需要执行的字节码
方法区
方法区内部 存放的有 类信息 执行引擎的热度衰减信息
本地方法栈
执行C语言的方法栈
执行引擎
java是一个 半解释半编译性语言
因为它同时具有 编译器(JIT) 和解释器
解释器的优点 : 延迟低 启动快
JIT的优点 : 速度快
JIT的热度衰减
1 在一定时间内 如果方法的调用次数达不到启动JIT的高度 那这个方法的调用计数器就会减少一半
这个过程叫做 热度的衰减 这段时间叫 半衰周期
可以通过 -XX:-UseCounterDecay 来关闭热度衰减 这样而来只要系统运行的时间足够长
那么大部分的代码都会通过JIT来编译
可以通过 -XX:CounterHalfLifeTime 来设置半衰期的时间 单位为秒s
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。