学习黑马JVM的笔记

JVM详解

  • 一、JVM介绍
    • 1.什么是JVM?
    • 2.有什么好处
    • 3.学习路线
  • 二、内存结构
    • 1.程序计数器(Program Counter Registe)
      • 1.定义
      • 2.作用
      • 3.特点
      • 4.演示
    • 2.虚拟机栈(Java Virtual Machine Stacks)
      • 1.定义
      • 2.演示
      • 3.问题解析
      • 4.栈内存溢出
      • 5.线程运行诊断(linux中)
    • 3.本地方法栈
    • 4.堆(Heap)
      • 1.定义
      • 2.特点
      • 3.堆内存溢出
      • 4.堆内存诊断
        • 1.jps工具
        • 2.jmap工具
        • 3.jconsole工具
        • 4.演示
          • 1.jps
          • 2.jmap
          • 3.jconsole
          • 4.jvisualvm 也是一个可视化工具,功能更加强大
    • 5.方法区
      • 1.定义
      • 2.特点
      • 3.组成
      • 4.方法区内存溢出
        • 1.8以前导致永久代内存溢出
        • 1.8以后导致元空间内存溢出
      • 5.运行时常量池
      • 6.StringTable(串池)
        • 1.StringTable特性
        • 2.StringTable位置
        • 3.StringTable垃圾回收
        • 4.StringTable性能调优
    • 6.直接内存
      • 1.定义
      • 2.ByteBuffer
      • 3.直接内存内存溢出
      • 4.直接内存释放原理
      • 5.分配和回收原理
      • 6.禁用显示回收对直接内存的影响
  • 三、垃圾回收
    • 1.如何判断对象可以回收
      • 1.引用计数法
        • 1.定义
        • 2.弊端
      • 2.可达性分析算法(java虚拟机采用的方法)
        • 1.定义
        • 2.哪些对象可以作为GC Root?
        • 3.如何查看GC Root对象
      • 3.五种引用
        • 1.强引用:
        • 2.软引用:
        • 3.弱引用
        • 4.虚引用:
        • 5.终结器引用:
    • 2.垃圾回收算法
      • 1.标记清除算法
      • 2.标记整理算法
      • 3.复制算法
    • 3.分代回收
      • 3.1相关VM参数
    • 4.垃圾回收器
      • 1.相关概念
        • 1.并行收集
        • 2.并发收集
        • 3.吞吐量
      • 2.串行回收器
        • 1.特点
        • 2.Serial收集器
        • 3.ParNew 收集器
        • 4.Serial Old 收集器
      • 3.吞吐量优先
        • 1.Parallel Scavenge 收集器
        • 2.**Parallel Old 收集器**
      • 4.响应时间优先
        • 1.CMS 收集器
      • 5.G1
        • 1.定义
        • 2.使用场景
        • 3.相关JVM参数
        • 4.G1垃圾回收阶段
          • 1.Young Collection(新生代收集)
          • 2.Young Collection+ Concurrent Mark(新生代收集+并发标记)
          • 3.Mixed Collection(混合收集)
        • 5.Full GC
        • 6.Young Collection 跨代引用
        • 7.Remark(重新标记)
        • 8.JDK 8u20 字符串去重
        • 9.JDK 8u40 并发标记类卸载
        • 10.JDK 8u60 回收巨型对象
        • 11.JDK 9并发标记起始时间的调整
      • 5.垃圾回收调优
      • 5.GC 调优
        • 1.调优领域
        • 2.确定目标
        • 3.最快的GC是不发生GC
        • 4.新生代调优
        • 5.幸存区调优
        • 6.老年代调优
  • 四、类加载与字节码技术
    • 1.类文件结构
      • 1.魔数
      • 2.版本
      • 3.常量池
      • 4.访问标识与继承信息
      • 5.Field信息
      • 6.Method信息
      • 7 附加属性
    • 2.字节码指令
      • 1.入门
      • 2 javap 工具
      • 3.图解方法执行流程
        • 1.代码
        • 2.编译后的字节码文件
        • 3.**常量池载入运行时常量池**
        • 4.**方法字节码载入方法区**
        • 5.main 线程开始运行,分配栈帧内存
      • 4.通过字节码指令来分析问题
      • 5.构造方法
        • 1.cinit()V
        • 2.init()V
      • 6.方法调用
      • 7.多态原理
      • 8.异常处理
          • try-catch
          • 多个single-catch
          • finally
          • finally中的return
          • 被吞掉的异常
          • finally不带return
    • 3.语法糖 编译器处理
      • 1.默认构造函数
      • 2.自动拆装箱
      • 3.泛型集合取值
      • 4.可变参数
      • 5.foreach
      • 6.switch字符串
      • 7.switch枚举
      • 8.枚举类
      • 9.匿名内部类
    • 4.类加载阶段
      • 1.加载
      • 2.链接
        • 1.验证
        • 2.准备
        • 3.解析
      • 3.初始化
        • 1.发生时机
        • 2.以下情况不会初始化
    • 5.类加载器
      • 1.基本介绍
      • 2.启动类加载器
      • 3.扩展类加载器
      • 4.应用程序类加载器
      • 5.双亲委派模式
        • 概念
      • 6.线程上下文类加载器
      • 7.自定义类加载器
        • 1.使用场景
        • 2.步骤
        • 3.案例
      • 8.破坏双亲委派模式
    • 6.运行期优化
      • 1.即时编译
        • 1.分层编译
        • 2.既时编译器(JIT)与解释器的区别
        • 3.逃逸分析
        • 4.方法内联
          • 1.**内联函数**
          • **2.JVM内联函数**
          • 5.字段优化
        • 2.反射优化
  • 五、内存模型
    • 1.JAVA内存模型(JMM)
    • 2.原子性
      • 1.问题提出:结果不一定为0
      • 2.问题分析
      • 3.解决方法
    • 3.可见性
      • 1.问题提出:退不出的循环
      • 2.问题分析
      • 3.解决方法
    • 4.有序性
      • 1.问题提出:诡异的结果?
      • 2.问题分析
      • 3.解决方法
      • 4.有序性理解
        • 指令重排
      • 5.double-checked locking 模式实现单例
    • 5.happens-before
    • 6.CAS与原子类
      • 1.CAS
      • 2.乐观锁与悲观锁
      • 3.原子操作类
    • 7.synchronized优化
      • 1.轻量级锁
      • 2.膨胀锁
      • 3.重量锁
      • 4.偏向锁
      • 5.其他优化
        • 1.减少上锁的时间
        • 2.减少锁的粒度
        • 3.锁粗化
        • 4.锁消除
        • 5.读写分离

一、JVM介绍

1.什么是JVM?

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

2.有什么好处

1.一次编写,到处运行
2.自动内存管理,垃圾回收功能

JVM是一套规范

3.学习路线

JVM内存结构

垃圾回收

类加载与字节码技术

内存模型

学习黑马JVM的笔记_第1张图片

二、内存结构

1.程序计数器(Program Counter Registe)

1.定义

Program Counter Register 程序计数器(寄存器)

2.作用

  • 记住下一条jvm指令的执行地址

3.特点

  • 线程是私有的
  • 不会存在内存溢出(唯一不会出现溢出的区)

4.演示

1.记住下一条jvm指令的执行地址

Java源代码经过编译成为二进制字节码(JVM指令),二进制字节码经过解释器翻译为机器码,机器码交给CPU执行;
程序计数器 (通过寄存器来实现) 在解释器执行时将下一条指令地址记住,解释器下次就会根据程序计数器中指令地址区执行下一条指令。
学习黑马JVM的笔记_第2张图片

2.线程是私有的

有多个线程,每个线程会有一个时间片,在线程1执行的时候会执行线程1的字节码,时间片用完会停止执行给其他线程使用。每个线程都有自己的程序寄存器。学习黑马JVM的笔记_第3张图片

2.虚拟机栈(Java Virtual Machine Stacks)

1.定义

Java Virtual Machine Stacks (Java 虚拟机栈 )

  • 每个线程运行时需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧(正在运行的方法),对应着当前正在执行的那个方法

2.演示

每个方法运行时需要的内存就是一个栈帧

学习黑马JVM的笔记_第4张图片

3.问题解析

1,垃圾回收是否涉及栈内存?
答:没有涉及,栈帧在运行完方法是将方法弹出栈,被自动回收掉,根本不需要垃圾回收。

2,栈内存是越大越好吗?
答:不是,栈内存越大,会让线程数变小,因为物理内存是一定的。

3.方法内的局部变量是否线程安全?
答:如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。(传入对象且返回对象需要考虑线程安全)

4.栈内存溢出

  • 栈帧过多导致栈内存溢出
  • 栈帧过大导致栈内存溢出

5.线程运行诊断(linux中)

定位

  • 用 top 定位那个进程对CPU的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • jstack 进程id
    • 可以根据线程id找到有问题的线程,进一步定位到问题代码的行号

3.本地方法栈

本地方法接口:不是由Java编写的方法

调用本地方法时就是使用的本地方法栈。

native method

学习黑马JVM的笔记_第5张图片

程序计数器和栈都是线程私有的

4.堆(Heap)

1.定义

Heap 堆

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

2.特点

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

3.堆内存溢出

对象内的内存满了,就会溢出。就像一直在一个ArrayList中一致添加数据就会导致堆内存溢出,下列代码就会导致堆内存溢出
学习黑马JVM的笔记_第6张图片

4.堆内存诊断

windos中

1.jps工具
  • 查看当前系统中有哪些Java进程
  • 列出所有正在运行的java进程,其中jps命令也是一个java程序,前面的数字就是对应的进程id。
2.jmap工具
  • 查看堆内存占用情况 jmap -heap 进程号
3.jconsole工具
  • 图形界面的,多功能的监测工具,可以连续监测
4.演示
1.jps

学习黑马JVM的笔记_第7张图片

2.jmap

jmap -heap 进程号

学习黑马JVM的笔记_第8张图片 学习黑马JVM的笔记_第9张图片
3.jconsole

学习黑马JVM的笔记_第10张图片

4.jvisualvm 也是一个可视化工具,功能更加强大

学习黑马JVM的笔记_第11张图片

