jvm-第五节类加载器

本篇知识点概况

  1. 字节码相关知识
  1. 字节码的基础知识,了解字节码的概念,用途,特点
  2. 字节码的分析工具使用
  3. class文件格式格式与详解
  4. 字节码的指令:基础存储运算指令,异常处理,装箱拆箱,数组
  5. 热点探测之jit与字节码
  1. 类加载与类加载器相关知识
  1. 一个类的生命周期
  2. jdk提供的三种类加载器
  3. 自定义类加载器
  1. 问题:

字节码相关知识

1.字节码的基础知识,了解字节码的概念,用途,特点


  1. 字节码是一堆指令的集合,它具有平台无关性,可以运行在任何支持jvm的平台上;是Java程序在运行时的执行单元;它是一种中间形式的代码,是源代码和机器码之间的桥梁
  2. jvm-第五节类加载器_加载


2.字节码的分析工具使用

  1. 以这个工具举例,可以查看具体的指令,可以修改里面的值,github地址https://github.com/ingokegel/jclasslib

3.class文件格式格式与详解

  1. class文件中各个部分的含义
  1. class文件里是16进制的(一个字节占8位,一个十六进制占4位,所以cafebabe占32位),前四个字节0xCAFEBABE;作用是标识这是一个有效的class文件;
  2. 后面的 0000 0034表示版本;其中第五六个字节表示次版本号,第七八字节是主版本号(0034(16)=52(10));Java 版本从45开始jdk1.1每发行一个大版本就加一

  3. 常量池 0010 表示常量的数量,常量池中常见的俩类常量,字面量以及符号引用,字面量一般是静态常量,符号引用是类的全限定名字段名,方法名;
  4. jvm-第五节类加载器_类加载器_02

  5. 访问标志(Access Flags):(了解)
  • 访问标志指示了类或者接口的访问权限和属性,如是否是public、final、abstract等。
  • 访问标志由两个字节表示,使用特定的标志位来表示不同的访问属性。
  1. 类索引、父类索引和接口索引(This Class, Super Class, Interfaces):(了解)
  • 类索引指向当前类在常量池中的类描述符的常量项。
  • 父类索引指向当前类的直接父类在常量池中的类描述符的常量项。
  • 接口索引表存储了当前类所实现的接口的索引,每个接口索引指向常量池中的接口描述符的常量项。
  1. 字段表(Fields):(了解)
  • 字段表用于描述类或接口中定义的字段信息,包括字段的访问标志、名称、描述符等。
  • 字段表中的每一项都包含了字段的相关信息,可以是类变量、实例变量或常量。
  1. 方法表(Methods):(了解)
  • 方法表用于描述类或接口中定义的方法信息,包括方法的访问标志、名称、描述符等。
  • 方法表中的每一项都包含了方法的相关信息,包括方法的参数列表、返回值类型、异常表等。
  1. 属性表(Attributes):(了解)
  • 属性表用于存储与类、字段或方法相关的附加信息,如注解、源文件信息、行号表等。
  • 属性表包含了属性的名称、长度以及具体的属性值。

4.字节码的指令:基础存储运算指令,异常处理,装箱拆箱,数组(了解)

  1. 基础存储运算指令:包括将值加载到操作数栈上、从操作数栈上存储值到变量中等操作。例如:
  • iload:将int类型的变量加载到操作数栈上
  • istore:将int类型的值存储到变量中
  1. 异常处理指令:用于处理异常情况,包括抛出异常和捕获异常。例如:
  • athrow:抛出异常
  • try-catch:捕获和处理异常
  1. 装箱拆箱指令:用于基本类型和对应的包装类之间的转换。例如:
  • invokestatic:调用静态方法进行装箱或拆箱
  • invokevirtual:调用包装类的实例方法进行装箱或拆箱
  1. 数组指令:用于创建和操作数组。例如:
  • newarray:创建一个基本类型的数组
  • anewarray:创建一个引用类型的数组
  • arraylength:获取数组的长度
  • iaload:将int类型的值加载到操作数栈上

5.热点探测之jit与字节码

  1. java程序运行时主要就是执行字节码指令,解释执行时需要翻译成机器码,这个效率比较低,为了提高效率就有了jit(just in time compiler);
  2. 热点代码,比如for里的代码就会缓存起来,为了下次用
  3. 热点探测:在 HotSpot 虚拟机中的热点探测是 JIT 优化的条件,热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执
    行次数,如果执行次数超过一定的阈值就认为它是“热点方法”
    虚拟机为每个方法准备了两类计数器:方法调用计数器(
    Invocation Counter)和回边计数器(Back Edge Counter)。在确定虚拟机运行参数的前提下,这
    两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发 JIT 编译。
  4. 方法调用计数器:用于统计方法被调用的次数,方法调用计数器的默认阈值在 C1 模式下是 1500 次,在 C2 模式在是 10000 次,可通过 -XX: CompileThreshold 来设定;
    而在分层编译的情况下,-XX: CompileThreshold 指定的阈值将失效,此时将会根据当前待编译的方法数以及编译线程数来动态调整。当方法计数器和回边
    计数器之和超过方法计数器阈值时,就会触发 JIT 编译器。
  5. 回边计数器:用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge),该值用于计算是否触发 C1 编译的阈值,
    在不开启分层编译的情况下,C1 默认为 13995,C2 默认为 10700,可通过 -XX: OnStackReplacePercentage=N 来设置;而在分层编译的情况下,-XX:
    OnStackReplacePercentage 指定的阈值同样会失效,此时将根据当前待编译的方法数以及编译线程数来动态调整。
    建立回边计数器的主要目的是为了触发 OSR(On StackReplacement)编译,即栈上编译。在一些循环周期比较长的代码段中,当循环达到回边计数器阈值时,JVM 会认为这段是热点代码,JIT 编译器就会将这段代码编译成机器语言并缓存,在该循环时间段内,会直接将执行代码替换,执行缓存的机器语
    言。

