文章目录

  • Java虚拟机
    • 1.引言
      • 1.1.什么是 JVM ?
      • 1.2.学习 JVM 有什么用 ?
      • 1.3.常见的 JVM
      • 1.4.学习路线
    • 2.JVM 内存结构
      • 2.1.程序计数器
        • 2.1.1.定义
        • 2.1.2.作用
      • 2.2.虚拟机栈
        • 2.2.1.定义
        • 2.2.2.栈内存溢出
        • 2.2.3.线程运行诊断
      • 2.3.虚拟机栈
      • 2.4.堆
        • 2.4.1.定义
        • 2.4.2.堆内存溢出
        • 2.4.3.堆内存诊断
      • 2.5.方法区
        • 2.5.1.定义
        • 2.5.2.组成
        • 2.5.3.方法区内存溢出
        • 2.5.4.运行时常量池
        • 2.5.5.StringTable
      • 2.6.直接内存
        • 2.6.1.定义
        • 2.6.2.分配和回收原理
    • 3.JVM 垃圾回收
      • 3.1.如何判断对象可以回收
        • 3.1.1.引用计数法
        • 3.1.2.可达性分析算法
        • 3.1.3.四种引用
      • 3.2.垃圾回收算法
        • 3.2.1.标记清除
        • 3.2.2.标记整理
        • 3.2.3.复制
      • 3.3.分代垃圾回收
        • 3.3.1.相关 VM 参数
      • 3.4.垃圾回收器
        • 3.4.1.串行
        • 3.4.2.吞吐量优先
        • 3.4.3.响应时间优先(CMS)
        • 3.4.4. G1
        • 3.4.5. 垃圾回收调优
    • 4.类加载与字节码技术
      • 4.1.类文件结构
        • 4.1.1.魔数
        • 4.1.2.版本
        • 4.1.3.常量池
        • 4.1.4.访问标识与继承信息
        • 4.1.5.Field 信息
        • 4.1.6.Method 信息
        • 4.1.7.附加属性
      • 4.2.字节码指令
        • 4.2.1.入门
        • 4.2.2.javap 工具
        • 4.2.3.图解方法执行流程
        • 4.2.4.练习 - 分析 i++
        • 4.2.5.条件判断指令
        • 4.2.6.条件判断指令
        • 4.2.7.练习 - 判断结果
        • 4.2.8.练习 - 构造方法
        • 4.2.9.方法调用
        • 4.2.10.多态的原理
        • 4.2.11.异常处理
        • 4.2.12.练习 - finally 面试题
        • 4.2.13.synchronized
      • 4.3.编译期处理
        • 4.3.1.默认构造器
        • 4.3.2.自动拆装箱
        • 4.3.3.泛型集合取值
        • 4.3.4.可变参数
        • 4.3.5.foreach 循环
        • 4.3.6.switch 字符串
        • 4.3.7.switch 枚举
        • 4.3.8.枚举类
        • 4.3.9.try-with-resources
        • 4.3.10.方法重写时的桥接方法
        • 4.3.11.匿名内部类
      • 4.4.类加载阶段
        • 4.4.1.加载
        • 4.4.2.链接
        • 4.4.3.初始化
        • 4.4.4.练习
      • 4.5.类加载器
        • 4.5.1.启动类加载器
        • 4.5.2.扩展类加载器
        • 4.5.3.双亲委派模式
        • 4.5.4.线程上下文类加载器
        • 4.5.5.自定义类加载器
      • 4.6.运行期优化
        • 4.6.1.即时编译
        • 4.6.2.反射优化
    • 5.内存模型
      • 5.1.java 内存模型
        • 5.1.1.原子性
        • 5.1.2.问题分析
        • 5.1.3.解决方法
      • 5.2.可见性
        • 5.2.1.退不出的循环
        • 5.2.2.解决方法
        • 5.2.3.可见性
      • 5.3.有序性
        • 5.3.1.诡异的结果
        • 5.3.2.解决方法
        • 5.3.3.有序性理解
        • 5.3.4.happens-before
      • 5.4.CAS 与 原子类
        • 5.4.1.CAS
        • 5.4.2.乐观锁与悲观锁
        • 5.4.3.原子操作类
      • 5.5.synchronized 优化
        • 5.5.1.轻量级锁
        • 5.5.2.锁膨胀
        • 5.5.3.重量锁
        • 5.5.4.偏向锁
        • 5.5.5.其它优化

Java虚拟机

1.引言

  1. 什么是 JVM ?
  2. 学习 JVM 有什么用 ?
  3. 常见的 JVM
  4. 学习路线

1.1.什么是 JVM ?

定义:
Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)(也是一种规范,各大厂商需要创建自己的虚拟机必须遵循这个规范)

好处:

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

比较:
jvm jre jdk

1.2.学习 JVM 有什么用 ?

  • 面试
  • 理解底层的实现原理
  • 中高级程序员的必备技能

1.3.常见的 JVM

1.4.学习路线


2.JVM 内存结构

  1. 程序计数器
  2. 虚拟机栈
  3. 本地方法栈
  4. 方法区

2.1.程序计数器

2.1.1.定义

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

  • 作用,是记住下一条jvm指令的执行地址
  • 特点
    • 1)是线程私有的
    • 2)不会存在内存溢出

2.1.2.作用


2.2.虚拟机栈

2.2.1.定义


Java Virtual Machine Stacks (Java 虚拟机栈)

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