5.方法区

1.定义

Method Area 方法区

2.特点

  • 线程共享的区域

  • 启动时创建

  • 存储跟类结构相关的信息,属性、方法、构造方法

3.组成

1.6版本:PermGen 永久代(实现)
1.7版本及以后:Metaspace 元空间(实现)

永久代:字符串常量池在方法区中
方法区在jvm内存中
元空间:字符串常量池在堆中
方法区在本地内存

学习黑马JVM的笔记_第12张图片

4.方法区内存溢出

  • 1.8以前会导致永久代内存溢出
    演示永久代内存溢出java. lang. OutOfMemoryError: PermGen space
-XX:MaxPermSize=8m
  • 1.8之后会导致元空间内存溢出
    演示元空间内存溢出java. lang . OutOfMemoryError: Metaspace
-XX:MaxMetaspaceSize=8m
1.8以前导致永久代内存溢出

学习黑马JVM的笔记_第13张图片

1.8以后导致元空间内存溢出

学习黑马JVM的笔记_第14张图片

可能溢出场景:spring、mybatis

5.运行时常量池

  • 常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池:常量池是*.class文件中的,当该类被加载,他的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

学习黑马JVM的笔记_第15张图片

二进制字节码(类基本信息,常量池,类方法定,包含了虚拟机指令)

学习黑马JVM的笔记_第16张图片

学习黑马JVM的笔记_第17张图片

6.StringTable(串池)

程序运行时会将常量池中的字符串放入StringTable(串池)

字符串在java程序中被大量使用,为了避免每次都创建相同的字符串对象及内存分配,JVM内部对字符串对象的创建做了一定的优化(一组指针指向Heap中的String对象的内存地址)。

1.7版本之前StringTable放在方法区中,1.7之后放在堆中。原因:方法区的内存空间太小。

重点:下面判断

学习黑马JVM的笔记_第18张图片

结果输出为false。s4是一个String对象,底层会用到StringBuilder的tostring方法创建对象。

学习黑马JVM的笔记_第19张图片

结果为true

1.StringTable特性
  • 常量池中的字符串仅是符号, 在被用到时才会转化为对象
  • 利用串池的机制,避免创建重复的字符串对象
  • 字符串变量拼接的原理是StringBuilder(1.8)
  • 字符串常量拼接的原理是编译期优化(编译时会先去串池中查看是否有这个这个字符串对象,有的话就不用创建)
  • 使用intern方法,主动将串池中还没有的字符串对象放入串池
    • 1.8 将字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池的对象返回。
      • 注意:此时如果调用intern方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象
    • 1.6 将字符串对象尝试放入串池,如果有则不会放入,如果没有则会把此对象复制一份,放入串池,会把串池中的对象返回。
      • 注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象

1.8

学习黑马JVM的笔记_第20张图片

学习黑马JVM的笔记_第21张图片

因为ab在最开始就创建,串池中已经有ab这个字符串,new String(“ab”) 在堆里是一个对象,用intern方法,发现字符串常量池中有ab,所以s与ab不相等。

1.6.例题:

学习黑马JVM的笔记_第22张图片

最后false,x2是堆中的对象,x1是常量中cd,所以false.

2.StringTable位置

1.7版本之前StringTable放在方法区中,1.7之后放在堆中。原因:方法区的内存空间太小。

学习黑马JVM的笔记_第23张图片

学习黑马JVM的笔记_第24张图片

-Xmx10m

打印并查看串池信息:-XX:+PrintStringTableStatistics

3.StringTable垃圾回收

StringTable在内存紧张时,会发生垃圾回收

image-20210802204251999

4.StringTable性能调优
  • 调整

    -XX:StringTableSize=桶个数
    

6.直接内存

1.定义

direct memory

不属于jvm管理

  • 常见于nio操作时,用于数据缓冲区
  • 分配回收成本高,但读写性能高
  • 不受JVM内存回收管理

2.ByteBuffer

使用ByteBuffer比使用io的性能更高。

学习黑马JVM的笔记_第25张图片

在没有用ByteBuffer时,系统的内部操作时下面这样的
学习黑马JVM的笔记_第26张图片

使用了直接内存后,系统内部操作如下图。不再需要经过系统缓存区传给java缓冲区,他们共同划出一块缓冲区,java代码和系统都可以直接访问,大大的提升了效率。少了缓冲区的复制操作。
学习黑马JVM的笔记_第27张图片

3.直接内存内存溢出

学习黑马JVM的笔记_第28张图片

4.直接内存释放原理

直接内存的回收不是通过JVM的垃圾回收来释放的,拿到Unsafe对象,然后调用去分配和调用内存
学习黑马JVM的笔记_第29张图片

5.分配和回收原理

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

6.禁用显示回收对直接内存的影响

-XX:+DisableExpIicitGC  显式的

使用上面命令后,代码中调用显式回收将没有作用

System.gc();

三、垃圾回收

1.如何判断对象可以回收

1.引用计数法

1.定义

只要对象被引用就+1,引用两次就+2,如果某个变量不在引用就-1,当对象引用计数为0的时候就会被垃圾回收

2.弊端
学习黑马JVM的笔记_第30张图片 循环引用,A对象引用B对象,B对象引用计数+1,B对象引用A,A对象引用计数+1。当没有谁再引用他们,他们不能被垃圾回收,因为引用计数没有归零。python在早期垃圾回收用的引用计数法。

2.可达性分析算法(java虚拟机采用的方法)

1.定义
  • java虚拟机中的垃圾回收器采用的是可达性分析算法
  • 扫描堆中的对象,看是否能够沿着GC Root(根对象) 为起点的引用链找到该对象,找不到就可以进行垃圾回收
2.哪些对象可以作为GC Root?
  • //System Class

  • //Natice Stack(本地栈)

  • //锁(同步锁机制)

  • //Thread(活动线程)

  • a. java虚拟机栈中的引用的对象。

    b.本地方法栈中的JNI(native方法)引用的对象

    c.方法区中的类静态属性引用的对象。(一般指被static修饰的对象,加载类的时候就加载到内存中。)(static object)

    d.方法区中的常量引用的对象。 (object)

3.如何查看GC Root对象

通过MAT工具(Eclipse的Memory Analyzer)

3.五种引用

学习黑马JVM的笔记_第31张图片

1.强引用:

只有GC Root 都不引用该对象时,才会回收强引用对象。

2.软引用:

有用但非必须的引用

1.当GC Root 不在指向软引用对象时,且内存不足时,会回收软引用所引用的对象。
2.可以配合引用队列来释放软引用自身。

如上图 B对象不在引用A2对象且内存不足时,软引用所引用的A2对象会被回收。

使用:

public class Demo1 {
	public static void main(String[] args) {
		final int _4M = 4*1024*1024;
		//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
		List> list = new ArrayList<>();
		SoftReference ref= new SoftReference<>(new byte[_4M]);
	}
}

软引用本身不会被清理,需要使用引用队列

public class Demo1 {
	public static void main(String[] args) {
		final int _4M = 4*1024*1024;
		//使用引用队列,用于移除引用为空的软引用对象
		ReferenceQueue queue = new ReferenceQueue<>();
		//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
		List> list = new ArrayList<>();
		SoftReference ref= new SoftReference<>(new byte[_4M],queue);

		//遍历引用队列,如果有元素,则移除
		Reference poll = queue.poll();
		while(poll != null) {
			//引用队列不为空,则从集合中移除该元素
			list.remove(poll);
			//移动到引用队列中的下一个元素
			poll = queue.poll();
		}
	}
}
3.弱引用

1.当GC Root 不再指向弱引用对象时,不管内存是否不足,会回收弱引用所引用的对象。
2.可以配合引用队列来释放弱引用自身。

弱引用的使用和软引用类似,只是将 SoftReference 换为了 WeakReference

4.虚引用:

必须配合引用队列使用,主要配合 ByteBuffer 使用,当虚引用对象所引用的对象被回收以后,虚引用对象就会被放入引用队列中,由 Reference Handler 线程调用虚引用相关方法释放直接内存。

  • 虚引用的一个体现是释放直接内存所分配的内存,当被引用对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中,然后调用Cleaner的clean方法来释放直接内存
  • 如上图,B对象不再引用ByteBuffer对象,ByteBuffer就会被回收。但是直接内存中的内存还未被回收。这时需要将虚引用对象Cleaner放入引用队列中,然后调用它的clean方法来释放直接内存
5.终结器引用:

无需手动编码,在其内部配合引用队列使用,

在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

  • 当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到被引用的对象,然后调用被引用对象的finalize方法。调用以后,该对象再第二次GC就可以被垃圾回收了
  • 如上图,B对象不再引用A4对象。这是终结器引用对象就会被放入引用队列中,引用队列会根据它,找到它所引用的对象。然后调用被引用对象的finalize方法。调用以后,该对象就可以被垃圾回收了

引用队列:软引用和弱引用可以配合引用队列

2.垃圾回收算法

1.标记清除算法

学习黑马JVM的笔记_第32张图片
定义:在执行垃圾回收时,先标记完引用对象,然后垃圾收集器根据标识清除没有被标记的对象

优点:速度快
缺点:容易产生大量的内存碎片,如上图,清理没有引用的对象后,会存在内存的空间浪费。

2.标记整理算法

学习黑马JVM的笔记_第33张图片
定义:在执行垃圾回收时,先标记完引用的对象,然后清除没有被引用的对象,最后整理剩余的空间,避免因内存碎片导致的问题。

优点:不会存在内存碎片
缺点:速度慢,因为整理内存是为了避免内存浪费,所以整理需要消耗一定的时间,导致效率较低。

时间换取空间

3.复制算法

学习黑马JVM的笔记_第34张图片
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BwuRDFiy-1666189640764)(C:\Users\刘星\AppData\Roaming\Typora\typora-user-images\image-20210809153023339.png)]

定义:将内存分为两个等大小的区域,FROM和TO。先将FROM中被GC Root引用的对象进行标记,将存活的对象从FROM放入TO中,再回收FROM区域中没有被引用的对象。然后交换FROM和TO。

优点:这样避免的内存碎片的问题。
缺点:但需要双倍的内存空间。

