JVM(JAVA虚拟机)笔记

JVM 介绍

JVM:Java 程序的运行环境(Java二进制字节码的运行环境)

好处:

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查
  • 多态

比较:

JVM、JRE、JDK

JDK(JRE + 编译工具(javac、jps、jstack、jmap、jconsle等)) > JRE(JVM + 基础类库(lang包下的String类等)) > JVM(运行字节码文件)

在这里插入图片描述

常见的 JVM

在这里插入图片描述

学习路线

在这里插入图片描述

JVM 组成

  • 类加载器(类加载的几个阶段)
  • JVM 内存结构
  • 执行引擎

在这里插入图片描述

内存结构

1. 程序计数器

概念

在这里插入图片描述
PC(程序计数器)作用:记住下一条 JVM 指令的执行地址

通过寄存器来实现的,通过寄存器来读取存储地址

  • 特点
    • 线程私有的
    • 不会存在内存溢出

2. 虚拟机栈

概念

(栈内存是线程私有的)

  • 虚拟机栈:每个线程运行时需要的内存空间(由多个栈帧组成)

  • 栈帧:每个方法运行时需要的内存(包含:参数,局部变量,返回地址),一个栈帧对应一次方法调用

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

问题辨析

  1. 垃圾回收是否涉及栈内存
    答:不涉及,栈帧每次对应方法调用完后,都会弹出栈,不需要垃圾回收管理
  2. 栈内存分配越大越好吗?
    答:不是,栈内存划分的大,会让线程数变小

栈内存可以通过参数去指定:-Xss size(设置的是每个线程可以拥有的最大栈内存大小,而非总的栈空间)
在这里插入图片描述

  1. 方法内的局部变量是否线程安全?
    在这里插入图片描述

多个线程同时执行此方法,但是变量是局部变量。是线程私有的,每次调用会产生新的栈帧,所以不会出现线程安全问题,如果 将 x 变为 static int x = 0,则会出现线程问题。

  • 如果方法内局部变量没有逃离方法的作用访问,它就是现场安全的
  • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

栈内存溢出

(StackOverflowError)

  • 栈帧数量过多导致栈内存溢出
    • 递归调用
    • 两个类之间循环引用/依赖(Spring通过三级缓存解决)
  • 栈帧过大导致栈内存溢出

三方库转json,自定义的两个类循环依赖,转json出错
@JsonIgnore 处理,中断其中一个转json

线程运行诊断

案例1:CPU占用过多

目标:找到哪个线程对 cpu 占用过高
在这里插入图片描述

注意:jstack 后面的进程id 是10进制的,但是查出来的线程 id 是16 进制,需要做转换

在这里插入图片描述