问题辨析

  1. 垃圾回收是否涉及栈内存?(不会,一次次方法调用,会将栈内存弹出,不需要垃圾回收管理,垃圾回收只管理堆内存的对象)

  2. 栈内存分配越大越好吗?(-Xss size 来指定,栈内存越大,反而让线程数越小,物理内存大小是固定的)

  3. 方法内的局部变量是否线程安全?(不一定,可能会发生逃逸事件)
    1)如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
    2)如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全


    /*** 局部变量的线程安全问题*/
    public class Demo1_17 {public static void main(String[] args) {StringBuilder sb = new StringBuilder();sb.append(4);sb.append(5);sb.append(6);new Thread(()->{m2(sb);}).start();}public static void m1() {// 线程安全StringBuilder sb = new StringBuilder();sb.append(1);sb.append(2);sb.append(3);System.out.println(sb.toString());}public static void m2(StringBuilder sb) {// 非线程安全,有可能逃逸事件(逃离了当前方法的作用范围),多个线程对 StringBuilder sb 会共享sb.append(1);sb.append(2);sb.append(3);System.out.println(sb.toString());}public static StringBuilder m3() {StringBuilder sb = new StringBuilder();sb.append(1);sb.append(2);sb.append(3);// 将引用结果返回了,其他线程对 sb 会拿到引用,会共享,并发修改return sb;}
    }
    

2.2.2.栈内存溢出

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

    /*** 演示栈内存溢出 java.lang.StackOverflowError* -Xss256k*/
    public class Demo1_2 {private static int count;public static void main(String[] args) {try {method1();} catch (Throwable e) {e.printStackTrace();System.out.println(count);}}private static void method1() {count++;method1();}
    }
    


    -Xss256k 设置栈的内存大小为 256k
    循环引用导致,栈溢出。

    /*** json 数据转换*/
    public class Demo1_19 {public static void main(String[] args) throws JsonProcessingException {Dept d = new Dept();d.setName("Market");Emp e1 = new Emp();e1.setName("zhang");e1.setDept(d);Emp e2 = new Emp();e2.setName("li");e2.setDept(d);d.setEmps(Arrays.asList(e1, e2));// { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }ObjectMapper mapper = new ObjectMapper();System.out.println(mapper.writeValueAsString(d));}
    }class Emp {private String name;// 打破循环引用,转换 dept 时候,不转@JsonIgnoreprivate Dept dept;public String getName() {return name;}public void setName(String name) {this.name = name;}public Dept getDept() {return dept;}public void setDept(Dept dept) {this.dept = dept;}
    }
    class Dept {private String name;private List<Emp> emps;public String getName() {return name;}public void setName(String name) {this.name = name;}public List<Emp> getEmps() {return emps;}public void setEmps(List<Emp> emps) {this.emps = emps;}
    }
    
  • 栈帧过大导致栈内存溢出

2.2.3.线程运行诊断

案例1: cpu 占用过多

定位

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

案例2:程序运行很长时间没有结果(可能是线程死锁)
利用 jsatck 最后的打印信息出现

2.3.虚拟机栈

2.4.堆

2.4.1.定义

Heap 堆

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

特点

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

2.4.2.堆内存溢出

/*** 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space* -Xmx8m*/
public class Demo1_5 {public static void main(String[] args) {int i = 0;try {List<String> list = new ArrayList<>();String a = "hello";while (true) {list.add(a); // hello, hellohello, hellohellohellohello ...a = a + a;  // hellohellohellohelloi++;}} catch (Throwable e) {e.printStackTrace();System.out.println(i);}}
}

-Xmx8m 设置堆内存的大小,如果堆内存太大的话,不容易暴露出问题(调试时候可以设置小点),最终会导致内存溢出。

2.4.3.堆内存诊断

  1. jps 工具(查看当前系统中有哪些 java 进程)
  2. jmap 工具(查看堆内存占用情况 jmap - heap 进程id,抓取快照形式)
  3. jconsole 工具(图形界面的,多功能的监测工具,可以连续监测)
/*** 演示堆内存*/
public class Demo1_4 {public static void main(String[] args) throws InterruptedException {System.out.println("1...");Thread.sleep(30000);byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb,占用对空间System.out.println("2...");Thread.sleep(20000);array = null;   // byte 数组可以被 GC 回收System.gc();System.out.println("3...");Thread.sleep(1000000L);}
}

jmap 工具 演示





jconsole 工具 演示

案例

  • 垃圾回收后,内存占用仍然很高
/*** 演示查看对象个数 堆转储 dump(抓取内存快照)*/
public class Demo1_13 {public static void main(String[] args) throws InterruptedException {List<Student> students = new ArrayList<>();for (int i = 0; i < 200; i++) {students.add(new Student());
//            Student student = new Student();}Thread.sleep(1000000000L);}
}
class Student {private byte[] big = new byte[1024*1024];
}


jconsole 工具执行GC


jvisualvm 工具可视化的方式展示虚拟机


2.5.方法区

2.5.1.定义


JVM 规范-方法区定义

1)方法区是所有Java虚拟机线程共享的区域。
2)存储了和类的结构相关的信息,有类的成员变量、运行时常量池、方法数据、成员方法以及构造器方法的代码部分。
3)方法区在虚拟机启动时被创建,逻辑上是堆的组成部分,在概念上定义了方法区,具体的JVM厂商在实现时,究竟用的是啥不确定,是一种规范。(比如 HostPort虚拟机在JDK8以前是永久代,就是使用了堆内存的一部分。到了1.8以后,移除了永久代,采用元空间,采用的是本地内存,也就是操作系统的内存。方法区是规范,永久代或者元空间是实现)
如果方法区域中的内存不能用于满足分配请求,则 Java 虚拟机抛出 OutOfMemoryError。

2.5.2.组成


2.5.3.方法区内存溢出

  • 1.8 以前会导致永久代内存溢出

    /*** 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace* -XX:MaxMetaspaceSize=8m*/
    public class Demo1_8 extends ClassLoader { // ClassLoader 可以用来加载类的二进制字节码public static void main(String[] args) {int j = 0;try {Demo1_8 test = new Demo1_8();for (int i = 0; i < 10000; i++, j++) {// ClassWriter 作用是生成类的二进制字节码,用代码的形式ClassWriter cw = new ClassWriter(0);// 版本号, public, 类名, 包名, 父类, 接口cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);// 返回 byte[]byte[] code = cw.toByteArray();// 执行了类的加载test.defineClass("Class" + i, code, 0, code.length); // Class 对象}} finally {System.out.println(j);}}
    }
    

  • 1.8 之后会导致元空间内存溢出

    场景:运行期间动态产生 class 并加载的场景是非常非常多的(字节码技术,比如CGLib,导致内存溢出,1.8 及以后由操作系统管理,垃圾回收快)

  • Spring

  • Mybatis

2.5.4.运行时常量池

  • 常量池,就是一张常量表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

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

    // 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
    public class HelloWorld {public static void main(String[] args) {System.out.println("hello world");}
    }
    



    Classfile /E:/17.JAVA/StudyMaterial/Java复习资料/05.JVM(TODO)/黑马/代码/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.classLast modified 2022-6-1; size 567 bytesMD5 checksum 8efebdac91aa496515fa1c161184e354Compiled from "HelloWorld.java"
    public class cn.itcast.jvm.t5.HelloWorldminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:#1 = Methodref          #6.#20         // java/lang/Object."<init>":()V#2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;#3 = String             #23            // hello world#4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V#5 = Class              #26            // cn/itcast/jvm/t5/HelloWorld#6 = Class              #27            // java/lang/Object#7 = Utf8               <init>#8 = Utf8               ()V#9 = Utf8               Code#10 = Utf8               LineNumberTable#11 = Utf8               LocalVariableTable#12 = Utf8               this#13 = Utf8               Lcn/itcast/jvm/t5/HelloWorld;#14 = Utf8               main#15 = Utf8               ([Ljava/lang/String;)V#16 = Utf8               args#17 = Utf8               [Ljava/lang/String;#18 = Utf8               SourceFile#19 = Utf8               HelloWorld.java#20 = NameAndType        #7:#8          // "<init>":()V#21 = Class              #28            // java/lang/System#22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;#23 = Utf8               hello world#24 = Class              #31            // java/io/PrintStream#25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V#26 = Utf8               cn/itcast/jvm/t5/HelloWorld#27 = Utf8               java/lang/Object#28 = Utf8               java/lang/System#29 = Utf8               out#30 = Utf8               Ljava/io/PrintStream;#31 = Utf8               java/io/PrintStream#32 = Utf8               println#33 = Utf8               (Ljava/lang/String;)V
    {public cn.itcast.jvm.t5.HelloWorld();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 4: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       5     0  this   Lcn/itcast/jvm/t5/HelloWorld;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc           #3                  // String hello world5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 6: 0line 7: 8LocalVariableTable:Start  Length  Slot  Name   Signature0       9     0  args   [Ljava/lang/String;
    }
    SourceFile: "HelloWorld.java"
    

2.5.5.StringTable

串池
先看几道面试题:

// StringTable [ "a", "b" ,"ab" ]  数据上是 hashtable 结构,不能扩容,可设置大小
public class Demo1_22 {// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象// 执行以下 ldc 指令时候// ldc #2 会把 a 符号变为 "a" 字符串对象(在第一次转换对象之前会准备一块空间 StringTable)// ldc #3 会把 b 符号变为 "b" 字符串对象(属于懒加载到 StringTable 中)// ldc #4 会把 ab 符号变为 "ab" 字符串对象public static void main(String[] args) {String s1 = "a"; // 懒惰的String s2 = "b";String s3 = "ab";  // 串池中String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")  堆中,变量拼接String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab,常量拼接System.out.println(s3 == s5);  // trueSystem.out.println(s3 == s4);  // false}
}




Classfile /E:/17.JAVA/StudyMaterial/Java复习资料/05.JVM(TODO)/黑马/代码/jvm/out/production/jvm/cn/itcast/jvm/t1/stringtable/Demo1_22.classLast modified 2022-6-1; size 985 bytesMD5 checksum a5eb84bf1a7d8a1e725491f36237777bCompiled from "Demo1_22.java"
public class cn.itcast.jvm.t1.stringtable.Demo1_22minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref          #12.#36        // java/lang/Object."<init>":()V#2 = String             #37            // a#3 = String             #38            // b#4 = String             #39            // ab#5 = Class              #40            // java/lang/StringBuilder#6 = Methodref          #5.#36         // java/lang/StringBuilder."<init>":()V#7 = Methodref          #5.#41         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;#8 = Methodref          #5.#42         // java/lang/StringBuilder.toString:()Ljava/lang/String;#9 = Fieldref           #43.#44        // java/lang/System.out:Ljava/io/PrintStream;#10 = Methodref          #45.#46        // java/io/PrintStream.println:(Z)V#11 = Class              #47            // cn/itcast/jvm/t1/stringtable/Demo1_22#12 = Class              #48            // java/lang/Object#13 = Utf8               <init>#14 = Utf8               ()V#15 = Utf8               Code#16 = Utf8               LineNumberTable#17 = Utf8               LocalVariableTable#18 = Utf8               this#19 = Utf8               Lcn/itcast/jvm/t1/stringtable/Demo1_22;#20 = Utf8               main#21 = Utf8               ([Ljava/lang/String;)V#22 = Utf8               args#23 = Utf8               [Ljava/lang/String;#24 = Utf8               s1#25 = Utf8               Ljava/lang/String;#26 = Utf8               s2#27 = Utf8               s3#28 = Utf8               s4#29 = Utf8               s5#30 = Utf8               StackMapTable#31 = Class              #23            // "[Ljava/lang/String;"#32 = Class              #49            // java/lang/String#33 = Class              #50            // java/io/PrintStream#34 = Utf8               SourceFile#35 = Utf8               Demo1_22.java#36 = NameAndType        #13:#14        // "<init>":()V#37 = Utf8               a#38 = Utf8               b#39 = Utf8               ab#40 = Utf8               java/lang/StringBuilder#41 = NameAndType        #51:#52        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;#42 = NameAndType        #53:#54        // toString:()Ljava/lang/String;#43 = Class              #55            // java/lang/System#44 = NameAndType        #56:#57        // out:Ljava/io/PrintStream;#45 = Class              #50            // java/io/PrintStream#46 = NameAndType        #58:#59        // println:(Z)V#47 = Utf8               cn/itcast/jvm/t1/stringtable/Demo1_22#48 = Utf8               java/lang/Object#49 = Utf8               java/lang/String#50 = Utf8               java/io/PrintStream#51 = Utf8               append#52 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;#53 = Utf8               toString#54 = Utf8               ()Ljava/lang/String;#55 = Utf8               java/lang/System#56 = Utf8               out#57 = Utf8               Ljava/io/PrintStream;#58 = Utf8               println#59 = Utf8               (Z)V
{public cn.itcast.jvm.t1.stringtable.Demo1_22();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 4: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       5     0  this   Lcn/itcast/jvm/t1/stringtable/Demo1_22;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=3, locals=6, args_size=10: ldc           #2                  // String a2: astore_13: ldc           #3                  // String b5: astore_26: ldc           #4                  // String ab8: astore_39: new           #5                  // class java/lang/StringBuilder12: dup13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V16: aload_117: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;20: aload_221: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;27: astore        429: ldc           #4                  // String ab31: astore        533: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;36: aload_337: aload         539: if_acmpne     4642: iconst_143: goto          4746: iconst_047: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V50: returnLineNumberTable:line 11: 0line 12: 3line 13: 6line 14: 9line 15: 29line 17: 33line 21: 50LocalVariableTable:Start  Length  Slot  Name   Signature0      51     0  args   [Ljava/lang/String;3      48     1    s1   Ljava/lang/String;6      45     2    s2   Ljava/lang/String;9      42     3    s3   Ljava/lang/String;29      22     4    s4   Ljava/lang/String;33      18     5    s5   Ljava/lang/String;StackMapTable: number_of_entries = 2frame_type = 255 /* full_frame */offset_delta = 46locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]stack = [ class java/io/PrintStream ]frame_type = 255 /* full_frame */offset_delta = 0locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]stack = [ class java/io/PrintStream, int ]
}
/*** 演示字符串字面量也是【延迟】成为对象的*/
public class TestString {public static void main(String[] args) {int x = args.length;System.out.println(); // 字符串个数 2275System.out.print("1");System.out.print("2");System.out.print("3");System.out.print("4");System.out.print("5");System.out.print("6");System.out.print("7");System.out.print("8");System.out.print("9");System.out.print("0");System.out.print("1"); // 字符串个数 2285System.out.print("2");System.out.print("3");System.out.print("4");System.out.print("5");System.out.print("6");System.out.print("7");System.out.print("8");System.out.print("9");System.out.print("0");System.out.print(x); // 字符串个数 2285}
}

StringTable 特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
    • 1)1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
    • 2)1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回(调用 intern 的对象和返回的对象是两个对象)
// JDK 1.7之后的
public class Demo1_23 {// 串池,字面量 ["ab", "a", "b"]public static void main(String[] args) {// x 在串池中String x = "ab";// s 在堆中 "ab"String s = new String("a") + new String("b");// 堆  new String("a")   new String("b") new String("ab")String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入并返回串池中的对象,如果没有则放入串池, 会把串池中的对象返回System.out.println( s2 == x);  // trueSystem.out.println( s == x );  // false}
}
// JDk 1.6
public class Demo1_23 {// 串池,字面量 ["a", "b", "ab"]public static void main(String[] args) {// s 在堆中 "ab"String s = new String("a") + new String("b");// 堆  new String("a")   new String("b") new String("ab")String s2 = s.intern(); // 串池中没有,s2 和 s 不一样String x = "ab";System.out.println( s2 == x);  // true 串池中System.out.println( s == x );  // false}
}

面试题:

/*** 演示字符串相关面试题*/
public class Demo1_21 {public static void main(String[] args) {String s1 = "a";String s2 = "b";String s3 = "a" + "b"; // abString s4 = s1 + s2;   // new String("ab")String s5 = "ab";String s6 = s4.intern();// 问System.out.println(s3 == s4); // falseSystem.out.println(s3 == s5); // trueSystem.out.println(s3 == s6); // trueString x2 = new String("c") + new String("d"); // new String("cd")x2.intern();String x1 = "cd";// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢System.out.println(x1 == x2);  // true}
}

StringTable 位置

演示:

/*** 演示 StringTable 位置* 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit // 关闭开关* 在jdk6下设置 -XX:MaxPermSize=10m*/
public class Demo1_6 {public static void main(String[] args) throws InterruptedException {List<String> list = new ArrayList<String>();int i = 0;try {for (int j = 0; j < 260000; j++) {list.add(String.valueOf(j).intern());i++;}} catch (Throwable e) {e.printStackTrace();} finally {System.out.println(i);}}
}



-XX:-UseGCOverheadLimit

StringTable 垃圾回收

/*** 演示 StringTable 垃圾回收* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc // 添加一些打印信息的开关*/
public class Demo1_7 {public static void main(String[] args) throws InterruptedException {int i = 0;try {for (int j = 0; j < 100000; j++) { // j=100, j=10000String.valueOf(j).intern();i++;}} catch (Throwable e) {e.printStackTrace();} finally {System.out.println(i);}}
}


StringTable 性能调优

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

案例演示:

/*** 演示串池大小对性能的影响* -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009*/
public class Demo1_24 {public static void main(String[] args) throws IOException {try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {String line = null;long start = System.nanoTime();while (true) {line = reader.readLine();if (line == null) {break;}// 入池line.intern();}System.out.println("cost:" + (System.nanoTime() - start) / 1000000);}}
}

参数 -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
输出:

参数 -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
输出:

如果项目中的字符串比较多(重复的),可以将桶的个数增大,减少哈希碰撞,哈希查找速度增快

/*** 演示 intern 减少内存占用* -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics* -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000*/
public class Demo1_25 {public static void main(String[] args) throws IOException {List<String> address = new ArrayList<>();System.in.read();// 480 万个字符串for (int i = 0; i < 10; i++) {try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {String line = null;long start = System.nanoTime();while (true) {line = reader.readLine();if(line == null) {break;}// address.add(line.intern());address.add(line);}System.out.println("cost:" +(System.nanoTime()-start)/1000000);}}System.in.read();}
}

jvisualvm查看:


address.add(line.intern()); 更改代码:

2.6.直接内存

2.6.1.定义

Direct Memory

  • 常见于 NIO 操作时,用于数据缓冲区(ByteBuffer)
  • 分配回收成本较高,但读写性能高(属于系统的)
  • 不受 JVM 内存回收管理(内存泄露或者溢出?存在吗?)

Java不能直接操作系统的内存,只能调用系统的函数,本地方法

案例演示:

/*** 演示 ByteBuffer 作用*/
public class Demo1_9 {static final String FROM = "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";static final String TO = "E:\\a.mp4";static final int _1Mb = 1024 * 1024;public static void main(String[] args) {io(); // io 用时:1535.586957 1766.963399 1359.240226directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592}private static void directBuffer() {long start = System.nanoTime();try (FileChannel from = new FileInputStream(FROM).getChannel();FileChannel to = new FileOutputStream(TO).getChannel();) {ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);while (true) {int len = from.read(bb);if (len == -1) {break;}bb.flip();to.write(bb);bb.clear();}} catch (IOException e) {e.printStackTrace();}long end = System.nanoTime();System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);}private static void io() {long start = System.nanoTime();try (FileInputStream from = new FileInputStream(FROM);FileOutputStream to = new FileOutputStream(TO);) {byte[] buf = new byte[_1Mb];while (true) {int len = from.read(buf);if (len == -1) {break;}to.write(buf, 0, len);}} catch (IOException e) {e.printStackTrace();}long end = System.nanoTime();System.out.println("io 用时:" + (end - start) / 1000_000.0);}
}



演示直接内存溢出

/*** 演示直接内存溢出*/
public class Demo1_10 {static int _100Mb = 1024 * 1024 * 100;public static void main(String[] args) {List<ByteBuffer> list = new ArrayList<>();int i = 0;try {while (true) {ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);list.add(byteBuffer);i++;}} finally {System.out.println(i);}// 方法区是jvm规范, jdk6 中对方法区的实现称为永久代//                  jdk8 对方法区的实现称为元空间}
}

输出:

2.6.2.分配和回收原理

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

禁用显式回收对直接内存的影响:

/*** 禁用显式回收对直接内存的影响*/
public class Demo1_26 {static int _1Gb = 1024 * 1024 * 1024;/** 在JVM调优时候会加一下参数* -XX:+DisableExplicitGC 显式的,让代码中的 System.gc(); 无效,不让手动的触发FullGC* 暂停时间较长,影响性能==>导致直接内存得不到释放(byteBuffer-->zhijie neicun)* 只能等真正的垃圾回收,但是可以调用 unsafe 对象调用 freeMemory */public static void main(String[] args) throws IOException {ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);System.out.println("分配完毕...");System.in.read();System.out.println("开始释放...");byteBuffer = null;System.gc(); // 显式的垃圾回收,Full GCSystem.in.read();}
}

直接内存分配的底层原理:Unsafe

/*** 直接内存分配的底层原理:Unsafe*/
public class Demo1_27 {static int _1Gb = 1024 * 1024 * 1024;public static void main(String[] args) throws IOException {Unsafe unsafe = getUnsafe();// 分配内存long base = unsafe.allocateMemory(_1Gb);unsafe.setMemory(base, _1Gb, (byte) 0);System.in.read();// 释放内存unsafe.freeMemory(base);System.in.read();}/*** 反射获取 unsafe 对象* @return Unsafe 对象*/public static Unsafe getUnsafe() {try {Field f = Unsafe.class.getDeclaredField("theUnsafe");f.setAccessible(true);Unsafe unsafe = (Unsafe) f.get(null);return unsafe;} catch (NoSuchFieldException | IllegalAccessException e) {throw new RuntimeException(e);}}
}


直接内存释放原理:

3.JVM 垃圾回收

  1. 如何判断对象可以回收
  2. 垃圾回收算法
  3. 分代垃圾回收
  4. 垃圾回收器
  5. 垃圾回收调优

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

3.1.1.引用计数法

3.1.2.可达性分析算法

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

工具

演示GC Roots

/*** 演示GC Roots*/
public class Demo2_2 {public static void main(String[] args) throws InterruptedException, IOException {List<Object> list1 = new ArrayList<>();list1.add("a");list1.add("b");System.out.println(1);System.in.read();list1 = null;System.out.println(2);System.in.read();System.out.println("end...");}
}





3.1.3.四种引用

  1. 强引用
    1)只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
  2. 软引用(SoftReference)
    1)仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
    2)可以配合引用队列来释放软引用自身
  3. 弱引用(WeakReference)
    1)仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
    2)可以配合引用队列来释放弱引用自身
  4. 虚引用(PhantomReference)
    1)必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
  5. 终结器引用(FinalReference)
    1)无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize
    方法,第二次 GC 时才能回收被引用对象








大意:直接内存不在jvm中,没办法被垃圾回收,因此虚引用关联,当ByteBuffer被回收时,虚引用直接进入引用队列,本地方法随即根据引用队列中的相关信息调用unsafe清除本地方法的内存。就是虚引用和引用队列同时存在,betybuffer没有强行用了 虚引用就会进入虚引用队列,一个检查虚引用队列的线程会启用unsafe把直接内存释放掉,采用虚引用的目的就是释放直接引用

finalizeHandler 线程


演示软引用:

/*** 演示软引用* -Xmx20m -XX:+PrintGCDetails -verbose:gc*/
public class Demo2_3 {private static final int _4MB = 4 * 1024 * 1024;public static void main(String[] args) throws IOException {/*List<byte[]> list = new ArrayList<>();for (int i = 0; i < 5; i++) {list.add(new byte[_4MB]);}System.in.read();*/soft();}public static void soft() {// 强引用-->软引用// list --> SoftReference --> byte[]List<SoftReference<byte[]>> list = new ArrayList<>();for (int i = 0; i < 5; i++) {SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);System.out.println(ref.get());list.add(ref);System.out.println(list.size());}System.out.println("循环结束:" + list.size());for (SoftReference<byte[]> ref : list) {System.out.println(ref.get());}}
}

演示软引用, 配合引用队列

/*** 演示软引用, 配合引用队列*/
public class Demo2_4 {private static final int _4MB = 4 * 1024 * 1024;public static void main(String[] args) {List<SoftReference<byte[]>> list = new ArrayList<>();// 引用队列,配合软引用,回收软引用ReferenceQueue<byte[]> queue = new ReferenceQueue<>();for (int i = 0; i < 5; i++) {// 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);System.out.println(ref.get());list.add(ref);System.out.println(list.size());}// 从队列中获取无用的 软引用对象,并移除Reference<? extends byte[]> poll = queue.poll();while( poll != null) {// 移除软引用对象list.remove(poll);// 取下一个软引用对象poll = queue.poll();}System.out.println("===========================");for (SoftReference<byte[]> reference : list) {System.out.println(reference.get());}}
}

演示弱引用

/*** 演示弱引用* -Xmx20m -XX:+PrintGCDetails -verbose:gc*/
public class Demo2_5 {private static final int _4MB = 4 * 1024 * 1024;public static void main(String[] args) {// 强引用-->弱引用//  list --> WeakReference --> byte[]List<WeakReference<byte[]>> list = new ArrayList<>();for (int i = 0; i < 10; i++) {WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);list.add(ref);for (WeakReference<byte[]> w : list) {System.out.print(w.get()+" ");}System.out.println();}System.out.println("循环结束:" + list.size());}
}

3.2.垃圾回收算法

3.2.1.标记清除

定义: Mark Sweep

  • 速度较快
  • 会造成内存碎片(空间不连续,不能放大对象)

3.2.2.标记整理

定义:Mark Compact

  • 速度慢(对象在整理过程中要移动,改变对该对象的引用地址)
  • 没有内存碎片

3.2.3.复制

定义:Copy

  • 不会有内存碎片
  • 需要占用双倍内存空间(适合存活较少的对象)
  • 并交换 From 和 To 的位置,保证 To 总是空闲的空间

3.3.分代垃圾回收

  • 对象首先分配在伊甸园区域
  • 伊甸园空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to 保证 to 中永远空闲
  • minor gc 会引发 stop the world,暂停其它用户的线程,只让垃圾回收线程工作,等垃圾回收结束,用户线程才恢复运行(STW是因为对象的地址会发生改变)
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
  • 当一个对象在伊甸园区放不下,From 也放不下,老年代也放不下时候,就会触发一个 FullGC
  • 新生代存活的对象较少,采用的是复制算法,老年代存活的对象较多,采用的是标记+整理或者标记+清除

3.3.1.相关 VM 参数

含义 参数
堆初始大小 -Xms
堆最大大小 Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio (默认为8)
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

演示内存的分配策略

/***  演示内存的分配策略*/
public class Demo2_1 {private static final int _512KB = 512 * 1024;private static final int _1MB = 1024 * 1024;private static final int _6MB = 6 * 1024 * 1024;private static final int _7MB = 7 * 1024 * 1024;private static final int _8MB = 8 * 1024 * 1024;// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGCpublic static void main(String[] args) throws InterruptedException {new Thread(() -> {ArrayList<byte[]> list = new ArrayList<>();list.add(new byte[_6MB]);list.add(new byte[_512KB]);
//            list.add(new byte[_8MB]);}).start();System.out.println("sleep....");Thread.sleep(1000L);}
}



再放一个,list.add(new byte[_512KB]);

只放一个大对象 list.add(new byte[_8MB]); 大对象直接竟升到老年代。

直接放两个个大对象 list.add(new byte[_8MB]); 报错。(一个线程内的 OutOfMemoryError 并不会导致整个Java进程的结束)

3.4.垃圾回收器

  1. 串行
    1)单线程
    2)堆内存较小,适合个人电脑
  2. 吞吐量优先
    1)多线程
    2)堆内存较大,多核 cpu
    3)让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高
  3. 响应时间优先
    1)多线程
    2)堆内存较大,多核 cpu
    3)尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5

3.4.1.串行

-XX:+UseSerialGC = Serial + SerialOld 分别工作在新生代(复制算法)与老年代(标记整理算法)

安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准 进行选定的,例如方法调用、循环跳转、异常跳转 等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点。

3.4.2.吞吐量优先

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC 这两个开关是联动的只用开启一个,另外一个自动开启,新生代(复制算法)与老年代(标记整理算法)

-XX:+UseAdaptiveSizePolicy 采用自适应的调整新生代大小(不一定是8:1:1)
-XX:GCTimeRatio=ratio 根据目标不同动态的调整堆的大小。 目标1:吞吐量(堆内存增大,回收次数少,吞吐量大)
-XX:MaxGCPauseMillis=ms 目标2:最大暂停毫秒数(与目标1冲突,堆内存空间减小)
-XX:ParallelGCThreads=n 控制GC线程数

3.4.3.响应时间优先(CMS)

采用的是并发标记清除方式,标记清除算法,在工作的同时,用户线程也能工作,有并发的效果,进一步减少 STW 的时间,在某些部分不需要STW,是工作在老年代的一款垃圾回收器。如果并发失败就会退化为 SerialOld 单线程的基于标记整理的老年代垃圾回收器。

这个地方老师没讲好,初始标记应该是标记第二层的节点,标记的是老年代中gcRoot直接连的或者老年代中被年轻代结点引用的对象(==TODO初始标记和并发标记,标记的是什么?==重新标记扫描整个堆)
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC (这两个配套使用)
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads(一般1/CPU) 并行的垃圾线程和并发的垃圾线程
-XX:CMSInitiatingOccupancyFraction=percent 触发CMS的内存占比,为浮动垃圾预留一定的内存空间
-XX:+CMSScavengeBeforeRemark 做重新标记之前,对新生代进行一次垃圾回收,那么新生代的存活对象少了,扫描的对象就少了。

概述:要清理CPU1线程的垃圾,则先初始标记,就是标记GCroot,然后对CPU1进行链追踪,把追踪到的对象都进行重新标记,最后清理掉CPU1中没有标记的对象
(因为采用的是标记清除算法,老年代内存碎片产生太多,造成分配对象时候,minorGC和老年代都不足,就会造成并发失败,CMS就会退化为单线程的串行 SerialOld 垃圾回收的时间就会很长,本来是响应时间优先的。)

3.4.4. G1

定义:Garbage First(one)

  • 2004 论文发布
  • 2009 JDK 6u14 体验
  • 2012 JDK 7u4 官方支持
  • 2017 JDK 9 默认(废弃了CMS)

适用场景

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
  • 超大堆内存,会将堆划分为多个大小相等的 Region 区域,每个区域都可以作为伊甸园,幸存区和老年代
  • 整体上是 标记+整理 算法,两个区域之间是 复制 算法

相关 JVM 参数
-XX:+UseG1GC Java8 需要通过此开关打开
-XX:G1HeapRegionSize=size 设置区域的大小,1,2,4,8,16M
-XX:MaxGCPauseMillis=time
1)G1 垃圾回收阶段

2)Young Collection

  • 会 STW



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

    4)Mixed Collection
    会对 E、S、O 进行全面垃圾回收
  • 最终标记(Remark)会 STW
  • 拷贝存活(Evacuation)会 STW
    -XX:MaxGCPauseMillis=ms
  • 因为每次清理块的时间是一样的,所以在用户设置的时间内尽可能的回收性价比比较高的块。在垃圾清理时,并不需要一下清理所有的新生代老年代区域,只需要清理那些需要清理的小块(有选择的进行回收,回收的价值较高的区域,同时达到暂停时间最短以及,回收的内存最大)。
  • 叫 G1 是因为优先回收那些垃圾最多的老年代区域,主要的目的是达到暂停时间短的目标。