空间换取时间

3.分代回收

学习黑马JVM的笔记_第35张图片
学习黑马JVM的笔记_第36张图片回收流程

  • 对象首先分配在伊甸园区域
  • 新生代伊甸园空间不足时,就会触发minor gc,伊甸园和幸存区From中存活的的对象复制到幸存区To中,存活的对象年龄加1并交换幸存区from和幸存区to。
  • minor gc会引发stop the world ,暂停其他用户的线程,等垃圾回收结束后,用户线程才恢复。
  • 当对象寿命超过阈值时,会从新生代注入到老年代,最大寿命是15(4bit)。
  • 当老年代空间不足,会先尝试触发minor gc, 如果空间仍不足,那么就触发full gc,stop the world 的时间更长。

3.1相关VM参数

堆初始大小 :-Xms

堆最大大小 -Xmx 或 -XX:MaxHeapSize=size

新生代大小: -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )

幸存区比例(动态): -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy

幸存区比例 :-XX:SurvivorRatio=ratio

晋升阈值: -XX:MaxTenuringThreshold=threshold

晋升详情: -XX:+PrintTenuringDistribution

GC详情: -XX:+PrintGCDetails -verbose:gc

FullGC 前 MinorGC: -XX:+ScavengeBeforeFullGC

4.垃圾回收器

1.相关概念

并行执行的线程之间不存在切换;并发操作系统会根据任务调度系统给线程分配线程的 CPU 执行时间,线程的执行会进行切换。

1.并行收集

并行:多个事情同一时刻进行
在同一时刻,有多个程序在多个处理器上运行(每个处理器运行一个程序)。

并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态

2.并发收集

并发: 指在某时刻只有一个事件在发生,某个时间段内由于 CPU 交替执行,可以发生多个事件。 在同一cpu上同时运行多个程序。

指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续执行,而垃圾收集程序在另一个CPU上。

3.吞吐量

即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))。例如:虚拟机共运行100分组,垃圾收集器花掉1分钟,那么吞吐量就是99%。

2.串行回收器

-XX:+UseSerialGC = Serial + SerialOld

学习黑马JVM的笔记_第37张图片

1.特点
  • 单线程
  • 内存较小,个人电脑(CPU核数较少)
  • 安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象
  • 因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态。
2.Serial收集器

Serial收集器是最基本的、发展历史最悠久的收集器。

特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)

3.ParNew 收集器

ParNew收集器其实就是Serial收集器的多线程版本。

特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,采用复制算法,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题

4.Serial Old 收集器

Serial Old是Serial收集器的老年代版本

特点:同样是单线程收集器,采用标记-整理算法

3.吞吐量优先

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n
学习黑马JVM的笔记_第38张图片
  • 多线程
  • 堆内存较大,多核CPU
  • 单位时间内,STW(stop the world,停掉其他所有工作线程)时间最短
  • JDK1.8默认使用的垃圾回收器
1.Parallel Scavenge 收集器

与吞吐量关系密切,故也称为吞吐量优先收集器

特点

属于新生代收集器,也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与ParNew收集器类似)

该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)

GC自适应调节策略

Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

Parallel Scavenge收集器使用两个参数控制吞吐量:

  • XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
  • XX:GCRatio 直接设置吞吐量的大小
2.Parallel Old 收集器

是Parallel Scavenge收集器的老年代版本

