一、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、多态原理
方法调用:
- invokespecial 即确定的方法 如类独有的private 成员方法
- invokestatic 静态方法
- invokevirtual 不确定最终调用方法,需要查找多次才能知道具体内存地址 – 与多态原理相关
当执行invokevirtual指令时:
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象的实际Class
- Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
- 查vtable方法表得到方法的具体地址
- 执行方法的字节码
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 ClassLoader | JAVAHOME/ire/lib | 无法直接访问 |
Extension ClassLoader | JAVAHOME/ire/lib/ext | 上级为Bootstrap,显示为null |
Application ClassLoader | classpath | 上级为Extension |
自定义类加载 | 自定义 | 上级为Application |
**双亲委派模型:**找上级,先找扩展类加载器,再找启动类加载器,如果上级无法完成加载任务,捕获未找到异常(ClassNotFoundException),子加载器再尝试自己加载。
为什么要使用双亲委派模型
防止内存中出现多份同样的字节码,如果由各个类加载器自行加载的话,用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证。要让类对象进行比较有意义,前提是他们要被同一个类加载器加载。
自定义类加载器 需继承ClassLoader,重写findClass方法。如果要打破双亲委派模型的则需要重写loadClass()方法。
2、运行期优化
对热点代码的优化 – 逃逸分析、方法内联、字段优化
即时编译器(JIT)与解释器的区别
- 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- JIT是将一些字节码编译为机器码,并存入CodeCache,下次遇到相同的代码,直接执行,无需再编译
- 解释器是将字节码解释为针对所有平台都通用的机器码
- JIT会根据平台类型,生成平台特定的机器码