5)Full GC

  • SerialGC

    • 1)新生代内存不足发生的垃圾收集 - minor gc
    • 2)老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC
    • 1)新生代内存不足发生的垃圾收集 - minor gc
    • 2)老年代内存不足发生的垃圾收集 - full gc
  • CMS
    • 1)新生代内存不足发生的垃圾收集 - minor gc
    • 2)老年代内存不足(如果回收的速度高于产生垃圾的速度,还是属于并发垃圾回收阶段,不叫FullGC,如果回收的速度跟不上产生垃圾的速度,并发收集失败,退化为一个串行的收集,此时的STW时间很长,采取FullGC)
  • G1
    • 1)新生代内存不足发生的垃圾收集 - minor gc
    • 2)老年代内存不足(老年代所占比例达到堆的45%,触发并发标记阶段以及混合手收集阶段,如果回收的速度高于产生垃圾的速度,还是属于并发垃圾回收阶段,不叫FullGC,如果回收的速度跟不上产生垃圾的速度,并发收集失败,退化为一个串行的收集,此时的STW时间很长,采取FullGC)

6)Young Collection 跨代引用(TODO听不明白这里)

  • 新生代回收的跨代引用(老年代引用新生代)问题(减少老年代的搜索范围)

补充一下:在进行对象引用创建时,会有一个查找过程,查找该引用是否被其他区域对象所引用,若是,则在RSet集中标注, 也就是这里的脏Card

  • 卡表与 Remembered Set
  • 在引用变更时通过 post-write barrier + dirty card queue
  • concurrent refinement threads 更新 Remembered Set