特点:多线程,采用标记-整理算法(老年代没有幸存区

4.响应时间优先

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark

学习黑马JVM的笔记_第39张图片

  • 多线程
  • 堆内存较大,多核CPU
  • 尽可能让单次STW时间变短(尽量不影响其他线程运行)
1.CMS 收集器

Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器

特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务

CMS收集器的运行过程分为下列4步:

初始标记:标记GC Roots对象。速度很快但是仍存在Stop The World问题

并发标记:进行GC Roots Tracing 的过程,找出GC Roots对象所关联的对象且用户线程可并发执行

重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(可达对象变不可达)。仍然存在Stop The World问题

并发清除:对没有标记的对象进行清除回收

CMS收集器内存回收过程是与用户线程一起并发执行

5.G1

1.定义

Garbage First

  • 2004年论文发布
  • 2009 JDK 6u14体验
  • 2012 JDK 7u4官方支持
  • 2017 JDK 9 默认
2.使用场景
  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停时间是200ms。
  • 超大堆内存,会将堆划分为多个等大的Region。
  • 整体上是标记 - 整理算法两个Region(区域)之间是复制算法
3.相关JVM参数
-XX:+UserG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time

第一个参数:开启G1
第二个参数:设置Region大小,必须设置成,1,2,4,8这样的大小
第三个参数:设置暂停时间ms

4.G1垃圾回收阶段

学习黑马JVM的笔记_第40张图片

第一阶段对新生代进行收集(Young Collection),第二阶段对新生代的收集同时会执行并发标记(Young Collection+ Concurrent Mark) ,第三阶段对新生代、新生代幸存区和老年区进行混合收集(Mixed Collection),以此循环。

Garbage First 将堆划分大小相等的一个个区域,每个区域都可以作为新生代、幸存区和老年代。

E代表伊甸园区域
S代表幸存区
O代表老年代

1.Young Collection(新生代收集)
  • 会STW(Stop The World),但相对于时间还是比较短的

学习黑马JVM的笔记_第41张图片

学习黑马JVM的笔记_第42张图片
  • 新生代垃圾回收会将幸存对象以复制算法复制到幸存区。

学习黑马JVM的笔记_第43张图片

  • 新生代垃圾回收会将幸存对象以复制算法复制到幸存区,幸存区存活的对象达到阈值后会以复制算法复制到老年代
2.Young Collection+ Concurrent Mark(新生代收集+并发标记)

初始标记:找到GC Root(根对象)
并发标记:和应用程序并发执行,针对区域内所有的存活对象进行标记。

  • Young GC时会进行GC Root的**初始标记 **

  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定

    -XX:InitiatingHeapOccupancyPercent=percent   (默认45%)
    
学习黑马JVM的笔记_第44张图片 当o占到45%就进行并发标记了
3.Mixed Collection(混合收集)

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

最终标记:在并发标记的过程中,可能会漏掉一些对象,因为并发标记的同时,其他用户线程还在工作,产生一些垃圾,所以进行最终标记。清理没被标记的对象。

  • 最终标记(Remark)会STW
  • 拷贝存活(Evacuation)会STW
-XX:MaxGCPauseMillis=ms

学习黑马JVM的笔记_第45张图片

过程:在进行混合回收时,新生代垃圾回收会将幸存对象以复制算法复制到幸存区,幸存区存活的对象达到阈值后会以复制算法复制到老年代,老年代中根据最大暂停时间有选择的进行回收,回收价值最高的,将老年代中存活下来的对象以复制算法重新赋值到一个新的老年代中。

5.Full GC

SerialGC

  • 新生代内存不足发生的垃圾收集- minor gc
  • 老年代内存不足发生的垃圾收集- full gc

ParallelGC

  • 新生代内存不足发生的垃圾收集- minor gc
  • 老年代内存不足发生的垃圾收集- full gc

CMS

  • 新生代内存不足发生的垃圾收集- minor gc
  • 老年代内存不足,并发失败后,进行串行收集 full gc

G1

  • 新生代内存不足发生的垃圾收集- minor gc
  • 老年代内存不足,当垃圾回收速度跟不上产生速度,退化为一个串行收集,开始Full GC
6.Young Collection 跨代引用
  • 卡表与Remembered Set

    • Remembered Set存在于E中,用于保存新生代对象对应的脏卡
      • 脏卡: 老年代被划分为多个区域(一个区域512K),如果该
        区域引用了新生代对象,则该区域被称为脏卡
  • 在引用变更时通过post-write barried + dirty card queue

  • concurrent refinement threads更新Remembered Set

  • 新生代回收的跨代引用(老年代引用新生代)问题

    学习黑马JVM的笔记_第46张图片

在进行新生代回收时要找到GC Root根对象。有一部分GC Root对象是来自老年代,老年代存活的对象很多,如果遍历老年代找根对象效率非常低,采用卡表(Card Table)的技术,将老年代分成一个个Card,每个Card差不多512k, 老年代其中一个对象引用了新生代对象,那么就称这个Card为脏卡(dirty card)。

学习黑马JVM的笔记_第47张图片

将来进行垃圾回收时不需要找整个老年代,只需要找脏卡区就行了

7.Remark(重新标记)
pre-write barrier+ satb_mark_queue

在垃圾回收时,收集器处理对象的过程中

黑色:已被处理,需要保留的
灰色:正在处理中的
白色:还未处理的

学习黑马JVM的笔记_第48张图片

但是在并发标记过程中,有可能A被处理了以后未引用C,但该处理过程还未结束,在处理过程结束之前A引用了C,这时就会用到remark

过程如下:

  • 之前C未被引用,这时A引用了C,就会给C加一个写屏障,写屏障的指令会被执行,将C放入一个队列当中,并将C变为处理中状态
  • 并发标记阶段结束以后,重新标记阶段会STW,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它

学习黑马JVM的笔记_第49张图片

8.JDK 8u20 字符串去重

过程

  • 将所有新分配的字符串(底层是char[])放入一个队列
  • 当新生代回收时,G1并发检查是否有重复的字符串
  • 如果字符串的值一样,就让他们引用同一个字符串对象
  • 注意,其与String.intern的区别
    • intern关注的是字符串对象
    • 字符串去重关注的是char[]
    • 在JVM内部,使用了不同的字符串标

优点与缺点

  • 节省了大量内存
  • 新生代回收时间略微增加,导致略微多占用CPU
9.JDK 8u40 并发标记类卸载

在并发标记阶段结束以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类

10.JDK 8u60 回收巨型对象
  • 一个对象大于region的一半时,就称为巨型对象
  • G1不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

学习黑马JVM的笔记_第50张图片

11.JDK 9并发标记起始时间的调整
  • 并发标记必须在堆空间占满前完成,否则退化为FullGC

  • JDK9之前需要使用-Xx: Initiat ingHeapOccupancyPercent

  • JDK9可以动态调整

    • -XX:InitiatingHeapoccupancyPercent 用来设置初始值

    • 进行数据采样并动态调整

    • 总会添加一个安全的空档空间

5.垃圾回收调优

5.GC 调优

查看虚拟机参数命令

"F:\JAVA\JDK8.0\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"

可以根据参数去查询具体的信息

1.调优领域
  • 内存
  • 锁竞争
  • CPU占用
  • IO
  • GC
2.确定目标

低延迟/高吞吐量? 选择合适的GC

  • CMS G1 ZGC
  • ParallelGC
  • Zing
3.最快的GC是不发生GC

首先排除减少因为自身编写的代码而引发的内存问题

  • 查看Full GC前后的内存占用,考虑以下几个问题
    • 数据是不是太多?
    • 数据表示是否太臃肿
      • 对象图
      • 对象大小
    • 是否存在内存泄漏
4.新生代调优
  • 新生代的特点
    • 所有的new操作分配内存都是非常廉价的
      • TLAB
    • 死亡对象回收零代价
    • 大部分对象用过即死(朝生夕死)
    • MInor GC 所用时间远小于Full GC
  • 新生代内存越大越好么?
    • 不是
      • 新生代内存太小:频繁触发Minor GC,会STW,会使得吞吐量下降
      • 新生代内存太大:老年代内存占比有所降低,会更频繁地触发Full GC。而且触发Minor GC时,清理新生代所花费的时间会更长
    • 新生代内存设置为内容纳[并发量*(请求-响应)]的数据为宜
5.幸存区调优
  • 幸存区需要能够保存 当前活跃对象+需要晋升的对象
  • 晋升阈值配置得当,让长时间存活的对象尽快晋升
6.老年代调优

四、类加载与字节码技术

学习黑马JVM的笔记_第51张图片

1.类文件结构

首先获得.class文件
方法:

javac X:.../XX.java

以下是字节码文件

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e 
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63 
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01 
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63 
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f 
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16 
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13 
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61 
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46 
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e 
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74 
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61 
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61 
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f 
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72 
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76 
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a 
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01 
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00 
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00 
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00 
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00 
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a 
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b 
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00 
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00 
0001120 00 00 02 00 14

根据 JVM 规范,类文件结构如下

u4 			  magic
u2             minor_version;    
u2             major_version;    
u2             constant_pool_count;    
cp_info        constant_pool[constant_pool_count-1];    
u2             access_flags;    
u2             this_class;    
u2             super_class;   
u2             interfaces_count;    
u2             interfaces[interfaces_count];   
u2             fields_count;    
field_info     fields[fields_count];   
u2             methods_count;    
method_info    methods[methods_count];    
u2             attributes_count;    
attribute_info attributes[attributes_count];

1.魔数

u4 magic
对应着字节码文件的0~3个字节
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
表示是java

2.版本

u2 minor_version;

u2 major_version;

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
00 00 表示小版本
00 34 主版本,表示52,代表JDK8

3.常量池

学习黑马JVM的笔记_第52张图片

8~9 字节,表示常量池长度,00 23 (35) 表示常量池有 #1~#34项,注意 #0 项不计入,也没有值 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

第#1项 0a 表示一个 Method 信息,00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 项来获得 这个方法的【所属类】和【方法名】 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

第#2项 09 表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和 # 23 项 来获得这个成员变量的【所属类】和【成员变量名】 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

略…

4.访问标识与继承信息

学习黑马JVM的笔记_第53张图片

21 表示该 class 是一个类,公共的
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 05
表示根据常量池中 #5 找到本类全限定名
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 06
表示根据常量池中 #6 找到父类全限定名
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
表示接口的数量,本类为 0
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

5.Field信息

学习黑马JVM的笔记_第54张图片

表示成员变量数量,本类为 0
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

6.Method信息

表示方法数量,本类为 2
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成

7 附加属性

00 01 表示附加属性数量
00 13 表示引用了常量池 #19 项,即【SourceFile】
00 00 00 02 表示此属性的长度
00 14 表示引用了常量池 #20 项,即【HelloWorld.java】
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00 0001120 00 00 02 00 14

2.字节码指令

1.入门

public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令
2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
b7 => invokespecial 预备调用构造方法,哪个方法呢?
00 01 引用常量池中 #1 项,即【 Method java/lang/Object.“init”)V 】
b1 表示返回

public static void main(java.lang.String[]); 主方法的字节码指令
b2 => getstatic 用来加载静态变量,哪个静态变量呢?
00 02 引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】
12 => ldc 加载参数,哪个参数呢?
03 引用常量池中 #3 项,即 【String hello world】
b6 => invokevirtual 预备调用成员方法,哪个方法呢?
00 04 引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
b1 表示返回

2 javap 工具

自己分析类文件结构太麻烦了,Oracle 提供了 javap 工具来反编译 class 文件

[root@localhost ~]# javap -v HelloWorld.class
Classfile /root/HelloWorld.class
Last modified Jul 7, 2019; size 597 bytes
MD5 checksum 361dca1c3f4ae38644a9cd5060ac6dbc
Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#21 // java/lang/Object."":()V
#2 = Fieldref #22.#23 //
java/lang/System.out:Ljava/io/PrintStream;
#3 = String #24 // hello world
#4 = Methodref #25.#26 // java/io/PrintStream.println:
(Ljava/lang/String;)V
#5 = Class #27 // cn/itcast/jvm/t5/HelloWorld
#6 = Class #28 // java/lang/Object
#7 = Utf8 
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcn/itcast/jvm/t5/HelloWorld;
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 MethodParameters
#19 = Utf8 SourceFile
#20 = Utf8 HelloWorld.java
#21 = NameAndType #7:#8 // "":()V
#22 = Class #29 // java/lang/System
#23 = NameAndType #30:#31 // out:Ljava/io/PrintStream;
#24 = Utf8 hello world
#25 = Class #32 // java/io/PrintStream
#26 = NameAndType #33:#34 // println:(Ljava/lang/String;)V
#27 = Utf8 cn/itcast/jvm/t5/HelloWorld
#28 = Utf8 java/lang/Object
#29 = Utf8 java/lang/System
#30 = Utf8 out
#31 = Utf8 Ljava/io/PrintStream;
#32 = Utf8 java/io/PrintStream
#33 = Utf8 println
#34 = Utf8 (Ljava/lang/String;)V
{
public cn.itcast.jvm.t5.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t5/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field
java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method
java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
MethodParameters:
Name Flags
args
}

3.图解方法执行流程

1.代码
package cn.itcast.jvm.t3.bytecode;
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
public class Demo3_1 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
2.编译后的字节码文件
[root@localhost ~]# javap -v Demo3_1.class
Classfile /root/Demo3_1.class
Last modified Jul 7, 2019; size 665 bytes
MD5 checksum a2c29a22421e218d4924d31e6990cfc5
Compiled from "Demo3_1.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#26 // java/lang/Object."":()V
#2 = Class #27 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #28.#29 //
java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #30.#31 // java/io/PrintStream.println:(I)V
#6 = Class #32 // cn/itcast/jvm/t3/bytecode/Demo3_1
#7 = Class #33 // java/lang/Object
#8 = Utf8 
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcn/itcast/jvm/t3/bytecode/Demo3_1;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
...
3.常量池载入运行时常量池

常量池也属于方法区,只不过这里单独提出来了
学习黑马JVM的笔记_第55张图片

4.方法字节码载入方法区

(stack=2,locals=4) 对应操作数栈有2个空间(每个空间4个字节),局部变量表中有4个槽位
学习黑马JVM的笔记_第56张图片

5.main 线程开始运行,分配栈帧内存

main 线程开始运行,分配栈帧内存
学习黑马JVM的笔记_第57张图片
绿色:局部变量表
红色:操作数栈

执行引擎开始执行字节码

bipush 10

  • 将一个 byte 压入操作数栈

    (其长度会补齐 4 个字节),类似的指令还有

    • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
    • ldc 将一个 int 压入操作数栈
    • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
    • 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池

istore 1
将操作数栈栈顶元素弹出,放入局部变量表的slot 1中
对应代码中的

a = 10

学习黑马JVM的笔记_第58张图片

ldc #3
读取运行时常量池中#3,即32768(超过short最大值范围的数会被放到运行时常量池中),将其加载到操作数栈中
学习黑马JVM的笔记_第59张图片

注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的

istore 2
将操作数栈中的元素弹出,放到局部变量表的2号位置

iload1 iload2

将局部变量表中1号位置和2号位置的元素放入操作数栈中

  • 因为只能在操作数栈中执行运算操作

学习黑马JVM的笔记_第60张图片

iadd
将操作数栈中的两个元素弹出栈并相加,结果在压入操作数栈中
学习黑马JVM的笔记_第61张图片

istore 3

将操作数栈中的元素弹出,放入局部变量表的3号位置

getstatic #4

在运行时常量池中找到#4,发现是一个对象

在堆内存中找到该对象,并将其引用放入操作数栈中
学习黑马JVM的笔记_第62张图片

iload 3

将局部变量表中3号位置的元素压入操作数栈中

invokevirtual 5

找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法

生成新的栈帧(分配 locals、stack等)

传递参数,执行新栈帧中的字节码

学习黑马JVM的笔记_第63张图片

执行完毕,弹出栈帧

清除 main 操作数栈内容
学习黑马JVM的笔记_第64张图片

return
完成 main 方法调用,弹出 main 栈帧,程序结束

4.通过字节码指令来分析问题

代码

public class Demo2 {
	public static void main(String[] args) {
		int i=0;
		int x=0;
		while(i<10) {
			x = x++;
			i++;
		}
		System.out.println(x); //接过为0
	}
}

最终的x结果为0, 通过分析字节码指令即可知晓

Code:
     stack=2, locals=3, args_size=1	//操作数栈分配2个空间,局部变量表分配3个空间
        0: iconst_0	//准备一个常数0
        1: istore_1	//将常数0放入局部变量表的1号槽位 i=0
        2: iconst_0	//准备一个常数0
        3: istore_2	//将常数0放入局部变量的2号槽位 x=0	
        4: iload_1		//将局部变量表1号槽位的数放入操作数栈中
        5: bipush        10	//将数字10放入操作数栈中,此时操作数栈中有2个数
        7: if_icmpge     21	//比较操作数栈中的两个数,如果下面的数大于上面的数,就跳转到21。这里的比较是将两个数做减法。因为涉及运算操作,所以会将两个数弹出操作数栈来进行运算。运算结束后操作数栈为空
       10: iload_2		//将局部变量2号槽位的数放入操作数栈中,放入的值是0
       11: iinc          2, 1	//将局部变量2号槽位的数加1,自增后,槽位中的值为1
       14: istore_2	//将操作数栈中的数放入到局部变量表的2号槽位,2号槽位的值又变为了0
       15: iinc          1, 1 //1号槽位的值自增1
       18: goto          4 //跳转到第4条指令
       21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       24: iload_2
       25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
       28: return

5.构造方法

1.cinit()V
public class Demo3 {
	static int i = 10;

	static {
		i = 20;
	}

	static {
		i = 30;
	}

	public static void main(String[] args) {
		System.out.println(i); //结果为30
	}
}

编译器会按从上至下的顺序,收集所有 static 静态代码块静态成员赋值的代码,合并为一个特殊的方法 cinit()V :

stack=1, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #3                  // Field i:I
         5: bipush        20
         7: putstatic     #3                  // Field i:I
        10: bipush        30
        12: putstatic     #3                  // Field i:I
        15: return
2.init()V
public class Demo4 {
	private String a = "s1";

	{
		b = 20;
	}

	private int b = 10;

	{
		a = "s2";
	}

	public Demo4(String a, int b) {
		this.a = a;
		this.b = b;
	}

	public static void main(String[] args) {
		Demo4 d = new Demo4("s3", 30);
		System.out.println(d.a);
		System.out.println(d.b);
	}
}

编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在后

Code:
     stack=2, locals=3, args_size=3
        0: aload_0
        1: invokespecial #1                  // Method java/lang/Object."":()V
        4: aload_0
        5: ldc           #2                  // String s1
        7: putfield      #3                  // Field a:Ljava/lang/String;
       10: aload_0
       11: bipush        20
       13: putfield      #4                  // Field b:I
       16: aload_0
       17: bipush        10
       19: putfield      #4                  // Field b:I
       22: aload_0
       23: ldc           #5                  // String s2
       25: putfield      #3                  // Field a:Ljava/lang/String;
       //原始构造方法在最后执行
       28: aload_0
       29: aload_1
       30: putfield      #3                  // Field a:Ljava/lang/String;
       33: aload_0
       34: iload_2
       35: putfield      #4                  // Field b:I
       38: return

6.方法调用

public class Demo5 {
	public Demo5() {

	}

	private void test1() {

	}

	private final void test2() {

	}

	public void test3() {

	}

	public static void test4() {

	}

	public static void main(String[] args) {
		Demo5 demo5 = new Demo5();
		demo5.test1();
		demo5.test2();
		demo5.test3();
		Demo5.test4();
	}
}

不同方法在调用时,对应的虚拟机指令有所区别

  • 私有、构造、被final修饰的方法,在调用时都使用invokespecial指令
  • 普通成员方法在调用时,使用invokevirtual指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定
  • 静态方法在调用时使用invokestatic指令

7.多态原理

因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用invokevirtual指令

在执行invokevirtual指令时,经历了以下几个步骤

  • 先通过栈帧中对象的引用找到对象
  • 分析对象头,找到对象实际的Class
  • Class结构中有vtable
  • 查询vtable找到方法的具体地址
  • 执行方法的字节码

8.异常处理

try-catch
public class Demo1 {
	public static void main(String[] args) {
		int i = 0;
		try {
			i = 10;
		}catch (Exception e) {
			i = 20;
		}
	}
}

对应字节码指令

Code:
     stack=1, locals=3, args_size=1
        0: iconst_0
        1: istore_1
        2: bipush        10
        4: istore_1
        5: goto          12
        8: astore_2
        9: bipush        20
       11: istore_1
       12: return
     //多出来一个异常表
     Exception table:
        from    to  target type
            2     5     8   Class java/lang/Exception
  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测2~4行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
  • 8行的字节码指令 astore_2 是将异常对象引用存入局部变量表的2号位置(为e)
多个single-catch
public class Demo1 {
	public static void main(String[] args) {
		int i = 0;
		try {
			i = 10;
		}catch (ArithmeticException e) {
			i = 20;
		}catch (Exception e) {
			i = 30;
		}
	}
}

对应的字节码

Code:
     stack=1, locals=3, args_size=1
        0: iconst_0
        1: istore_1
        2: bipush        10
        4: istore_1
        5: goto          19
        8: astore_2
        9: bipush        20
       11: istore_1
       12: goto          19
       15: astore_2
       16: bipush        30
       18: istore_1
       19: return
     Exception table:
        from    to  target type
            2     5     8   Class java/lang/ArithmeticException
            2     5    15   Class java/lang/Exception
  • 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用
finally
public class Demo2 {
	public static void main(String[] args) {
		int i = 0;
		try {
			i = 10;
		} catch (Exception e) {
			i = 20;
		} finally {
			i = 30;
		}
	}
}

对应字节码

Code:
     stack=1, locals=4, args_size=1
        0: iconst_0
        1: istore_1
       -------------------------------------------------
        //try块
        2: bipush        10
        4: istore_1
        //try块执行完后,会执行finally    
        5: bipush        30
        7: istore_1
        8: goto          27
       -------------------------------------------------
       //catch块     
       11: astore_2 //异常信息放入局部变量表的2号槽位
       12: bipush        20
       14: istore_1
       //catch块执行完后,会执行finally        
       15: bipush        30
       17: istore_1
       -------------------------------------------------
       18: goto          27
       //出现异常,但未被Exception捕获,会抛出其他异常,这时也需要执行finally块中的代码   
       21: astore_3
       22: bipush        30
       24: istore_1
       -------------------------------------------------
       25: aload_3
       26: athrow  //抛出异常
       27: return
     Exception table:
        from    to  target type
            2     5    11   Class java/lang/Exception
            2     5    21   any
           11    15    21   any

可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch剩余的异常类型流程

注意:虽然从字节码指令看来,每个块中都有finally块,但是finally块中的代码只会被执行一次

finally中的return
public class Demo3 {
	public static void main(String[] args) {
		int i = Demo3.test();
        //结果为20
		System.out.println(i);
	}

	public static int test() {
		int i;
		try {
			i = 10;
			return i;
		} finally {
			i = 20;
			return i;
		}
	}
}

对应字节码

Code:
     stack=1, locals=3, args_size=0
        0: bipush        10
        2: istore_0
        3: iload_0
        4: istore_1  //暂存返回值
        5: bipush        20
        7: istore_0
        8: iload_0
        9: ireturn	//ireturn会返回操作数栈顶的整型值20
       //如果出现异常,还是会执行finally块中的内容,没有抛出异常
       10: astore_2
       11: bipush        20
       13: istore_0
       14: iload_0
       15: ireturn	//这里没有athrow了,也就是如果在finally块中如果有返回操作的话,且try块中出现异常,会吞掉异常!
     Exception table:
        from    to  target type
            0     5    10   any
  • 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以finally的为准
  • 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
  • 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常
  • 所以不要在finally中进行返回操作
被吞掉的异常
public class Demo3 {
   public static void main(String[] args) {
      int i = Demo3.test();
      //最终结果为20
      System.out.println(i);
   }

   public static int test() {
      int i;
      try {
         i = 10;
         //这里应该会抛出异常
         i = i/0;
         return i;
      } finally {
         i = 20;
         return i;
      }
   }
}

会发现打印结果为20,并未抛出异常

finally不带return
public class Demo4 {
	public static void main(String[] args) {
	//最终结果为10
		int i = Demo4.test();
		System.out.println(i);
	}

	public static int test() {
		int i = 10;
		try {
			return i;
		} finally {
			i = 20;
		}
	}
}

对应字节码

Code:
     stack=1, locals=3, args_size=0
        0: bipush        10
        2: istore_0 //赋值给i 10
        3: iload_0	//加载到操作数栈顶
        4: istore_1 //加载到局部变量表的1号位置
        5: bipush        20
        7: istore_0 //赋值给i 20
        8: iload_1 //加载局部变量表1号位置的数10到操作数栈
        9: ireturn //返回操作数栈顶元素 10
       10: astore_2
       11: bipush        20
       13: istore_0
       14: aload_2 //加载异常
       15: athrow //抛出异常
     Exception table:
        from    to  target type
            3     5    10   any

finally中有return时,会直接在finally中退出,导致try、catch中的return失效。

3.语法糖 编译器处理

所谓的语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利

注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。

1.默认构造函数

public class Candy1 {

}

经过编译期优化后

public class Candy1 {
   //这个无参构造器是java编译器帮我们加上的
   public Candy1() {
      //即调用父类 Object 的无参构造方法,即调用 java/lang/Object." ":()V
      super();
   }
}

2.自动拆装箱

基本类型和其包装类型的相互转换过程,称为拆装箱

在JDK 5以后,它们的转换可以在编译期自动完成

public class Demo2 {
   public static void main(String[] args) {
      Integer x = 1;
      int y = x;
   }
}

转换过程如下

public class Demo2 {
   public static void main(String[] args) {
      //基本类型赋值给包装类型,称为装箱
      Integer x = Integer.valueOf(1);
      //包装类型赋值给基本类型,称谓拆箱
      int y = x.intValue();
   }
}

3.泛型集合取值

泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:

public class Demo3 {
   public static void main(String[] args) {
      List list = new ArrayList<>();
      list.add(10);
      Integer x = list.get(0);
   }
}

对应字节码

Code:
    stack=2, locals=3, args_size=1
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."":()V
       7: astore_1
       8: aload_1
       9: bipush        10
      11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      //这里进行了泛型擦除,实际调用的是add(Objcet o)
      14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

      19: pop
      20: aload_1
      21: iconst_0
      //这里也进行了泛型擦除,实际调用的是get(Object o)   
      22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
//这里进行了类型转换,将Object转换成了Integer
      27: checkcast     #7                  // class java/lang/Integer
      30: astore_2
      31: return

所以调用get函数取值时,有一个类型转换的操作

Integer x = (Integer) list.get(0);

如果要将返回结果赋值给一个int类型的变量,则还有自动拆箱的操作

int x = (Integer) list.get(0).intValue();

4.可变参数

public class Demo4 {
   public static void foo(String... args) {
      //将args赋值给arr,可以看出String...实际就是String[] 
      String[] arr = args;
      System.out.println(arr.length);
   }

   public static void main(String[] args) {
      foo("hello", "world");
   }
}

可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。 同 样 java 编译器会在编译期间将上述代码变换为:

public class Demo4 {
   public Demo4 {}

    
   public static void foo(String[] args) {
      String[] arr = args;
      System.out.println(arr.length);
   }

   public static void main(String[] args) {
      foo(new String[]{"hello", "world"});
   }
}

注意,如果调用的是foo(),即未传递参数时,等价代码为foo(new String[]{}),创建了一个空数组,而不是直接传递的null

5.foreach

public class Demo5 {
	public static void main(String[] args) {
        //数组赋初值的简化写法也是一种语法糖。
		int[] arr = {1, 2, 3, 4, 5};
		for(int x : arr) {
			System.out.println(x);
		}
	}
}

编译器会帮我们转换为

public class Demo5 {
    public Demo5 {}

	public static void main(String[] args) {
		int[] arr = new int[]{1, 2, 3, 4, 5};
		for(int i=0; i

如果是集合使用foreach

public class Demo5 {
   public static void main(String[] args) {
      List list = Arrays.asList(1, 2, 3, 4, 5);
      for (Integer x : list) {
         System.out.println(x);
      }
   }
}

集合要使用foreach,需要该集合类实现了Iterable接口,因为集合的遍历需要用到迭代器Iterator

public class Demo5 {
    public Demo5 {}
    
   public static void main(String[] args) {
      List list = Arrays.asList(1, 2, 3, 4, 5);
      //获得该集合的迭代器
      Iterator iterator = list.iterator();
      while(iterator.hasNext()) {
         Integer x = iterator.next();
         System.out.println(x);
      }
   }
}

6.switch字符串

public class Demo6 {
   public static void main(String[] args) {
      String str = "hello";
      switch (str) {
         case "hello" :
            System.out.println("h");
            break;
         case "world" :
            System.out.println("w");
            break;
         default:
            break;
      }
   }
}

在编译器中执行的操作

public class Demo6 {
   public Demo6() {
      
   }
   public static void main(String[] args) {
      String str = "hello";
      int x = -1;
      //通过字符串的hashCode+value来判断是否匹配
      switch (str.hashCode()) {
         //hello的hashCode
         case 99162322 :
            //再次比较,因为字符串的hashCode有可能相等
            if(str.equals("hello")) {
               x = 0;
            }
            break;
         //world的hashCode
         case 11331880 :
            if(str.equals("world")) {
               x = 1;
            }
            break;
         default:
            break;
      }

      //用第二个switch在进行输出判断
      switch (x) {
         case 0:
            System.out.println("h");
            break;
         case 1:
            System.out.println("w");
            break;
         default:
            break;
      }
   }
}

过程说明:

  • 在编译期间,单个的switch被分为了两个
    • 第一个用来匹配字符串,并给x赋值
      • 字符串的匹配用到了字符串的hashCode,还用到了equals方法
      • 使用hashCode是为了提高比较效率,使用equals是防止有hashCode冲突(如BM和C.)
    • 第二个用来根据x的值来决定输出语句

7.switch枚举

public class Demo7 {
   public static void main(String[] args) {
      SEX sex = SEX.MALE;
      switch (sex) {
         case MALE:
            System.out.println("man");
            break;
         case FEMALE:
            System.out.println("woman");
            break;
         default:
            break;
      }
   }
}

enum SEX {
   MALE, FEMALE;
}

编译器中执行的代码如下

public class Demo7 {
   /**     
    * 定义一个合成类(仅 jvm 使用,对我们不可见)     
    * 用来映射枚举的 ordinal 与数组元素的关系     
    * 枚举的 ordinal 表示枚举对象的序号,从 0 开始     
    * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1     
    */ 
   static class $MAP {
      //数组大小即为枚举元素个数,里面存放了case用于比较的数字
      static int[] map = new int[2];
      static {
         //ordinal即枚举元素对应所在的位置,MALE为0,FEMALE为1
         map[SEX.MALE.ordinal()] = 1;
         map[SEX.FEMALE.ordinal()] = 2;
      }
   }

   public static void main(String[] args) {
      SEX sex = SEX.MALE;
      //将对应位置枚举元素的值赋给x,用于case操作
      int x = $MAP.map[sex.ordinal()];
      switch (x) {
         case 1:
            System.out.println("man");
            break;
         case 2:
            System.out.println("woman");
            break;
         default:
            break;
      }
   }
}

enum SEX {
   MALE, FEMALE;
}

8.枚举类

enum SEX {
   MALE, FEMALE;
}

转换后的代码

public final class Sex extends Enum {   
   //对应枚举类中的元素
   public static final Sex MALE;    
   public static final Sex FEMALE;    
   private static final Sex[] $VALUES;
   
    static {       
    	//调用构造函数,传入枚举元素的值及ordinal
    	MALE = new Sex("MALE", 0);    
        FEMALE = new Sex("FEMALE", 1);   
        $VALUES = new Sex[]{MALE, FEMALE}; 
   }
 	
   //调用父类中的方法
    private Sex(String name, int ordinal) {     
        super(name, ordinal);    
    }
   
    public static Sex[] values() {  
        return $VALUES.clone();  
    }
    public static Sex valueOf(String name) { 
        return Enum.valueOf(Sex.class, name);  
    } 
   
}

9.匿名内部类

public class Demo8 {
   public static void main(String[] args) {
      Runnable runnable = new Runnable() {
         @Override
         public void run() {
            System.out.println("running...");
         }
      };
   }
}

转换后的代码

public class Demo8 {
   public static void main(String[] args) {
      //用额外创建的类来创建匿名内部类对象
      Runnable runnable = new Demo8$1();
   }
}

//创建了一个额外的类,实现了Runnable接口
final class Demo8$1 implements Runnable {
   public Demo8$1() {}

   @Override
   public void run() {
      System.out.println("running...");
   }
}

如果匿名内部类中引用了局部变量

public class Demo8 {
   public static void main(String[] args) {
      int x = 1;
      Runnable runnable = new Runnable() {
         @Override
         public void run() {
            System.out.println(x);
         }
      };
   }
}

转化后代码

public class Demo8 {
   public static void main(String[] args) {
      int x = 1;
      Runnable runnable = new Runnable() {
         @Override
         public void run() {
            System.out.println(x);
         }
      };
   }
}

final class Demo8$1 implements Runnable {
   //多创建了一个变量
   int val$x;
   //变为了有参构造器
   public Demo8$1(int x) {
      this.val$x = x;
   }

   @Override
   public void run() {
      System.out.println(val$x);
   }
}

注意这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是final的:因为在创建
Candy11$1对象时,将X的值赋值给了对象val的x属性,所以X不应该再发生变化。如果变化,那么val的x属性没有机会再跟着一起变化

4.类加载阶段

1.加载

将类的字节码载入方法区中
内部采用C++的instanceKlass描述java类,它的重要field有:
_java_mirror 即java的类镜像,例如对String来说,就是String.class,作用是把klass保留给java使用
_super 即父类
_fields 即成员变量
_methods 即方法
_constants 即类加载器
_vtable 虚方法表
_itable 接口方法表
如果这个类还有父类没有加载,先加载父类
加载链接可能是交替运行的
注:instanceKlass 这样的元数据是存储在方法区(1.8后的元空间内),但 _java_mirror是存储在堆中
可以通过前面介绍的HSDB工具查看
学习黑马JVM的笔记_第65张图片

2.链接

1.验证
  • 验证类是否复核JVM规范,安全性检查
    例如:修改HelloWorld。class的魔数,在控制台运行,报出异常
E:\git\jvm\out\production\jvm>java cn.itcast.jvm.t5.HelloWorld
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value
3405691578 in class file cn/itcast/jvm/t5/HelloWorld
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at
java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
2.准备
  • 为static变量分配空间,设置默认值
    • static 变量在JDK 7 之前存储于instanceKlass末尾(存储于方法区),从JDK 7开始,存储于_java_mirror 末尾(存储于堆中)
    • static变量分配空间赋值时两个步骤,分配空间准备阶段完成,赋值初始化阶段完成
    • 如果 static 变量是final的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
    • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
3.解析
  • 迅即将常量池中的符号引用图换位直接引用的过程

3.初始化

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

1.发生时机
  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
    Class.forName
  • new 会导致初始化
2.以下情况不会初始化
  • 访问类的 static final 静态常量(基本类型和字符串)
  • 类对象.class 不会触发初始化
  • 创建该类对象的数组
  • 类加载器的.loadClass方法
  • Class.forNamed的参数2为false时

验证类是否被初始化,可以看改类的静态代码块是否被执行

5.类加载器

1.基本介绍

Java虚拟机设计团队有意把类加载阶段中的**“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”**(ClassLoader)

类与类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等

名称 加载的类 说明
Bootstrap ClassLoader(启动类加载器) JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader(拓展类加载器) JAVA_HOME/jre/lib/ext 上级为Bootstrap,显示为null
Application ClassLoader(应用程序类加载器) classpath 上级为Extension
自定义类加载器 自定义 上级为Application

2.启动类加载器

Bootstrap ClassLoader

可通过在控制台输入指令,使得类被启动类加器加载

用 Bootstrap 类加载器加载类:

package cn.itcast.jvm.t3.load;
public class F {
static {
System.out.println("bootstrap F init");
}
}

执行

package cn.itcast.jvm.t3.load;
public class Load5_1 {
public static void main(String[] args) throws ClassNotFoundException {
Class aClass = Class.forName("cn.itcast.jvm.t3.load.F");
System.out.println(aClass.getClassLoader());
}
}

输出

E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5
bootstrap F init
null
  • -Xbootclasspath 表示设置 bootclasspath
  • 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
  • 可以用这个办法替换核心类
    • java -Xbootclasspath:
    • java -Xbootclasspath/a:<追加路径>
    • java -Xbootclasspath/p:<追加路径>

3.扩展类加载器

Extension ClassLoader

如果classpath和JAVA_HOME/jre/lib/ext 下有同名类,加载时会使用拓展类加载器加载。当应用程序类加载器发现拓展类加载器已将该同名类加载过了,则不会再次加载。

将代码打包放在JAVA_HOME/jre/lib/ext下,

package cn.itcast.jvm.t3.load;
public class G {
static {
System.out.println("ext G init");
}
}

打包

E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)

执行

package cn.itcast.jvm.t3.load;
public class Load5_1 {
public static void main(String[] args) throws ClassNotFoundException {
Class aClass = Class.forName("cn.itcast.jvm.t3.load.F");
System.out.println(aClass.getClassLoader());
}
}

输出

ext G init
sun.misc.Launcher$ExtClassLoader@29453f44

4.应用程序类加载器

Application ClassLoader

自己写的类都是由应用程序类加载器加载

Classs aClass=Class.forName("cn.itcast.jvm.t3.load.F");
System.out.println(aClass.getClassLoader());

5.双亲委派模式

概念

指一个类在收到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类加载器去完成,其父类加载器在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中。若父类加载器在接收到类加载请求后发现自己也无法加载该类(通常原因是该类的Class文件在父类的类加载路径中不存在),则父类会将该信息反馈给子类并向下委派子类加载器加载该类,直到该类被成功加载,若找不到该类,则JVM会抛出ClassNotFoud异常。

即调用类加载器ClassLoader 的 loadClass 方法时,查找类的规则

loadClass源码

protected Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先查找该类是否已经被该类加载器加载过了
        Class c = findLoadedClass(name);
        //如果没有被加载过
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                //看是否被它的上级加载器加载过了 Extension的上级是Bootstarp,但它显示为null
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    //看是否被启动类加载器加载过
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
                //捕获异常,但不做任何处理
            }

            if (c == null) {
                //如果还是没有找到,先让拓展类加载器调用findClass方法去找到该类,如果还是没找到,就抛出异常
                //然后让应用类加载器去找classpath下找该类
                long t1 = System.nanoTime();
                c = findClass(name);

                // 记录时间
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

6.线程上下文类加载器

默认使用应用程序类加载器

7.自定义类加载器

1.使用场景
  • 想加载非 classpath 随意路径中的类文件
  • 通过接口来使用实现,希望解耦时,常用在框架设计
  • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
2.步骤
  • 继承ClassLoader父类
  • 要遵从双亲委派机制,重写 findClass 方法
    • 不是重写loadClass方法,否则不会走双亲委派机制
  • 读取类文件的字节码
  • 调用父类的 defineClass 方法来加载类
  • 使用者调用该类加载器的 loadClass 方法
3.案例
public class Load7 {
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader();
Class c1 = classLoader. loadClass( name: "MapImpl1");
Class C2 = classLoader. loadClass( name: "MapImp11");
System. out. println(c1 == c2);
MyClassLoader classLoader2 = new MyClassLoader();
Class C3 = classLoader2.loadClass( name: "MapImp11");
System. out. println(c1 == C3);
}
}
class MyClassLoader extends ClassLoader {
@Override. //. name. 就是类名称
protected Class findClass(String name) throws ClassNotFoundException {
String path ="e:\\myclasspath\\"+name+".class" ;
try {
ByteArrayOutputStream OS = new ByteArray0utputStream( ); .
Files. copy(Paths . get(path), os);
//得到字节数组
byte[] bytes = os. toByteArray();
// byte[] -> *.c1ass 数组编程字节码
return defineClass(name, bytes, of, 0,bytes.length);
} catch (IOException e) {
e. printStackTrace();
}
}

8.破坏双亲委派模式

  • 双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK1.2面世以前的“远古”时代
    • 建议用户重写findClass()方法,在类加载器中的loadClass()方法中也会调用该方法
  • 双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的
    • 如果有基础类型又要调用回用户的代码,此时也会破坏双亲委派模式
  • 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的
    • 这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等

6.运行期优化

1.即时编译

1.分层编译

JVM 将执行状态分成了 5 个层次:

  • 0层:(解释器)解释执行,用解释器将字节码翻译为机器码
  • 1层:使用 C1 即时编译器编译执行(不带 profiling)
  • 2层:使用 C1 即时编译器编译执行(带基本的profiling)
  • 3层:使用 C1 即时编译器编译执行(带完全的profiling)
  • 4层:使用 C2 即时编译器编译执行

profiling(分析) 是指在运行过程中收集一些程序执行状态,例如方法调用次数,循环的回边次数

2.既时编译器(JIT)与解释器的区别
  • 解释器
    • 将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
    • 是将字节码解释为针对所有平台通用的机器码
  • 即时编译器
    • 将一些字节码编译为机器码,并存入Code Cache(代码缓存),下次遇到相同的代码,直接执行,无需再编译
    • 根据平台类型,生成平台特定的机器码

大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行

对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。

执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由 来),并优化这些热点代码

3.逃逸分析

java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术

逃逸分析的 JVM 参数如下:

  • 开启逃逸分析:-XX:+DoEscapeAnalysis
  • 关闭逃逸分析:-XX:-DoEscapeAnalysis
  • 显示分析结果:-XX:+PrintEscapeAnalysis

逃逸分析技术在 Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加这个参数

4.方法内联
1.内联函数

内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换

2.JVM内联函数

C++是否为内联函数由自己决定,Java由编译器决定。Java不支持直接声明为内联函数的,如果想让他内联,你只能够向编译器提出请求: 关键字final修饰 用来指明那个函数是希望被JVM内联的

总的来说,一般的函数都不会被当做内联函数,只有声明了final后,编译器才会考虑是不是要把你的函数变成内联函数

5.字段优化
2.反射优化

五、内存模型

java 内存模型是Java Memory Model (JMM)的意思。

1.JAVA内存模型(JMM)

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

JMM:屏蔽掉各种硬件操作系统内存访问差异,以实现让java程序在各种平台下都能达到一致性的内存访问结果。

JMM体现在以下几个方面

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

2.原子性

指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着"同生共死"的感觉 。

1.问题提出:结果不一定为0

static int i = 0;
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i++;
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}

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

2.问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作。

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic   i // 获取静态变量i的值
iconst_ _1    //准备常量1
i add         //自增
gutstatic  i  //将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic   i // 获取静态变量i的值
iconst_ 1     //准备常量1
isub          //自减
putstatic   i // 将修改后的值存入静态变量i

而Java的内存模型如下,完成静态变量的自增,自减需要在主存线程内存中进行数据交换:
学习黑马JVM的笔记_第66张图片

如果是单线程回顺序执行(不会交错)没有出现问题
下面是单线程的字节码

// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
getstatic i // 线程1-获取静态变量i的值 线程内i=1
iconst_1 // 线程1-准备常量1
isub // 线程1-自减 线程内i=0
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=0

如果是多线程则可能会出现交错运行:
出现负数的情况:

// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1

出现正数的情况

// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1

3.解决方法

synchronized(同步关键字)

语法

synchronized(对象){
	要作为原子操作的代码
}

用synchronized解决并发问题

static int i = 0;
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
synchronized (obj) {
i++;
}
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
synchronized (obj) {
i--;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}

3.可见性

1.问题提出:退不出的循环

先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止

static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
Thread.sleep(1000);		//这里停止的线程是主线程
run = false; // 线程t不会如预想的停下来
}