案例2:程序运行了很长时间没有结果(死锁
通过 “jstack 进程id” 来定位分析
在这里插入图片描述
在 jstack 命令输出的 最后

3. 本地方法栈

概念

Java 虚拟机调用本地方法(native method)时,需要给这些本地方法提供的内存空间
本地方法:用 C、C++ 实现的方法,通过 Java 间接调用到

  • clone()、hashCode()、wait()、notify()、notifyAll()

4. 堆

概念

  • 通过 new 关键字,创建对象都会使用堆内存

特点

  • 它是线程共享的,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制

堆内存溢出

  • 堆内存耗尽:OutOfMemoryError
  • 控制堆空间的参数:-Xmx1024m

堆内存诊断

  1. jps工具
  • 查看当前系统中有哪些 java 进程(可以查看对应的 进程Id
  1. jmap 工具 jmap -heap 进程id(只能查看某一时刻)
  • 查看堆内存占有情况
    在这里插入图片描述
    在这里插入图片描述
  1. jconsole 工具
  • 图形界面的,多功能的监测工具,可以连续监测(连续监测)

在这里插入图片描述
可以查看 堆内存使用、线程、类、CPU 占用率

在这里插入图片描述

jconsole 也可以和 jstack 一样监测死锁

  1. jvisualvm

案例:垃圾回收后,内存占用仍然很高

和 jconsole 功能类似,但是更好用

在这里插入图片描述

  • 堆 Dump (堆转储)
    抓取当前堆的快照,进一步对里面的内容进行分析(jmap、jconsole不支持)

在这里插入图片描述
点击 查找 可以查看堆中的 最大对象

点进去最大对象后的详细信息:
在这里插入图片描述

5. 方法区

概念

所有 Java 虚拟机线程共享的区域,存储和类相关的信息(方法/构造器代码、运行时常量池等)(逻辑是堆的一部分)

方法区是规范,永久代、元空间都是它的一种实现

方法区也会 OOM

在这里插入图片描述

永久代

JDK 1.6:永久代作为方法区的实现
包含的内容

  • 存储类的信息
  • 类加载器
  • 运行时常量池(字符串表)

元空间

JDK1.8:元空间作为方法区的实现(不是由JVM管理,被移动到本地内存中)

  • 类信息
  • 类加载器
  • 运行时常量池

注意:串池(StringTables)不在放在方法区的实现(元空间),而放到堆中了

方法区内存溢出

模拟元空间溢出
在这里插入图片描述

模拟永久代溢出
在这里插入图片描述

Spring(Aop)、Mybatis(Mapper实现类)
都使用字节码技术 cglib 生成代理类

运行期间动态生成类字节码,完成动态的类的加载

在这里插入图片描述

运行时常量池

类生成二进制字节码信息,(方法区)包含

  • 类基本信息
  • 常量池
  • 类方法定义
  • 虚拟机指令(指令内容的详细信息存储在常量池中)
常量池

反编译命令:javap -v 类名.class
在这里插入图片描述
在这里插入图片描述
综上,
常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

运行时常量池:上面提到常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入 运行时常量池 ,并把里面的 符号地址 变为 真实地址

即:常量池中保存的常量池信息(最初是在字节码文件里)当class文件对应类被加载时会被放入到运行时常量池

在这里插入图片描述

StringTable(串池)

在这里插入图片描述
常量池中的信息,都会被加载都运行时常量池中,这时 a b ab 都是常量池中的符号,还没有变为 Java 字符串对象(得等到具体执行到相应的代码时,才会变为字符串对象)

  • 提前准备好一个空间:StringTable[](串池) 初始为空(数据结构为 哈希表)(大小固定,不能扩容)
  • ldc #2 会把 a 符号 变为 “a” 字符串对象
  • 之后,会把 字符串对象作为 key,到 StringTable 中找,有没有一个取值相同的 key
  • 如果没有的话,会把 字符串对象 放入 StringTable(串池)中

注意:

  • 字符串对象是懒创建的,遇不到时,不会把它创建出来
  • 创建出来时会放入串池(如果串池中就有,则会用串池对象的)

在这里插入图片描述
反编译结果显示,这里的 s1 + s2 会创建新的 String 对象

所以 s3 == s4 结果为 false

在这里插入图片描述
串池中有了,会沿用串池中已有的对象
(javac 编译器优化)

结果为 s3 == s5 结果为 ture

StringTable特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池

jdk1.8 intern() 方法:

  • 如果当前对象对应的字符串不在串池中,则会将当前对象放入串池,并返回串池中的对象
  • 如果当前对象对应的字符串在串池中有了,则直接返回串池中的字符串对象

jdk1.6 中
如果当前字符串对象不在串池中存在时,会复制一份(重新创建一份字符串对象)放入串池(此时堆中的字符串和串池中的对象不是同一份)

例子:
eth1:

String s = new String("a") + new String("b");  // StringBuilder -> new String("ab") 在堆中
String s2 = s.intern();  // (尝试将字符串对象添加到串池中,如果有则不添加),最后返回串池中的字符串对象

System.out.println(s2 == "ab"); // true
System.out.println(s == "ab"); // true

eth2:

String x = "ab";  // 字符串字面量,在串池中
String s = new String("a") + new String("b");  // StringBuilder -> new String("ab") 在堆中
String s2 = s.intern();  // (尝试将字符串对象添加到串池中,如果有则不添加),最后返回串池中的字符串对象

System.out.println(s2 == "ab"); // true (s2指向串池中的字符串对象)
System.out.println(s == "ab"); // false (s指向堆中的字符串对象)

最前面的测试题:

String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();  // 返回串池中的字符串"ab"对象

System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true

String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();

// 如果掉换了【最后两行代码位置】,则输出为true
System.out.println(x1 == x2); // false

StringTable的位置

在这里插入图片描述
jdk1.6 中,StringTable 是常量池的一部分,都在永久代中(永久代只有 fullGc 的时候才会回收【老年代空间不足】,导致StringTable回收效率不高,占用大量内存,导致永久代内存不足)
jdk1.8中,StringTable 被放在堆中了(堆里的StringTable,只要minorGC 就会触发回收)

StringTable 垃圾回收机制:

StringTable 也会受到垃圾回收的管理,当内存空间不足时,StringTable 中那些 没有被引用字符串常量 也会被垃圾回收

在这里插入图片描述

StringTable 性能调优:
StringTable 是基于哈希表实现(过小了容易哈希冲突【效率慢】,过大又很占内存)

调优:其实就是调整 HashTable 哈希表的大小
在这里插入图片描述

• 调整-XX:StringTableSize=桶个数
• 考虑将字符串对象是否入池

在这里插入图片描述
这个案例中,字符串入池可以节省内存(自动去重),此时 list 中存放的是串池对象的引用,堆中创建的对象会被 GC 回收

6. 直接内存

基本使用

属于系统内存(操作系统内存)
Direct Memory
• 常见于 NIO 操作时(IO操作的一种),用于数据缓冲区(Todo:看完 netty 来看)
• 分配回收成本较高,但读写性能高
• 不受JVM 内存回收管理

  1. 为什么使用了直接内存,读写效率会非常高?

在这里插入图片描述
Java 本身不具备磁盘读写的能力,如果要调用磁盘读写,必须调用操作系统提供的函数(调用本地方法)(cpu 从用户态切换到内核态)
内存:cpu 就可以读取磁盘的内容了(Java读取的话:磁盘文件 -> 系统缓存区 -> Java 缓存区【Java才能读取】)

在这里插入图片描述
使用了 byteBuffer 之后:会在操作系统中分配一块 直接内存 (Java 可以直接读取)

内存溢出

  • 不受 JVM 内存回收管理

直接内存溢出案例:
ByteBuffer.allocateDirect 方法会在 直接内存中分配一块 大小为 参数的空间
在这里插入图片描述

释放原理

在这里插入图片描述

  • 直接内存的释放实际 是通过 unsafe(freeMemory方法) 管理的(而不是 垃圾回收【垃圾回收只能释放Java内存】)
  • 直接垃圾回收 回收掉ByteBuffer 会触发 Cleaner 通过回调 间接 触发 freeMemory方法

在这里插入图片描述

  • ByteBuffer.allocateDirect 方法 就是调用了 unsafe.allocateMemory 方法完成对直接内存的分配

在这里插入图片描述

  • Cleaner 后面的 create 方法的第二个参数对象(回调任务) 类实现了 Runnable 接口的 run 方法,run 方法里调用了 freeMemory 对象

在这里插入图片描述

  • Cleaner(虚引用类型),当它关联的对象(create 方法的第一个参数)被回收时,会触发 Cleaner 的 clean 方法

• 使用了 Unsate 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
• ByteBuffer 的实现类内部,使用了 Cleaner(虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHiandler 线程通过 Cleaner 的clean 方法调用 freeMemory 来释放直接内存

禁用显式回收时,对直接内存释放的影响
加 -XX:+DisableExplicitGC 禁用显式的垃圾回收,System.gc 失效,但 unsafe.freeMemory 也可能跟着失效

  • 因为 unsafe.freeMemory 的调用是 ByteBuffer 对象没有任何指向,然后被 GC 回收后,会通过 Cleaner 的 clean 方法回调 unsafe.freeMemory 方法回收直接内存的

  • 如下显式的 System.gc 调用后,直接内存还是没有被回收,只有等系统下次自动 GC 时,才会回收掉 ByteBuffer 对象,从而间接触发 unsafe.freeMemory 回收直接内存
    在这里插入图片描述

  • 此时,虽然 System.gc 显式垃圾回收失效了,但是可以直接通过手动调用 unsafe.freeMemory 方法管理直接内存

垃圾回收

如何判断对象可以回收

  1. 引用计数法

一个对象被变量引用一次,计数器就 +1,引用几次,计数器就为几,如果某个变量不再引用时,计数器 -1,当引用计数器为 0 时,则会被垃圾回收
在这里插入图片描述
会有循环引用问题

  1. 可达性分析算法(Java 虚拟机是通过可达性分析算法来回收对象)

要确定一系列根对象
根对象:肯定不能被当成垃圾回收的对象

在垃圾回收之前,会对堆中的所有对象进行一遍扫描,然后查看当前对象是否被根对象直接或间接引用,如果是,则当前对象不能被回收,反之,如果当前对象没有被引用,则会被当作垃圾回收

• Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
• 扫描堆中的对象,看是否能够沿着GC Root对象起点的引用链找到该对象,找不到,表示可以回收

•哪些对象可以作为 GC Root?

在这里插入图片描述

  • System Class
    系统类,由启动类加载器加载的类,核心的类(作为GC ROOT 对象,包括 Object、String、HashMap 一些核心的类对象)
  • Native Stack
    操作系统方法执行时,所引用的一些 Java 对象作为根对象
  • Thread
    活动线程,线程还在运行,其中线程需要的一些对象肯定不能被回收了(栈帧内所使用的一些局部变量所引用的 对象 可以作为根对象)
  • Busy Monitor
    同步锁对象,正在加锁的对象,也是可以作为根对象

GC ROOT 指的是堆中的对象,而不是其引用

注意:根对象当没有被引用时,也可以被回收

四种引用

  • 强、软、弱、虚、终结器 — 引用

在这里插入图片描述
实线代表强引用

强引用:一般创建且通过赋值运算符赋值给一个变量,变量就强引用了这个对象(只要沿着GCRoot 引用链能找到这个对象,就不能被回收)
软引用:垃圾回收后,且内存仍然不足时,就会被回收
弱引用:只要发生垃圾回收,就会被回收掉
在这里插入图片描述

注意,软引用和弱引用本身也是对象,如果指向的对象被回收了,则软引用、弱引用会被放入引用队列,这样就可以方便回收软/弱对象本身了

虚引用:在虚引用 引用的对象被垃圾回收时,虚引用对象自己就会放入引用队列,从而间接由一个线程来调用虚引用对象的方法,然后调用 unsafe.freeMemory 去释放直接内存

在这里插入图片描述

类似回调要回收直接内存

终结器引用:当 当前对象重写了 Object 类的终结方法(finallize方法),并且没有强引用 引用时,它就可以被当成垃圾回收

在这里插入图片描述

当没有强引用引用时,虚拟机会创建上面对象的 终结器引用,当这个对象要被垃圾回收时,终结引用也会被加入 引用队列,再由其他线程先调用 finallize方法,再去回收(优先级低,不容易被回收)

虚/终结器引用必须配合引用队列使用(创建时,会直接关联引用队列,强制!软/弱 非强制)

在这里插入图片描述

软引用举例:

在这里插入图片描述

配合引用队列,获取软引用对象,并删除(队列软引用构造器的作为第二个参数)
在这里插入图片描述

弱引用举例:
在这里插入图片描述

回收算法

  • 标记—清除
    先标记存活对象,再把未标记的垃圾对象(无gc root 引用)清除(快、但是有内存碎片,不连续)
    在这里插入图片描述

  • 标记—整理
    先标记存活对象,将可用对象向前移动,让内存更为紧凑(避免内存碎片)(效率较低)
    在这里插入图片描述

  • 复制算法
    将内存分为大小相等的两个区域,from 和 to,to这个区域是空闲的,首先在from区域标记不能被回收的存活对象,将存活的对象,复制到 to 的区域中(过程中会整理),复制完成后,清空 from 区域的内存,接着交换 from 和 to 定义

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

上面3种回收算法,在 JVM 中会分情况使用不同算法

分代回收

  • 在JVM堆内存中调整新生代大小的方式有两种主要途径:调整新生代/老年代的比例或直接指定新生代的大小

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

把堆内存分为 新生代、老年代
新生代分为:伊甸园、幸存区from、幸存区to
对不同区域使用不同的回收算法(新生代回收频繁、老年代回收不频繁)

分代回收机制:

  • 创建的对象,先分配到伊甸园中
  • 当伊甸园满了之后,就会触发一次垃圾回收(新生代叫Minor GC:可达性分析),回收后,存活对象复制到 to 区域中,并且幸存对象,寿命+1,伊甸园清空
  • from 区域、to 区域的名字定义交换
  • 然后新对象又在伊甸园分配,当伊甸园又满时,再触发第二次垃圾回收
  • 第二次垃圾回收时,不止分析伊甸园可回收对象,还要分析幸存者区可回收对象,回收垃圾对象
  • 将伊甸园存活对象放入 to 区域(寿命+1)、且将 from 区域存活对象放入 to 区域(寿命+1)
  • 当幸存区对象,寿命超过某个阈值15 时,就会将其移动到 老年代

注意:minor gc 会引发 stop the world(在发生垃圾回收的时候,必须暂停其他的用户线程),由垃圾回收线程完成垃圾回收的动作,当 回收动作完后,用户线程才可以继续执行(防止回收时用户线程也执行,对象地址引用混乱)

在这里插入图片描述

当新生代和老年代空间都满了时(不能存放新对象),触发 full GC(minor 后空间仍不足才触发)

老年代采用 标记+清除/整理 算法

如果 full GC 后,老年代空间仍不足,则会触发 OOM

总结:
在这里插入图片描述

GC

相关 VM 参数
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
GC 是 minor gc、full gc 就显示的是 full gc

在这里插入图片描述

当新生代空间不足时,一些对象寿命没有到 15,也会被移动老年代
大对象直接晋升到老年代

在这里插入图片描述

在这里插入图片描述

一个子线程 OOM,不会使得整个 Java 进程结束,不影响主线程继续执行

三种垃圾回收器

  1. 串行(SerialGC)
  • 单线程
  • 适用场景:堆内存较小,适合个人电脑(cpu个数少)
  1. 吞吐量优先(ParallelGC)
  • 多线程
  • 场景:堆内存较大,多核 cpu
  • 目标:让 单位时间内,STW 的时间最短
  • 吞吐量=(运行用户代码时间)/(运行用户代码时间+运行垃圾收集时间)
  1. 响应时间优先(CMS)
  • 多线程
  • 场景:堆内存较大,多核 cpu
  • 注重尽可能让垃圾回收时 单次 STW时间短

串行

在这里插入图片描述
老年代:标记整理

吞吐量优先

在这里插入图片描述
ration 默认值 是 99,代表 1 / (1 + 99),代表 100 分钟内,运行 1 分钟内的垃圾回收时间(达不到时,会调大堆内存时间,减少回收次数)
200 ms 指每次回收时间

多线程,多个垃圾回收线程并行执行,但用户线程 STW 了

jdk 1.8 默认开启

老年代:标记整理

响应时间优先
在这里插入图片描述

CMS 垃圾回收器(某些情况会退回串行)
concurrent:并发(其中一些阶段,可以和用户线程并发执行)(只有部分时间才 STW)
可能影响应用程序的吞吐量(因为有一部分用cpu用来垃圾回收了,用户线程能使用的cpu就少了)

年轻代回收:与Parallel GC类似,采用并行复制算法。
老年代回收:采用 并发标记-清除 算法,大部分垃圾回收工作与应用线程并发执行,减少停顿时间(某些情况会退回串行)。

  • 老年代发生内存不足,cpu 到安全点暂停下来,CMS 进行 初始标记(STW)(只标记根对象GC root,比较快)、然后用户线程恢复运行,垃圾回收线程可以进行 并发标记(应用程序响应时间短),并发标记完成后,进行 重新标记,需要 STW (因为并发标记阶段,用户线程也在运行,可能对回收过程有影响,产生新垃圾对象,改变对象的引用等,会对并发标记阶段进行干扰),重新标记完成后,用户线程 恢复运行,此时再进行一次 并发清理
  • 只有 初始标记重新标记 阶段 才会 STW

老年代:标记清除

G1

定义:Garbage First
• 2004论文发布
• 2009 JDK 6u14体验
• 2012 JDK 704 官方支持
• 2017 JDK9 默认

适用场景
• 同时注重吞吐量(Throughput) 和低延迟(Low Latency),默认的暂停目标是 200 ms
• 超大堆内存,会将堆划分为多个大小相等的 Region
• 整体上是标记+整理算法,两个区域之间是复制算法
相关 JVM 参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time

代替 CMS 回收器

  1. G1 垃圾回收阶段

在这里插入图片描述

循环的过程

Mixed GC:收集整个新生代和部分老年代的垃圾收集,目前只有G1有这种行为

  1. Young Collection(新生代回收)

在这里插入图片描述
每个区域都可以独立作为伊甸园,幸存区,老年代
每一个region代表一种区域 伊甸园 幸存区0 幸存区1 老年代

E:伊甸园(会设置总的大小)

当 伊甸园 满了之后,会触发 STW(新生代垃圾回收),会将幸存对象以拷贝的形式拷贝的幸存者区。

在这里插入图片描述

当幸存区满了,也会触发回收(和之前一样),当到一定年龄,会被放入老年区

在这里插入图片描述

  1. Young Collection + CM(新生代回收+CM并发标记)

(初始标记 和 并发标记)

初始标记:找到那些根对象进行标记(会发生STW)(Young GC 时进行)
并发标记:从根对象出发,顺着引用链,标记其他的对象(与用户线程共同执行,不会STW)(老年代终于堆内存空间 45%+的时候触发)

• 在 YoungGC 时会进行 GC Root 的初始标记
• 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM 参数决定
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)

当老年代占比,占总堆内存的 45% 的时候,就进行 并发标记

在这里插入图片描述

  1. Mixed Collection

会对 E、S、O 进行 全面 垃圾回收

在 Mixed Collection 的 最终标记拷贝存活 阶段都会 STW

最终标记(并发标记可能会漏掉一些对象,因为并发标记时,其他线程也在工作,可能会产生新垃圾对象),所以需要在 混合回收 的阶段,STW,进行 最终标记

在这里插入图片描述

伊甸区和幸存者区都会回收,未回收的对象拷贝到其他 S 区、够年龄的复制到老年代区域,老年代一些被 并发标记 阶段,回收一部分没用的对象,也会复制到 其他 O 区域,
G1 会根据 STW(最大暂停时间) 时间,有选择的进行回收O(有时候堆内存空间太大了,老年代回收时间比较长,可能达不到最大暂停时间目标,G1才会选择性的挑出回收价值最多的(能回收的内存多的) 区域进行回收。(否则则会全回收)

G1垃圾回收器的整个回收过程分为以下几个阶段:
1) 初始标记阶段:STW,标记 GC Roots 直接引用的对象。
2) 并发标记阶段:与用户线程并发执行,标记所有可达对象。
3) 最终标记阶段:STW,修正并发标记阶段可能出现的标记错误。
4) 清理阶段:对回收区域进行评估和清理

  1. Full GC
  • SerialGC
    • 新生代内存不足发生的垃圾收集 -minor gc
    • 老年代内存不足发生的垃圾收集 -full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 -minor gc
    • 老年代内存不足发生的垃圾收集 -full gc
  • CMS
    • 新生代内存不足发生的垃圾收集. -minor gc
    • 老年代内存不足(当垃圾回收速度小于产生速度,则会触发 串行回收【full gc】,否则不会 full gc
  • G1
    • 新生代内存不足发生的垃圾收集 -minor gc
    • 老年代内存不足(当垃圾回收速度小于产生速度,则会触发 多线程回收【full gc】,否则不会 full gc

跨代引用(Young GC):每个 region 都有一个 remember set,记录其他区域对当前区域对象的引用(老年代对象对新生代对象的引用),垃圾回收时,可以快速扫描哪些外部引用,引用了当前对象,快速的判断当前对象是否可回收(不用扫描整个 老年代区)

  1. Young Collection 跨代引用(新生代垃圾回收—跨代引用)

在Java虚拟机(JVM)的垃圾回收机制中,新生代垃圾回收(Minor GC)是一个频繁发生的操作,主要用于清理新生代中的对象。然而,新生代中的对象可能会被老年代中的对象引用,这种跨代引用需要扫描整个老年代区域。为了解决这个问题,JVM引入了 Remembered Set(记忆集) 的概念。

在这里插入图片描述
找新生代对象的根对象(有一部分来自 老年代)(直接遍历的话,需要花费很长时间)

通过 card table 来标记,当有个 card 引用 新生代中 的对象,则会标记 为 脏 card

Eden 区会通过 remembered set 标记当前对象被那些 脏 card 引用,垃圾回收时,只需要通过 remembered set 找到脏卡,再通过 脏卡 遍历 GC ROOT,再进行垃圾标记,然后进行复制—清除,加速新生代垃圾回收

  1. Remark(重标记)

并发标记 时的处理状态
在这里插入图片描述

黑色:已经处理完(存活的)
灰色:还在处理中(最终变成黑色存活)
白色:没有被处理(如果被强引用,则会变成 黑色 存活下来,否则白色,清除)

初始状态时:
在这里插入图片描述
开始并发标记(和用户线程同时执行)

由于 B 是被强引用的,标记为黑色,假如有用户线程同时对 C 的引用进行了修改

在这里插入图片描述

假如先断开了引用,但是此时并发标记,标记为会被回收(白色)

在这里插入图片描述
然后处理 完 C 后,又有用户线程改变了 C 的引用,但是此时 C 已经被标记为需要被清理,并且 A 已经标记为 处理过了,不会再 顺着 A 去标记 C 对象,此时,C 对象还是会被清理(这是有问题的)

所以,重标记阶段引入了一个概念:内存屏障

  • 并发标记阶段,当对象的引用发生改变时,JVM 就给他加入一个写屏障(对象引用发生改变,写屏障的指令就会执行,将对象加入到 队列当中,并将对象标记为 灰色 处理中)
    在这里插入图片描述
    在这里插入图片描述
  • 并发标记 执行完成后,重标记 阶段会遍历 队列中 的元素,进行重新处理,判断是否可以被回收,就避免了上面的问题!
  1. JDK 8u20 字符串 去重
  • 优点:节省大量内存
  • 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
    -XX:+UseStringDeduplication
String s1 = new String("hello"); // char[]{'h', 'e', '1', 'I', 'o'}
String s2 = new String("hello"); // char[]{'h', 'e', '1', '1', 'o'}
  • 将所有新分配的字符串放入一个队列

  • 当新生代回收时,G1 并发检查是否有字符串重复

  • 如果它们值一样,让它们引用同一个 char[]
    在这里插入图片描述

  • 注意,与 string.intern() 不一样

    • String.intern() 关注的是字符串对象
    • 而字符串去重关注的是 char[](字符串对象不一样,但是指向同一个 char 数组)
    • 在JVM内部,使用了不同的字符串表
  1. JDK 8u40 并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用(类对应的对象实例都被回收),当类对应的对象实例都被回收,并且这个类所在的类加载器的所有类都不再使用,则卸载这个类加载器所加载的所有类(只能是自定义类加载器)
-XX:+ClassUnloadingwithConcurrentMark 默认启用

  1. JDK 8u60 回收巨型对象

在这里插入图片描述

  • 一个对象大于region 的一半时,称之为巨型对象
  • G1 不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0的巨型对象就可以在新生代垃圾回收时处理掉(老年代区域对巨型对象引用为 0 时,就可以回收)
  1. JDK 9 并发标记起始时间的调整
  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC
  • JDK 9之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDK 9可以动态调整
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间
  1. JDK 9 更高效的回收
  • 250+增强
  • 180+bug修复
  • https://docs.oracle.com/en/java/javase/12/gctuning

GC 调优

查看虚拟机运行参数

  • java -XX:+PrintFlagsFinal -version | findstr “GC”
  • java -XX:+PrintFlagsFinal -version | grep “GC”

预备知识

  • 掌握GC 相关的 VM参数,会基本的空间调整
  • 掌握相关工具
  • 明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则
  1. 调优领域
  • 内存
  • 锁竞争
  • cpu 占用
  • io
  1. 确定目标
  • 【低延迟】还是【高吞吐量】,选择合适的回收器
  • CMS,G1,ZGC(低延迟)
  • ParallelGC(高吞吐量)
  • Zing
  1. 最快的GC是不发生GC
  • 查看 FullGC前后的内存占用,考虑下面几个问题
    • 数据是不是太多?
      • resultSet = statement.executeQuery(“select * from 大表”)
    • 数据表示是否太臃肿?
      • 对象图
      • 对象大小
  • 是否存在内存泄漏?
    • static Map map = (一直加强引用对象,不删除)
    • 考虑如下:
    • 第三方缓存实现
  1. 新生代调优
  • 新生代的特点
    • 所有的 new燥作的内存分配非常廉价(伊甸园创建对象非常快)
      • TLAB thread-local allocation buffer(每个线程都会在伊甸园中分配一块私有区域,用自己私有的内存进行对象内存的分配)
    • 死亡对象的回收代价是零
    • 大部分对象用过即死(大多数对象用过就会被回收)
    • Minor GC 的时间远远低于 Full GC

新生代能容纳所有【并发量 * 一次(请求-响座)产生的对象内存】的数据

幸存区大到能保留【当前活跃对象+需要晋升对象】(防止有的存活时间短的对象,被提前晋升到老年代,进而推迟到 full gc 时才能回收)

晋升阈值配置得当,让长时间存活对象尽快晋升
-XX :MaxTenuringThreshold=threshold(最大晋升阈值)
-XX: +PrintTenuringDistribution(打印不同年龄的对象所占用空间)

Desired survivor size 48286924 bytes, new threshold 10 (max 10)
- age 1: 28992024 bytes, 28992024 total
- age 2: 1366864 bytes, 30358888 total
- age 3: 1425912 bytes, 31784800 total
  1. 老年代调优

以 CMS为例

  • CMS 的老年代内存越大越好
  • 先尝试不做调优,如果没有 Full GC那么已经…,否则先尝试调优新生代
  • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大1/4~1/3
    • -XX: CMSInitiatingOccupancyFraction=percent(值越小,full gc 发生的时间越早)
  1. 案例
  • 案例1 Full GC 和 Minor GC频繁

增大了新生代的内存导致minorgc更少触发,并且survivor区增大,就不会让本不是生命周期那么长的对象进入老年区,从而给老年区节省空间,进一步就减少了老年区出发fullGC

  • 案例2 请求高峰期发生 Full GC,单次暂停时间特别长(CMS)

    查看 GC 日志,发现重新标记 时间比较长(初始标记/并发标记一般比较快)

    CMS 重新标记时,会扫描整个的堆内存(老年代+新生代),请求高峰期时,新生代对象个数比较多,所以扫描标记的对象就比较多(根据对象找他的引用)

    通过下面的参数,在重新标记阶段发生之前,先对新生代的垃圾进行一次清理,重新标记阶段需要标记的对象就少很多
    在这里插入图片描述
    就解决了 重新标记阶段 暂停时间长的问题

  • 案例3 老年代充裕情况下,发生 Full GC(CMS jdk 1.7)

(CMS 有可能因为空间不足,并发失败,或者空间碎片比较多,都会产生 full gc)
但是排查,老年代空间充裕

1.7 永久代实现方法区

永久代空间不足,也会触发整个堆的 full gc

类加载

在这里插入图片描述

类文件结构

在这里插入图片描述
编译 java 文件为 二进制字节码 .class 文件

在这里插入图片描述

  1. 魔数:文件的特定类型(标识文件类型)
    在这里插入图片描述
  2. 版本

在这里插入图片描述
3. 常量池

记录了 Java 类中的各种信息:类的信息、父类信息、方法信息、成员变量信息、方法属性等…

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 访问标识与继承信息

在这里插入图片描述

  1. field 信息
    在这里插入图片描述

  2. method 信息
    在这里插入图片描述

  3. 附加属性

在这里插入图片描述

字节码指令

21 b7 这些都叫字节码指令
在这里插入图片描述

方法体内有一些字节,就是由 JVM 来执行的方法的代码

构造方法:5个字节(字节码指令)(JVM 可以识别平台无关的字节码指令,最终解释为机器码执行)
main 方法:9个字节

2a:(aload_0)把局部变量表中的变量加载到操作数栈中,JVM 执行的时候,大多数时间会在操作数栈上读取数据(aload_0:把局部变量表 0 槽位上的变量加载到操作数栈中)
b7:方法调用
00 01:方法引用
b1:表示返回

在这里插入图片描述

javap 工具

javap 反编译 class 二进制字节码文件

-v 参数,输出类文件的详细信息

  • 文件本身的属性信息
  • 类的全包类名(jdk 8、public)
  • 常量池(引用 #6.#21,后面的注释就是对应拼出来的)
  • 方法信息:构造方法(参数信息、访问修饰符、代码)、main 方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

图解运行流程

图解方法执行流程:

在这里插入图片描述

在这里插入图片描述

Java 代码被执行时,都发生了什么?

  • 类加载器加载 main 方法所在的类(类加载就是将class文件的数据读到内存中)
  • 其中常量池 (class文件中的常量池数据)中的数据会被放入 运行时常量池(方法区的一部分) 中
  • Short.MAX_VALUE 范围内的和字节码指令存储在一起,否则则存储在常量池中

3)常量池载入运行时常量池
在这里插入图片描述

4)方法字节码 载入方法区
在这里插入图片描述
5)main 线程开始执行,分配栈帧内存