7)Remark(重新标记,防止被标记的垃圾重新被引用)

  • pre-write barrier + satb_mark_queue

黑色已经标记完,灰色正在处理中,白色尚未处理





8)JDK 8u20 字符串去重

  • 优点:节省大量内存
  • 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加

-XX:+UseStringDeduplication 开启字符串去重功能开关

String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果它们值一样,让它们引用同一个 char[] 数组
  • 注意,与 String.intern() 不一样
    • 1)String.intern() 关注的是字符串对象
    • 2)而字符串去重关注的是 char[]
    • 3)在 JVM 内部,使用了不同的字符串表

9)JDK 8u40 并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类(类以及类的实例都不用了,自定义的类加载器)。
-XX:+ClassUnloadingWithConcurrentMark 默认启用

10)JDK 8u60 回收巨型对象

  • 一个对象大于 region 的一半时,可能占用多个 regin,称之为巨型对象(存在于巨型对象区)
  • G1 不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉

11)JDK 9 并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC
  • JDK 9 之前需要使用(老年代与堆内存的占比阈值) -XX:InitiatingHeapOccupancyPercent (减少FullGC的发生,提前让垃圾回收开始,让并发标记混合收集提前开始)
  • JDK 9 可以动态调整这个阈值
    • 1)-XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 2)进行数据采样并动态调整
    • 3)总会添加一个安全的空档空间