2.问题分析

1.在初始状态,t线程刚开始从内存读取了run的值到工作内存。

学习黑马JVM的笔记_第67张图片

2.因为t线程要频繁从主内存中读取run的值,JIT(即时编译器)会将run的值缓存到自己工作内存的高速缓存中,减少对贮存中对run的访问,提高效率。
学习黑马JVM的笔记_第68张图片

3.1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。

3.解决方法

volatile(易变关键字)

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存

volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
Thread.sleep(1000);		//这里停止的线程是主线程
run = false; // 线程t不会如预想的停下来
}

前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见不能保证原子性,仅用在一个写线程, 多个读线程的情况。

synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低。

4.有序性

1.问题提出:诡异的结果?

int num = 0;
boolean ready = false;
//线程1执行此方法
public void actor1(I_ _Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
//线程2执行此方法
public void actor2(I_ _Result r) {
num = 2;
ready = true;
} 

2.问题分析

执行结果有多少种?
情况1:线程1先执行,这时ready= false,所以进入else 分支结果为1。
情况2:线程2先执行num=2,但没来得及执行ready= true,线程1执行,还是进入else分支,结果为1。
情况3:线程2执行到ready= true,线程1执行,这回进入if分支,结果为4 (因为num已经执行过了)。
情况4:线程2执行ready=true,切换到线程1,进入if分支,相加为0,再切回线程2执行num= 2

3.解决方法

volatile 修饰的变量,可以禁用指令重排

int num = 0;
volatile boolean ready = false;
//线程1执行此方法
public void actor1(I_ _Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
//线程2执行此方法
public void actor2(I_ _Result r) {
num = 2;
ready = true;
} 

4.有序性理解

指令重排
  • JVM 会在不影响正确性的前提下,可以调整语句的执行顺序
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; // 较为耗时的操作
j = ...;

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时, 既可以是

i = ...; // 较为耗时的操作
j = ...;

也可以是

j = ...;
i = ...; // 较为耗时的操作

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。

5.double-checked locking 模式实现单例

例如著名的 double-checked locking 模式实现单例

public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
	synchronized (Singleton.class) {
	// 也许有其它线程已经创建实例,所以再判断一次
		if (INSTANCE == null) {
			INSTANCE = new Singleton();
		}
	}
}
return INSTANCE;
}
}

