JVM之类加载与字节码(二)

3. 编译期处理

什么是语法糖

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

以下代码分析的前提

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

3.1 默认构造器

public class Candy1 {
}

编译成class后的代码:

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

3.2 自动拆装箱

这个特性是 JDK 5 开始加入的, 代码片段1 :

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

这段代码在 JDK 5 之前是无法编译通过的,必须改写为 代码片段2 :

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

        显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编 译阶段被转换为 代码片段2

3.3 泛型集合取值

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

public class Candy3 {public static void main(String[] args) {List<Integer> list = new ArrayList<>();list.add(10); // 实际调用的是 List.add(Object e)Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);}
}

所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作:

// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0)

如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:

// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();

擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息

public cn.itcast.jvm.t3.candy.Candy3();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/candy/Candy3;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=10: new #2 // class java/util/ArrayList3: dup4: invokespecial #3 // Method java/util/ArrayList."
<init>":()V7: astore_18: aload_19: bipush 1011: invokestatic #4 // Method
java/lang/Integer.valueOf:(I)Ljava/lang/Integer;14: invokeinterface #5, 2 // InterfaceMethod
java/util/List.add:(Ljava/lang/Object;)Z19: pop20: aload_121: iconst_022: invokeinterface #6, 2 // InterfaceMethod
java/util/List.get:(I)Ljava/lang/Object;27: checkcast #7 // class java/lang/Integer30: astore_231: returnLineNumberTable:line 8: 0line 9: 8line 10: 20line 11: 31LocalVariableTable:Start Length Slot Name Signature0     32     0   args [Ljava/lang/String;8     24     1   list Ljava/util/List;LocalVariableTypeTable:Start Length Slot Name Signature8    24     1   list Ljava/util/List<Ljava/lang/Integer;>;

使用反射,仍然能够获得这些信息:

public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
}
Method test = Candy3.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes();
for (Type type : types) {if (type instanceof ParameterizedType) {ParameterizedType parameterizedType = (ParameterizedType) type;System.out.println("原始类型 - " + parameterizedType.getRawType());Type[] arguments = parameterizedType.getActualTypeArguments();for (int i = 0; i < arguments.length; i++) {System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);}}
}

输出

原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object

3.4 可变参数

可变参数也是 JDK 5 开始加入的新特性:

Java代码