12)JDK 9 更高效的回收

  • 250+增强
  • 180+bug修复
  • https://docs.oracle.com/en/java/javase/12/gctuning/

3.4.5. 垃圾回收调优

/*
查看虚拟机运行参数
"C:\Program Files\Java\jdk1.8.0_91\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"*/
public class Demo2_8 {}
E:\17.JAVA\StudyMaterial\Java复习资料\05.JVM(TODO)\黑马\代码\jvm\out\production\jvm\cn\itcast\jvm\t1\stringtable>java -XX:+PrintFlagsFinal -version | findstr "GC"uintx AdaptiveSizeMajorGCDecayTimeScale         = 10                                  {product}uintx AutoGCSelectPauseMillis                   = 5000                                {product}bool BindGCTaskThreadsToCPUs                   = false                               {product}uintx CMSFullGCsBeforeCompaction                = 0                                   {product}uintx ConcGCThreads                             = 0                                   {product}bool DisableExplicitGC                         = false                               {product}bool ExplicitGCInvokesConcurrent               = false                               {product}bool ExplicitGCInvokesConcurrentAndUnloadsClasses  = false                               {product}uintx G1MixedGCCountTarget                      = 8                                   {product}uintx GCDrainStackTargetSize                    = 64                                  {product}uintx GCHeapFreeLimit                           = 2                                   {product}uintx GCLockerEdenExpansionPercent              = 5                                   {product}bool GCLockerInvokesConcurrent                 = false                               {product}uintx GCLogFileSize                             = 8192                                {product}uintx GCPauseIntervalMillis                     = 0                                   {product}uintx GCTaskTimeStampEntries                    = 200                                 {product}uintx GCTimeLimit                               = 98                                  {product}uintx GCTimeRatio                               = 99                                  {product}bool HeapDumpAfterFullGC                       = false                               {manageable}bool HeapDumpBeforeFullGC                      = false                               {manageable}uintx HeapSizePerGCThread                       = 87241520                            {product}uintx MaxGCMinorPauseMillis                     = 4294967295                          {product}uintx MaxGCPauseMillis                          = 4294967295                          {product}uintx NumberOfGCLogFiles                        = 0                                   {product}intx ParGCArrayScanChunk                       = 50                                  {product}uintx ParGCDesiredObjsFromOverflowList          = 20                                  {product}bool ParGCTrimOverflow                         = true                                {product}bool ParGCUseLocalOverflow                     = false                               {product}uintx ParallelGCBufferWastePct                  = 10                                  {product}uintx ParallelGCThreads                         = 13                                  {product}bool ParallelGCVerbose                         = false                               {product}bool PrintClassHistogramAfterFullGC            = false                               {manageable}bool PrintClassHistogramBeforeFullGC           = false                               {manageable}bool PrintGC                                   = false                               {manageable}bool PrintGCApplicationConcurrentTime          = false                               {product}bool PrintGCApplicationStoppedTime             = false                               {product}bool PrintGCCause                              = true                                {product}bool PrintGCDateStamps                         = false                               {manageable}bool PrintGCDetails                            = false                               {manageable}bool PrintGCID                                 = false                               {manageable}bool PrintGCTaskTimeStamps                     = false                               {product}bool PrintGCTimeStamps                         = false                               {manageable}bool PrintHeapAtGC                             = false                               {product rw}bool PrintHeapAtGCExtended                     = false                               {product rw}bool PrintJNIGCStalls                          = false                               {product}bool PrintParallelOldGCPhaseTimes              = false                               {product}bool PrintReferenceGC                          = false                               {product}bool ScavengeBeforeFullGC                      = true                                {product}bool TraceDynamicGCThreads                     = false                               {product}bool TraceParallelOldGCTasks                   = false                               {product}bool UseAdaptiveGCBoundary                     = false                               {product}bool UseAdaptiveSizeDecayMajorGCCost           = true                                {product}bool UseAdaptiveSizePolicyWithSystemGC         = false                               {product}bool UseAutoGCSelectPolicy                     = false                               {product}bool UseConcMarkSweepGC                        = false                               {product}bool UseDynamicNumberOfGCThreads               = false                               {product}bool UseG1GC                                   = false                               {product}bool UseGCLogFileRotation                      = false                               {product}bool UseGCOverheadLimit                        = true                                {product}bool UseGCTaskAffinity                         = false                               {product}bool UseMaximumCompactionOnSystemGC            = true                                {product}bool UseParNewGC                               = false                               {product}bool UseParallelGC                            := true                                {product}bool UseParallelOldGC                          = true                                {product}bool UseSerialGC                               = false                               {product}
java version "1.8.0_291"
Java(TM) SE Runtime Environment (build 1.8.0_291-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, mixed mode)

