jvm-第五节类加载器
# jvm-第五节类加载器
本篇知识点概况
- 字节码相关知识
- 字节码的基础知识,了解字节码的概念,用途,特点
- 字节码的分析工具使用
- class文件格式格式与详解
- 字节码的指令:基础存储运算指令,异常处理,装箱拆箱,数组
- 热点探测之jit与字节码
- 类加载与类加载器相关知识
- 一个类的生命周期
- jdk提供的三种类加载器
- 自定义类加载器
- 问题:
字节码相关知识
1.字节码的基础知识,了解字节码的概念,用途,特点
- 字节码是一堆指令的集合,它具有平台无关性,可以运行在任何支持jvm的平台上;是Java程序在运行时的执行单元;它是一种中间形式的代码,是源代码和机器码之间的桥梁
2.字节码的分析工具使用
- 以这个工具举例,可以查看具体的指令,可以修改里面的值,github地址https://github.com/ingokegel/jclasslib
3.class文件格式格式与详解
- class文件中各个部分的含义
- class文件里是16进制的(一个字节占8位,一个十六进制占4位,所以cafebabe占32位),前四个字节0xCAFEBABE;作用是标识这是一个有效的class文件;
- 后面的 0000 0034表示版本;其中第五六个字节表示次版本号,第七八字节是主版本号(0034(16)=52(10));Java 版本从45开始jdk1.1每发行一个大版本就加一
- 常量池 0010 表示常量的数量,常量池中常见的俩类常量,字面量以及符号引用,字面量一般是静态常量,符号引用是类的全限定名字段名,方法名;
- 访问标志(Access Flags):(了解)
- 访问标志指示了类或者接口的访问权限和属性,如是否是public、final、abstract等。
- 访问标志由两个字节表示,使用特定的标志位来表示不同的访问属性。
- 类索引、父类索引和接口索引(This Class, Super Class, Interfaces):(了解)
- 类索引指向当前类在常量池中的类描述符的常量项。
- 父类索引指向当前类的直接父类在常量池中的类描述符的常量项。
- 接口索引表存储了当前类所实现的接口的索引,每个接口索引指向常量池中的接口描述符的常量项。
- 字段表(Fields):(了解)
- 字段表用于描述类或接口中定义的字段信息,包括字段的访问标志、名称、描述符等。
- 字段表中的每一项都包含了字段的相关信息,可以是类变量、实例变量或常量。
- 方法表(Methods):(了解)
- 方法表用于描述类或接口中定义的方法信息,包括方法的访问标志、名称、描述符等。
- 方法表中的每一项都包含了方法的相关信息,包括方法的参数列表、返回值类型、异常表等。
- 属性表(Attributes):(了解)
- 属性表用于存储与类、字段或方法相关的附加信息,如注解、源文件信息、行号表等。
- 属性表包含了属性的名称、长度以及具体的属性值。
4.字节码的指令:基础存储运算指令,异常处理,装箱拆箱,数组(了解)
- 基础存储运算指令:包括将值加载到操作数栈上、从操作数栈上存储值到变量中等操作。例如:
iload
:将int类型的变量加载到操作数栈上istore
:将int类型的值存储到变量中
- 异常处理指令:用于处理异常情况,包括抛出异常和捕获异常。例如:
athrow
:抛出异常try-catch
:捕获和处理异常
- 装箱拆箱指令:用于基本类型和对应的包装类之间的转换。例如:
invokestatic
:调用静态方法进行装箱或拆箱invokevirtual
:调用包装类的实例方法进行装箱或拆箱
- 数组指令:用于创建和操作数组。例如:
newarray
:创建一个基本类型的数组anewarray
:创建一个引用类型的数组arraylength
:获取数组的长度iaload
:将int类型的值加载到操作数栈上
5.热点探测之jit与字节码
-
java程序运行时主要就是执行字节码指令,解释执行时需要翻译成机器码,这个效率比较低,为了提高效率就有了jit(just in time compiler);
-
热点代码,比如for里的代码就会缓存起来,为了下次用
-
热点探测:在 HotSpot 虚拟机中的热点探测是 JIT 优化的条件,热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执
行次数,如果执行次数超过一定的阈值就认为它是“热点方法”
虚拟机为每个方法准备了两类计数器:方法调用计数器(
Invocation Counter)和回边计数器(Back Edge Counter)。在确定虚拟机运行参数的前提下,这
两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发 JIT 编译。
-
方法调用计数器:用于统计方法被调用的次数,方法调用计数器的默认阈值在 C1 模式下是 1500 次,在 C2 模式在是 10000 次,可通过 -XX: CompileThreshold 来设定;
而在分层编译的情况下,-XX: CompileThreshold 指定的阈值将失效,此时将会根据当前待编译的方法数以及编译线程数来动态调整。当方法计数器和回边
计数器之和超过方法计数器阈值时,就会触发 JIT 编译器。
-
回边计数器:用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge),该值用于计算是否触发 C1 编译的阈值,
在不开启分层编译的情况下,C1 默认为 13995,C2 默认为 10700,可通过 -XX: OnStackReplacePercentage=N 来设置;而在分层编译的情况下,-XX:
OnStackReplacePercentage 指定的阈值同样会失效,此时将根据当前待编译的方法数以及编译线程数来动态调整。
建立回边计数器的主要目的是为了触发 OSR(On StackReplacement)编译,即栈上编译。在一些循环周期比较长的代码段中,当循环达到回边计数器阈值时,JVM 会认为这段是热点代码,JIT 编译器就会将这段代码编译成机器语言并缓存,在该循环时间段内,会直接将执行代码替换,执行缓存的机器语
言。
6.字节码总结:都是一些需要了解的东西,在以后jvm调优,会用到
类加载与类加载器相关知识
1.一个类的生命周期
- 一个类的生命周期:加载,验证,准备,解析,初始化,使用,卸载
- 这里有个顺序问题,解析的顺序可能在初始化之后才开始,为了支持Java的动态绑定;
- 加载的时机:
- 初次使用时
- 引用了类中的某个静态属性
- 反射调用
- 子类继承时调用
- 验证:包括四种验证,文件格式,元数据,字节码,符号引用验证,验证重要但不是必要步骤,如果觉得没必要可以通过参数关闭-Xverify:none
- 准备:为类中的static修饰的属性赋初始值,这是赋值表
- 解析:将符号引用替换成直接引用的过程
- 初始化:
- 当遇到四个关键字 new getstatic putstatic 和invokestatic时就会触发初始化
- 触发反射时
- 触发子类时,父类要初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的那个类),虚拟机会先初始化这个主类
- 下面的案例子类调用父类的属性时,子类和父类的加载情况和初始化情况,这块建议看一下字节码,看他是否加载了子类
- 线程安全:由于一个类的初始化时一个一个线程来的,其他线程都阻塞,所以是线程安全的
2.jdk提供的三层类加载器
- bootstrap classloader:加载核心类库,任何加载行为都要经过他,c++编写,随着jvm启动;
- extention classloader:主要加载lib/ext 下的jar和class文件,这是一个java类继承了 urlClassLoader
- application classloader:是默认的Java类加载器,加载classPath下的jar class文件,我们写的代码首先用这个加载器
- custom classloader:自定义加载器,支持一些扩展,下面详细说
- 类加载器问题;对于任意一个类,同一个类加载器,同一个类,确定jvm中该类的唯一性,注意每一个类加载器有自己独立的命名空间
- 双亲委派机制:向上委托,向下加载,可以避免重复加载一个类
3.自定义类加载器tomcat
- 先说结论, 如何解决tomcat通过war发布服务违背双亲委派机制的问题?
- 将第三方依赖放入tomcat的公共目录,由公共加载器加载就实现了共享,使用webappclassloader加载器加在web就先实现了每个web有自己独立的类加载器,实现了隔离;
- 为什么说tomcat通过war发布服务是违背了双亲委派机制,因为tomcat有一个webappclassloader,拥有加载web的优先权,这意味着当他需要加载类时会首先搜索自己的路径( 即
WEB-INF/classes
和WEB-INF/lib
目录 ); - 如果一个jvm运行着俩个不同版本的web,是如何解决的呢?看下面的代码
自定义类加载器-扩展
-
spi:service provider interface,是一套被第三方实现,扩展的api,他不是在编译时检查,而是在运行时加载,表现为当我们写一行代码 class.forName("com.mysql.jdbc.Driver"),不会报错 ,详细的解释如下
-
在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
异常。
-
问题
- 为什么要拆箱装箱
- integerCache
- 双亲委派机制的原名: Parent Delegation Mode , 父级委托模型 这个名称更贴切一些