public class Candy4 {public static void foo(String... args) {String[] array = args; // 直接赋值System.out.println(array);}public static void main(String[] args) {foo("hello", "world");}
}

被编译器转换后的代码

可变参数 String... args 其实是一个 String[] args ,从下面的代码中就可以看出来

public class Candy4 {public static void foo(String[] args) {String[] array = args; // 直接赋值System.out.println(array);}public static void main(String[] args) {foo(new String[]{"hello", "world"});}
}

注意:如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会传递 null 进去

3.5 foreach 循环

仍是 JDK 5 开始引入的语法糖

1.数组的 foreach 循环

Java代码

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

被编译器转换后的代码

public class Candy5_1 {public Candy5_1() {}public static void main(String[] args) {int[] array = new int[]{1, 2, 3, 4, 5};for(int i = 0; i < array.length; ++i) {int e = array[i];System.out.println(e);}}
}

2.集合的 foreach 循环

Java代码

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

被编译器转换后的代码

实际被编译器转换为对迭代器的调用

public class Candy5_2 {public Candy5_2() {}public static void main(String[] args) {List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);Iterator iter = list.iterator();while(iter.hasNext()) {Integer e = (Integer)iter.next();System.out.println(e);}}
}

注意:foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器( Iterator )

3.6 switch

字符串从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖

1. switch 字符串

Java代码

public class Candy6_1 {public static void choose(String str) {switch (str) {case "hello": {System.out.println("h");break;}case "world": {System.out.println("w");break;}}}
}

注意 switch 配合 String 和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚

被编译器转换后的代码

public class Candy6_1 {public Candy6_1() {}public static void choose(String str) {byte x = -1;switch(str.hashCode()) {case 99162322: // hello 的 hashCodeif (str.equals("hello")) {x = 0;}break;case 113318802: // world 的 hashCodeif (str.equals("world")) {x = 1;}}switch(x) {case 0:System.out.println("h");break;case 1:System.out.println("w");}}
}

可以看到,执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较。

为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢?hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突,例如 BM 和 C,这两个字符串的hashCode值都是 2123 ,如果有如下Java代码:

public class Candy6_2 {public static void choose(String str) {switch (str) {case "BM": {System.out.println("h");break;}case "C.": {System.out.println("w");break;}}}
}

被编译器转换后的代码

public class Candy6_2 {public Candy6_2() {}public static void choose(String str) {byte x = -1;switch(str.hashCode()) {case 2123: // hashCode 值可能相同,需要进一步用 equals 比较if (str.equals("C.")) {x = 1;} else if (str.equals("BM")) {x = 0;}default:switch(x) {case 0:System.out.println("h");break;case 1:System.out.println("w");}}}
}

2.switch 枚举

Java代码

enum Sex {MALE, FEMALE
}
public class Candy7 {public static void foo(Sex sex) {switch (sex) {case MALE:System.out.println("男"); break;case FEMALE:System.out.println("女"); break;}}
}

被编译器转换后的代码

public class Candy7 {/*** 定义一个合成类(仅 jvm 使用,对我们不可见)* 用来映射枚举的 ordinal 与数组元素的关系* 枚举的 ordinal 表示枚举对象的序号,从 0 开始* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1*/static class $MAP {// 数组大小即为枚举元素个数,里面存储case用来对比的数字static int[] map = new int[2];static {map[Sex.MALE.ordinal()] = 1;map[Sex.FEMALE.ordinal()] = 2;}}public static void foo(Sex sex) {int x = $MAP.map[sex.ordinal()];switch (x) {case 1:System.out.println("男");break;case 2:System.out.println("女");break;}}
}

3.7 枚举类

JDK 7 新增了枚举类,以前面的性别枚举为例:

Java代码

enum Sex {MALE, FEMALE
}

转换后代码

public final class Sex extends Enum<Sex> {public static final Sex MALE;public static final Sex FEMALE;private static final Sex[] $VALUES;static {MALE = new Sex("MALE", 0);FEMALE = new Sex("FEMALE", 1);$VALUES = new Sex[]{MALE, FEMALE};}/*** Sole constructor. Programmers cannot invoke this constructor.* It is for use by code emitted by the compiler in response to* enum type declarations.** @param name - The name of this enum constant, which is the identifier* used to declare it.* @param ordinal - The ordinal of this enumeration constant (its position* in the enum declaration, where the initial constant isassigned*/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);}
}

3.8 try-with-resources

JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources

语法格式

try(资源变量 = 创建资源对象){} catch( ) {}

Java代码

其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、 Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try-with-resources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码

public class Candy9 {public static void main(String[] args) {try(InputStream is = new FileInputStream("d:\\1.txt")) {System.out.println(is);} catch (IOException e) {e.printStackTrace();}}
}

转换后代码

public class Candy9 {public Candy9() {}public static void main(String[] args) {try {InputStream is = new FileInputStream("d:\\1.txt");Throwable t = null;try {System.out.println(is);} catch (Throwable e1) {// t 是我们代码出现的异常t = e1;throw e1;} finally {// 判断了资源不为空if (is != null) {// 如果我们代码有异常if (t != null) {try {is.close();} catch (Throwable e2) {// 如果 close 出现异常,作为被压制异常添加t.addSuppressed(e2);}} else {// 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 eis.close();}}}} catch (IOException e) {e.printStackTrace();}}
}

为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常):

Java代码

public class Test6 {public static void main(String[] args) {try (MyResource resource = new MyResource()) {int i = 1/0;} catch (Exception e) {e.printStackTrace();}}
}class MyResource implements AutoCloseable {public void close() throws Exception {throw new Exception("close 异常");}
}

输出:

java.lang.ArithmeticException: / by zeroat test.Test6.main(Test6.java:7)Suppressed: java.lang.Exception: close 异常at test.MyResource.close(Test6.java:18)at test.Test6.main(Test6.java:6)
  • 如以上代码所示,两个异常信息都不会丢。

3.9 方法重写时的桥接方法

方法重写时对返回值分两种情况:

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类

Java代码

class A {public Number m() {return 1;}
}
class B extends A {@Override// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类public Integer m() {return 2;}
}

子类转换后的代码

class B extends A {public Integer m() {return 2;}// 此方法才是真正重写了父类 public Number m() 方法public synthetic bridge Number m() {// 调用 public Integer m()return m();}
}

其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突,可以用下面反射代码来验证:

for (Method m : B.class.getDeclaredMethods()) {System.out.println(m);
}

会输出:

public java.lang.Integer test.candy.B.m()
public java.lang.Number test.candy.B.m()

3.10 匿名内部类

Java代码

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

转换后的代码

// 额外生成的类
final class Candy11$1 implements Runnable {Candy11$1() {}public void run() {System.out.println("ok");}
}public class Candy11 {public static void main(String[] args) {Runnable runnable = new Candy11$1();}
}

引用局部变量的匿名内部类的Java代码

public class Candy11 {public static void test(final int x) {Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("ok:" + x);}};}
}

转换后代码:

// 额外生成的类
final class Candy11$1 implements Runnable {int val$x;Candy11$1(int x) {this.val$x = x;}public void run() {System.out.println("ok:" + this.val$x);}
}public class Candy11 {public static void test(final int x) {Runnable runnable = new Candy11$1(x);}
}

注意

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

4. 类加载阶段

4.1 加载

将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