预备知识

  • 掌握 GC 相关的 VM 参数,会基本的空间调整
  • 掌握相关工具
  • 明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则

1)调优领域

  • 内存
  • 锁竞争
  • cpu 占用
  • io

2)确定目标

  • 【低延迟】还是【高吞吐量】,选择合适的回收器
  • CMS,G1(管理超大堆内存),ZGC【高响应时间】
  • ParallelGC【高吞吐量】
  • Zing【超低延迟】

3)最快的 GC
答案是不发生 GC

查看 FullGC 前后的内存占用,考虑下面几个问题

  • 数据是不是太多?

    • 1)resultSet = statement.executeQuery(“select * from 大表 limit n”)
  • 数据表示是否太臃肿?
    • 1)对象图(用的哪个属性查那个,不要一次性全部查出来)
    • 2)对象大小 16字节至少 Integer 24 int 4(能用基本类型的不用包装类型)
  • 是否存在内存泄漏?
    • 1)static Map map = (像map中一直放某个无用但一直存活的对象)
    • 2)软
    • 3)弱
    • 4)第三方缓存实现

4)新生代调优

新生代的特点

  • 所有的 new 操作的内存分配非常廉价 (TLAB thread-local allocation buffer,每个线程私有的内存,分配新对象的缓冲区,不会产生线程安全问题)
  • 死亡对象的回收代价是零(只采用复制算法复制存活的对象,不用管死亡的对象)
  • 大部分对象用过即死
  • Minor GC 的时间远远低于 Full GC