在这里插入图片描述

  • 为 main 线程的main方法分配栈帧内存(绿色代表局部变量表,蓝色是操作数栈
    在这里插入图片描述
    stack=2:最大操作数栈深度为 2(存储数据和字节码指令)
    local=4:局部变量表是 4(存储局部变量)

6)执行引擎开始执行字节码(方法区的代码)

在这里插入图片描述

给 a 变量赋值

在这里插入图片描述
槽位:args、a、b、c,局部变量表的这些槽位分别用来存储对应变量的值

给 b 变量赋值
在这里插入图片描述
因为 32768 超过了 Short.MAX_VALUE,所以存储在了 运行时常量池中,ldc #3,会从常量池中加载这个数字到操作数栈中

在这里插入图片描述
a + b 的操作在操作数栈中才能完成

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在常量池中找到成员变量的引用(堆中),将引用放入操纵数栈中
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

练习—分析 i++
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

练习

在这里插入图片描述

  • iload_x 将 0 放入操作数栈
  • iinc_x 在局部变量表中,将 x 自增,变为 1
  • istore_x 将操作数栈中的 0 放入 局部变量表 x 对应的槽位,覆盖 原来的 1,所以结果一直是 0

在这里插入图片描述
在这里插入图片描述
所以 i = 30

在这里插入图片描述
构造代码块比构造函数先执行