  • _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
  • _super 即父类
  • _fields 即成员变量
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即类加载器 +
  • _vtable 虚方法表
  • _itable 接口方法表

如果这个类还有父类没有加载,先加载父类

加载和链接可能是交替运行的

注意

  • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中
  • 可以通过前面介绍的 HSDB 工具查看

4.2 链接

链接分为验证、准备、解析三个子阶段

4.2.1 验证

验证类是否符合 JVM 规范,安全性检查

用 UE 等支持二进制的编辑器修改 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/HelloWorldat 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)

修改完 HelloWorld.class 的魔数后,报错信息如上图所示,说明验证没有通过

4.2.2 准备

为 static 变量分配空间,设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶 段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

4.2.3 解析

将常量池中的符号引用解析为直接引用

package cn.itcast.jvm.t3.load;
/**
* 解析的含义
*/
public class Load2 {public static void main(String[] args) throws ClassNotFoundException,
IOException {ClassLoader classloader = Load2.class.getClassLoader();// loadClass 方法不会导致类的解析和初始化Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");new C();System.in.read();}
}class C {D d = new D();
}class D {
}

4.3 初始化

1.初始化时调用的方法

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

2.初始化发生的时机

概括得说,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName 的参数 2 为 false 时

3.实验验证初始化发生的时机

实验用到的类

class A {static int a = 0;static {System.out.println("a init");}
}class B extends A {final static double b = 5.0;static boolean c = false;static {System.out.println("b init");}
}

验证(实验时请先全部注释,每次只执行其中一个)

public class Load3 {static {System.out.println("main init");}public static void main(String[] args) throws ClassNotFoundException {// 1. 静态常量(基本类型和字符串)不会触发初始化System.out.println(B.b);// 2. 类对象.class 不会触发初始化System.out.println(B.class);// 3. 创建该类的数组不会触发初始化System.out.println(new B[0]);// 4. 不会初始化类 B,但会加载 B、AClassLoader cl = Thread.currentThread().getContextClassLoader();cl.loadClass("cn.itcast.jvm.t3.B");// 5. 不会初始化类 B,但会加载 B、AClassLoader c2 = Thread.currentThread().getContextClassLoader();Class.forName("cn.itcast.jvm.t3.B", false, c2);// 1. 首次访问这个类的静态变量或静态方法时System.out.println(A.a);// 2. 子类初始化,如果父类还没初始化,会引发System.out.println(B.c);// 3. 子类访问父类静态变量,只触发父类初始化System.out.println(B.a);// 4. 会初始化类 B,并先初始化类 AClass.forName("cn.itcast.jvm.t3.B");}
}

5. 类加载器

以 JDK 8 为例:

5.1 启动类加载器

用 Bootstrap 类加载器加载类

Java类

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:

5.2 扩展类加载器

Java类

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

执行

public class Load5_2 {public static void main(String[] args) throws ClassNotFoundException {Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");System.out.println(aClass.getClassLoader());}
}

输出

classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2

写一个同名的类

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

打个 jar 包

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%)

将 jar 包拷贝到 JAVA_HOME/jre/lib/ext

重新执行 Load5_2

输出

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

5.3 双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 1. 检查该类是否已经加载Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {// 2. 有上级的话,委派上级 loadClassc = parent.loadClass(name, false);} else {// 3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoaderc = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {}if (c == null) {long t1 = System.nanoTime();// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载c = findClass(name);// 5. 记录耗时sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}
}

例如:

public class Load5_3 {public static void main(String[] args) throws ClassNotFoundException {Class<?> aClass = Load5_3.class.getClassLoader().loadClass("cn.itcast.jvm.t3.load.H");System.out.println(aClass.getClassLoader());}
}

执行流程为:

1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有

2. sun.misc.Launcher$AppClassLoader // 2 处,委派上级 sun.misc.Launcher$ExtClassLoader.loadClass()

3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有

4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader 查找

5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有

6. sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在 JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher$AppClassLoader 的 // 2 处

7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在 classpath 下查找,找到了

5.4 线程上下文类加载器

我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写

Class.forName("com.mysql.jdbc.Driver")

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?

让我们追踪一下源码:

public class DriverManager {// 注册驱动的集合private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers= new CopyOnWriteArrayList<>();// 初始化驱动static {loadInitialDrivers();println("JDBC DriverManager initialized");}

先不看别的,看看 DriverManager 的类加载器:

System.out.println(DriverManager.class.getClassLoader());

打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

继续看 loadInitialDrivers() 方法:

private static void loadInitialDrivers() {String drivers;try {drivers = AccessController.doPrivileged(new PrivilegedAction<String>
() {public String run() {return System.getProperty("jdbc.drivers");}});} catch (Exception ex) {drivers = null;}// 1)使用 ServiceLoader 机制加载驱动,即 SPIAccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {ServiceLoader<Driver> loadedDrivers =
ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}return null;}});println("DriverManager.initialize: jdbc.drivers = " + drivers);// 2)使用 jdbc.drivers 定义的驱动名加载驱动if (drivers == null || drivers.equals("")) {return;}String[] driversList = drivers.split(":");println("number of Drivers:" + driversList.length);for (String aDriver : driversList) {try {println("DriverManager.Initialize: loading " + aDriver);// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());} catch (Exception ex) {println("DriverManager.Initialize: load failed: " + ex);}}
}

先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此 可以顺利完成类加载

再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)

约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

这样就可以使用

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);Iterator<接口类型> iter = allImpls.iterator();while(iter.hasNext()) {iter.next();}

