【JVM学习03】类加载与字节码技术

文章目录

    • 1、 字节码指令
      • 1)异常处理
      • 2)Synchronized
    • 2、编译期处理
      • 1)默认构造器
      • 2)自动拆装箱
      • 3)泛型擦除
    • 3、类加载阶段
      • 1)加载
      • 2) 链接
      • 3)初始化
      • 4)练习
    • 5、类加载器
      • 1)启动类的加载器
      • 2)扩展类的加载器
      • 3)双亲委派模式

1、 字节码指令

1)异常处理

try-catch

public class Code_15_TryCatchTest { 

    public static void main(String[] args) { 
        int i = 0;
        try { 
            i = 10;
        }catch (Exception e) { 
            i = 20;
        }
    }
}

对应字节码指令

Code:
     stack=1, locals=3, args_size=1
        0: iconst_0
        1: istore_1
        2: bipush        10
        4: istore_1
        5: goto          12
        8: astore_2
        9: bipush        20
       11: istore_1
       12: return
     //多出来一个异常表
     Exception table:
        from    to  target type
            2     5     8   Class java/lang/Exception

  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测 2~4 行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
  • 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 2 号位置(为 e )

多个 single-catch

public class Code_16_MultipleCatchTest { 

    public static void main(String[] args) { 
        int i = 0;
        try { 
            i = 10;
        }catch (ArithmeticException e) { 
            i = 20;
        }catch (Exception e) { 
            i = 30;
        }
    }
}

对应的字节码

Code:
     stack=1, locals=3, args_size=1
        0: iconst_0
        1: istore_1
        2: bipush        10
        4: istore_1
        5: goto          19
        8: astore_2
        9: bipush        20
       11: istore_1
       12: goto          19
       15: astore_2
       16: bipush        30
       18: istore_1
       19: return
     Exception table:
        from    to  target type
            2     5     8   Class java/lang/ArithmeticException
            2     5    15   Class java/lang/Exception

  • 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用

finally

public class Code_17_FinallyTest { 
    
    public static void main(String[] args) { 
        int i = 0;
        try { 
            i = 10;
        } catch (Exception e) { 
            i = 20;
        } finally { 
            i = 30;
        }
    }
}

对应字节码

Code:
     stack=1, locals=4, args_size=1
        0: iconst_0
        1: istore_1
        // try块
        2: bipush        10
        4: istore_1
        // try块执行完后,会执行finally    
        5: bipush        30
        7: istore_1
        8: goto          27
       // catch块     
       11: astore_2 // 异常信息放入局部变量表的2号槽位
       12: bipush        20
       14: istore_1
       // catch块执行完后,会执行finally        
       15: bipush        30
       17: istore_1
       18: goto          27
       // 出现异常,但未被 Exception 捕获,会抛出其他异常,这时也需要执行 finally 块中的代码   
       21: astore_3
       22: bipush        30
       24: istore_1
       25: aload_3
       26: athrow  // 抛出异常
       27: return
     Exception table:
        from    to  target type
            2     5    11   Class java/lang/Exception
            2     5    21   any
           11    15    21   any

  • 可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
  • 注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次

finally 中的 return

public class Code_18_FinallyReturnTest { 

    public static void main(String[] args) { 
        int i = Code_18_FinallyReturnTest.test();
        // 结果为 20
        System.out.println(i);
    }

    public static int test() { 
        int i;
        try { 
            i = 10;
            return i;
        } finally { 
            i = 20;
            return i;
        }
    }
}

对应字节码

Code:
     stack=1, locals=3, args_size=0
        0: bipush        10
        2: istore_0
        3: iload_0
        4: istore_1  // 暂存返回值
        5: bipush        20
        7: istore_0
        8: iload_0
        9: ireturn	// ireturn 会返回操作数栈顶的整型值 20
       // 如果出现异常,还是会执行finally 块中的内容,没有抛出异常
       10: astore_2
       11: bipush        20
       13: istore_0
       14: iload_0
       15: ireturn	// 这里没有 athrow 了,也就是如果在 finally 块中如果有返回操作的话,且 try 块中出现异常,会吞掉异常!
     Exception table:
        from    to  target type
            0     5    10   any

  • 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以finally的为准
  • 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
  • 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常
  • 所以不要在finally中进行返回操作

