JVM从看懂到看开Ⅲ -- 类加载与字节码技术【下】
文章目录
- 编译期处理
- 默认构造器
- 自动拆装箱
- 泛型集合取值
- 可变参数
- foreach 循环
- switch 字符串
- switch 枚举
- 枚举类
- try-with-resources
- 方法重写时的桥接方法
- 匿名内部类
- 类加载阶段
- 加载
- 链接
- 初始化
- 相关练习和应用
- 类加载器
- 类与类加载器
- 启动类加载器
- 拓展类加载器
- 双亲委派模式
- 自定义类加载器
- 拓:线程上下文类加载器
- 运行期优化
- 即时编译
- 分层编译
- 方法内联
- 字段优化
- 反射优化
编译期处理
所谓的 语法糖
,其实就是指 java 编译器把 .java
源码编译为 .class
字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利
注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。
默认构造器
public class Candy1 {}
经过编译期优化后;
public class Candy1 {//这个无参构造器是java编译器帮我们加上的public Candy1() {//即调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()Vsuper();}
}
自动拆装箱
基本类型和其包装类型的相互转换过程,称为拆装箱
在JDK 5以后,它们的转换可以在编译期自动完成
public class Demo2 {public static void main(String[] args) {Integer x = 1;int y = x;}
}
转换过程如下;
public class Demo2 {public static void main(String[] args) {//基本类型赋值给包装类型,称为装箱Integer x = Integer.valueOf(1);//包装类型赋值给基本类型,称谓拆箱int y = x.intValue();}
}
泛型集合取值
泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除
的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:
public class Demo3 {public static void main(String[] args) {List<Integer> list = new ArrayList<>();list.add(10); //实际调用的是add(Objcet o)Integer x = list.get(0); //实际调用的是get(Object o)}
}
对应字节码:
Code: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;//这里进行了泛型擦除,实际调用的是add(Objcet o)14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z19: pop20: aload_121: iconst_0//这里也进行了泛型擦除,实际调用的是get(Object o) 22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
//这里进行了类型转换,将Object转换成了Integer27: checkcast #7 // class java/lang/Integer30: astore_231: return
所以在取值时,编译器真正生成的字节码中,还要额外做一些类型转换的操作:
//需要将Object转化为Integer
Integer x = (Integer) list.get(0);
如果前面的x变量类型修改为int基本类型,则还有自动拆箱的操作:
//需要将Object转化为Integer,并执行拆箱操作
int x = (Integer) list.get(0).intValue();
注意:
①擦除的是字节码上的泛型信息,可以看到LocalVariableTypeTable仍然保留了方法参数泛型的信息
②使用反射,我们可以获得方法的参数和返回值的泛型信息,但是方法内局部变量的泛型信息我们是拿不到的:
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
可变参数
可变参数也是 JDK 5 开始加入的新特性,例如:
public class Demo4 {public static void foo(String... args) {//将args赋值给arr,可以看出String...实际就是String[] String[] arr = args;System.out.println(arr.length);}public static void main(String[] args) {foo("hello", "world");}
}
可变参数 String… args 其实是一个 String[] args
,从代码中的赋值语句中就可以看出来。 同 样 java 编译器会在编译期间将上述代码变换为:
public class Demo4 {public Demo4 {}public static void foo(String[] args) {String[] arr = args;System.out.println(arr.length);}public static void main(String[] args) {foo(new String[]{"hello", "world"});}
}
注意:如果调用的是foo(),即未传递参数时,等价代码为foo(new String[]{}),创建了一个空数组,而不是直接传递的null
foreach 循环
仍是 JDK 5 开始引入的语法糖,数组的循环:
public class Demo5 {public static void main(String[] args) {//数组赋初值的简化写法也是一种语法糖。int[] arr = {1, 2, 3, 4, 5};for(int x : arr) {System.out.println(x);}}
}
编译器会帮我们转换为;
public class Demo5 {public Demo5 {}public static void main(String[] args) {int[] arr = new int[]{1, 2, 3, 4, 5};for(int i=0; i<arr.length; ++i) {int x = arr[i];System.out.println(x);}}
}
如果是集合使用foreach:
public class Demo5 {public static void main(String[] args) {List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);for (Integer x : list) {System.out.println(x);}}
}
集合要使用foreach,需要该集合类实现了Iterable接口,因为集合的遍历需要用到迭代器Iterator:
public class Demo5 {public Demo5 {}public static void main(String[] args) {List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);//获得该集合的迭代器Iterator<Integer> iterator = list.iterator();while(iterator.hasNext()) {Integer x = (Integer)iterator.next();System.out.println(x);}}
}
注意:
foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中Iterable 用来获取集合的迭代器( Iterator )
switch 字符串
从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:
public class Demo6 {public static void main(String[] args) {String str = "hello";switch (str) {case "hello" :System.out.println("h");break;case "world" :System.out.println("w");break;default:break;}}
}
在编译器中执行的操作:
public class Demo6 {public Demo6() {}public static void main(String[] args) {String str = "hello";int x = -1;//通过字符串的hashCode+value来判断是否匹配switch (str.hashCode()) {//hello的hashCodecase 99162322 ://再次比较,因为字符串的hashCode有可能相等if(str.equals("hello")) {x = 0;}break;//world的hashCodecase 11331880 :if(str.equals("world")) {x = 1;}break;default:break;}//用第二个switch在进行输出判断switch (x) {case 0:System.out.println("h");break;case 1:System.out.println("w");break;default:break;}}
}
过程说明:
- 在编译期间,单个的switch被分为了两个
- 第一个用来匹配字符串,并给x赋值
- 字符串的匹配用到了字符串的hashCode,还用到了equals方法
- 使用hashCode是为了提高比较效率,使用equals是防止有hashCode冲突(如BM和C.)
- 第二个用来根据x的值来决定输出语句
- 第一个用来匹配字符串,并给x赋值
switch 枚举
public class Demo7 {public static void main(String[] args) {SEX sex = SEX.MALE;switch (sex) {case MALE:System.out.println("man");break;case FEMALE:System.out.println("woman");break;default:break;}}
}enum SEX {MALE, FEMALE;
}
编译器中执行的代码如下;
public class Demo7 {/** * 定义一个合成类(仅 jvm 使用,对我们不可见) * 用来映射枚举的 ordinal 与数组元素的关系 * 枚举的 ordinal 表示枚举对象的序号,从 0 开始 * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1 */ static class $MAP {//数组大小即为枚举元素个数,里面存放了case用于比较的数字static int[] map = new int[2];static {//ordinal即枚举元素对应所在的位置,MALE为0,FEMALE为1map[SEX.MALE.ordinal()] = 1;map[SEX.FEMALE.ordinal()] = 2;}}public static void main(String[] args) {SEX sex = SEX.MALE;//将对应位置枚举元素的值赋给x,用于case操作int x = $MAP.map[sex.ordinal()];switch (x) {case 1:System.out.println("man");break;case 2:System.out.println("woman");break;default:break;}}
}enum SEX {MALE, FEMALE;
}
枚举类
JDK 7 新增了枚举类,以前面的性别枚举为例:
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 { //调用构造函数,传入枚举元素的值及ordinalMALE = new Sex("MALE", 0); FEMALE = new Sex("FEMALE", 1); $VALUES = new Sex[]{MALE, FEMALE}; }//调用父类中的方法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); } }
try-with-resources
JDK 7 开始新增了对需要关闭的资源处理的特殊语法try-with-resources
;
try(资源变量 = 创建资源对象) {} catch() {}
其中资源对象需要实现 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 块中的 e is.close(); } } } } catch (IOException e) {e.printStackTrace(); } }
}
为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?
这是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常):
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 zero at 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)
如以上代码所示,两个异常信息都不会丢。
方法重写时的桥接方法
我们都知道,方法重写时对返回值分两种情况:
- 父子类的返回值完全一致
- 子类返回值可以是父类返回值的子类
class A { public Number m() { return 1; }
}
class B extends A { @Override // 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类 public Integer m() { return 2; }
}
对于子类,java 编译器会做如下处理:
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()
没有命名冲突,可以用下面反射代码来验证:
public static void main(String[] args) {for(Method m : B.class.getDeclaredMethods()) {System.out.println(m);}}
结果:
public java.lang.Integer cn.ali.jvm.test.B.m()
public java.lang.Number cn.ali.jvm.test.B.m()
匿名内部类
源代码:
public class Candy10 {public static void main(String[] args) {Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("running...");}};}
}
转换后的代码:
public class Candy10 {public static void main(String[] args) {// 用额外创建的类来创建匿名内部类对象Runnable runnable = new Candy10$1();}
}// 创建了一个额外的类,实现了 Runnable 接口
final class Candy10$1 implements Runnable {public Demo8$1() {}@Overridepublic void run() {System.out.println("running...");}
}
引用局部变量的匿名内部类,源代码:
public class Candy11 { public static void test(final int x) { Runnable runnable = new Runnable() { @Override public 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
属性没有机会再跟着一起变化
注意上面代码中,有些变量中含有
$
符号,这些符号一般会在jvm内部使用,方便jvm的操作。java源代码中不会出现这种符号。
类加载阶段
加载
- 将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,其内部采用 C++ 的 instanceKlass 来描述 java 类,它的重要 field 有:
_java_mirror
即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用_super
即父类_fields
即成员变量_methods
即方法_constants
即常量池_class_loader
即类加载器_vtable
虚方法表_itable
接口方法
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
instanceKlass
保存在方法区。JDK 8以后,方法区位于元空间中,而元空间又位于本地内存中_java_mirror
则是保存在堆内存中InstanceKlass
和*.class(JAVA镜像类)
互相保存了对方的地址- 类的对象在对象头中保存了
*.class
的地址(也就是说类型指针)。让对象可以通过其找到方法区中的instanceKlass
,从而获取类的各种信息
注意:
①instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中
②可以通过前面介绍的 HSDB 工具查看
链接
链接阶段可以分为三个小的步骤:
-
验证
:验证类是否符合 JVM规范、安全性检查我们可以试验一下:用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行,结果如下:
-
准备
:为 static 变量分配空间,设置默认值- static变量在JDK 7以前是存储与instanceKlass末尾。但在JDK 7以后就存储在_java_mirror末尾了
- static变量在分配空间和赋值是在两个阶段完成的。分配空间在准备阶段完成,赋值在初始化阶段完成(在类的构造方法中)
- 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成(在类的构造方法中)
-
解析
:将常量池中的符号引用解析为直接引用- 符号引用也就是说仅仅是一个符号,并不知道这个符号对应着内存的哪一块空间,但是经过了解析之后变成了直接引用,就知道这些引用的是哪一块位置了。
怎么理解解析这一步骤呢,我们可以通过一段代码来看看:
我们看看load.C的常量池:
我们可以看到JVM_CONSTANT_UnresolvedClass
,是一个未经解析的类,也就是说后面的类D仅仅是一个符号,既没有加载也没有解析。
而如果我们使用的是new C(),我们再来看load.C的常量池;
可以看到我们的类D已经被解析了拥有了地址。
初始化
初始化阶段就是执行类构造器<cinit>()V
方法的过程,虚拟机会保证这个类的『构造方法』的线程安全
<cinit>()V
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的
注意:
编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如:
发生时机
类的初始化的懒惰的,以下情况会初始化:
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
以下情况不会初始化:
- 访问类的 static final 静态常量(基本类型和字符串)
- 前面我们说过,在类的链接阶段就完成了
- 类对象.class 不会触发初始化
- 创建该类对象的数组
- 类加载器的.loadClass方法
- Class.forNamed的参数2为false时
验证类是否被初始化,可以看改类的静态代码块是否被执行
验证
实验时先全部注释,每次只执行其中一个,然后看类的静态代码块是否被执行:
public class Load1 {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、A
// ClassLoader cl = Thread.currentThread().getContextClassLoader();
// cl.loadClass("cn.ali.jvm.test.classload.B");// 5. 不会初始化类 B,但会加载 B、A
// ClassLoader c2 = Thread.currentThread().getContextClassLoader();
// Class.forName("cn.ali.jvm.test.classload.B", false, c2);// 1. 首次访问这个类的静态变量或静态方法时
// System.out.println(A.a);// 2. 子类初始化,如果父类还没初始化,会引发
// System.out.println(B.c);// 3. 子类访问父类静态变量,只触发父类初始化
// System.out.println(B.a);// 4. 会初始化类 B,并先初始化类 A
// Class.forName("cn.ali.jvm.test.classload.B");}}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");}
}
相关练习和应用
从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化;
public class Load2 {public static void main(String[] args) {System.out.println(E.a);System.out.println(E.b);// 会导致 E 类初始化,因为 Integer 是包装类System.out.println(E.c);}
}class E {public static final int a = 10;public static final String b = "hello";public static final Integer c = 20;static {System.out.println("E cinit");}
}
典型应用 - 完成懒惰初始化单例模式
public class Singleton {private Singleton() { } // 内部类中保存单例private static class LazyHolder { static final Singleton INSTANCE = new Singleton(); }// 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员 public static Singleton getInstance() { return LazyHolder.INSTANCE; }
}
以上的实现特点是:
- 懒惰实例化
- 初始化时的线程安全是有保障的
类加载器
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(ClassLoader)
类与类加载器
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等
以JDK 8为例:
Extension ClassLoader(拓展类加载器)去getParent的时候得到的是个null,因为Bootstrap ClassLoader(启动类加载器)是用C++写的,不会让我们的Java代码直接访问。
启动类加载器
可通过在控制台输入指令,使得类被启动类加器加载
例如:
注意:
拓展类加载器
如果classpath
和JAVA_HOME/jre/lib/ext
下有同名类,加载时会使用拓展类加载器加载。当应用程序类加载器发现拓展类加载器已将该同名类加载过了,则不会再次加载
其本质就是双亲委派模式的应用
双亲委派模式
双亲委派模式,即调用类加载器ClassLoader 的 loadClass 方法时,查找类的规则。总的来说就是委派上级来优先做类的加载,上级没有再自己来完成类的加载。
要注意这四种类加载器并没有继承的关系,只是级别不一样
loadClass源码;
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException
{synchronized (getClassLoadingLock(name)) {// 首先查找该类是否已经被该类加载器加载过了Class<?> c = findLoadedClass(name);//如果没有被加载过if (c == null) {long t0 = System.nanoTime();try {//看是否被它的上级加载器加载过了 Extension的上级是Bootstarp,但它显示为nullif (parent != null) {c = parent.loadClass(name, false);} else {//看是否被启动类加载器加载过c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader//捕获异常,但不做任何处理}if (c == null) {//如果还是没有找到,先让拓展类加载器调用findClass方法去找到该类,如果还是没找到,就抛出异常//然后让应用类加载器去找classpath下找该类long t1 = System.nanoTime();c = findClass(name);// 记录时间sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}
}
自定义类加载器
使用场景:
- 想加载非 classpath 随意路径中的类文件
- 通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
- 继承ClassLoader父类
- 要遵从双亲委派机制,重写 findClass 方法
- 不是重写loadClass方法,否则不会走双亲委派机制
- 读取类文件的字节码
- 调用父类的 defineClass 方法来加载类
- 使用者调用该类加载器的 loadClass 方法
破坏双亲委派模式
- 双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK1.2面世以前的“远古”时代
- 建议用户重写findClass()方法,在类加载器中的loadClass()方法中也会调用该方法
- 双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的
- 如果有基础类型又要调用回用户的代码,此时也会破坏双亲委派模式
- 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的
- 这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等
拓:线程上下文类加载器
我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写:
Class.forName("com.mysql.jdbc.Driver")
也是可以让 com.mysql.jdbc.Driver
正确加载的,你知道是怎么做的吗?
让我们追踪一下源码:
先不看别的,看看 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() 方法:
先看 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 中:
运行期优化
即时编译
分层编译
JVM 将执行状态分成了 5 个层次:
- 0层:解释执行,用解释器将字节码翻译为机器码
- 1层:使用 C1 即时编译器编译执行(不带 profiling)
- 2层:使用 C1 即时编译器编译执行(带基本的profiling)
- 3层:使用 C1 即时编译器编译执行(带完全的profiling)
- 4层:使用 C2 即时编译器编译执行
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等
即时编译器(JIT)与解释器的区别
- 解释器
- 将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- 是将字节码解释为针对所有平台都通用的机器码
- 即时编译器
- 将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
- 根据平台类型,生成平台特定的机器码
对于大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter(解释器) < C1 < C2,总的目标是发现热点代码(hotspot名称的由 来),并优化这些热点代码
我们来看一段代码:
大致就是每创建1000个对象记一次时间,重复200次,我们发现在第145次之后创建时间会有一个明显的锐减:
这里就使用了一种优化手段称之为【逃逸分析】,它会分析这个new Object出来的对象在循环外面是否会被用到、会不会被其它方法所引用,结果发现没有。也就是说这个对象不会逃逸,外层用不到,既然用不到那么就没有必要创建它。
之所以后来消耗的时间这么短是因为JIT进行了逃逸分析之后,把对象创建的字节码给替换掉了,干脆不创建对象(C2编译器为了优化可能会把原本的字节码改得面目全非)。
可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析
逃逸分析是C2 即时编译器做出的优化
参考文档:
https://docs.oracle.com/en/java/javase/12/vm/java-hotspot-virtual-machine-performance-enhancements.html#GUID-D2E3DC58-D18B-4A6C-8167-4A1DFB4888E4
方法内联
也属于即时编译器优化手段的一种
private static int square(final int i) {return i * i;
}
System.out.println(square(9));
如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置:
System.out.println(9 * 9);
还能够进行常量折叠(constant folding)
的优化:
System.out.println(81);
注意:
C++是否为内联函数由自己决定,Java由编译器决定。Java不支持直接声明为内联函数的,如果想让他内联,你只能够向编译器提出请求: 关键字final修饰 用来指明那个函数是希望被JVM内联的,如
public final void doSomething() { // to do something
}
总的来说,一般的函数都不会被当做内联函数,只有声明了final后,编译器才会考虑是不是要把你的函数变成内联函数
相关jvm命令:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印inlining 信息
-XX:CompileCommand=dontinline,*JIT2.square 禁止某个方法 inlining
-XX:+PrintCompilation 打印编译信息
*JIT2.square
*
通配任意报名- JIT2匹配类
- square匹配JIT2类中的方法
字段优化
针对成员变量或者是静态成员变量的读写操作优化。
都是通过即时编译器优化生成的IR图来精简最后的机器码(在生成机器码前就好像人浏览了一遍代码,通过前后关联情况手动消除了一些重复或无效的代码),去掉无效代码,减少在code cache中存储的机器码的大小,节省内存,提高程序运行速度。
主要包括:
- 缓存读取、
- 去除重复操作、
- 分支优化、
- 不可达分支消除
对象字段读取优化
- 优化一:缓存读取
- 优化二:去重去不可达分支,可达分支优化
对象字段存储优化
同一个字段先后被存储两次,两次操作中间没有对该字段的读取操作,没有被方法调用或者没有被间接存储到其他字段,JVM会消除第一处的冗余赋值指令(Volatile同样可以阻止该优化,强制属性值实时刷入内存);
class Foo {int a = 0; // 冗余重复代码,被优化(IR图中被去除)void bar() {a = 1; // 冗余重复代码,被优化(IR图中被去除)a = 2; // 最终执行的赋值语句}
}// 优化后程序
class Foo { void bar() {a = 2; // 最终执行的赋值语句}
}
局部变量死存储优化
与对象的字段值一样包含去除重复或者无效代码,优化分支选择。
// 重复代码消除
int bar(int x, int y) {int t = x*y; // 重复冗余代码,IR图中被去除t = x+y;return t;
}// 分支代码优化
int bar(boolean f, int x, int y) {int t = x*y; // 编译后此处为int t ,具体赋值操作留到if程序块中if (f)t = x+y;return t; // 如果走这条路才会对t进行第一行的赋值操作(int t = x*y) 并返回
}// 不可达分支
int bar(int x) {if (false) // 不可达分支,IR图中不会被编译return x;elsereturn -x;
}
注意:
可能出现异常时,无法进行字段优化
反射优化
public class Reflect1 {public static void foo() {System.out.println("foo...");}public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {Method foo = Demo3.class.getMethod("foo");for(int i = 0; i<=16; i++) {foo.invoke(null);}}
}
foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现
invoke方法源码:
@CallerSensitive
public Object invoke(Object obj, Object... args)throws IllegalAccessException, IllegalArgumentException,InvocationTargetException
{if (!override) {if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {Class<?> caller = Reflection.getCallerClass();checkAccess(caller, clazz, obj, modifiers);}}//MethodAccessor是一个接口,有3个实现类,其中有一个是抽象类MethodAccessor ma = methodAccessor; // read volatileif (ma == null) {ma = acquireMethodAccessor();}return ma.invoke(obj, args);
}
会由DelegatingMehodAccessorImpl去调用NativeMethodAccessorImpl
NativeMethodAccessorImpl源码;
class NativeMethodAccessorImpl extends MethodAccessorImpl {private final Method method;private DelegatingMethodAccessorImpl parent;private int numInvocations;NativeMethodAccessorImpl(Method var1) {this.method = var1;}//每次进行反射调用,会让numInvocation与ReflectionFactory.inflationThreshold的值(15)进行比较,并使使得numInvocation的值加一//如果numInvocation>ReflectionFactory.inflationThreshold,则会调用本地方法invoke0方法public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());this.parent.setDelegate(var3);}return invoke0(this.method, var1, var2);}void setParent(DelegatingMethodAccessorImpl var1) {this.parent = var1;}private static native Object invoke0(Method var0, Object var1, Object[] var2);
}
//ReflectionFactory.inflationThreshold()方法的返回值
private static int inflationThreshold = 15;
- 一开始if条件不满足,就会调用本地方法invoke0
- 随着numInvocation的增大,当它大于ReflectionFactory.inflationThreshold的值16时,就会本地方法访问器替换为一个运行时动态生成的访问器,来提高效率
- 这时会从反射调用变为正常调用,即直接调用 Reflect1.foo()
相关文章:
JVM从看懂到看开Ⅲ -- 类加载与字节码技术【下】
文章目录编译期处理默认构造器自动拆装箱泛型集合取值可变参数foreach 循环switch 字符串switch 枚举枚举类try-with-resources方法重写时的桥接方法匿名内部类类加载阶段加载链接初始化相关练习和应用类加载器类与类加载器启动类加载器拓展类加载器双亲委派模式自定义类加载器…...
服务器常用的41个状态码及其对应的含义
服务器常用的状态码及其对应的含义如下: 100——客户必须继续发出请求 101——客户要求服务器根据请求转换HTTP协议版本 200——交易成功 201——提示知道新文件的URL 202——接受和处理、但处理未完成 203——返回信息不确定或不完整 204——请求收到&#…...
万里数据库加入龙蜥社区,打造基于“龙蜥+GreatSQL”的开源技术底座
近日,北京万里开源软件有限公司(以下简称“万里数据库”)及 GreatSQL 开源社区签署了 CLA(Contributor License Agreement,贡献者许可协议),正式加入龙蜥社区(OpenAnolis)…...
为什么不推荐使用CSDN?
CSDN粪坑 94%的讲得乱七八糟前言不搭后语互相矛盾的垃圾(还包含直接复制粘贴其他源的内容)3%的纯搬运(偷窃)2%个人日记 (以上99%中还夹杂着很多明明都是盗版资源还要上传卖钱的 ) 1%黄金程序员时间有限&am…...
apisix 初体验
文章目录前言一、参考资料二、安装1.安装依赖2.安装apisix 2.53.apisix dashboard三、小试牛刀3.1 上游(upstream)3.2 路由(route)四、遇到的问题前言 APISIX 是一个微服务API网关,具有高性能、可扩展性等优点。它基于…...
time时间模块
time时间模块 目录time时间模块1.概述2.查看不同类型的时钟3.墙上时钟time3.1.time()当前时间戳3.2.ctime()格式化时间4.单调时钟计算测量时间5.cpu处理器时钟时间6.性能计数器7.时间组成8.处理时区9.解析和格式化时间1.概述 time模块允许访问多种类型的时钟,分别用…...
如何判断反馈电路的类型-反馈类型-三极管
如何判断反馈电路的类型 反馈电路类型很多,可根据不同的标准分类: ①根据反馈的极性分:有正反馈和负反馈。 ②根据反馈信号和输出信号的关系分:有电压反馈和电流反馈。 ③根据反馈信号和输入信号的关系分:有串联反…...
C++ 实现生命游戏 Live Game
#include"stdlib.h" #include"time.h" #include"unistd.h" using namespace std; #define XSIZE 80 #define YSIZE 30 #include"iostream" using namespace std ; // 初始化生命 void initLive(int a[YSIZE][XSIZE]) { // …...
什么是QoS?QoS是如何工作的?QoS的实验配置如何进行?
QoS(Quality of Service)是服务质量的简称。对于网络业务来说,服务质量包括哪些方面呢? 从传统意义上来讲,无非就是传输的带宽、传送的时延、数据的丢包率等,而提高服务质量无非也就是保证传输的带宽&…...
AcWing 840. 模拟散列表
题目描述 餐前小菜: 在讨论本题目之前先看一个简单的问题:给出 NNN 个正整数 (a1,a2,...,an)(a_1,a_2,...,a_n)(a1,a2,...,an),再给出 MMM 个正整数 (x1,x2,...,xm)(x_1,x_2,...,x_m)(x1,x2,...,xm),问这 MMM 个数中…...
【网络工程】常见HTTP响应状态码
前言 什么是HTTP响应状态码? HTTP状态码(HTTP Status Code)是表示网页服务器超文本传输协议响应状态的3位数字代码 HTTP响应码被分为五大类 信息响应(100~199)成功响应(200~299)重定向响应&am…...
Python之ruamel.yaml模块详解(二)
Python之ruamel.yaml模块详解(二)4 将YAML解析为Python对象并修改5 使用旧API将YAML解析为Python对象并修改6 使用[]和.get()访问合并的键:7 使用insert()方法插入内容8 使用yaml.indent()更改默认缩进9 使用yaml.compact()隔行显示10 同一数…...
若依框架 --- 偶发的el-select无法选择的问题
👏作者简介:大家好,我是小童,Java开发工程师,CSDN博客博主,Java领域新星创作者 📕系列专栏:前端、Java、Java中间件大全、微信小程序、微信支付、若依框架、Spring全家桶 Ǵ…...
【Linux】tmpfile 使用介绍
tmpfile 使用介绍 1 介绍 很多情况下,需要系统自动识别/tmp、/var/tmp下的临时目录,并将其自动清理其中的过期文件。这个工具就是systemd-tmpfiles。 网上很多博客使用tmpwatchcron的方法来管理临时文件和临时存放文件的目录,在后期的版本…...
实现光线追踪重投影的方法
光线追踪重投影方法 重投影这项技术一般用于时间性帧复用技术上,例如TAA(Temporal Anti-Aliasing)反走样或者抗锯齿技术。读这篇文章最好先对TAA这类技术的算法流程有了解。 1.TAA抗锯齿技术简介 先简单介绍下TAA抗锯齿的原理,在游戏中,当前…...
Hyperbolic Representation Learning for CV
Contents Hyperbolic geometry[CVPR 2020] Hyperbolic visual embedding learning for zero-shot recognitionIntroductionApproachHyperbolic Label Embedding LearningHyperbolic Image Embedding LearningExperiment[CVPR 2020] Hyperbolic Image EmbeddingsIntroduction...
In Context Learning 相关分享
个人知乎详见 https://zhuanlan.zhihu.com/p/603650082/edit 1. 前言 随着大模型(GPT3,Instruction GPT,ChatGPT)的横空出世,如何更高效地提示大模型也成了学术界与工业界的关注,因此In-context learning…...
【前端笔试题一】:解析url路径中的query参数
前言 本文记录下在笔试过程中的前端笔试编程题目,会持续更新 1. 题目: 解析 url 路径中的 query 参数,比如:‘http://building/#/skeleton?serialNumber2023020818332821073&jobNo210347&target%7B%22a%22%3A%22b%22%2C…...
K_A12_001 基于STM32等单片机采集火光火焰传感参数串口与OLED0.96双显示
K_A12_001 基于STM32等单片机采集火光火焰传感参数串口与OLED0.96双显示一、资源说明二、基本参数参数引脚说明三、驱动说明IIC地址/采集通道选择/时序对应程序:四、部分代码说明1、接线引脚定义1.1、STC89C52RC火光火焰模块1.2、STM32F103C8T6火光火焰模块五、基础知识学习与相…...
Java基础42 枚举与注解
枚举与注解一、枚举(enumeration)1.1 自定义类实现枚举1.2 enum关键字实现枚举1.2.1 enum的注意事项1.2.2 enum的使用练习1.2.3 enum的常用方法1.2.4 enum的使用细节及注意事项1.2.5 enum练习二、注解(Annotation)2.1 Override&am…...
shell的变量和引用
文章目录二、变量和引用2.1 什么是变量2.2变量的命名2.3 变量的类型2.3.1 根据数据类型分类2.3.2 根据作用域分类2.4 变量的定义2.5 shell中的引用2.6 变量的运算练习:二、变量和引用 在程序设计语言中,变量是一个非常重要的概念。也是初学者在进行Shel…...
基于PHP的招聘网站
摘要在Internet高速发展的今天,我们生活的各个领域都涉及到计算机的应用,其中包括在线招聘的网络应用,在外国在线招聘已经是很普遍的方式,不过国内的在线招聘可能还处于起步阶段。招聘网站具有招聘信息功能的双向选择,…...
轻松使用 Python 检测和识别车牌(附代码)
车牌检测与识别技术用途广泛,可以用于道路系统、无票停车场、车辆门禁等。这项技术结合了计算机视觉和人工智能。 本文将使用Python创建一个车牌检测和识别程序。该程序对输入图像进行处理,检测和识别车牌,最后显示车牌字符,作为…...
DVWA—CSRF-Medium跨站请求伪造中级
注意: 1、这里对XSS(Stored)关卡不熟悉的可以从这里去看http://t.csdn.cn/ggQDK 2、把难度设置成 Medium 一、这一关同样我们需要埋下伏笔,诱使用户点击来提交,首先从XSS(Stored)入手。 注意:在前面介绍…...
【电商】后台订单生成
结合商品流转的电商系列介绍了一些了,商品已经采购入库、价格税率设置好了、活动及相关模板也已经准备完毕,下面就应该上架销售了,现在接着聊下订单的生成。 订单从产生到最终的关闭需要经历很多的环节,订单也是电商系统的核心数据…...
作为公司,这个5款在线软件工具赶紧安利起来!
2023年了 ,您的企业还没使用在线软件工具吗?自从用了在线工具之后,感觉打开了新办公世界的大门,效率蹭蹭蹭地往上涨啊。对于喜欢追求效率和便捷的我来说,在线实在是太棒了!今天安利几个非常不错的在线软件工…...
面试(七)为什么一般希望将析构函数定义为虚函数
class B { public:~B() // 基类析构函数不为虚函数{cout << "B::~B()" << endl;} };class D : public B { public:~D(){cout << "D::~D()" << endl;} };void Test(B* t) {delete t;t nullptr; }int main() {B *pb new B;Test…...
MySQL必会四大函数-时间函数
一、时间日期获取函数 获取当前日期(date)函数:curdate() mysql> select curdate(); 2023-02-09 获取当前时间(time)函数:curtime() select curtime(); 08:49:27 获取当前时间戳(date &…...
震惊!邻桌的程序猿做可视化报告竟然比我还快,带着好奇心我打开了他的电脑,发现惊天秘密,原因竟是...
其实,本文就是想分享一个做可视化的捷径! 制作可视化的方式有千千万。 Excel 控若能轻车熟路驾驭 VBA,能玩出各种花来,再不济借助图表插件外援也能秒杀一众小白选 手。 会编程的,Echarts 几十行代码,分分…...
mathtype7与word冲突,无法安装,不显示工具栏的问题解决
首先无法安装,或安装出错时,要清理注册表防止以后再次出现该问题,以此记录留作备份。打开注册表的方法是键盘winr键同时按(win就是Alt旁边像窗户图标的键),正常的话会跳出一个叫“运行”的家伙,…...
网站建设技术支持有什么/一手项目对接app平台
击上方蓝色字体,选择“标星公众号”优质文章,第一时间送达 大家好,我是燕子!Sentinel是阿里巴巴开源的限流器熔断器,并且带有可视化操作界面。在日常开发中,限流功能时常被使用,用于对某些接口…...
网站 电信已备案 联通/永久免费个人网站申请注册
问题描述 给定一个字符串,你需要从第start位开始每隔step位输出字符串对应位置上的字符。 输入格式 第一行一个只包含小写字母的字符串。 第二行两个非负整数start和step,意义见上。 输出格式 一行,表示对应输出。 样例输入 abcdefg 2 2 样例…...
广州互联网公司排名前20/seo问答
描述 欢迎来到猫咪系列题目之猫咪银行。 这也是猫咪占领世界的计划之一,通过开设猫咪银行出售 flag 来学习人类割韭菜的技巧。 通过理财一般来说都得不到,找漏洞 涉及购买、货币的一般先考虑溢出 找溢出点 可以修改的 value 有买入分钟和买入份额 挨…...
中国包装设计网/网站如何优化
display和visibility的用法和区别 更多 2013/11/17 来源:css学习浏览量:892 学习标签: css display visibility本文导读:大多数人很容易将CSS属性display和visibility混淆,它们看似没有什么不同,其实它们的…...
如何做好网络营销管理/西安seo代运营
E此浏览器不支持画布C我一路 看 过 千山和万水C我的脚 踏 遍 天南和地北F 我都无所谓日晒或是风 吹F 鲜红的纯粹路边那朵蔷 薇C关掉了 手 机 管他谁是谁C不要去 理 会 是是与非非F 从不觉疲惫天亮走到天 黑F F C黄昏中的堡 垒 (多颓废)F G如果迎着风 就飞Em Am俯瞰这世界 有多美…...
unas做网站服务器/广州信息流推广公司排名
Flink 相关的组件和作业的稳定性通常是比较关键的,所以得需要对它们进行监控,如果有异常,则需要及时告警通知。本章先会教会教会大家如何利用现有 Flink UI 上面的信息去发现和排查问题,会指明一些比较重要和我们非常关心的指标&a…...