来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC
  • Servlet 初始化器
  • Spring 容器
  • Dubbo(对 SPI 进行了扩展)

接着看 ServiceLoader.load 方法:

public static <S> ServiceLoader<S> load(Class<S> service) {// 获取线程上下文类加载器ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类 LazyIterator 中:

private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn = nextName;nextName = null;Class<?> c = null;try {c = Class.forName(cn, false, loader);} catch (ClassNotFoundException x) {fail(service,"Provider " + cn + " not found");}if (!service.isAssignableFrom(c)) {fail(service,"Provider " + cn + " not a subtype");}try {S p = service.cast(c.newInstance());providers.put(cn, p);return p;} catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x);}throw new Error(); // This cannot happen
}

5.5 自定义类加载器

什么时候需要自定义类加载器

  • 1)想加载非 classpath 随意路径中的类文件
  • 2)都是通过接口来使用实现,希望解耦时,常用在框架设计
  • 3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤: 

1. 继承 ClassLoader 父类

2. 要遵从双亲委派机制,重写 findClass 方法 注意不是重写 loadClass 方法,否则不会走双亲委派机制

3. 读取类文件的字节码

4. 调用父类的 defineClass 方法来加载类

5. 使用者调用该类加载器的 loadClass 方法

6.运行期优化

6.1 即时编译

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/80350.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C高级 作业 day4 8/7

