JVM完整图文学习笔记 (含拓展知识广度学习) 第三章: 类加载与字节码技术

目录

编译期处理——语法糖

默认构造器

自动拆装箱

泛型集合取值

可变参数

foreach循环

switch 字符串

switch 枚举

枚举类

try-with-resources

方法重写时的桥接方法

匿名内部类

类加载阶段(重点!)

 加载

链接

(1)验证

(2)准备

(3)解析

初始化

(1)详细步骤

(2)发生的时机

典型应用 - 完成懒惰初始化单例模式

类加载器

启动类加载器

扩展类加载器 

双亲委派模式 

自定义类加载器

运行期优化 

即时编译

(1)分层编译

即时编译器(JIT)与解释器的区别:

逃逸分析

(2)方法内联 

(3)反射优化


编译期处理——语法糖

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

默认构造器

public class Candy1 {

}

编译成class后的代码:

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

自动拆装箱

这个特性是 JDK 5 开始加入的, 如下代码 :

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

这段代码在 JDK 5 之前是无法编译通过的,必须改写下面这样 :

public class Candy2 {
    public static void main(String[] args) {
        //基本类型转包装类型→装箱
        Integer x = Integer.valueOf(1);
        //包装类型转基本类型→拆箱
        int y = x.intValue();
    }
}

泛型集合取值

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

public class Candy3 {
    public static void main(String[] args) {
        List 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();

可变参数

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

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 ,从代码中的赋值语句中就可以看出来。 同 样 java 编译器会在编译期间将上述代码变换为:

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 进去

foreach循环

仍是 JDK 5 开始引入的语法糖,数组的循环:

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);
        }
    }
}

如果是集合呢?

public class Candy5_2 {
    public static void main(String[] args) {
        List 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 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 )

对于foreach的遍历原理有两种情况,一种是面对实现了迭代器的集合对象,它的遍历原理是通过其自定义的迭代器来进行遍历,而如果是数组类遍历对象,它的底层是通过普通for循环的原理,之所以它不能修改包装类的数值是因为自动拆箱的过程有转变为了基本数据类型,而如果是对象数组,就没有自动拆箱的说法,以for循环的地址遍历形式,就会直接进行引用式传递。对于基本类型数组,foreach循环直接访问每个元素的值,因为基本类型的值在内存中是连续存储的。对于基本类型包装类数组,foreach循环首先会将每个包装类对象拆箱成基本类型值,然后再访问该值。 

switch 字符串

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

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 的 hashCode
                if (str.equals("hello")) {
                    x = 0;
                }
                break;
            case 113318802: // world 的 hashCode
                if (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 ,如果有如下代码:

public class Candy6_1 {
    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_1 {
    public Candy6_1() {
    }

    public static void choose(String var0) {
        byte var2 = -1;
        switch(var0.hashCode()) {
        case 2123:
            if (var0.equals("C.")) {
                var2 = 1;
            } else if (var0.equals("BM")) {
                var2 = 0;
            }
        default:
            switch(var2) {
            case 0:
                System.out.println("h");
                break;
            case 1:
                System.out.println("w");
            }

        }
    }
}

        在某些情况下,使用 switch 语句可能比使用一系列嵌套的 if-else 语句更快。这是因为 switch 语句在编译时会生成一种称为 “跳转表”(Jump Table) “查找表”(Lookup Table)的数据结构,将 case 标签与相应的代码块关联起来。

        当执行 switch 语句时,它会根据给定的表达式的值,直接跳转到相应 case 标签的代码块。这个跳转是通过表中每个标签与对应代码块的偏移量来实现的,因此它的执行时间是常数级别的,并不随着 case 的数量增加而线性增长

        相比之下,使用嵌套的 if-else 语句,每个条件都需要逐个检查,直到找到匹配的条件为止。在有大量条件的情况下,这会导致性能下降,因为需要逐个比较条件,直到找到匹配的条件或执行到最后一个 else 分支。

switch 枚举

public 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;
        }
    }
}

会被编译器转换为:

枚举类型的 ordinal 是枚举常量在枚举声明中的序号,从0开始递增。每个枚举常量都有一个与之对应的 ordinal 值。

例如,考虑以下枚举类型的定义:

enum Season {
    SPRING, SUMMER, AUTUMN, WINTER
}

其中,SPRING 的 ordinal 值是0,SUMMER 的 ordinal 值是1,AUTUMN 的 ordinal 值是2,WINTER 的 ordinal 值是3。通过 ordinal,可以根据枚举常量在枚举声明中的位置进行比较、排序或根据序号索引枚举常量。

