汇编
汇编的目的是把汇编语言转为机器语言。
链接
链接是要解决目标文件之间的互相依赖关系,当a文件中的aa方法中调用了b文件的bb方法时,在汇编完成后,a文件的bb方法并没有准确的内存地址,链接后会转换为虚拟地址,虚拟地址可以依据一定的规则转换为实际地址,即可以运行时找到该方法。
编译过程
一个.java程序要想被执行,首先需要编译器将高级的.java程序文件编译成.class字节码片段。编译过程大致可以分为1个准备过程和3个处理过程
准备过程:初始化插入式注解处理器。
解析与填充符号表过程,包括:词法、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树。填充符号表。产生符号地址和符号信息。
插入式注解处理器的注解处理过程:插入式注解处理器的执行阶段。
分析与字节码生成过程,包括
-
标注检查。对语法的静态信息进行检查。
数据流及控制流分析。对程序动态运行过程进行检查。
解语法糖。将简化代码编写的语法糖还原为原有的形式,(Java虚拟机运行时并不直接支持这些语法,它们在编译阶段被还原回原始的基础语法结构,这个过程就称为解语法糖。)
字节码生成。将前面各个步骤所生成的信息转化成字节码。
上述3个处理过程里,执行插入式注解时又可能会产生新的符号,如果有新的符号产生,就必须转回到之前的解析、填充符号表的过程中重新处理这些新符号。
常见语法糖
泛型(泛型擦除)
switch 支持 String(其实比较的是hashcode是否相等)
for each循环
lambda表达式
自动装箱拆箱(valueOf方法)
可变长度参数(转换为数据)
枚举
内部类
条件编译
断言
数值字面量10_0000
try with source
运行阶段
字节码经过JVM(解释器)的处理后生成电脑可以直接执行的机器码,至此java程序才能得以正确运行。编译器无论在何时、在何种状态下把Class文件转换成与本地基础设施(硬件指令集、操作系统)相关的二进制机器码,它都可以视为整个编译过程的后端.
编译器
目前主流的两款商用Java虚拟机(HotSpot、OpenJ9)里,Java程序最初都是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成本地机器码,并以各种手段尽可能地进行代码优化,运行时完成这个任务的后端编译器被称为即时编译器。
编译的触发条件
HotSpot是采用基于计数器的热点检测,虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”。
编译器模式
混合模式(默认)
解释器interpreter
编译器JIT
解释执行
会逐条的将字节码转换成机器码然后执行。
编译执行
先将左右的字节码编译成机器码然后执行。
混合模式
解释代码中有大量重复的时候可以将其编译到本地(热点代码检测)热点代码检测
多次调用的方法:方法计数器,监测方法执行频率
多次调用的循环:循环计数器,检测循环执行频率
参数设置
-Xmixed:默认为混合模式
-Xint 使用解释模式,执行稍慢,启动很快
-Xcomp 使用纯编译模式,执行很快,启动很慢
从加载到运行
加载
通过一个类的全限定名来获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的各种数据的访问入口。
验证
确保字节码文件数据是否为符合jvm虚拟机可识别的格式。包括文件格式、字节码格式、元数据和符号引用验证。
准备
为类变量赋值为初始化值,实例变量不赋值。如果被final修饰,则会编程常量,在编译的时候就已经存在。所以在这里会显示的初始化。
解析
将常量池中的符号引用转换成对应的直接引用。在字节码文件中可以看到这个常量。
初始化:执行类构造器的方法。
public class TestClInit { private static int num = 1; static { num = 2; } public static void main(String[] args) { System.out.println(num); }}
图片中执行clinit方法的位置,先后定义一个常量1,2执行putstatic操作。
init vs clinit
init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化。
主动使用vs被动使用
如果调用了clinit方法,就是主动使用。
主动使用场景
-XX +TraceClassLoading 跟踪类的加载信息
使用new、反射、clone、反序列化等关键词。
主动调用类的静态方法,即执行invokestatic指令。方法本身是不会出现在clinit里面。
使用类、接口中的静态字段(非final)。
使用reflect反射包方法。
在初始化子类时,如果父类还没有初始化,则会执行父类的clinit方法,但是不适用于接口,如果实现某个接口,并不会对该接口进行初始化。
main方法。
接口中的default方法,如果直接实现或间接实现类进行初始化,那么该接口先初始化。
被动使用场景:没有初始化,不意味着没有加载
通过子类去访问父类的静态字段,只会初始化父类而子类不会进行初始化。
通过数组定义类的引用,不会进行初始化。
User[] users = new User[10]; User[0] user = new User();//在这里才会初始化
引用private static final NUM= 1;常量不会触发类或接口的初始化,在准备阶段已经赋值。
//如果是要在初始阶段Random 初始化完成重新赋值,那么就会触发类的初始化。private static final NUM= new Random().nextInt(10);
使用ClassLoader的loadClass方法,也不会触发初始化。
类的卸载(方法区的垃圾回收)
回收常量池中废弃的常量和不再使用的引用类型
类和接口全限定名
方法的名称和描述符
字段的名和描述符
只要该常量没有被引用,就会被回收。和回收堆中对象类似。
如何判断类不被使用的3个条件。
该类的所有实例被回收,包括类的子类的实例也不存在。
加载该类的类加载器也被回收。
类对应的Class也没有被引用过,在反射的时候会使用。
如果不执行该回收操作,那么就会有可能发生OOM当类的对应的Class对象结束生命周期,那么就不会再指向方法区中的二进制数据, 那么该对象的生命周期才会结束,才会执行后面的方法区垃圾回收。因此回收的必要条件是Class对象相关联的所有指针都被切断。条件比较苛刻,主要是在于加载器的卸载难度较大:
启动类加载器和扩展类加载器在运行期间是不可能被卸载。
自定义加载器只有在上下文环境比较简单,才会有可能被卸载。