在这里插入图片描述

方法调用

在这里插入图片描述
在这里插入图片描述

  • invokespecial/invokestatic 在编译阶段是可以确定调哪个类的哪个方法(静态绑定)(构造方法、私有方法、静态方法)
  • invokevirtual 在编译期间不能调用对象的哪个方法,需要运行时确定(动态绑定)(public 可能出现方法重写)

多态?

上面通过对象调用静态方法时,会产生两条没用的虚拟机指令:
20.21:aload_1、pop,入栈后直接出栈(无用指令)

多态原理

invokevirtual(多态调用)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

HSDB 工具

在这里插入图片描述

通过 HSDB 连接刚才的程序
在这里插入图片描述

通过sqql 语句查找 JVM 中的对象(内存地址)

在这里插入图片描述

点击对应的对象地址,可以查找到对象的信息(对象在内存中实际的结构)(对应c++的struct)

在这里插入图片描述
在这里插入图片描述

对象的信息:

  • 对象头(一共16字节)
    • mark(对象hash码、锁标记)(8字节)
    • metadata(类型指针、找到对象的class类)(8字节)
  • 对象成员变量

通过 mem 命令,查看 mark、metadata 对应的 指针地址
在这里插入图片描述

在这里插入图片描述

对象在内存中的完整表示(方法区,包含类的所有信息)