public class Candy7 {
    /**     
	* 定义一个合成类(仅 jvm 使用,对我们不可见)
    * 用来映射枚举的 ordinal 与数组元素的关系
    * 即 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;
        }
    }
}

枚举类

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

public enum Sex {
    MALE,FEMALE
}

会被编译器转换为:

public final class Sex extends Enum {
    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};
    }
    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 class Candy10 {
   public static void main(String[] args) {
      Runnable runnable = new Runnable() {
         @Override
         public 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() {}

   @Override
   public 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 对象的值后,如果不是 final 声明的 x 值发生了改变,匿名内部类则值不一致。因此,Java 编译器要求匿名内部类只能访问外部的 final 变量,以确保在生成的代代码中,匿名内部类只能读取外部变量的值,而不能修改它们。这样可以避免在多线程环境下出现竞态条件和数据不一致的问题。


类加载阶段(重点!)

 加载

将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类(或者说记录了java类的各种结构数据),它的重要 field 有:

  • _java_mirror 即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用

java程序不能直接跨语言访问 instanceKlass 来获取类的各种数据,而是通过 _java_mirror 这个桥梁来实现在java程序中获取 instanceKlass 中保存的java类数据。

  • _super 即父类
  • _fields 即成员变量
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即类加载器
  • _vtable 虚方法表
  • _itable 接口方法

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

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

JVM完整图文学习笔记 (含拓展知识广度学习) 第三章: 类加载与字节码技术_第1张图片

instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),其通过镜像映射 _java_mirror 到中,其持有了instanceKlass 的内存地址,而instanceKlass 也持有改镜像在堆中的地址

类的对象实例在对象头中保存了 *.class 的地址。让对象可以通过该地址找到对应的镜像,再通过其找到元空间中的instanceKlass,从而获取该类的各种信息


链接

(1)验证

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

如果不符合规范会抛出异常

在验证阶段,虚拟机对字节码进行静态分析,并检查类文件的合法性。它会执行字节码的格式检查、语义检查、字节码操作数栈和局部变量表检查等。目的是确保类文件的字节码是符合规范并且安全的,以防止潜在的安全漏洞或运行时错误。

(2)准备

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

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

(3)解析

        将符号引用转换为直接引用,让程序能够定位到具体的方法、字段或类。为了更好地理解解析阶段的工作,以下是一个简单的代码例子:

public class MyClass {
    public static void main(String[] args) {
        MyInterface obj = new MyImplementation();
        obj.method();
    }
}

interface MyInterface {
    void method();
}

class MyImplementation implements MyInterface {
    @Override
    public void method() {
        System.out.println("Hello World");
    }
}

        解析阶段发生在虚拟机加载 MyClass 类时。在解析阶段,虚拟机会处理 MyInterface 接口和 MyImplementation 类,将符号引用转换为直接引用。

        具体来说,解析阶段会将 MyInterface 解析为实际的内存地址,以便于在运行时调用接口的方法。对于 MyImplementation 类,解析阶段会将它解析为实际的类信息,以便于在内存中创建对象。

其实对于类加载过程是一个懒加载的过程,你加载一个含有引用变量Data data = new Data(); 的对象时,实际上data只是一个符号而已,并不是一个地址的实际指向,只有经过解析阶段后,data才是对应了实际的内存地址(实际的类信息)


初始化

        在Java中,初始化实例化是两个不同的概念。

  • 初始化指的是在类加载过程的初始化阶段对类的静态变量进行赋初始值,并执行静态初始化块和类的初始化方法的过程。它是类加载过程中的最后一个阶段,在类被首次主动使用时触发。
  • 实例化是指创建一个类的对象,通过 new 关键字调用类的构造方法来完成对象的创建和初始化。实例化过程是在初始化阶段之后进行的。

(1)详细步骤

  1. 静态变量初始化:对类的静态变量进行初始化。静态变量可以通过静态赋值语句或静态初始化块来初始化。静态变量的初始化按照程序中的代码顺序依次进行。

  2. 执行静态初始化块:如果类中包含静态初始化块(使用 static 关键字声明的代码块),它会在类初始化过程中执行。静态初始化块可以用于执行一些静态变量的复杂初始化逻辑,或进行一些静态资源的加载。静态初始化块可存在多个,并且按照在类中出现的顺序依次执行。

  3. 执行类的初始化方法:类的初始化方法是一个特殊的方法,由编译器自动生成。它的方法名是 (类初始化器),没有任何参数和返回值。初始化方法包含了类的所有静态变量的赋值和静态初始化块的执行逻辑。在初始化阶段,虚拟机会执行这个类初始化方法。

  • 虚拟机保证类的初始化在多线程环境中是线程安全的,使用类初始化锁来确保只有一个线程进行类的初始化操作
  • 一个类只会初始化一次。多次加载同一个类并不会触发多次初始化,因为虚拟机会在内存中缓存已经加载的类。
  • 类的初始化过程是可见的,可以通过静态变量的修改或静态初始化块中的输出语句来观察类的初始化行为。

(2)发生的时机

类的初始化的懒惰的,以下情况会导致类初始化:

  • main 方法所在的类,总会被首先初始化
  • 首次访问(注意不是赋值噢)这个类的静态变量(非基本型)或静态方法时
  • 子类初始化,如果父类还没初始化,会引发父类的初始化
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

 以下情况不会导致类初始化:

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

典型应用 - 完成懒惰初始化单例模式

public class Singleton {

    private Singleton() { } 
    // 内部类中保存单例
    private static class LazyHolder { 
        static final Singleton INSTANCE = new Singleton(); 
    }
    // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员 
    public static Singleton getInstance() { 
        return LazyHolder.INSTANCE; 
    }
}

以上的实现特点是:

  • 懒惰实例化
  • 初始化时的线程安全是有保障的(因为静态变量被访问操作会导致类的初始化,虚拟机保证类的初始化在多线程环境中是线程安全的,使用类初始化锁来确保只有一个线程进行类的初始化操作。)

类加载器

JDK 8 为例:

名称 加载的类 说明
Bootstrap ClassLoader(启动类加载器) JAVA_HOME/jre/lib 无法直接访问(因为是C++写的)
Extension ClassLoader(拓展类加载器) JAVA_HOME/jre/lib/ext 上级为Bootstrap,显示为null
Application ClassLoader(应用程序类加载器) classpath 上级为Extension
自定义类加载器 自定义 上级为Application

启动类加载器

        启动类加载器(Bootstrap Class Loader)是Java虚拟机内置的一个特殊类加载器,它负责加载Java的核心类库,包括java.langjava.util等。启动类加载器是虚拟机实现的一部分,通常由C++编写,不是一个普通的Java类加载器。

  • 启动类加载器是Java虚拟机内置的类加载器,作为虚拟机的一部分,它不是一个Java类。
  • 启动类加载器位于Java运行时环境的核心组件中,通常由操作系统提供或由虚拟机实现。
  • 启动类加载器负责加载Java的核心类库,这些类库提供了Java语言的基本功能和API。
  • 启动类加载器仅加载Java核心类库,如java.langjava.util等。它不加载应用程序的类,也不加载Java扩展或第三方库的类(其实是可以加载的,但是要改配置)
  • 启动类加载器是类加载器层次结构的根,它没有父类加载器。
  • 当Java虚拟机启动时,启动类加载器会优先加载Java核心类库。这样,在加载其他类之前,Java运行时环境就具备了基本的功能。

扩展类加载器 

        扩展类加载器(Extension Class Loader)是Java虚拟机内置的一个类加载器,负责加载Java扩展类库

  • 扩展类加载器负责加载Java扩展类库(JAR文件),这些库通常位于${JAVA_HOME}/lib/ext目录下,默认是由JDK提供的一些Java扩展类库。
  • Java扩展类库是为Java平台提供附加功能的库,例如Java Bean的工具类、XML处理器、数据库驱动程序等。
  • 需要注意的是,扩展类加载器可以被开发人员所扩展或定制。可以通过在Java虚拟机启动时指定扩展类路径,来加载位于自定义目录下的扩展类库
  • 如果classpath和JAVA_HOME/jre/lib/ext 下有同名类,加载时会使用拓展类加载器加载。当应用程序类加载器发现拓展类加载器已将该同名类加载过了,则不会再次加载

双亲委派模式 

  • 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  • 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  • 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

总结就是小的往大的推责任,大的搞不好就丢过小的,直到最小的也搞不好就会抛出ClassNotFoundException

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

自定义类加载器

使用场景

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

实现步骤

  1. 继承ClassLoader父类
  2. 要遵从双亲委派机制,重写 findClass 方法
    • 不是重写loadClass方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的 defineClass 方法来加载类
  5. 使用者调用该类加载器的 loadClass 方法

运行期优化 

即时编译

(1)分层编译

JVM 将执行状态分成了 5 个层次:

  • 0层:解释执行,用解释器将字节码翻译为机器码
  • 1层:使用 C1 即时编译器编译执行(不带 profiling)
  • 2层:使用 C1 即时编译器编译执行(带基本的profiling)
  • 3层:使用 C1 即时编译器编译执行(带完全的profiling)
  • 4层:使用 C2 即时编译器编译执行

profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等

即时编译器(JIT)与解释器的区别
  • 解释器
    • 将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
    • 是将字节码解释为针对所有平台都通用的机器码
  • 即时编译器
    • 将一些字节码编译为机器码,并存入 Code Cache下次遇到相同的代码,直接执行,无需再编译
    • 根据平台类型,生成平台特定的机器码(都运行了,还怕什么代码平台不兼容)

对于大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由 来),并优化这些热点代码

逃逸分析

逃逸分析(Escape Analysis)是编译器和运行时环境的一个技术,用于分析程序中的对象的生命周期和作用域,并确定是否可以在栈上分配对象内存,以及是否可以进行栈上替代优化

逃逸分析可以帮助编译器做出一些优化决策,以提高程序的性能和效率。以下是逃逸分析的一些关键概念和作用:

  1. 对象逃逸:

    • 当一个对象在一个方法内被创建后,如果它不仅仅是在该方法内部使用,还可能被传递给其他方法或线程使用,那么就称为对象逃逸
    • 对象逃逸可能导致对象的生命周期延长,并且可能需要使用堆内存分配和垃圾回收机制,对程序的性能有影响。
  2. 逃逸分析的作用:

    • 逃逸分析可以分析对象的作用域,确定对象的使用范围是否超出了定义时的方法或线程。如果对象没有逃逸,可以进行一些优化,例如将对象分配在栈上而不是堆上。
    • 当对象分配在栈上时,可以减少垃圾回收的开销,提高内存访问的效率,并利用线程栈的局部性原理来提高程序的运行速度。
  3. 逃逸分析的优化:

    • 基于逃逸分析的优化包括栈上替代(Stack Allocation)标量替换(Scalar Replacement)和锁消除(Lock Elimination)等
    • 栈上替代:将对象分配在栈上,减少堆内存的分配和垃圾回收的开销。
    • 标量替换:将对象拆分成多个独立的标量数据类型,将其存储在栈上或寄存器中,减少对象的访问成本。
    • 锁消除:当逃逸分析确认对象不会被其他线程访问时,可以消除对象的同步操作,减少锁的开销。

(2)方法内联 

举个栗子:

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(8);

方法内联(Method Inlining)是编译器优化的一种技术,它将方法调用处的代码替换为被调用方法的实际代码,以减少方法调用的开销,并提高程序的性能。

通常情况下,方法调用会引入一定的开销,包括栈帧的创建和销毁、参数传递、跳转等。而方法内联则尝试在编译时将方法调用处的代码直接替换为被调用方法的代码块,从而避免了这些开销

方法内联的优点包括:

  1. 减少方法调用开销:方法调用会涉及到栈帧的创建和销毁参数传递等操作,而方法内联可以消除这些开销,从而提高程序的执行效率。
  2. 提高指令缓存命中率:方法内联会将被调用方法的代码嵌入到调用处,减少了程序的指令跳转,有利于提高指令缓存的命中率,进而提高程序的性能。

然而,方法内联也存在一些限制和潜在的问题:

  1. 代码膨胀:方法内联会将被调用方法的代码嵌入到每一个调用处,这可能导致编译后的代码变得冗长,增加了代码的体积。
  2. 编译时间增加:方法内联需要在编译时分析和处理大量的代码,从而可能增加编译的时间
  3. 代码重复:方法内联会导致被调用方法的代码在调用处重复出现,如果被调用方法较大或调用次数较多,可能导致代码重复,降低了代码的可维护性

JVM内联函数
C++ 是否为内联函数由自己决定,Java 由编译器决定。Java 不支持直接声明为内联函数的,如果想让他内联,你只能够向编译器提出请求: 关键字 final 修饰 用来指明那个函数是希望被 JVM 内联的,如

public final void doSomething() {  
        // to do something  
}

总的来说,一般的函数都不会被当做内联函数,只有声明了final后,编译器才会考虑是不是要把你的函数变成内联函数

JVM内建有许多运行时优化。首先短方法更利于JVM推断。流程更明显,作用域更短,副作用也更明显。如果是长方法JVM可能直接就跪了。


(3)反射优化

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 volatile
    if (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,jvm,学习,笔记)