以上的实现特点是:

  • 懒惰实例化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁

但在多线程环境下,上面的代码是有问题的, INSTANCE = new Singleton() 对应的字节码为:

0: new #2 // class cn/itcast/jvm/t4/Singleton
3: dup
4: invokespecial #3 // Method "":()V
7: putstatic #4 // Field INSTANCE:Lcn/itcast/jvm/t4/Singleton;

其中4 7两步的顺序不是固定的,也许jvm会优化为:先将引用地址赋值给INSTANCE量后,再执行构造方法,如果两个线程tl, t2按如”下时间序列执行:

时间1 t1 线程执行到INSTANCE = new Singleton();
时间2 t1 线程分配空间,为Singleton对象生成 了引用地址(0处)
时间3 t1 线程将引用地址赋值给INSTANCE, 这时INSTANCE != null (7处)
时间4
t2线程进入getInstance()方法,发现INSTANCE != null (synchronized块外) ,直接返回
INSTANCE
时间5 t1 线程执行Singleton的构造方法(4处)

这时tl还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么t2拿到的是将是一个未初始化完毕的单例

对INSTANCE使用volatile修饰即可,可以禁用指令重排,但要注意在JDK 5以上的版本的volatile才会真正有效

public final class Singleton {
private Singleton() { }

private  volatile static Singleton INSTANCE = null;
public static Singleton getInstance() {
	// 实例没创建,才会进入内部的 synchronized代码块
	if (INSTANCE == null) {
		synchronized (Singleton.class) {
		// 也许有其它线程已经创建实例,所以再判断一次
			if (INSTANCE == null) {
				INSTANCE = new Singleton();
			}
		}
	}
	return INSTANCE;
}
}

