[笔记]Java虚拟机中类的加载

2017-09-18  本文已影响110人  蓝灰_q

.class/字节码

计算机的硬件底层,至今还是0/1电路,人们用汇编语言实现了在硬件电路上运行程序,汇编的每一条指令,都有对应的机器码。
但是汇编语言与硬件架构和操作系统有关,在一台设备上正常的机器码,换台设备可能就无法运行,我们又不可能为每种设备都去重新开发一遍软件。
所以,当年Java提出了“一次编写,到处运行”的口号,其核心就是在开发者和硬件设备之间插入一层Java虚拟机,开发者面向的Java虚拟机是一致的,Java虚拟机再去和各种底层OS和硬件交互。
Java虚拟机里运转的就不再是机器码,而是字节码,也叫中间语言(在开发语言和汇编语言中间),其实就是我们编译出来的.class类文件,字节码直接在Java虚拟机中运行,虚拟机再根据当前所在的操作系统和硬件环境,转换成机器码。

字节码和类的关系

.class文件里存放的是对类的定义和描述,Java虚拟机需要把.class文件(从硬盘/网络/数据库等)加载到内存,并完成加载、连接和初始化,才能生成Java虚拟机能使用的java类。
java类在使用上最大的特定是运行时加载,就是说不必在编译时初始化和连接,而是在程序运行过程中,动态地加载和连接,比如用接口做动态实例化、用自定义的ClassLoader做热修复等,都是这种动态扩展的体现。

类的加载

类直到被使用时才会加载到虚拟机内存,用完以后再从内存中卸载,整个生命周期可以分为7个阶段:
加载-->(连接:验证-->准备-->解析)-->初始化-->使用-->卸载
这7个阶段,开始的顺序基本可以保证,但是并不是串行,一个阶段还在执行中,就会激活下一个阶段。
加载的是二进制字节流,而且可以选用自定义的ClassLoader。
解析有时候会在初始化之后才执行,这种行为叫做动态绑定。
类的初始化不是对象的初始化,对象的初始化是创建实例。
类的卸载不是在使用之后立即卸载,而是在GC中,经过严格的过滤(没有实例、没有ClassLoader、没有引用/反射),符合条件才能从内存中卸载(而且方法区一般很少做GC,默认ClassLoader的生命周期甚至与进程一致)。

java的类是运行时加载,但是引用一个类不一定会触发类的加载,因为引用分为主动引用和被动引用,只有主动引用能触发类的加载。

主动引用
以下5种情况,是必须知道类的各种信息的,只有这5种情况会触发类的加载。
1.new类的实例、读/写静态变量、调用静态方法时。
如果在读静态的final字段,其所在的类并不会被加载,因为这种字段在编译时会统一放到一个NoInitialization类的常量池中,与所在的类就没关系了。
2.反射调用时(java.lang.reflect)
3.初始化子类时,必须初始化父类(如果是接口,就不需要初始化父接口),所以java.lang.Object总是最先被初始化的类。
4.虚拟机启动时执行main()函数的那个主类
5.JDK1.7动态语言支持中,需要访问静态变量/静态方法时。

被动引用
被动引用的情况下,类不会被加载,比如:
1.通过子类使用父类的成员变量(只有直接定义字段的类才会被初始化)
2.数组定义中引用的类,不会触发初始化(虚拟机只做了一个一维数组)
3.静态常量(被编译到NoInitialization类的常量池中)

关于类的初始化和对象的初始化
类的初始化不是对象的初始化,类的加载有7个阶段,类的初始化是其中之一,类的初始化并不调用类的构造器;
类加载完成后,才可以实现对象的初始化,为对象去分配内存,用类的构造器初始化对象,为对象初始化实例变量,赋值实例变量。
子类的初始化列表不能初始化父类或者祖先类的成员,因为在执行初始化时,只会初始化一次父类中的对象,子类中的初始化没有机会得到执行。
使用静态字段时,只有直接定义字段的类才会被初始化,哪怕代码上是通过派生类使用的静态字段(即只触发父类的初始化,不触发子类的初始化)。

一个类从加载到初始化,分5个阶段:

class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;
 
    private SingleTon() {
        count1++;
        count2++;
    }
 
    public static SingleTon getInstance() {
        return singleTon;
    }
}
 
public class Test {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        System.out.println("count1=" + singleTon.count1);
        System.out.println("count2=" + singleTon.count2);
    }
}

最终执行结果是:
count1=1;
count2=0;
因为static 静态代码段会依次执行,第一行中count1和count2被调用后,都被赋值为1,但是第三行static代码中,count2又被赋值为0了。

类加载器和双亲委派

类加载器是放在JVM外部实现的,这样应用程序可以按照自己的方式去获取二进制字节流。当然,虚拟机自己也有一个类加载器,就是启动类加载器(C++实现的Bootstrap ClassLoader),Bootstrap ClassLoader并不是Java类,所以无法获得它的引用。
虚拟机同时拥有很多个加载器。

类的和类的加载器
类的和类的加载器是深层绑定的,虚拟机加载同一个Class文件时,如果使用了不同的类加载器,这两个类就必定不相等(每个ClassLoader都有独立的类名称空间)。
在卸载类时(卸载方法区中的类对象),除了要判断没有类的实例,没有类的引用和反射,还要判断加载这个类的ClassLoader是否已经被回收(因为方法区的类信息里有指向ClassLoader的引用)。

双亲委派
因为虚拟机中同时存在多个类加载器,就会存在类的重复加载,甚至可能影响核心的Java API(如加载了一个自定义的String类),为了协调这些类加载器的关系,Java推荐了双亲委派机制。
双亲委派的核心其实是一条链式结构,把虚拟机自带的Bootstrap ClassLoader作为链的开始,后面扩展了Extention ClassLoader,再后面扩展了Application ClassLoader,再后面可以扩展各种自定义的ClassLoader。
在双亲委派机制下,类的加载就实现了父类加载器优先,所有可以加载这个类的ClassLoader中,会向上递归到最顶层那个ClassLoader,这样越基础的类就是由越上层的加载器进行加载,这样可以避免重复加载,并保护核心的Java API。
双亲委派机制可能被破坏,因为它不是强制性的要求。比如为了兼容较老的JDK版本使用findClass而不是loadClass;比如为了在基础的类中调用下层的代码,可以通过线程上下文加载器去实现让父类加载器请求子类加载器完成加载;再比如就是现在比较火的热部署/热更新,通过把新的dexElement插入到系统的dexElement之前,实现热部署/热更新。

引用

《深入理解Java虚拟机》
类加载和对象的初始化过程
Java 类加载机制 ClassLoader Class.forName 内存管理 垃圾回收GC
JVM源码分析之JDK8下的僵尸(无法回收)类加载器
关于Java类加载双亲委派机制的思考(附一道面试题)

上一篇 下一篇

猜你喜欢

热点阅读