被吞掉的异常

public static int test() { 
      int i;
      try { 
         i = 10;
         //  这里应该会抛出异常
         i = i/0;
         return i;
      } finally { 
         i = 20;
         return i;
      }
   }

会发现打印结果为 20 ,并未抛出异常

finally 不带 return

public static int test() { 
		int i = 10;
		try { 
			return i;
		} finally { 
			i = 20;
		}
	}

对应字节码

Code:
     stack=1, locals=3, args_size=0
        0: bipush        10
        2: istore_0 // 赋值给i 10
        3: iload_0	// 加载到操作数栈顶
        4: istore_1 // 加载到局部变量表的1号位置
        5: bipush        20
        7: istore_0 // 赋值给i 20
        8: iload_1 // 加载局部变量表1号位置的数10到操作数栈
        9: ireturn // 返回操作数栈顶元素 10
       10: astore_2
       11: bipush        20
       13: istore_0
       14: aload_2 // 加载异常
       15: athrow // 抛出异常
     Exception table:
        from    to  target type
            3     5    10   any

2)Synchronized

public class Code_19_SyncTest { 

    public static void main(String[] args) { 
        Object lock = new Object();
        synchronized (lock) { 
            System.out.println("ok");
        }
    }

}

对应字节码

Code:
     stack=2, locals=5, args_size=1
        0: bipush        10
        2: istore_1
        3: new           #2                  // class com/nyima/JVM/day06/Lock
        6: dup //复制一份,放到操作数栈顶,用于构造函数消耗
        7: invokespecial #3                  // Method com/nyima/JVM/day06/Lock."":()V
       10: astore_2 //剩下的一份放到局部变量表的2号位置
       11: aload_2 //加载到操作数栈
       12: dup //复制一份,放到操作数栈,用于加锁时消耗
       13: astore_3 //将操作数栈顶元素弹出,暂存到局部变量表的三号槽位。这时操作数栈中有一份对象的引用
       14: monitorenter //加锁
       //锁住后代码块中的操作    
       15: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       18: iload_1
       19: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
       //加载局部变量表中三号槽位对象的引用,用于解锁    
       22: aload_3    
       23: monitorexit //解锁
       24: goto          34
       //异常操作    
       27: astore        4
       29: aload_3
       30: monitorexit //解锁
       31: aload         4
       33: athrow
       34: return
     //可以看出,无论何时出现异常,都会跳转到27行,将异常放入局部变量中,并进行解锁操作,然后加载异常并抛出异常。      
     Exception table:
        from    to  target type
           15    24    27   any
           27    31    27   any

2、编译期处理

所谓的 语法糖 ,其实就是指 java 编译器把 .java 源码编译为 .class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利 注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。

1)默认构造器

public class Candy1 { 

}

经过编译期优化后

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

2)自动拆装箱

基本类型和其包装类型的相互转换过程,称为拆装箱 在 JDK 5 以后,它们的转换可以在编译期自动完成

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

转换过程如下

public class Candy2 { 
   public static void main(String[] args) { 
      // 基本类型赋值给包装类型,称为装箱
      Integer x = Integer.valueOf(1);
      // 包装类型赋值给基本类型,称谓拆箱
      int y = x.intValue();
   }
}

3)泛型擦除

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

public class Candy3 { 
   public static void main(String[] args) { 
      List<Integer> list = new ArrayList<Integer>();
      list.add(10);
      Integer x = list.get(0);
   }
}

对应字节码

Code:
    stack=2, locals=3, args_size=1
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList." ":()V
       7: astore_1
       8: aload_1
       9: bipush        10
      11: 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;)Z

      19: pop
      20: aload_1
      21: iconst_0
      // 这里也进行了泛型擦除,实际调用的是get(Object o)   
      22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
// 这里进行了类型转换,将 Object 转换成了 Integer
      27: checkcast     #7                  // class java/lang/Integer
      30: astore_2
      31: return

