JVM类加载

本文深入探讨了Java字节码指令,包括a++操作的字节码分析,循环中的x++行为,以及构造方法的生成。同时讲解了类加载过程,包括验证、准备、解析和初始化阶段,强调了双亲委派模型的作用。此外,还讨论了方法重写时的桥接方法,匿名内部类与final变量的关系,以及异常处理中finally块的字节码实现。
摘要由CSDN通过智能技术生成

一、JVM – 字节码指令

1、练习1

已知知识: 该程序的栈帧中内部结构含有局部变量表、操作数栈、PC计数器;其中操作数栈的宽度是4字节

从字节码角度分析:a++ 相关题目

public static void main (String[] args){
	int a = 10;
	int b = a++ + ++a + a--;
}
//结果a 为 11 ; b 为 34

a++ 和 ++a的区别是先执行iload 还是先执行iinc

  • a++ :将局部变量表中变量a的值压入操作数栈,(操作数栈添有数值为10的4字节空间),在局部变量表中执行变量a+1,a = 11;

  • ++a:在局部变量表中执行变量a+1,a = 12,再将变量a的值压入操作数栈,此时操作数栈含有数值为10的4字节空间 和 数值为12的4字节空间,两个数值相加为22

  • a–:将局部变量表中变量a的值压入操作数栈,(操作数栈开辟数值为12的4字节空间),在局部变量表中执行变量a-1,a = 11;

  • 因而得到b: 22 + 12 = 34

2、练习2

从字节码角度分析代码运行结果

public static void main(String[] args){
	int i = 0;
	int x = 0;
	while(i < 10){
		x = x++;
		i++;
	}
	System.out.println(x);//结果是0
}

x++,字节码指令先iload 再iinc

对代码句 x = x++

  • 将x的值压入操作数栈,此时操作数栈添有数值为0

  • 局部变量表中执行x+1,此时局部变量中x存储的值为1

  • 将操作数栈中的数值0 赋值给 x,此时 x 存储的值为0

    即无论循环进行多少次,x的值始终都是0

3、构造方法

编译器会按从上至下的顺序,收集所有static静态代码块和静态成员赋值的代码,合并为一个特殊的构造方法cinit()v: , 该方法会在类加载的初始化阶段被调用

public class Demo{
	static int i = 10;
	static {
		i = 20;
	}
	static{
		i = 30;
	}
}
对应字节码指令
0:bipush	10
2:putstatic	#2
5:bipush	20
7:putstatic	#2
10:bipush	30
12:putstatic	#2
15:return

编译器会按从上至下的顺序,收集所有{}静态代码块和成员变量赋值的代码,形成新的构造方法init V:,但原始的构造方法内的代码总是在最后。

Demo d = new Demo();
字节码指令
0:new
3:dup
4:invokespecial

在创建对象过程:

  • 使用new关键字,分配对象在堆内存的空间,分配成功后会将对象引用放入操作数栈;

  • 然后将引用地址复制一份也放入操作数栈(即此刻有两个引用地址在操作数栈中);

  • 再 根据栈顶的引用地址去调用对象的构造方法,出栈(即此时还剩一个引用地址在操作数栈中)

  • 再 将操作数栈的引用地址赋值给局部变量表中的变量d

先执行cinit方法再执行init方法 ,创建实例化对象时: 父类静态成员 - 自身静态成员 - 父类成员变量 - 自身成员变量 - 父类构造方法 -自身构造方法。

4、多态原理

方法调用:

  1. invokespecial 即确定的方法 如类独有的private 成员方法
  2. invokestatic 静态方法
  3. invokevirtual 不确定最终调用方法,需要查找多次才能知道具体内存地址 – 与多态原理相关

当执行invokevirtual指令时:

  1. 先通过栈帧中的对象引用找到对象
  2. 分析对象头,找到对象的实际Class
  3. Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
  4. 查vtable方法表得到方法的具体地址
  5. 执行方法的字节码

5、异常处理

finally
public static void main(String[] args){
	int i = 0;
	try{
		i = 10;
	}catch{
		i = 20;
	}finally{
		i = 30;
	}
}
对应字节码指令
0:iconst_0
1:istore_1 		//0->i 
2:bipush 	10	// try --- 
4:istore_1		// 10 -> i 
5:bipush 	30	// finally 
7:istore_1		// 30 -> i 
8:goto 		27	//return
11:astore_2		//catch Exceptin->e
12:bipush 	20 
14:istore_1		// 20 -> i 
15:bipush 	30	// finally 
17;istore_1		// 30 -> i 
18:goto 	27	// return 
21:astore3 		//catch any -> slot3
22:bipush 30 	// finally 
24:istore1 		// 30 ->i 
25:aload_3 		// <- slot 3 
26:athrow 		//throw 
27:return

可以看到finally的代码被复制了3份,分别放入try流程、catch流程以及catch剩余的异常类型流程。

练习 下面题目输出什么

public class demo{
	public static void main(String[] args){
		int res = test();
		System.out.println(res);
	}
	public static int test(){
		int i = 10;
		try{
			return i;
		}finally{
			i = 20;
		}
	}
}

具体执行过程 将10暂存,目的是为了固定返回值,然后执行finally块代码,再将暂存的值压入栈顶,所以返回的是10,即返回的值不会受到finally的影响

try-with-resources
try(资源变量 = 创建资源对象){

}catch(){
	
}
//资源对象需要实现AutoCloseable接口

尽管编写的程序不含有finally语句块,但编译时java会加上finally语句块

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()没有命名重名

7、匿名内部类

public class Demo{
	public static void test(final int x)		{
		Runnable runnable = new Runnable(){
			@Override
			public void run(){
				System.out.println(x)
			}
		}
	}
}
编译后即为该内部类提供一个int成员变量以及含有int x的构造方法,当内部类需要用到x时就是用到自身的属性,而不是传入的参数x,因而x必须是final,如果x发生变化,匿名内部类的对应成员变量不会变化;
故而语法上 匿名内部类引用局部变量时,局部变量必须是final。

二、类加载

**验证:**检查加载的 class 文件的正确性;

准备:

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

解析:

将常量池中的符号引号解析为直接引用,符号引用就理解为一个标示,而在直接引用直接指向内存中的地址

**初始化:**对静态变量和静态代码块执行初始化

发生的时机

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

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

不会导致类初始化的情况

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

1、类加载器

名称加载哪的类说明
Bootstrap ClassLoaderJAVAHOME/ire/lib无法直接访问
Extension ClassLoaderJAVAHOME/ire/lib/ext上级为Bootstrap,显示为null
Application ClassLoaderclasspath上级为Extension
自定义类加载自定义上级为Application

**双亲委派模型:**找上级,先找扩展类加载器,再找启动类加载器,如果上级无法完成加载任务,捕获未找到异常(ClassNotFoundException),子加载器再尝试自己加载。

为什么要使用双亲委派模型

防止内存中出现多份同样的字节码,如果由各个类加载器自行加载的话,用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证。要让类对象进行比较有意义,前提是他们要被同一个类加载器加载。

自定义类加载器 需继承ClassLoader,重写findClass方法。如果要打破双亲委派模型的则需要重写loadClass()方法。

2、运行期优化

对热点代码的优化 – 逃逸分析、方法内联、字段优化

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

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  • JIT是将一些字节码编译为机器码,并存入CodeCache,下次遇到相同的代码,直接执行,无需再编译
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT会根据平台类型,生成平台特定的机器码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值