6.字节码总结:都是一些需要了解的东西,在以后jvm调优,会用到

类加载与类加载器相关知识

1.一个类的生命周期


  1. 一个类的生命周期:加载,验证,准备,解析,初始化,使用,卸载
  2. jvm-第五节类加载器_加载_03

  3. 这里有个顺序问题,解析的顺序可能在初始化之后才开始,为了支持Java的动态绑定;
  4. 加载的时机:
  1. 初次使用时
  2. 引用了类中的某个静态属性
  3. 反射调用
  4. 子类继承时调用
  1. 验证:包括四种验证,文件格式,元数据,字节码,符号引用验证,验证重要但不是必要步骤,如果觉得没必要可以通过参数关闭-Xverify:none

  2. 准备:为类中的static修饰的属性赋初始值,这是赋值表
  3. jvm-第五节类加载器_字节码_04

  4. 解析:将符号引用替换成直接引用的过程
  5. 初始化:
  1. 当遇到四个关键字 new getstatic putstatic 和invokestatic时就会触发初始化
  2. 触发反射时
  3. 触发子类时,父类要初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的那个类),虚拟机会先初始化这个主类
  5. 下面的案例子类调用父类的属性时,子类和父类的加载情况和初始化情况,这块建议看一下字节码,看他是否加载了子类
  1. 线程安全:由于一个类的初始化时一个一个线程来的,其他线程都阻塞,所以是线程安全的

2.jdk提供的三层类加载器

  1. bootstrap classloader:加载核心类库,任何加载行为都要经过他,c++编写,随着jvm启动;
  2. extention classloader:主要加载lib/ext 下的jar和class文件,这是一个java类继承了 urlClassLoader
  3. application classloader:是默认的Java类加载器,加载classPath下的jar class文件,我们写的代码首先用这个加载器
  4. custom classloader:自定义加载器,支持一些扩展,下面详细说
  5. 类加载器问题;对于任意一个类,同一个类加载器,同一个类,确定jvm中该类的唯一性,注意每一个类加载器有自己独立的命名空间

  6. 双亲委派机制:向上委托,向下加载,可以避免重复加载一个类
  7. jvm-第五节类加载器_类加载器_05

3.自定义类加载器tomcat

  1. 先说结论, 如何解决tomcat通过war发布服务违背双亲委派机制的问题?
  1. 将第三方依赖放入tomcat的公共目录,由公共加载器加载就实现了共享,使用webappclassloader加载器加在web就先实现了每个web有自己独立的类加载器,实现了隔离;
  1. 为什么说tomcat通过war发布服务是违背了双亲委派机制,因为tomcat有一个webappclassloader,拥有加载web的优先权,这意味着当他需要加载类时会首先搜索自己的路径( 即WEB-INF/classesWEB-INF/lib目录 );
  2. 如果一个jvm运行着俩个不同版本的web,是如何解决的呢?看下面的代码

自定义类加载器-扩展

  1. spi:service provider interface,是一套被第三方实现,扩展的api,他不是在编译时检查,而是在运行时加载,表现为当我们写一行代码 class.forName("com.mysql.jdbc.Driver"),不会报错 ,详细的解释如下
  1. 在Java的SPI(Service Provider Interface)机制中,Class.forName("com.mysql.jdbc.Driver")并不需要引入对应的JAR文件来让代码编译通过。这是因为在SPI机制中,服务提供者的具体实现类是通过类路径(Classpath)动态加载的,而不是在编译时就确定的。
    当你调用Class.forName("com.mysql.jdbc.Driver")时,JVM会尝试在类路径上查找并加载com.mysql.jdbc.Driver类。如果找到了该类,JVM就会加载它并执行相应的静态代码块。这时,com.mysql.jdbc.Driver类会向JVM注册自己作为MySQL数据库的驱动程序。这样,在后续的代码中,你就可以使用java.sql.DriverManager类来获取MySQL数据库的连接。
    如果你在运行时没有将MySQL驱动程序的JAR文件放在类路径上,Class.forName("com.mysql.jdbc.Driver")会抛出ClassNotFoundException异常。但是,如果你确保MySQL驱动程序的JAR文件已经由其他方式加载(如通过Tomcat的公共库目录),那么Class.forName("com.mysql.jdbc.Driver")就不会抛出异常,因为类加载器已经能够找到并加载了com.mysql.jdbc.Driver类。
    需要注意的是,最新的MySQL驱动已经迁移到了com.mysql.cj.jdbc.Driver类,而不再是com.mysql.jdbc.Driver。因此,如果你使用的是较新的MySQL驱动版本,应该使用Class.forName("com.mysql.cj.jdbc.Driver")来加载驱动类。
    总结来说,Class.forName("com.mysql.jdbc.Driver")不会在编译时检查类的存在与否,而是在运行时动态加载类。如果类路径上存在对应的类,加载就会成功,否则会抛出ClassNotFoundException异常。

问题

  1. 为什么要拆箱装箱
  2. integerCache
  3. 双亲委派机制的原名: Parent Delegation Mode , 父级委托模型 这个名称更贴切一些