类中的多态的方法是存在于 vtable 虚方法表中(静态、final、私有、构造都不会在这个表中)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

从堆中找到对象,通过对象找到方法区对应的类文件信息,然后通过类文件信息中的方法表,找到实际方法的地址

一句话总结:invokevirtual指令调用的对象vtable中的方法

虚方法表是在类的加载过程中的链接阶段就会生成虚方法表(vtable)

小结

当执行 invokevirtual指令时,

  1. 先通过栈帧中的对象引用找到对象
  2. 分析对象头,找到对象的实际 Class
  3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
  4. 查表得到方法的具体地址
  5. 执行方法的字节码

异常

在这里插入图片描述
在这里插入图片描述
异常表:
在这里插入图片描述
监测区间 [2, 5) 行的代码,如果出现 type 对应异常,则跳到 target 对应的行

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
athrow 是用来抛出 catch 块没有捕捉到的异常
finally 分支相当于 copy 了 三份:

  • try 分支后
  • catch 分支后
  • 没有被 catch 捕捉的异常后

在这里插入图片描述
返回结果为 20
在这里插入图片描述
建议不要在finally里return,会吞掉异常
在这里插入图片描述
正常打印 20

在这里插入图片描述
结果为 10
因为 try 块 中的 值做了 暂存,finally 块执行完后,会将暂存的值加载到操作数栈顶,进行返回。