5.happens-before

happens-before 关系是用来描述两个操作的内存可见性的。如果操作 X happens-before 操作 Y,那么 X 的结果对于 Y 可见。
Java 内存模型底层是通过内存屏障(memory barrier)来禁止重排序的。

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

  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();   //mian线程会等待t1线程运行完再继续运行
System.out.println(x);
  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通 过t2.interrupted 或 t2.isInterrupted)
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
},"t2");
t2.start();
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);

6.CAS与原子类

1.CAS

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

对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B;如果不相等,则什么都不做。

// 需要不断尝试
while(true) {
int 旧值 = 共享变量 ; // 比如拿到了当前值 0
int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1
/*
这时候如果别的线程把共享变量改成了 5,本线程的正确结果 1 就作废了,这时候
compareAndSwap 返回 false,重新尝试,直到:
compareAndSwap 返回 true,表示我本线程做修改的同时,别的线程没有干扰
*/
if( compareAndSwap ( 旧值, 结果 )) {
// 成功,退出循环
}

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。结合 CAS 和 volatile 可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下。

  • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令,下面是直接使用 Unsafe 对象进 行线程安全保护的一个例子

import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class TestCAS {
public static void main(String[] args) throws InterruptedException {
DataContainer dc = new DataContainer();
int count = 5;
dc.increase();
}
});
t1.start();
t1.join();
System.out.println(dc.getData());
}
}
class DataContainer {
private volatile int data;
static final Unsafe unsafe;
static final long DATA_OFFSET;
static {
try {
// Unsafe 对象不能直接调用,只能通过反射获得
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
try {
// data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性
DATA_OFFSET =
unsafe.objectFieldOffset(DataContainer.class.getDeclaredField("data"));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
public void increase() {
int oldValue;
while(true) {
// 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解
oldValue = data;
// cas 尝试修改 data 为 旧值 + 1,如果期间旧值被别的线程改了,返回 false
if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue +
1)) {
return;
}
}
}
public void decrease() {
int oldValue;
while(true) {
oldValue = data;
if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue -
1)) {
return;
}
}
}

