类的加载过程
类的加载过程分为三个主要阶段:加载阶段、连接阶段、初始化阶段
加载阶段:负责加载类的二进制数据文件,就是对应的class文件。
加载
在加载阶段虚拟机需要完成以下三个工作:
- 通过类的全局限定名获取定义此类的二进制字节流。
- 将这个字节流代表的静态存储结构转换为方法区域的运行时数据结构
- 在内存中生成代表这个类的class对象,作为方法区这个类的各种数据访问的入口。
验证
验证是链接阶段的第一步,为了确保class文件的字节流中包含的信息符合当前虚拟机的规范。验证阶段消耗的性能比较大。
主要包括:
文件格式验证、元数据验证、字节码验证、符号引用验证。
文件格式验证
- 是否以魔数0xCAFEBABE开头
- 主、次版本号是否在当前虚拟机处理范围之内
- 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的
元数据验证
- 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
- 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)
字节码验证
第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
符号引用验证
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配
首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中
其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
public static int value = 123;//这个时候初始值是0
在“通常情况”下初始值是零值,那相对的会有一些“特殊情况”:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,假设上面类变量value的定义变为:
public static final int value = 123;//这个时候初始值是123
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,
初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码
破坏双亲委派机制
第一次破坏
通过重写loadClass函数定义用户自定义的类加载器。
第二次破坏
由于自身的缺陷导致,双亲委派机制很好的解决了基础类的类加载器问题,但是当基础类要调用用户代码此时就会有问题,为了解决这个问题比较典型的就是JNDI,他的代码是启动类加载器去加载,他需要调用应用程序的代码,此时引入了线程上下文类加载器,这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
第三次破坏
由于用户对程序动态性的追求,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换(HotSwap)、模块热部署(HotDeployment)等
弄懂了OSGi的实现,就可以算是掌握了类加载器的精髓。
HotSpot虚拟机
为什么要使用解释器和编译器并存的架构
解释器率先发挥作用,省去编译的时间,立即发挥作用。程序运行后,编译器开始发挥作用,将越来越多的代码编译成本地代码,提高执行效率。
程序何时使用解释器,何时使用编译器
判断一段代码是不是热点代码,是不是需要触发编译器,这样的行为成为热点探测。主要有两种热点探测方式:基于采样和基于计数器。
基于采样:虚拟机周期检查各个线程的栈顶如果发现某个方法触发了阈值,开始触发编译器。
基于计数器:虚拟机为每个方法建立计数器,统计执行的次数,超过阈值触发即时编译。
这里的计数次数不是一个绝对次数,是一段时间内,如果一段时间没有达到阈值,计数器次数会衰减一半
java编译器与C/C++编译器
Java与C/C++的编译器对比实际上代表了最经典的即时编译器与静态编译器的对比