5.7.多态原理
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用invokevirtual指令。
在执行invokevirtual指令时,经历了以下步骤:
- 先通过栈帧中对象的引用找到对象
- 分析对象头,找到对象实际的Class
- Class结构中有vtable
- 查询vtable找到方法的具体地址
- 执行方法的字节码
5.8.异常处理
1、try-catch
public class Demo1 { 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 //准备一个常数0 1: istore_1 //将常数0放在局部变量表的1号槽位上 2: bipush 10 //从常量池中拿取10 4: istore_1 //将常数10放在局部变量表的1号槽位上 5: goto 12 //如果不发生异常跳到12步 8: astore_2 //出现异常调到异常表 9: bipush 20 //从常量池中拿取20 11: istore_1 //将常数20放在局部变量表的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)
2、多个single-catch
public class Demo1 { 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 //准备一个常量0 1: istore_1 //将常量0放入本地变量表的1号槽位上 2: bipush 10 //从常量池中获取10这个变量 4: istore_1 //将常量10放入本地变量表中的1号槽位中 5: goto 19 //如果没有发生异常就跳转的19行 8: astore_2 //发生异常匹配异常表中target为8的异常 9: bipush 20 //从常量池中拿出20 11: istore_1 //放入本地变量表中的1号槽位 12: goto 19 //跳转到19号 15: astore_2 //发生异常匹配异常表中target为15的异常 16: bipush 30 //从常量池中拿出30 18: istore_1 //放入本地变量表中的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位置被共用(astore_2)
3、finally
public class Demo2 { 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 //准备一个常量0 1: istore_1 //将常量0放入本地变量表的1号槽位上 //try块 2: bipush 10 //常量池中取出10 4: istore_1 //将常量10放入本地变量表的1号槽位上 //try块执行完后,会执行finally 5: bipush 30 //从常量池中拿出30 7: istore_1 //30放到一号槽位上 8: goto 27 //跳转到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块中的代码只会被执行一次。
4、finally中的return
public class Demo3 { public static void main(String[] args) { int i = Demo3.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 //常量池中拿取10,放到操作数栈中 2: istore_0 //将10放入局部变量表中的0号槽位上 3: iload_0 //将0号槽位的值放入操作数栈中 4: istore_1 //暂存返回值 5: bipush 20 //常量池中拿取20 7: istore_0 //将20放入局部变量表中的0号槽位上 8: iload_0 //将0号槽位的值放入操作数栈中 9: ireturn //ireturn会返回操作数栈顶的整型值20 //如果出现异常,还是会执行finally块中的内容,没有抛出异常 10: astore_2 //出异常的情况,异常会被吞掉,还是拿取20 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中进行返回操作。
5、finally中return值吞掉异常
public class Demo3 { public static void main(String[] args) { int i = Demo3.test(); //最终结果为20 System.out.println(i); } public static int test() { int i; try { i = 10; //这里应该会抛出异常 i = i/0; return i; } finally { i = 20; return i; } } }
- 会发现打印结果为20,并未抛出异常。
6、finally不带return
public class Demo4 { public static void main(String[] args) { int i = Demo4.test(); System.out.println(i); } 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
5.9.Synchronized
public class Demo5 { public static void main(String[] args) { int i = 10; Lock lock = new Lock(); synchronized (lock) { System.out.println(i); } } } class Lock{}
- 对应字节码文件
Code: stack=2, locals=5, args_size=1 0: bipush 10 //常量池中拿取10,放在操作数栈中 2: istore_1 //将10 放到局部变量表中1号槽位 3: new #2 //new对象,对应常量池中#2的位置 // class com/nyima/JVM/day06/Lock 6: dup //复制一份,放到操/作数栈顶,用于构造函数消耗 7: invokespecial #3 // Method com/nyima/JVM/day06/Lock."<init>":()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
6.编译期处理
所谓的语法糖,其实就是指java编译器把.java源码编译为.class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们的一个额外福利。
6.1.默认构造方法
public class Demo19 { }
- 经过编译期优化之后
public class Demo19 { //这个无参构造器是java编译器帮我们加上的 public Demo19() { //即调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V super(); } }
6.2.自动拆装箱
- 基本类型和其包装类型的相关转换过程,称为拆装箱。
- 在JDK5以后,他们的转换可以在编译期自动完成。
public class Demo20{ public static void main(String[] args){ Integer x = 1; int y = x; } }
- 经过编译期优化之后
public class Demo20{ public static void main(String[] args){ //基本类型赋值给包装类型,称为装箱 Integer x = Integer.valueOf(1); //包装类型赋值给基本类型,称谓拆箱 int y = x.intValue(); } }
6.3.泛型集合取值
- 泛型也是在JDK5开始加入的特性,但java在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当作了Object类型来处理。
public class Demo3 { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); 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."<init>":()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();
6.4.可变参数
public class Demo21{ public static void foo(String... args){ //将args赋值给arr,可以看出String...实际就是String[] String[] arr = args; System.out.println(arr.length); } public static void main(){ 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。
6.5.foreach 循环
1、数组使用foreach
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); } } }
2、集合使用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 = iterator.next(); System.out.println(x); } } }
6.6.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的hashcode值 case 99162322 : //判断完hashcode在次比较字符串,因为hashcode有可能一样的情况 if(str.equals("hello")) { //相等给x付0 x = 0; } break; //world的hashcode值 case 11331880 : if(str.equals("world")) { //在比较字符串,相等给x符1 x = 1; } break; default: break; } //另起一个switch,根据x进行判断 switch (x) { case 0: //x是0 ,证明匹配hello System.out.println("h"); break; case 1: //x是1 ,证明匹配world System.out.println("w"); break; default: break; } } }
过程说明:
- 在编译期间,单个switch被分为两个
- 第一个用来匹配字符串,并赋给x
- 字符串的匹配用到了字符串的hashCode,还用到了equals方法。
- 使用hashCode是为了提高比较的效率,equals是为了防止有hashCode冲突(如BM和C.)。
- 第二个用来根据x的值来决定输出语句。
6.7.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{ //定义一个数组,数组大小为枚举类的于元素个数 static int[] map = new int[2]; static { //ordinal即枚举元素对应所在位置,MALE为0,FEEMALE为1。 map[SEX.MALE.ordinal()] = 1; map[SEX.FEEMALE.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; }
6.8.枚举类
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 { //调用构造函数,传入枚举元素的值及ordinal MALE = new Sex("MALE",0); MALE = 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); } }
6.9.桥接方法
我们都知道,方法重写时对返回值分两种情况:
- 父子类的返回值完全一样
- 子类返回值可以是父类返回值的子类(比较绕口,看下面例子)
class A{ public Number m(){ return 1; } } class B extends A{ @Override 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(); } }
其中桥接方法比较特殊,仅对JVM可见,并且与原来的public Integer m()没有命名冲突,可以用下面反射代码来验证:
for(Method m : B.class.getDeclaredMethods()){ System.out.println(m); }
6.10.匿名内部类
public class Demo21 { public static void main(String[] args){ Runnable runnable = new Runnable() { @Override public void run() { System.out.println("running..."); } }; } }
- 转换后的代码
public class Demo21{ public static void main(String[] args){ //用额外创建的类,来创建匿名内部类 Runnable runnable = new Demo21$1(); } //创建了一个额外的类来实现Runnable接口 final class Demo21$1 implements Runnable{ public Demo21$1(){} @Override public void run(){ System.out.println("running..."); } } }
- 如果匿名内部类中引用了局部变量。
public class Demo21 { public static void main(String[] args) { int x = 1; Runnable runnable = new Runnable() { @Override public void run() { System.out.println(x); } }; } }
- 转换后的代码
public class Demo21 { public static void main(String[] args) { int x = 1; Runnable runnable = new Demo21$2(final x); } } final class Demo21$2 implements Runnable{ //多创建了一个变量 int val$x; //变为了有参构造器 public Demo21$2(int x){ this.val$x = x; } @Override public void run(){ System.out.println(val$x); } }
**注意:**局部变量必须是final的,因为在创建Demo21$2对象时,将x的值赋值给了Demo212 对象的 v a l 2对象的val2对象的valx属性,所以x不能再发生改变秒如果变化 ,那么val$x属性没有机会跟着一起改变。
7.类加载阶段
7.1.加载
- 将类的字节码载入方法区(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,从而获取类的各种信息
7.2.链接
1、验证
验证类是否符合JVM规范,安全性检查。
2、准备
为static变量分配空间,设置默认值。
- static变量在JDK7以前是存储与instacneKlass末尾。但在JDK7以后就存储在_java_mirror末尾了,也就是存在于堆中。
- static变量在分配空间和赋值是在两个阶段完成的。分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果static变量是final的基本类型,以及字符串常量,那么编译阶段就确定了,赋值在准备阶段完成。
- 如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成。
- 3、解析
解析的含义:将常量池中的符号引用解析为直接地址。
验证:
- 运行测试代码:
/** * 验证解析的含义 */ public class Demo22 { public static void main(String[] args) throws ClassNotFoundException, IOException { ClassLoader classLoader = Demo22.class.getClassLoader(); classLoader.loadClass("com.lixiang.C"); //用于阻塞主线程 System.in.read(); } } class C{ D d = new D(); } class D{ }
- 查看当前运行的java进程:jps
- 连接HSDB工具:java -cp JDK的安装目录\lib\sa-jdi.jar sun.jvm.hotspot.HSDB,输入进程id查找
java -cp D:\JDK\jdk1.8\lib\sa-jdi.jar sun.jvm.hotspot.HSDB
- 改用new的方式进行创建
/** * 验证解析的含义 */ public class Demo22 { public static void main(String[] args) throws ClassNotFoundException, IOException { new C(); //用于阻塞主线程 System.in.read(); } } class C{ D d = new D(); } class D{ }
7.3.初始化
初始化阶段就是执行类构造器clinit()方法的过程,虚拟机会保证这个类的【构造方法】的线程安全。
clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。
注意:
编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如
public class Test{ static { i = 0; //给变量赋值可以正常编译通过 System.out.println(); //这句编译器会提示“非法向前引用” } static int i = 1; }
发生时机
- 测试代码
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"); } }
- 类的初始化是懒惰的,以下情况不会触发类的初始化
(1)final修饰的静态变量不会触发初始化
//访问B中final修饰的变量 System.out.println(B.b);
(2)类对象.class不会触发初始化
//访问B的class对象 System.out.println(B.class);
(3)创建类的数组不会触发初始化
//创建B类型的数组 System.out.println(new B[10]);
(4)ClassLoader不会初始化类,但是会加载类
//创建classLoader不会初始化B类,但是会加载B、A类 ClassLoader c1 = Thread.currentThread().getContextClassLoader(); c1.loadClass("com.lixiang.B");
(5)Class.forName(),initizlize参数设置成false不会初始化操作
//Class.forName中initizlize参数设置成false不会初始化类B,但会加载B、A ClassLoader c2 = Thread.currentThread().getContextClassLoader(); Class.forName("com.lixiang.B",false,c2);
- 会发生类初始化操作的情况
(1)首次访问这个类的静态变量或静态方法时
//访问A中不被final修饰的静态变量 System.out.println(A.a);
(2)子类初始化,如果父类还没初始化,会先引发父类的初始化
//访问B中的静态变量,会先加载A的初始化 System.out.println(B.c);
(3)子类访问父类的静态变量,只会触发父类的初始化
//子类B访问父类A中的静态变量a,只会初始化A System.out.println(B.a);
(4)Class.forName(“”)会初始化类B,但会先初始化类A
Class.forName("com.lixiang.B");
8.类加载器
Java虚拟机设计团队有意把类加载阶段中的 ”通过一个类的全限定名来获取该类的二进制字节流” 这个动作放到Java虚拟机外部去实现,以便让应用程序自己去决定如何获取所需类。实现这个动作的代码被称为**“类加载器”**(ClassLoader)。
8.1.类与类加载器
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:**比较两个是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,**否则,即使这两个类来源于同一个Class文件,
被同一个Java虚拟机加载,只要加载它们的类加载器不同,那么这两个必定不相等。
JDK 8为例
名称 | 加载的类 | 说明 |
Bootstrap ClassLoader(启动类加载器) | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader(扩展类加载器) | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
Application ClassLoader(应用程序类加载器) | classpath | 上级为Extension |
自定义类加载器 | 自定义 | 上级为Appliaction |
8.2.启动类加载器
可以通过在控制台输入指令,使得类被启动类加载器加载。
/** * 启用Bootstrap类加载器加载类 */ public class Demo25 { public static void main(String[] args) throws ClassNotFoundException { Class<?> aClass = Class.forName("com.lixiang.F"); System.out.println(aClass.getClassLoader()); //获取当前的类加载器 } } class F{ static { System.out.println("bootstrap F init"); } }
8.3.拓展类加载器
如果classpath和JAVA_HOME/jre/lib/ext下有同名类,加载时会使用拓展类加载器加载。当应用程序类加载器发现拓展类加载器已经将该同名类加载过了,则不会再次加载。
8.4.双亲委派模式
双亲委派模式,即调用类加载器Classloader的loadClass方法时,查找类的规则
loadClass源码:
private final ClassLoader parent; public ClassLoader(ClassLoader parent) { this.parent = parent; } protected Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException{ synchronized (getClassLoadingLock(name)){ //首先要先看下该类是否已经被该类加载器加载过了 Class<?> c = findLoadClass(name); //如果没有被加载过 if (c == null){ long t0 = System.nanoTime(); try{ //看是否被它的上级加载器加载过了Exception的上级是Bootstrap,但是它显示为null if(parent != null){ c = parent.loadClass(name,resolve); }else { //看是否被启动类加载器加载过 c = finBootstrapClassOrNull(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; } }
8.5.自定义类加载器
使用场景
- 想加载非classpath随意路径中的类文件
- 通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器
步骤
- 继承ClassLoader父类
- 要遵从双亲委派机制,重写findClass方法
- 不是重写loadClass方法,否则不会走双亲委派机制
- 读取类文件的字节码
- 调用父类的defineClass方法来加载类
- 使用者调用该类加载器的loadClass方法
自定义类加载器
public class MyClassLoader extends ClassLoader { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { String path = "D:\\MyClassLoader\\"+name+".class"; try { ByteArrayOutputStream os = new ByteArrayOutputStream(); Files.copy(Paths.get(path),os); //得到字节数组 byte[] bytes = os.toByteArray(); //byte[] -> *.class return defineClass(name,bytes,0,bytes.length); }catch (IOException e){ e.printStackTrace(); throw new ClassNotFoundException("类文件未找到",e); } } }
破坏双亲委派模式
- 双亲委派模型的第一次 “被破坏” 其实发生在双亲委派模型出现之前。
- 建议用户重写findClass()方法,在类加载器中的loadClass()方法中也会调用该方法
- 双亲委派模型的第二次 “被破坏” 是由这个模型自身的缺陷导致的
- 如果有基础类型又要调用回用户的代码,此时也会破坏双亲委派模式
- 双亲委派模型的第三次 “被破坏” 是由于用户堆程序动态性的最=追求而导致的
- 这里所说的 “动态性“ 指的是一些非常”热“门的名词: 代码热替换(Hot Swap)、模块热部署(Hot Deployment)等
9.运行期优化
9.1.分层编译
1、案例
/** * -XX:+PrintCompilation -XX:DoEscapeAnalysis */ public class JIT1 { public static void main(String[] args) { for (int i = 0; i < 200; i++) { long start = System.nanoTime(); for (int j = 0; j < 1000; j++) { new Object(); } long end = System.nanoTime(); System.out.printf("%d\t%d\n",i,(end - start)); } } }
2、JVM将执行状态分成了5个层次
- 0层:解释执行,用解释器将字节码翻译为机器码
- 1层:使用C1即时编译器编译执行(不带profiling)
- 2层:使用C1即时编译器编译执行(带基本的profiling)
- 3层:使用C1即时编译器编译执行(带完全的profiling)
- 4层:使用C2即时编译器编译执行
profiling是指在运行中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等。
3、即时编译器(JIT)与解释器的区别
- 解释器
- 将字节解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释。
- 是将字节码解释为针对所有平台都通用的机器码。
- 即时编译器
- 将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需在编译。
- 根据平台类型,生成平台特定的机器码。
对于大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行,另一方面,对于仅占据小部分的热点代码,
我们则可以编译成机器码,以达到理想的运行速度。执行效率上简单比较一下Interpreter < C1 < C2 ,总的目标是发现热点代码(hotspot名称的由来),并优化这些热点代码。
4、逃逸分析
逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot虚拟机可以分析新创建对象的使用范围,并决定是否在Java堆上分配内存的一项技术。
逃逸分析的JVM参数如下:
- 开启逃逸分析:-XX:+DoEscapeAnalysis
- 关闭逃逸分析:-XX:-DoEscapeAnalysis
- 显示分析结果:-XX:+PrintEscapeAnalysis
逃逸分析技术在Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加这个参数。
5、对象逃逸状态
全局逃逸(GlobalEscape)
- 即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:
- 对象是一个静态变量
- 对象是一个已经发生逃逸的对象
- 对象作为当前方法的返回值
参数逃逸(ArgEscape)
- 即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。
- 没有逃逸
- 即方法中的对象没有发生逃逸
6、逃逸分析优化
针对上面第三点,当一个对象没有逃逸时,可以得到以下几个虚拟机的优化
锁消除
我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁。
例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作。
锁消除的JVM参数如下:
- 开启锁消除:-XX:+EliminateLocks
- 关闭锁消除:-XX:-EliminateLocks
锁消除在JDK8中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上。
标量替换
首先要明白标量和聚合量,基础类型和对象引用可以理解为标量,他们不能被进一步的分解。而能被进一步分解的量就是聚合量,比如:对象。
对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做标量替。
这样,如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能。
标量替换的JVM参数如下:
- 开启标量替换:-XX:+EliminateAllocations
关闭标量替换:-XX:-EliminateAllocations
显示标量替换详情:-XX:+PrintEliminateAllocations
栈上分配
当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了GC压力,提高了应用程序性能。
9.2.方法内联
1、内联函数
内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换。
2、JVM内联函数
C++是否为内联函数由自己决定,Java由编译器决定。Java不支持直接声明为内联函数的,如果想让他内联,你只能够向编译器提出请求: 关键字final修饰 用来指明那个函数是希望被JVM内联的,如
public final void doSomething() { // to do something }
总的来说,一般的函数都不会被当做内联函数,只有声明了final后,编译器才会考虑是不是要把你的函数变成内联函数。
第二个原因则更重要:方法内联
如果JVM检测到一些小方法被频繁的执行,它会把方法的调用替换成方法体本身,如:
private int add4(int x1, int x2, int x3, int x4) { //这里调用了add2方法 return add2(x1, x2) + add2(x3, x4); } private int add2(int x1, int x2) { return x1 + x2; }
方法调用被替换后
private int add4(int x1, int x2, int x3, int x4) { //这里调用了add2方法 return x1 + x2 + x3 + x4; }
9.3.反射优化
//foo.invoke()前面0-15次调用使用的是MethodAccessor的NativeMethodAccessorImpl实现 public class Reflect { public static void foo(){ System.out.println("foo..."); } public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Method method = Reflect.class.getMethod("foo"); for (int i = 0; i < 16; i++) { method.invoke(null); } } }
一开始if条件不满足,就会调用本地方法invoke0
随着numInvocation的增大,当它大于ReflectionFactory.inflationThreshold的值16时,就会本地方法访问器替换为一个运行时动态生成的访问器,来提高效率
这时会从反射调用变为正常调用,即直接调用 Reflect1.foo()
10.虚拟机工具
10.1.jps命令
1、JPS是什么?
jps (JVM Process Status Tool)是其中的典型jvm工具。除了名字像 UNIX 的 ps 命令之外,它的功能也和 ps 命令类似:可以列出正在运行的虚拟机进程,
并显示虚拟机执行主类(Main Class, main()函数所在的类)名称以及这些进程的本地虚拟机唯- ID (Local Virtual Machine Identifier, LVMID),虽然功能比较单一,但它是使用频率最高的 JDK 命令行工具。
2、实战使用
- jps -l 输出主类的全名,如果进程执行的是Jar包则输出Jar路径
jps -l
10.2.jstat命令
1、jstat是什么
jstat(JVM Statistics Monitor Tool)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或远程-虚拟机进程中的类加载、内存、垃圾收集、
JIT编译等运行数据,在没有GU图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。
2、jstat命令使用
jstat -gc 2764 250 20 //2764表示进程id,250表示250毫秒打印一次,20表示一共打印20次
S0C:第一个幸存区的大小 S1C:第二个幸存区的大小 S0U:第一个幸存区的使用大小 S1U:第二个幸存区的使用大小 EC:伊甸园区的大小 EU:伊甸园区的使用大小 OC:老年代大小 OU:老年代使用大小 MC:方法区大小 MU:方法区使用大小 CCSC:压缩类空间大小 CCSU:压缩类空间使用大小 YGC:年轻代垃圾回收次数 YGCT:年轻代垃圾回收消耗时间 FGC:老年代垃圾回收次数 FGCT:老年代垃圾回收消耗时间 GCT:垃圾回收消耗总时间
10.3.jinfo命令
1、jinfo是什么
jinfo(Configuration Info for Java)的作用是实时地查看和调整虚拟机各项参数。使用jps命令的-v参数可以查看虚拟机启动时显示指定的参数列表
但如果想知道未被显示指定的参数的系统默认值,除了去找资料外,就只能使用info的-flag选项进行查询了。
2、jinfo命令使用
jinfo 1444(进程id) jinfo -flag CMSInititingOccuancyFraction 1444(进程id)
10.4.jmap命令
1、jmap是什么
Jmap (Memory Map for Java)命令用于生成堆转储快照。如果不使用 jmap 命令,要想获取 Java 堆转储快照,还有一些比较“暴力”的手段:-XX: +HeapDumpOnOutOfMemoryError 参数,可以让虚拟机在 OOM 异常出现之后自动生成 dump 文件,用于系统复盘环节
和 info 命令一样,jmap 有不少功能在 Windows 平台下都是受限的,除了生成 dump 文件的- dump 选项和用于查看每个类的实例、空间占用统计的-histo选项在所有操作系统都提供之外,其余选项都只能在Linux/Solaris 下使用。
2、jmap常用命令
- -dump
- 生成 Java 堆转储快照。格式为:-dump: format=b, file=
windows: jmap -dump:format=b,file=d:\a.bin 1234 mac: jmap -dump:format=b,file=/Users/daniel/deskTop
-histo more分页去查看
- 显示堆中对象统计信息,包括类、实例数量、合计容量
B :byte
C : char
I :Int
10.5.jhat命令
1、jhat是什么
- Sun JDK 提供 jhat (JVM Heap Analysis Tool)命令常与 jmap 搭配使用,来分析 jmap 生成的堆 转储快照。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在浏览器中查看
特点:
jhat分析工作是一个耗时而且消耗硬件资源的过程
jhat 的分析功能相对来说比较简陋
2、jhat使用
10.6.jstack命令
1、jstack是什么
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待都是导致线程长时间停顿的常见原因。线程出现停顿的时候通过jstack来查看哥哥线程的调用堆栈,就可以知道没有响应线程到底在后台做些什么事情,或者等待者什么资源。
2、Jstack怎么做
常用命令jstack -l 3500(进程id)
jstack -F 当正常输出的请求不被响应时,强制输出线程堆栈 Force
线上程序一般不能kill进程pid的方式直接关闭
shutdownHook :在关闭之前执行的任务