所以调用 get 函数取值时,有一个类型转换的操作。

Integer x = (Integer) list.get(0);

如果要将返回结果赋值给一个 int 类型的变量,则还有自动拆箱的操作

int x = (Integer) list.get(0).intValue();

使用反射可以得到,参数的类型以及泛型类型。泛型反射代码如下:


    public static void main(String[] args) throws NoSuchMethodException { 
        // 1. 拿到方法
        Method method = Code_20_ReflectTest.class.getMethod("test", List.class, Map.class);
        // 2. 得到泛型参数的类型信息
        Type[] types = method.getGenericParameterTypes();
        for(Type type : types) { 
            // 3. 判断参数类型是否,带泛型的类型。
            if(type instanceof ParameterizedType) { 
                ParameterizedType parameterizedType = (ParameterizedType) type;

                // 4. 得到原始类型
                System.out.println("原始类型 - " + parameterizedType.getRawType());
                // 5. 拿到泛型类型
                Type[] arguments = parameterizedType.getActualTypeArguments();
                for(int i = 0; i &lt; arguments.length; i++) { 
                    System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
                }
            }
        }
    }

    public Set&lt;Integer&gt; test(List&lt;String&gt; list, Map&lt;Integer, Object&gt; map) { 
        return null;
    }

输出:

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

3、类加载阶段

1)加载

  • 将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

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

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

【JVM学习03】类加载与字节码技术_第1张图片

  • instanceKlass保存在方法区。JDK 8以后,方法区位于元空间中,而元空间又位于本地内存中
  • _java_mirror则是保存在堆内存中
  • InstanceKlass和.class(JAVA镜像类)互相保存了对方的地址
  • 类的对象在对象头中保存了.class的地址。让对象可以通过其找到方法区中的instanceKlass,从而获取类的各种信息
    注意

instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中

2) 链接

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

用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行 准备 为 static 变量分配空间,设置默认值

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

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

解析

类加载时,类的字节码载入方法区。解析时,将常量池中的符号引用解析为直接引用

public class Code_22_AnalysisTest { 


    public static void main(String[] args) throws ClassNotFoundException, IOException { 
        ClassLoader classLoader = Code_22_AnalysisTest.class.getClassLoader();
        Class<?> c = classLoader.loadClass("P3.C");

        // new C();
        System.in.read();
    }

}

class C { 
    D d = new D();
}

class D { 

}

  • 使用类加载器ClassLoader的classLoad方法,只会加载C这个类。
  • 加载即将类的字节码载入方法区中,但是也仅仅是字节码,不会对字节码做处理
  • 所以常量池中,类D仅仅是一个符号引用,并不知道它要引用哪个地址

但是

  • 使用new C(),new会触发加载、解析
  • 解析会将常量池中的符号引用解析为直接引用

3)初始化

()v 方法

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

类触发初始化的情况

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

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

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化(准备阶段已经创建好class文件)
  • 创建该类的数组不会触发初始化
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");
    }
}

4)练习

从字节码分析,使用 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 是包装类,初始化后会调用Integer.valueOf()进行赋值
        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; 
    }
}


以上的实现特点是:

  • 懒惰实例化
  • 初始化时的线程安全是有保障的

5、类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段 对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个 Java 虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等!

以JDK 8为例

【JVM学习03】类加载与字节码技术_第2张图片

1)启动类的加载器

可通过在控制台输入指令,使得类被启动类加器加载

2)扩展类的加载器

如果 classpath 和 JAVA_HOME/jre/lib/ext 下有同名类,加载时会使用拓展类加载器加载。当应用程序类加载器发现拓展类加载器已将该同名类加载过了,则不会再次加载。

3)双亲委派模式

双亲委派模式,即调用类加载器ClassLoader 的 loadClass 方法时,查找类的规则。

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. 有上级的话,委派上级 (递归调用)
          loadClass c = parent.loadClass(name, false); 
        } else { 
          // 3. 如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader(没有就没有了,不会再向上递归)
          c = 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; 
  } 
}

你可能感兴趣的:(JVM,jvm,学习,java)