在这里插入图片描述
finally 中不存在 return 的话,不会吞掉异常

synchronized

如何保证 synchronized 加锁,解锁成对存在
在这里插入图片描述
在这里插入图片描述

  • 9: 复制两份,是因为一个给 monitorenter用,一份给 monitorexit 用
  • 10: 将一份引用放入 2 号槽位了
  • monitorenter 对栈顶剩下的一个 lock 引用进行加锁操作

在这里插入图片描述

  • 根据异常表,如果 [12, 22) 代码没出现问题,会执行 21 的代码进行释放锁
  • 如果出现异常,会跳到 25 执行,也会执行 27 的代码进行释放锁操作

编译期处理

所谓的 语法糖,其实就是指java编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java 编译器给我们的一个额外福利(给糖吃嘛)注意,以下代码的分析,借助了javap工具,idea的反编译功能,idea 插件jclasslib 等工具。另外,编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价的java 源码方式,并不是编译器还会转换出中间的java 源码,切记。

语法糖

默认构造器

编译器会为类生成一个无参的默认构造器
在这里插入图片描述
自动拆装箱
在这里插入图片描述

valueOf 中 -128~127 会进行对象复用

泛型集合取值
在这里插入图片描述
泛型擦出
在这里插入图片描述

  • 27:checkcast 取出元素的时候,进行强制类型转换

在这里插入图片描述

  • 局部变量类型表,保存了 类型 信息
  • 但是通过反射不能获取这个信息

在这里插入图片描述
在这里插入图片描述

  • 但是这种返回值/参数中的 泛型信息,是可以通过反射获取到的

可变参数

在这里插入图片描述
在这里插入图片描述
foreach 循环

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

switch 字符串

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

switch 枚举

在这里插入图片描述
在这里插入图片描述

会生成一个静态内部类,会通过 一个数组进行映射,然后再将枚举转换成映射后的值进行比较。

枚举类

枚举类的实例对象是有限的
在这里插入图片描述

  • 都继承了 Enum 父类
  • 内部静态的成员常量
  • 在静态代码块中,进行赋值
    在这里插入图片描述
  • 构造方法私有化
  • values() 方法,获取当前类的所有枚举对象
  • valueOf() 方法,获取名称为 name 对应的枚举对象(HashMap)

try-with-resources

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
方法重写时的桥接方法

在这里插入图片描述
在这里插入图片描述

匿名内部类

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

类加载

加载

  • 将类的字节码 *载入方法区 *中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
    • _java_miror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给java 使用
    • _super 即父类
    • _fields 即成员变量
    • _methods 即方法
    • _constants 即常量池
    • _class_loader 即类加载器
    • _vtable 虚方法表
    • itable 接口方法表
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的

注意

  • instanceKlass 这样的【元数据】是存储在方法区(1.8后的元空间内),但 _java_mirror 是存储在堆中
  • 可以通过前面介绍的 HSDB 工具查看
    在这里插入图片描述
    Person.class 就是 java 类的镜像

链接

验证、准备、解析

  • 验证:验证类是否符合 JVM规范,安全性检查
    用UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行

在这里插入图片描述

  • 准备:为 static 变量 分配空间,设置默认值

    • static 变量在JDK 7 之前存储于 instanceKlass 末尾,从JDK 7 开始,存储于 _java_mirror 末尾
    • static 变量分配空间和赋值是两个步骤,分配空间准备阶段完成,赋值初始化阶段完成
    • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
    • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
  • 解析:将常量池中的 符号引用 解析为 直接引用
    在这里插入图片描述
    因为链接过程中的解析需要把常量池中的符号引用 FiledRef 也就是代表类D的符号转换成直接引用 所以就会把类D加载进来

在这里插入图片描述
上面的代码,只加载了类,所以这里 Index=2 是未解析,后面的 Constant Value 对应的 D 类仅仅是一个符号

在这里插入图片描述
如果我们直接 new 了 类的对象

在这里插入图片描述

就会将符号地址,转换为 直接地址

