深入理解JVM(二,JVM初识线程上下文类加载器和java字节码)
线程上下文类加载器的引入。
最好最常见的引入:对应得数据库连接包中
Class.forName("org.postgresql.Driver").newInstance(); String url ="jdbc:postgresql://localhost/myDB" //myDB为数据库名 String user="myuser"; String password="mypassword"; Connection conn= DriverManager.getConnection(url,user,password);
通过上面可知,这些连接方式,肯定是最初已经加载了的,显然是bootstrap加载器已经将相关类加载。因此才能初始时我们可这样直接写(反问:什么都有依据,那么你凭什么就可以直接使用?原因就在早已经加载了Connection类,这些具体的定制都是由厂商制定的,我们只是将他放在了对应的位置,加载使用)
因此引入ContextClassLoader来破坏双亲委托机制,来实现上面的SPI加载。
package com.auto.demo; /** * 当前类加载器(current classloader):每个都会尝试用自己的类加载器去加载其他类 * 这就是下面是null的原因; * ContextClassLoader(线程上下文加载器):Thread类中的getContextClassLoader与setContextClassLoader * 用于获取和设置上下文加载器。 * 没有setContextClassLoader进行设置,默认线程将继承其父类的线程的上下文加载器。 * java应运行时初始线程的上下文类加载器是系统类加载器。 * 线程上下文加载器的重要性: * ① SPI(Service Provide Interface) * ② 父ClassLoader可以使用当前线程Thread.currentThread.getContextoader()所指定的classloader加载的类。 * 这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的ClassLoader加载的类的情况, * 即是改变了双亲委托机制。 * ③ 在双亲委托机制模型下,来加载器是由上至下的,即下层的类加载会委托上层进行加载。但是对于SPI来说,有些接口是java核心 * 库所提供的,而在java核心库是由启动类加载器加载的,而这些接口的实现却来自于不同的jar包(各大厂商提供),java的启动类加 * 载器是不会加载其他来源的jar包的,这样传统的双亲委托模型就无法满足SPI的要求,而通过给当前线程设置上下文类加载器 * 来实现对于接口实现类的加载。(框架开发极其常用) * @author zhouyi * */ public class MyContextTest { public static void main(String[] args) { System.out.println(Thread.currentThread().getContextClassLoader()); // 系统类加载器 System.out.println(Thread.class.getClassLoader()); // 是由启动类加载加载,因此是null } }
运行结果:
public class MyContextTest implements Runnable { private Thread thread; public MyContextTest() { thread = new Thread(this); // 因为是Runnable所以必须放入线程中 thread.start(); } @Override public void run() { ClassLoader clLoader = this.thread.getContextClassLoader(); this.thread.setContextClassLoader(clLoader); System.out.println("class: " + clLoader.getClass()); System.out.println("parent: " + clLoader.getParent().getClass()); } public static void main(String[] args) { new MyContextTest(); } }
运行结果:
ServiceLoader
是服务提供者加载的集合,比如加载数据库连接,建议查看源码。
package com.auto.demo; import java.sql.Driver; import java.util.Iterator; import java.util.ServiceLoader; /** *线程上下文类加载器的一般使用模式(获取 - 使用 - 还原) *获取:ClassLoader clLoader = this.thread.getContextClassLoader(); *使用: *try { * Thread.currentThread().setContextClassLoader(targetClassLoader); * myMethod(); // 我们自己要做操作的方法 *} finally { * Thread.currentThread().getContextClassLoader(clLoader); // 还原 *} *myMethod():调用了Thread.currentThread().getContextClassLoader(targetClassLoader);获取当前线程的上下文类加载器做某些事。 * *当高层提供了统一的接口让低层实现,同时又要在高层加载(或实例化)低层的类时, *就必须通过县城上下文类加载器来帮助高层的ClassLoader找到并加载。 * @author zhouyi * */ public class MyContextTest { public static void main(String[] args) { ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class); // 拿到数据库连接驱动 Iterator<Driver> it = loader.iterator(); while (it.hasNext()) { Driver driver = it.next(); System.out.println("driver: " + driver + ",loader: " + driver.getClass().getClassLoader()); } System.out.println("当前线程上下文类加载器: " + Thread.currentThread().getContextClassLoader()); System.out.println("service的类加载器: " + ServiceLoader.class.getClassLoader()); } }
运行结果:
修改它的加载方式:(改为上层)
public class MyContextTest { public static void main(String[] args) { Thread.currentThread().setContextClassLoader(MyContextTest.class.getClassLoader().getParent()); ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class); // 拿到数据库连接驱动 Iterator<Driver> it = loader.iterator(); while (it.hasNext()) { Driver driver = it.next(); System.out.println("driver: " + driver + ",loader: " + driver.getClass().getClassLoader()); } System.out.println("当前线程上下文类加载器: " + Thread.currentThread().getContextClassLoader()); System.out.println("service的类加载器: " + ServiceLoader.class.getClassLoader()); } }
运行结果:
关于线程上下文类加载器,扩展到Servlet、tomcat和框架搭载相关的知识,有时间一定要去研究研究
接下来是java字节码(前段时间这个是写完了的,可是在提交时csdn出了bug,导致内容全清空,只好重新来过。)
字节码文件结构解析
前提准备软件:(这是IDEA可运用的jclasslib,字节码分析工具)
安装完成后,如下:
另外还需要一个字节码查看以及编辑工具:(这里如果是windows系统请用winhex,如果是mac系统请用hex_friend)只需要导入相应的.class文件即可如下:
上面的是我们程序的字节码:
package com.bytecode.stu; public class IOStudy { int a = 1; public int getA() { return a; } public void setA(int b) { this.a = b; } }
有了这些准备,我们开始分析。
1,使用java -verbose命令分析一个字节码文件,将会分析字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与类成员变量等信息,如下:
2,魔数(Magic number) : 所有的,class字节码文件的前4个字节都是魔数,魔数是固定值 : CA FE BA BE(上图查看,是这个JVM才会认可,执行)
3,版本号: 魔数之后的4个字节就是版本信息,次版本号是前两个字节:00 00 (Minor version),主版本号是后面2个字节:00 34(Major version)。由上面可知,34(16进制)对应的是52(10进制),即1.8.0于是去查看版本信息,结果如下:(_后的不是)
4,常量池(Constant Pool): 紧跟版本号之后的就是常量池入口,一个java类中定义的很多的信息都是由常量池来维护和描述的,可以将常量池看作是class文件的资源仓库,比如说java类中定义的方法与变量信息,都是存储在常量池中。常量池主要存储两类常量:字面量和符号引用。字面量如文本字符串,java中声明为final的常量值等;符号引用如类和接口的全局限定名(前面反射的内容:0(default)、1(public)、2(private)、4(protected)),字段的名称和描述符,方法的名称和描述符等。
5,常量池的总体结构:java类所对应的常量池主要是由常量池数量与常量池数组(常量表)这两部分共同构成,常量池数量紧跟在主版本号后面,占据2个字节;常量池数组则紧跟在常量池数量之后,常量池数组与一般的数组不同是,常量池数组中不同的元素的类型、结构都是不同的,长度当然也不同的,但是每一个元素的第一个数据都是u1类型的,该字节是一个标志位,占据一个字符,JVM在解析常量池时,会根据这个u1类型来获取元素的具体类型,值得注意的是,常量池数组中的元素个数 = 常量池 - 1(其中0暂时不使用),目的是满足某些常量池索引值的数据在特定的情况下需要表达【不引用任何一个常量池】的含义:其根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值;所以,常量值的索引从1,而不是0。
这里涉及了u1,那么就讲讲class字节码结构中常量池中11种数据类型的结构总表(JVM如何识别字节码,返回为对饮的class,就是按照下面的规则)
常量池结构总表
例如:
6,在JVM规范中,每个变量/字段都有相应的描述信息,描述信息主要的作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值,根据描述符规则,基本数据类型和代表无返回值的void类型都是用一个大写的字符来表示,对象类型则使用字符Lj加全限定名称来表示。为了压缩字节码文件的体积,对于基本数据类型,JVM都只是使用一个大写的字母来表示。如下:B - byte , C - char , D - double , F -float , I - int , J - long , S - short , Z - boolean , V - void , L - 对象全限定类型(如:Ljava/lang/String)。
7,对于数组类型来说,每个一维度使用一个前置的
[来表示,如int[] 被记录为:[I,String[][] 被记录为:[[Lj 1c13f ava/lang/String;
8,用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组()之内,如方法:
String getRealnamebyIdAndNickname(int id , String name);的描述符为:(I,Ljava/lang/String;)Ljava/lang/String
有了这些东西我们来都上面字节码:
紧跟在主版本号后面2个字节的常量池数量:00 19(10进制为24,有24个常量,根据这个去读),上面显示是25,也是和规则对一个的减一。
之后的是第一个常量。首先是描述符(第一个是u1类型):0A(10),去查看上面的常量池结构总表是CONSTANT_Methodref_info对应两个u2,即是00 04(声明方法的类描述符索引) 00 15(指向名称及类型描述符索引)。
接下来下一个常量,首先也是描述符:09(9),去查看上面的常量池结构总表是CONSTANT_Fieldref_info,对应两个u2,即是:00 03 0016(便不再做解释,自己去对应)
等等。。。。。。。。。。。。。。直到第24个读完为止,常量池结束。
完整的java字节码结构:
Access_Flag(访问标识符)
紧跟着是访问标识符这里的并包含全,比如上面的提到的private等,但是是可以并集的。比如上面000000F0行的 0×0021是由0×0020和0×0001的并集,表示为super和public。但是这里的归类依然是不全的额。
紧接着是This Class Name和Super Class Name这两个比较简单。分别都是占两个字节,分别是:00 03和00 04
紧接着就是接口interface,也是占两个字节,即是:00 00 ,没有接口
Fields(字段表)
再就是字段表,字段表是用于描述接口和类中声明的变量。这里的字段包含了类级别的变量以及实例变量,但是不包括方法的内部声明的局部变量。
除了下面给出的,首先要给出**fields_count,**是u2类型。即是 属性的数量:00 01
字段表结构:
对应上面的字段结构表可知: 00 02(access_flags)00 05(name_index) 00 06(decrptor_index) 00 00(attributes_count)
由于attributes_count是00 00所以最后一个不会再出现。
这里补充一点:凡是有_index的都是指向常量池的对应的项(需要转成10进制)
(方法表)Methods
接下来便是方法,其实和字段表的几乎一模一样。
field_count : 00 03,三个方法。method_info : 00 01 00 07 00 08 00 01(由于最后不为0,那么就有attribute_info)
那么我们就需要了解方法的属性结构(字段表也一样的)attribute_info
即是:attribute_name_index(00 09)和attribute_length(00 00 00 38)(这个56是code的内容),info是真正的code内容。
那么我们就得先看看Code,里面是我们方法的具体信息。我们引入:
先看看Code结构:他的作用是保存该方法的结构
attribute_length : 表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段。(这里需要注意前面已经有了这两个不会重复)
max_stack : (00 02;指向常量池2)表示这个方法运行的任何时刻所能达到的操作数栈的最大深度。
mac_locals : (00 01)表示该方法执行期间创建的局部变量数目,包含用来表示传入的参数的局部变量。
code_length : ( 00 00 00 0A)表示该方法的所包含的字节码的字节数以及具体的指令码。具体的字节码即是该方法被调用时,虚拟机所执行的字节码。(注记:length都是往后数多少个字节,即是code的正真内容)
exception_table,这里存放的是处理异常的信息。
看看except_table的信息,他的组成如上表。
start_pc和end_pc表示在code数组中的从start_pc和end_pc处的指令抛出异常会有这个表象来处理。
handler_pc表示处理异常的代码块的开始处。catch_type表示湖北处理的异常类型,他指向常量池里面的一个异常类。当catch_type为0时,表示处理所有的异常。
接下来的对应最好借助工具,因为已经很难分析了。
首先,是方法,下面的助记符是可以点进去的,以后若有看不懂的,就这样区查看即可。这里介绍几种。
显然用这个工具很多东西都是一目了然,因此推荐使用这个工具去分析。但是如果你不懂其中的规则也是枉然。所以建议先是了解结构和基础,再去借助工具。
附加属性–LineNumberTable,简单来说就是Java代码对应的行号。用来还原代码的。结构如下:
实例图:start PC是字节码对应的行号,Line Number是Java代码中对应的行号。
LocalVariableTable是和他基本一样的。
终极图:
G:\ownStudy\out\production\ownStudy>cd com G:\ownStudy\out\production\ownStudy\com>cd bytecode G:\ownStudy\out\production\ownStudy\com\bytecode>cd stu G:\ownStudy\out\production\ownStudy\com\bytecode\stu>javap -verbose IOStudy 警告: 二进制文件IOStudy包含com.bytecode.stu.IOStudy Classfile /G:/ownStudy/out/production/ownStudy/com/bytecode/stu/IOStudy.class Last modified 2019-4-27; size 483 bytes MD5 checksum 621af6f4b7c7b6ae6f41f110dbcf73bd Compiled from "IOStudy.java" public class com.bytecode.stu.IOStudy minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#21 // java/lang/Object."<init>":()V #2 = Fieldref #3.#22 // com/bytecode/stu/IOStudy.a:I #3 = Class #23 // com/bytecode/stu/IOStudy #4 = Class #24 // java/lang/Object #5 = Utf8 a #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/bytecode/stu/IOStudy; #14 = Utf8 getA #15 = Utf8 ()I #16 = Utf8 setA #17 = Utf8 (I)V #18 = Utf8 b #19 = Utf8 SourceFile #20 = Utf8 IOStudy.java #21 = NameAndType #7:#8 // "<init>":()V #22 = NameAndType #5:#6 // a:I #23 = Utf8 com/bytecode/stu/IOStudy #24 = Utf8 java/lang/Object { protected int a; descriptor: I flags: ACC_PROTECTED public com.bytecode.stu.IOStudy(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: return LineNumberTable: line 3: 0 line 4: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/bytecode/stu/IOStudy; public int getA(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field a:I 4: ireturn LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/bytecode/stu/IOStudy; public void setA(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return LineNumberTable: line 11: 0 line 12: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/bytecode/stu/IOStudy; 0 6 1 b I } SourceFile: "IOStudy.java"
- 深入理解JVM(十一)——Java内存模型与线程
- 深入理解Java类加载器(2):线程上下文类加载器
- 深入理解Java类加载器(2):线程上下文类加载器
- 深入理解Java类加载器(2):线程上下文类加载器
- 深入理解JVM-Java线程-实现方式,线程调度,状态
- 深入理解Java虚拟机JVM高级特性与最佳实践阅读总结—— 第十三章 线程安全与锁优化
- 深入理解Java类加载器(2):线程上下文类加载器
- 深入理解Java类加载器(二):线程上下文类加载器
- 深入理解Java虚拟机JVM高级特性与最佳实践阅读总结—— 第十二章 Java内存模型与线程
- 真正理解线程上下文类加载器:tomcat and Spring
- 深入理解JVM之Java字节码(.class)文件详解
- 深入理解JVM线程模型
- 2.[深入理解JVM笔记]Java内存模型与线程
- 深入理解Java类加载器(2):线程上下文类加载器
- 深入理解JVM - Java内存模型与线程
- 深入理解Java类加载器:线程上下文类加载器
- 深入理解JVM虚拟机 线程安全与锁优化
- 深入理解Java类加载器(2):线程上下文类加载器
- 深入理解Java类加载器:线程上下文类加载器
- 深入理解JVM(6)——类加载器