1.整理思维导图 2.写一个函数&#xff0c;获取用户的uid和gid并使用变量接收 3.整理冒泡排序、选择排序和快速排序的代码 冒泡排序 //冒泡排序 void bubble_sort(int a[],int len) {int count0;for(int i1;i<len;i) //控制轮数{for(int j0;j<len-i;j) //控制每一轮交换…

flex 弹性布局

Flex 布局的使用 任何一个容器都可以指定为 Flex 布局。 .box{ display: flex; //flex作为display的一个属性使用 } 行内元素也可以使用 Flex 布局。 .box{ display: inline-flex; } 注意&#xff1a;设为 Flex 布局以后&#xff0c;子元素的float、clear和vertical-align…

Java_理解方法调用

理解方法调用 首先什么是隐式参数 --->隐式参数是调用该方法的对象本身。 接下来方法的名称和参数列表被称为方法的签名&#xff08;signature&#xff09;。在Java中&#xff0c;方法的签名由方法的名称和参数列表组成&#xff0c;用于唯一标识一个方法。返回类型不是签名的…

Mysql存储引擎InnoDB

一、存储引擎的简介 MySQL 5.7 支持的存储引擎有 InnoDB、MyISAM、Memory、Merge、Archive、Federated、CSV、BLACKHOLE 等。 1、InnoDB存储引擎 从MySQL5.5版本之后&#xff0c;默认内置存储引擎是InnoDB&#xff0c;主要特点有&#xff1a; &#xff08;1&#xff09;灾难恢…

【MCU学习】GD32F427VG开发

&#xff08;一&#xff09;学习文档和例程 兆易创新GD32 MCU参考资料下载 1.GD232F4xx的Keil芯片支持包 2.标准固件库和示例程序 3.GD32F4xx_固件库使用指南_Rev1.2 4.用户手册&#xff1a;GD32F4xx_User_Manual_Rev2.8_CN 5.数据手册&#xff1a;GD32F427xx_Datasheet_Rev…

SpringMVC的注解

文章目录 前言前期准备ResponseBody 返回JSONRequestMapping 映射控制器GetMapping、PostMapping 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; SpringMVC框架只需要少量的配置即可快速实现Web应用程序开发&#xff0c;不需要大量的XML配置文件。 不…

电脑第一次使用屏幕键盘

操作流程 1.在键盘上同时按WinR打开运行; 2.输入control 3.找到设置中心 4.点击屏幕键盘 效果 具体怎么使用 我不咋清除 简单 测试了一下 可以用鼠标点击屏幕键盘的按键 用键盘 按字母键和数字键 是和屏幕键盘不同步的 其他 tab、shift、后退、enter好像同步

分立式BUCK电路原理与制作持续更新

一、分立式BUCK电路总体原理图 下面改图包含了电压环和电流环。 二、BUCK电路与LDO的区别 LDO不适合在压差大的环境下使用&#xff0c;因为三极管因为CE极承受了压差&#xff0c;压差越大损耗的功率就越大&#xff0c;将三极管换成MOS管&#xff0c;MOS管两端的压差很小所以效…

梳理日常开发涉及的负载均衡

负载均衡是当前分布式微服务时代最能提及的词之一&#xff0c;出于对分层、解耦、弱依赖、可配置、可靠性等概念的解读&#xff0c;一对一的模式变得不再可信赖&#xff0c;千变万化的网络环境中&#xff0c;冗余和备份显得格外重要&#xff0c;稍大型的系统就会存在大量微服务…

TextBrewer:融合并改进了NLP和CV中的多种知识蒸馏技术、提供便捷快速的知识蒸馏框架、提升模型的推理速度,减少内存占用

TextBrewer:融合并改进了NLP和CV中的多种知识蒸馏技术、提供便捷快速的知识蒸馏框架、提升模型的推理速度&#xff0c;减少内存占用 TextBrewer是一个基于PyTorch的、为实现NLP中的知识蒸馏任务而设计的工具包&#xff0c; 融合并改进了NLP和CV中的多种知识蒸馏技术&#xff0…

C高级-day4

#!/bin/bash function fun1(){arr[0]id -u $1arr[1]id -g $1echo ${arr[*]} }arr(fun1 ubuntu) echo ${arr[*]}冒泡排序 void Maopao(int arr[],int len){for(int i1;i<len;i){int count0;for(int j0;j<len-i;j){if(arr[j]>arr[j1]){int tarr[j];arr[j]arr[j1];arr[j…

智能优化算法——哈里鹰算法(Matlab实现)

目录 1 算法简介 2 算法数学模型 2.1.全局探索阶段 2.2 过渡阶段 2.3.局部开采阶段 3 求解步骤与程序框图 3.1 步骤 3.2 程序框图 4 matlab代码及结果 4.1 代码 4.2 结果 1 算法简介 哈里斯鹰算法(Harris Hawks Optimization&#xff0c;HHO)&#xff0c;是由Ali…

如何解决 Elasticsearch 查询缓慢的问题以获得更好的用户体验

作者&#xff1a;Philipp Kahr Elasticsearch Service 用户的重要注意事项&#xff1a;目前&#xff0c;本文中描述的 Kibana 设置更改仅限于 Cloud 控制台&#xff0c;如果没有我们支持团队的手动干预&#xff0c;则无法进行配置。 我们的工程团队正在努力消除对这些设置的限制…

arcgis宗地或者地块四至权利人信息提取教程

ARCGIS怎样将图斑四邻的名称及方位加入其属性表 以前曾发表过一篇《 如何把相邻图斑的属性添加在某个字段中》的个人心得,有些会员提出了进一步的要求,不但要相邻图斑的名称,还要求有方位,下面讲一下自己的做法。 基本思路是:连接相邻图斑质心,根据连线的角度确定相邻图斑…

c++游戏制作指南(三):c++剧情类文字游戏的制作

&#x1f37f;*★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★* &#x1f37f; &#x1f35f;欢迎来到静渊隐者的csdn博文&#xff0c;本文是c游戏制作指南的一部&#x1f35f; &#x1f355;更多文章请点击下方链接&#x1f355; &#x1f368; c游戏制作指南&#x1f3…

21、p6spy输出执行SQL日志

文章目录 1、背景2、简介3、接入3.1、 引入依赖3.2、修改database参数&#xff1a;3.3、 创建P6SpyLogger类&#xff0c;自定义日志格式3.4、添加spy.properties3.5、 输出样例 4、补充4.1、参数说明 1、背景 在开发的过程中&#xff0c;总希望方法执行完了可以看到完整是sql语…

供水管网漏损监测,24小时保障城市供水安全

供水管网作为城市生命线重要组成部分&#xff0c;其安全运行是城市建设和人民生活的基本保障。随着我国社会经济的快速发展和城市化进程的加快&#xff0c;城市供水管网的建设规模日益增长。然而&#xff0c;由于管网老化、外力破坏和不当维护等因素导致的供水管网漏损&#xf…

js修改img的src属性显示变换图片到前端页面,img的src属性显示java后台读取返回的本地图片

文章目录 前言一、HTML 图像- 图像标签&#xff08; <img>&#xff09;1.1图像标签的源属性&#xff08;Src&#xff09;1.2图像标签源属性&#xff08;Src&#xff09;显示项目中图片1.3图像标签源属性&#xff08;Src&#xff09;显示网络图片 二、图像标签&#xff08…

Docker实战-关于Docker镜像的相关操作(二)

导语   之前的分享中&#xff0c;我们介绍了关于Docker镜像的查询操作相关的内容&#xff0c;下面我们继续来介绍删除清理、导入导出、创建镜像等操作。 如何删除和清理镜像&#xff1f; 使用标签删除镜像 可以使用docker rmi 或者是 docker image rm 命令来删除镜像&#x…

segment-anything使用说明

文章目录 一. segment-anything介绍二. 官网Demo使用说明三. 安装教程四. python调用生成掩码教程五. python调用SAM分割后转labelme数据集 一. segment-anything介绍 Segment Anything Model&#xff08;SAM&#xff09;根据点或框等输入提示生成高质量的对象遮罩&#xff0c…