调优的本质是减少 STW 的时间 ,1、优化代码减少创建对象的内存使用 2、添加参数使用合适的垃圾回收器以及调整垃圾回收器的参数来减少 STW 的时间

新生代越大越好吗?(HosPort虚拟机老年代空间会减少,触发FullGC)

-Xmn
Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is performed in this region more often than in other regions. If the size for the young generation is too small, then a lot of minor garbage collections are performed. If the size is too large, then only full garbage collections are performed, which can take a long time to complete. Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size.(1/4到1/2,尽可能的大,主要的消耗时间都在复制上)

  • 新生代能容纳所有【并发量 * (请求-响应)】的数据
  • 幸存区大到能保留【当前活跃对象+需要晋升对象】
  • 晋升阈值配置得当,让长时间存活对象尽快晋升
    -XX:MaxTenuringThreshold=threshold调整最大竟升阈值 -XX:+PrintTenuringDistribution 打印开关

5)老年代调优

以 CMS 为例

  • CMS 的老年代内存越大越好
  • 先尝试不做调优,如果没有 Full GC 那么已经…,否则先尝试调优新生代
  • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
    -XX:CMSInitiatingOccupancyFraction=percent 触发FullGC的老年代内存占比(70%-80%)

6)案例

  • 案例1 Full GC 和 Minor GC频繁

分析可能是空间紧张,新生代空间紧张,导致幸存区的竟升阈值降低,提前进入老年代,进一步恶化,老年代存入了大量生命周期短的对象,触发FullGC。可以先增加新生代内存,让新生代的垃圾回收不那么频繁,增大幸存区的内存,让生命周期较短的对象留在新生代。

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

CMS 采用的是低延时,分析到底是CMS中的哪一个部分时间耗费的较长,查看GC日志。初始标记、并发标记、重新标记和并发清理。前两个部分比较快,重新标记阶段耗时较大,会扫描整个的堆内存(老年代、新生代)对象特别的多,能不能在重新标记之前,将新生代的垃圾进行回收一遍(minorGC)减少新生代的数量,调这个参数-XX:+CMSScavengeBeforeRemark

  • 初始标记:仅仅标记GC ROOTS的直接关联对象,并且世界暂停
  • 并发标记:使用GC ROOTS TRACING算法,进行跟踪标记,世界不暂停
  • 重新标记,因为之前并发标记,其他用户线程不暂停,可能产生了新垃圾,所以重新标记,世界暂停
  • 补充黄字的补充:并发标记过程中产生变动的对象会放入一个队列中供重新标记过程遍历
  • 有了并发标记,重新标记只是去找并发标记期间其他线程产生的浮动垃圾,垃圾会少,速度快(写屏障加入对列是G1回收器,这里的案例说的是CMS)
  • 案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7采用的是永久代作为方法区的实现)

永久代的不足也会导致 FullGC 的发生,源空间由操作系统垃圾回收。

4.类加载与字节码技术

  1. 类文件结构
  2. 字节码指令
  3. 编译期处理
  4. 类加载阶段
  5. 类加载器
  6. 运行期优化

4.1.类文件结构

一个简单的 HelloWorld.java

执行 javac -parameters -d . HellowWorld.java
编译为 HelloWorld.class 后是这个样子的:

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

4.1.1.魔数

0~3 字节,表示它是否是【class】类型的文件

4.1.2.版本

4~7 字节,表示类的版本 00 34(52) 表示是 Java 8

4.1.3.常量池











(以下的部分TOSEE文档P10和视屏P101)

4.1.4.访问标识与继承信息

4.1.5.Field 信息

4.1.6.Method 信息

4.1.7.附加属性

4.2.字节码指令

4.2.1.入门

接着上一节,研究一下两组字节码指令,一个是
public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令

另一个是 public static void main(java.lang.String[]); 主方法的字节码指令

请参考
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5

4.2.2.javap 工具

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



4.2.3.图解方法执行流程

1)原始 java 代码

2)编译后的字节码文件

E:\17.JAVA\StudyMaterial\Java复习资料\05.JVM(TODO)\黑马\代码\jvm\out\production\jvm\cn\itcast\jvm\t3\bytecode>javap -v Demo3_1.class
Classfile /E:/17.JAVA/StudyMaterial/Java复习资料/05.JVM(TODO)/黑马/代码/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_1.classLast modified 2022-6-1; size 635 bytesMD5 checksum 1a6413a652bcc5023f130b392deb76a1Compiled from "Demo3_1.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_1minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref          #7.#25         // java/lang/Object."<init>":()V#2 = Class              #26            // java/lang/Short#3 = Integer            32768#4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;#5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V#6 = Class              #31            // cn/itcast/jvm/t3/bytecode/Demo3_1#7 = Class              #32            // java/lang/Object#8 = Utf8               <init>#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#20 = Utf8               I#21 = Utf8               b#22 = Utf8               c#23 = Utf8               SourceFile#24 = Utf8               Demo3_1.java#25 = NameAndType        #8:#9          // "<init>":()V#26 = Utf8               java/lang/Short#27 = Class              #33            // java/lang/System#28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;#29 = Class              #36            // java/io/PrintStream#30 = NameAndType        #37:#38        // println:(I)V#31 = Utf8               cn/itcast/jvm/t3/bytecode/Demo3_1#32 = Utf8               java/lang/Object#33 = Utf8               java/lang/System#34 = Utf8               out#35 = Utf8               Ljava/io/PrintStream;#36 = Utf8               java/io/PrintStream#37 = Utf8               println#38 = Utf8               (I)V
{public cn.itcast.jvm.t3.bytecode.Demo3_1();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 6: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       5     0  this   Lcn/itcast/jvm/t3/bytecode/Demo3_1;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=4, args_size=10: bipush        102: istore_13: ldc           #3                  // int 327685: istore_26: iload_17: iload_28: iadd9: istore_310: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;13: iload_314: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V17: returnLineNumberTable:line 8: 0line 9: 3line 10: 6line 11: 10line 12: 17LocalVariableTable:Start  Length  Slot  Name   Signature0      18     0  args   [Ljava/lang/String;3      15     1     a   I6      12     2     b   I10       8     3     c   I
}
SourceFile: "Demo3_1.java"