初始化

<cinit>()V 方法

初始化即调用 <cinit>()V,虚拟机会保证这个类的【构造方法】的线程安全

在这里插入图片描述

<cinit>()V 方法:就是 static 静态代码块和静态成员默认值 合并为一个 <cinit>()V 方法
在这里插入图片描述

练习:

在这里插入图片描述

  • a,b 都是静态常量(且是基本类型/字符串常量),在类链接的 准备 阶段 就已经赋值了,不会导致初始化
  • c 是 包装类型,只能推迟到初始化阶段完成

在这里插入图片描述
在这里插入图片描述
上面的代码,只有调用 getInstance 方法,间接引用 内部类的静态属性时,静态内部类才会被初始化。

加载class类到方法区,然后下面的链接和初始化都是为了初始化方法区class类的成员变量,链接是初始化常量池字符引用为真正引用,初始化是给_mirror赋值

类加载器

在这里插入图片描述
启动类加载器:JAVA_HOME/jre/lib
扩展类加载器:JAVA_HOME/jre/lib/ext
应用程序类加载器:classpath
自定义类加载器:自定义

双亲委派机制

启动类加载器

在这里插入图片描述
查看类是由哪个类加载器加载的

在这里插入图片描述
null 代表启动类加载器加载器的(因为启动类加载器是 c++ 写的,不能直接访问到)

在这里插入图片描述

扩展类加载器

在这里插入图片描述

载 ext 下面有一个jar 包,里面有一个同名 G 类

在这里插入图片描述
发现是拓展类加载器加载的,而非应用类加载器

双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则

在这里插入图片描述
在这里插入图片描述

  1. 检查类是否已经加载
  2. 有上级的话,委派上级 loadClass
  3. 如果没有上级了(ExtClassLoader【Ext 的上级 Boot 不能直接获取出来】),则委派 BootstrapClassLoader
  4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
  5. 记录耗时

线程上下文类加载器

在这里插入图片描述

DriverManager 是 JAVA_HOME/jre/lib 包下的类

DriverManager 的 静态代码块,调用了 loadInitialDrivers() 方法

在这里插入图片描述
DriverManager 的类加载器是 启动类加载器,但是它应该搜索的是 “JAVA_HOME/jre/lib” 的类,这个包下没有 mysql 对应的驱动类

在这里插入图片描述
在这里插入图片描述

ClassLoad.getSystemLoader() 就是应用程序类加载器,说明 loadInitalDrivers() 方法里调用了 应用程序类加载器加载 Driver 类

jdk 需要在某些模式下打破双亲委派模式,有时候需要调用应用程序类加载器 加载,否则可能找不到这个类

在这里插入图片描述
SriverLoader<接口类型> allImpls = ServiceLoad.load(接口类型.class);

这里的接口类型可以传(java.sql.Driver),这样,就会找 META-INF/services/java.sql.Driver 文件,加载里面的实现类名,通过迭代器来获取对应的实现类

在这里插入图片描述

在这里插入图片描述
线程启动时,由JVM把应用程序类加载器赋值给线程的 ContextClassLoader(线程上下文加载器)

在这里插入图片描述

自定义类加载器

问问自己,什么时候需要自定义类加载器

  • 1) 想加载非 classpath 随意路径中的类文件
  • 2) 都是通过接口来使用实现,希望解耦时,常用在框架设计
  • 3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat 容器

步骤:

  1. 继承 ClassLoader 父类
  2. 要遵从双亲委派机制,重写 findClass 方法
    • 注意不是重写 loadClass 方法(重新了 ClassLoader 父类的 loadClass 方法),否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的 defineClass 方法来加载类
  5. 使用者调用该类加载器的loadClass 方法

示例:
准备好两个类文件放入E:myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 同一个 类加载器对象,多次加载 同一个类,加载的是同一个类对象
  • 不同 类加载器对象,加载 同一个类,对应的类对象,认为是不同的 类对象

为什么tomcat要打破双亲委派?

  1. 类隔离:Tomcat需要为 每个Web应用 提供 独立的类加载器WebAppClassLoader),以确保不同应用之间的类库相互隔离。如果使用传统的双亲委派机制,所有应用将共享相同的类加载器,这可能导致类冲突
  2. 解决类冲突:不同Web应用可能依赖相同第三方库的不同版本。通过打破双亲委派机制,Tomcat确保每个应用优先加载自己目录下的类(如/WEB-INF/classes和/WEB-INF/lib),避免类冲突
  3. 热部署:打破双亲委派机制便于实现热部署功能,即在不重启服务器的情况下更新Web应用。通过自定义类加载器,Tomcat可以在运行时检测到应用的类文件变化,并加载新的类文件

Tomcat打破双亲委派机制是为了实现Web应用之间的类隔离、解决类冲突、支持热部署以及优化资源利用。这种机制使得Tomcat能够灵活地管理Web应用的类加载,同时保证了应用之间的独立性和安全性

运行期优化

分层编译

在这里插入图片描述
打印分层编译优化信息/关闭逃逸分析
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
逃逸分析:上面代码中的 new Object() 不会载循环外使用(C2 优化器优化)

方法内联

在这里插入图片描述
打印内联优化信息/禁用内联优化(指定特定方法/JDK其他方法可能也会内联)
在这里插入图片描述

字段优化

针对成员变量/静态成员变量 读写操作的优化

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

elements.length 首次读取会缓存起来 -> int[] local

  • 局部变量缓存起来读取,不用再到 class 原数据那去找静态变量(但是如果不关方法内联,虚拟机会自动帮我们优化)

在这里插入图片描述

反射优化

在这里插入图片描述

在这里插入图片描述
DelegatingMethodAccessor 会直接调用 NativeMethodAcccessorImpl
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

从 第 17 次调用,虚拟机已经将反射方法调用变成了 普通方法调用

在这里插入图片描述

在这里插入图片描述

JMM(JAVA 内存模型)

概述

很多人将【java 内存结构】与【java 内存模型】傻傻分不清,【java 内存模型】是 Java Memory Model(JMM)的意思。

  • 关于它的权威解释,请参考 https://download.oracle.com/otu-pub/jcp/memory_model-1.0-pfd-spec-oth-JSpec/memory_model-1_0-pfd-spec.pdf?AuthParam=1562811549_4d4994cbd5b59d964cd2907ea22ca08b

  • 简单的说,JMM 定义了一套在 多线程 读写 共享数据 时(成员变量、数组)时,对数据的 可见性、有序性、和原子性规则和保障

原子性

原子性在学习线程时讲过,下面来个例子简单回顾一下:

问题提出,两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做5000次,结果是 0 吗?

在这里插入图片描述

Java 中对静态变量的自增,自减并不是原子操作

在这里插入图片描述

静态变量和局部变量自增的区别,看上面的字节指令可以发现

  • 之前 局部变量自增 是用 inc 指令,直接在局部变量表中自增
  • 静态变量 自增,需要准备一个常量 1 和 静态变量的值,放入操作数栈,通过 iadd 完成自增后,使用 putstatic 存入新值