2.乐观锁与悲观锁

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

3.原子操作类

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

可以使用 AtomicInteger 改写之前的例子:

// 创建原子整数对象
private static AtomicInteger i = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i.getAndIncrement(); // 获取并且自增 i++
// i.incrementAndGet(); // 自增并且获取 ++i
}
});
i.getAndDecrement(); // 获取并且自减 i--
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}

7.synchronized优化

Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word )。Mark Word(8个字节) 平常存储这个对象的 哈希码、分代年龄,当加锁时,这些信息根据情况被替换为标记为、线程锁记录指针、重量级锁指针、重量级锁指针、线程ID 等内容。

1.轻量级锁

每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

static Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        // 同步块 A
        method2();
    }
}
public static void method2() {
    synchronized( obj ) {
        // 同步块 B
    }
}
线程1 对象 Mark Word 线程2
访问同步代码块A,把Mark信息复制到线程1的锁记录 01(无锁) -
CAS修改Mark为线程1的锁记录地址 01(无锁) -
成功(加锁) 00(轻量锁)线程1的锁记录地址 -
执行同步代码块A 00(轻量锁)线程1的锁记录地址 -
访问同步代码快B,吧Mark复制到线程1的锁记录 00(轻量锁)线程1的锁记录地址 -
CAS修改Mark为线程1锁记录地址 00(轻量锁)线程1的锁记录地址 -
失败(发现是自己的锁) 00(轻量锁)线程1的锁记录地址 -
锁重入 00(轻量锁)线程1的锁记录地址 -
执行同步块B 00(轻量锁)线程1的锁记录地址 -
同步块B执行完毕 00(轻量锁)线程1的锁记录地址 -
同步块A执行完毕 00(轻量锁)线程1的锁记录地址 -
成功(解锁) 01(无锁) -
- 01(无锁) 访问同步块A,把Mark复制到线程2的锁记录
- 01(无锁) CAS修改Mark为线程2锁记录地址
- 00(轻量锁)线程2的锁记录地址 成功(加锁)
-

2.膨胀锁

如果再尝试加轻量级锁的过程中,CAS操作无法成功,这种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这是需要进行锁膨胀,将轻量级锁变为重量级锁。

static Object obj=new Object();
public static void method1(){
	synchronized(obj){
		// 同步块
	}
}
线程 1 对象 Mark 线程 2
把 Mark 复制到线程 1 的锁记录 01(无锁) -
CAS 修改 Mark 为线程 1 锁记录地址 01(无锁) -
成功(加锁) 00(轻量锁)线程 1 锁 记录地址 -
执行同步块 00(轻量锁)线程 1 锁 记录地址 -
执行同步块 00(轻量锁)线程 1 锁 记录地址 访问同步块,把 Mark 复制 到线程 2
执行同步块 00(轻量锁)线程 1 锁 记录地址 CAS 修改 Mark 为线程 2 锁 记录地址
执行同步块 00(轻量锁)线程 1 锁 记录地址 失败(发现别人已经占了 锁)
执行同步块 00(轻量锁)线程 1 锁 记录地址 CAS 修改 Mark 为重量锁
执行同步块 10(重量锁(monitorenter))重量锁指针 阻塞中
行完毕 10(重量锁)重量锁指针 阻塞中
失败(解锁) 10(重量锁)重量锁指针 阻塞中
释放重量锁,唤起阻塞线程竞争(monitorexit) 01(无锁) 阻塞中
- 10(重量锁) 竞争重量锁
- 10(重量锁) 成功(加锁)
-

3.重量锁

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

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

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

Java 7 之后不能控制是否开启自旋功能

自旋重试成功的情况

线程 1 (cpu 1 上) 对象 Mark 线程 2 (cpu 2 上)
- 10(重量锁) -
访问同步块,获取 monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行完毕 10(重量锁)重量锁指针 自旋重试
成功(解锁) 01(无锁) 自旋重试
- 10(重量锁)重量锁指针 成功(加锁)
- 10(重量锁)重量锁指针 执行同步块
-

自旋重试失败的情况

线程 1 (cpu 1 上) 对象 Mark 线程 2 (cpu 2 上)
- 10(重量锁) -
访问同步块,获取 monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
- 10(重量锁)重量锁指针 阻塞
-

4.偏向锁

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

  • 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
  • 访问对象的hashCode也会撤销偏向锁
  • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重置对象的Thread ID
  • 撤销偏向和重偏向都是批量进行的,以类为单位
  • 如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的
  • 可以主动使用-XX:-UseBiasedL ocking禁用偏向锁
static Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
		// 同步块 A
		method2();
	}
}
public static void method2() {
	synchronized( obj ) {
		// 同步块 B
	}
}
static Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
		// 同步块 A
		method2();
	}
}
public static void method2() {
	synchronized( obj ) {
		// 同步块 B
	}
}
线程 1 对象 Mark
访问同步块 A,检查 Mark 中是否有线程 ID 执行完毕
尝试加偏向锁 101(无锁可偏向)对象 hashCod
成功 101(无锁可偏向)线程ID
执行同步块 A 101(无锁可偏向)线程ID
访问同步块 B,检查 Mark 中是否有线程 ID 101(无锁可偏向)线程ID
是自己的线程 ID,锁是自己的,无需做更多操作 101(无锁可偏向)线程ID
执行同步块 B 101(无锁可偏向)线程ID
执行完毕 101(无锁可偏向)对象 hashCode

5.其他优化

1.减少上锁的时间

同步代码块中尽量短

2.减少锁的粒度

将一个锁拆分为多个所提高并发度,例如:

  • ConcurrentHashMap
  • LongAdder分为base和cells 两部分。没有并发争用的时候或者是cells数组正在初始化的时候,会使用CAS来累加值到base;有并发争用,会初始化cells数组,数组有多少个cell,就允许有多少线程并行修改,最后将数组中每个cell累加,再加上base就是最终的值
  • LinkedBlockingQueue入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高
3.锁粗化

多次循环进入同步块不如同步块内多次循环 另外 JVM 可能会做如下优化,把多次 append 的加锁操作 粗化为一次(因为都是对同一个对象加锁,没必要重入多次)

new StringBuffer().append("a").append("b").append("c");
``````java
new StringBuffer().append("a").append("b").append("c");
4.锁消除

JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候就会被即时编译器忽略掉所有同步操作。

5.读写分离

CopyOnWriteArrayList ConyOnWriteSet

参考:
https://wiki.openjdk.java.net/display/HotSpot/Synchronization
http://luojinping.com/2015/07/09/java锁优化/
https://www.infoq.cn/article/java-se-16-synchronized
https://www.jianshu.com/p/9932047a89be
https://www.cnblogs.com/sheeva/p/6366782.html
https://stackoverflow.com/questions/46312817/does-java-ever-rebias-an-individual-lock

你可能感兴趣的:(java虚拟机,java,jvm,1024程序员节)