3)class文件中的常量池载入运行时常量池

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

5)main 线程开始运行,分配栈帧内存
(stack=2最大操作数栈深度,locals=4局部变量的槽位个数)


6)执行引擎开始执行字节码
bipush 10

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

istore_1

  • 将操作数栈顶数据弹出,存入局部变量表的 slot 1

ldc #3

  • 从运行时常量池加载 #3 数据到操作数栈
  • 注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的

istore_2

iload_1

iload_2

iadd


istore_3


getstatic #4


iload_3


invokevirtual #5

  • 找到常量池 #5 项
  • 定位到方法区 java/io/PrintStream.println:(I)V 方法
  • 生成新的栈帧(分配 locals、stack等)
  • 传递参数,执行新栈帧中的字节码
  • 执行完毕,弹出栈帧
  • 清除 main 操作数栈内容

return

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

4.2.4.练习 - 分析 i++

目的:从字节码角度分析 a++ 相关题目
源码:

字节码:

Classfile /E:/17.JAVA/StudyMaterial/Java复习资料/05.JVM(TODO)/黑马/代码/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_2.classLast modified 2022-6-1; size 610 bytesMD5 checksum 5f6a35e5b9bb88d08249958a8d2ab043Compiled from "Demo3_2.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_2minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref          #5.#22         // java/lang/Object."<init>":()V#2 = Fieldref           #23.#24        // java/lang/System.out:Ljava/io/PrintStream;#3 = Methodref          #25.#26        // java/io/PrintStream.println:(I)V#4 = Class              #27            // cn/itcast/jvm/t3/bytecode/Demo3_2#5 = Class              #28            // java/lang/Object#6 = Utf8               <init>#7 = Utf8               ()V#8 = Utf8               Code#9 = Utf8               LineNumberTable#10 = Utf8               LocalVariableTable#11 = Utf8               this#12 = Utf8               Lcn/itcast/jvm/t3/bytecode/Demo3_2;#13 = Utf8               main#14 = Utf8               ([Ljava/lang/String;)V#15 = Utf8               args#16 = Utf8               [Ljava/lang/String;#17 = Utf8               a#18 = Utf8               I#19 = Utf8               b#20 = Utf8               SourceFile#21 = Utf8               Demo3_2.java#22 = NameAndType        #6:#7          // "<init>":()V#23 = Class              #29            // java/lang/System#24 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;#25 = Class              #32            // java/io/PrintStream#26 = NameAndType        #33:#34        // println:(I)V#27 = Utf8               cn/itcast/jvm/t3/bytecode/Demo3_2#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               (I)V
{public cn.itcast.jvm.t3.bytecode.Demo3_2();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 6: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       5     0  this   Lcn/itcast/jvm/t3/bytecode/Demo3_2;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=10: bipush        102: istore_13: iload_14: iinc          1, 17: iinc          1, 110: iload_111: iadd12: iload_113: iinc          1, -116: iadd17: istore_218: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;21: iload_122: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V25: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;28: iload_229: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V32: returnLineNumberTable:line 8: 0line 9: 3line 10: 18line 11: 25line 12: 32LocalVariableTable:Start  Length  Slot  Name   Signature0      33     0  args   [Ljava/lang/String;3      30     1     a   I18      15     2     b   I
}
SourceFile: "Demo3_2.java"

分析:

  • 注意 iinc 指令是直接在局部变量 slot 上进行运算
  • a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc










4.2.5.条件判断指令


几点说明:

  • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
  • goto 用来进行跳转到指定行号的字节码

源码:

字节码:

思考
细心的同学应当注意到,以上比较指令中没有 long,float,double 的比较,那么它们要比较怎么办?
参考JVM官方

4.2.6.条件判断指令

其实循环控制还是前面介绍的那些指令,例如 while 循环:

字节码是:

再比如 do while 循环:


最后再看看 for 循环:

字节码是:

注意
比较 while 和 for 的字节码,你发现它们是一模一样的,殊途也能同归

4.2.7.练习 - 判断结果

请从字节码角度分析,下列代码运行的结果:

4.2.8.练习 - 构造方法


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

<cinit>()V 方法会在类加载的初始化阶段被调用(类的构造方法)

2)<init>()V

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

执行步骤完整:父类的clinit-子类的clinit-父类的init-子类的init

4.2.9.方法调用

看一下几种不同的方法调用对应的字节码指令

字节码:

  • new 是创建【对象】,给对象①分配堆内存,执行②成功会将【对象引用】压入操作数栈
  • dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 "<init>":()V (会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量
  • 最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定(编译期间就能确定的)
  • 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态
  • 成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
  • 比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了
  • 还有一个执行 invokespecial 的情况是通过 super 调用父类方法

4.2.10.多态的原理

/*** 演示多态原理,注意加上下面的 JVM 参数,禁用指针压缩* -XX:-UseCompressedOops -XX:-UseCompressedClassPointers*/
public class Demo3_10 {public static void test(Animal animal) {animal.eat();System.out.println(animal.toString());}public static void main(String[] args) throws IOException {test(new Cat());test(new Dog());System.in.read();}
}abstract class Animal {public abstract void eat();@Overridepublic String toString() {return "我是" + this.getClass().getSimpleName();}
}class Dog extends Animal {@Overridepublic void eat() {System.out.println("啃骨头");}
}class Cat extends Animal {@Overridepublic void eat() {System.out.println("吃鱼");}
}

1)运行代码
停在 System.in.read() 方法上,这时运行 jps 获取进程 id
2)运行 HSDB 工具
进入 JDK 安装目录,执行

java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

进入图形界面 attach 进程 id


3)查找某个对象
打开 Tools -> Find Object By Query
输入 select d from cn.itcast.jvm.t3.bytecode.Dog d 点击 Execute 执行


4)查看对象内存结构
点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是 MarkWord,后 8 字节就是对象的 Class 指针

但目前看不到它的实际地址

5)查看对象 Class 的内存地址

可以通过 Windows -> Console 进入命令行模式,执行

mem 0x00000001299b4978 2

mem 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)
结果中第二行 0x000000001b7d4028 即为 Class 的内存地址



6)查看类的 vtable




7)验证方法地址

8)小结
当执行 invokevirtual 指令时,

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

4.2.11.异常处理

try-catch

  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
  • 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置

多个 single-catch 块的情况

  • 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用

multi-catch 的情况


finally

  • finally的原理是将finally{}块中的代码分别放在try{…}的最后和catch{…}的最后
  • 如果try除了问题,直接进入catch中,然后catch后面复制的finally就会被执行
  • 如果没有出问题,try{}后面的代码就会被执行
  • 如果出现catch的异常类不匹配或者try和catch中出现了新异常,此时的finally也会被执行

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

4.2.12.练习 - finally 面试题

finally 出现了 return
先问问自己,下面的题目输出什么?(20)