在这里插入图片描述

主内存:静态变量共享值在 主内存中
工作内存:线程中在工作内存

多线程下,有工作内存 和 主内存进行数据交换

在这里插入图片描述
单线程顺序执行,不会出现问题

在这里插入图片描述
多线程下,线程交替运行,可能出现覆盖

在这里插入图片描述

解决办法

在这里插入图片描述

在这里插入图片描述

减少加锁解锁 次数:

在这里插入图片描述

在这里插入图片描述

红色的圈叫做对象的 monitor 区域,每个对象都有自己的一个 monitor 区域(只有 使用了 synchrionized 加锁的对象才有)

  • owner:同一时刻只有一个线程成为 owner(只有当前对象的 owner 是空,当前线程才能使用 monitorenter 对当前对象进行锁定)
  • EntryList:已经有 owner,则将线程加入 EntryList(当前owner 释放锁后, monitorexit 执行,此时会通知 EntryList 的所有线程,可以去争抢 owner)
  • WaitSet:调用 wait 方法

在这里插入图片描述

可见性

在这里插入图片描述

运行后,发现线程不能退出循环

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

由于读取的是高速缓存的 旧值,所以 线程 读到的 run 一直是 true

解决办法
在这里插入图片描述

在这里插入图片描述

不能保证原子性

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
synchronized 即可以保证可见性,也可以保证原子性,属于重量级操作,性能低

在这里插入图片描述

为什么加了 System.out.println() 就能停止呢?

在这里插入图片描述
因为方法内部使用了 synchronized 同步关键字,可以防止当前线程在高速缓存中获取值,强制当前线程去 读取 主存中的值。(破坏了 JIT 优化)

有序性

在这里插入图片描述
在这里插入图片描述
除了上面的情况,还有可能是 0
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

解决办法:

在这里插入图片描述
用 volatile 修饰之后,线程从 volatile 关键字修饰的变量 读/写 数据的时候,已经不会发生指令重排序了

在这里插入图片描述
有序性理解

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  1. new 关键字,给对象分配空间,将对象引用,放入操作数栈
  2. dup 赋值一份引用,操作数栈就有 两份对象引用
  3. invokespecial 最上面的一份引用消耗掉,调用 构造方法
  4. putstatic 另一份引用,交给 INSTANCE 变量,进行赋值(局部变量表?)

在这里插入图片描述问题如上,用 volatile 修饰 INSTANCE 即可解决上面问题

在这里插入图片描述

happens-before

happens-before 规定了哪些写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结:

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加铁的其它线程对该变量的读可见

在这里插入图片描述

  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见

在这里插入图片描述

  • 线程 start 前对变量的写,对该线程开始后的对该变量的读可见

在这里插入图片描述

  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()(等待它结束)

在这里插入图片描述

  • 线程 t1 打断 t2 (interrupt X 前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)

在这里插入图片描述
也就是主线程中最后 sout(x) 和 t2 线程 if 条件里面的 sout(x),都是能读取到 x=10的(打断是一个标记,线程还会继续执行)

在这里插入图片描述

CAS

CSA 即 Compare and Swap,它体现的一种类观锁的思想,比如多个线程要对一个共享的整型变量执行 +1 操作:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

但是我们平时不需要直接调用 Unsafe
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

乐观锁和悲观锁

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
  • synchronized 是基于悲观锁的思根:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

原子操作类

juc(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:Atomiclnteger、AtomicBoolean等,它们底层就是采用CAS技术+volatile 来实现的。
可以使用 Atomiclnteger 改下之前的例子:

在这里插入图片描述

synchronized

Java Hotspot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。

  • Mark Word 平时存储这个对象的 哈希码、分代年龄
  • 加锁 时,这些信息就根据情况被替换为标记位、线程锁记录指针、重量级锁指针、线程ID 等内容

轻量级锁(没有其他线程竞争时)

在这里插入图片描述
锁升级

在这里插入图片描述
每个线程的栈帧都会包含一个 锁记录的结构,内部可以存储锁定对象的 Mark Word(当前对 当前象加锁,则会将 MarkWord信息暂存到 锁记录结构 中,释放锁的时候,会取出来,恢复)

在这里插入图片描述
在这里插入图片描述
由于线程1 和 线程2 在获取锁的时间上,没有重叠,轻量级锁就已经够用

  • 偏向锁:适用于单线程或低竞争场景,减少锁的开销,提高性能。
  • 轻量级锁:适用于多线程场景,减少锁的开销,但需要处理多线程竞争。

锁升级:

  • 偏向锁:如果其他线程尝试获取锁,偏向锁会 撤销升级轻量级锁或重量级锁
  • 轻量级锁:如果多个线程 竞争 锁,轻量级锁会升级为 重量级锁

锁膨胀(偏向锁->轻量级->重量级)(可以唤醒其他线程)

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行 锁膨胀,将轻量级锁变为重量级锁
**加粗样式**
在这里插入图片描述

如果当前有线程在使用共享锁对象,则需要锁升级,将 00轻量级锁 升级为 10 重量级锁,代表现在已经有其他线程在竞争锁了(需要通过特定方式释放,释放重量锁,唤起阻塞线程竞争)

重量级锁(竞争会出现自旋)

重量级铁竞争的时候,还可以使用自旋来进行优化,如果当前线程 自旋 成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

在Java 6之后自旋铁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  • 自旋会占用CPU时间,单核 CPU 自旋就是浪费,多核CPU 自旋才能发挥优势。
  • 好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞(等待时间长了划算)
  • Java 7 之后不能控制是否开启 自旋 功能

在这里插入图片描述

线程1加锁成功,此时线程2过来尝试获取 monitor,线程 2 不会马上阻塞(阻塞/唤醒时间消耗长)

不会立马阻塞,会自旋重试几次,如果此时 线程1 释放锁了,则它立马获取锁对象

在这里插入图片描述

多核 cpu,才能 自旋

偏向锁(重入用)

偏向锁是一种锁的状态,它的核心思想是 偏向于第一个获取锁的线程。如果在锁的生命周期内,只有一个线程访问该锁,那么这个线程可以 无锁 地执行同步代码块,从而减少锁的开销。

轻量级锁在没有竞争时(就自己这个线程),每次 重入 仍然需要执行 CAS操作。Java 6中引入了 偏向锁 来做进一步优化:只有第一次使用 CAS 将 线程 ID 设置到对象的 Mark Word 头,之后发现这个 线程ID 是自己的就表示没有竞争,不用重新 CAS。

  • 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW
  • 访问对象的 hashCode 也会撤销偏向锁(因为无锁状态下,对象头里存储的是对象的 hashCode,但是偏向锁的时候,存储了 线程ID, hashCode 无法存储,需撤销锁并升级,其他线程访问 对象 hashCode 时,在对象头找不到,所以会撤销。。)
  • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
  • 撒销偏向和重偏向都是批量进行的,以 为单位
  • 如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的
  • 可以主动使用-XX:-UseBiasedLocking 禁用偏向锁

其他优化

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值