类加载机制
动态加载与静态加载:通过new指令生成的对象属于静态加载,因为这个类必须在编译期提供,如果找不到会报错ClassNotFound;由forName加载的类属于动态加载,类的来源可以是本地文件也可以是网络二进制流;
装载:JVM通过类的全限定名将二进制流转化为方法区的数据结构并在内存中生成java.lang.Class对象,整个过程叫装载
绑定:一个方法的调用和方法所在的类关联起来的过程
动态绑定:有些对象对方法的调用在编译期间无法判断是哪一个,需要在运行时加以判断,这个过程叫做动态绑定,几乎所有的方法都是动态绑定的
静态绑定:对象和方法在编译期间进行绑定,只有final,static,private修饰的方法和构造方法是静态绑定。
因为以上方法都是不可以被子类重写的!注意:static final共同修饰一个方法时IDE是会发出warning的,如IDEA会警告“static method declared final”,因为功能重复了!
举例:x.func(args)的动态绑定过程描述:
当读到这条语句时,编译器会列出所有X类及其父类的名字为func的方法;
根据参数类型和次序找到最匹配的func方法;
先去当前类中寻找,然后再去寻找父类,以此类推。
注意:动态绑定仅针对方法!而不针对类的属性!如:Parent son = new Son(); sout(son.value);=>输出为parent.value属性值 若sout(son.val());=>输出为son的val方法 那么如果想通过第一句话的方式输出son的类属性应该怎么解决呢?getset方法!sout(son.getValue())即可!
下面进入正题,说说类加载过程吧
类加载大致分为一下几步(网上随便粘的图):loading、linking、Initialization
其中,解析的顺序不是固定的,可能在初始化之前,也可能是在初始化之后(因为动态绑定的缘故),其他的步骤顺序是固定的。
注意:加载和初始化是两码事!加载=创建java.lang.Class对象;初始化=执行静态代码块=加载+后续动作
说一下源头--------什么时候会触发这一系列操作(初始化)呢?JVM规范严格规定了以下几种情况:
1、new一个对象的时候
2、调用类的静态方法的时候,此时会触发指令invokestatic,从而触发类的初始化
3、引用类或接口的的静态属性的时候(final除外,因为常量类型的字段在编译期会被放入常量池中,直接读取即可无需初始化类,因为本质上没有直接引用这个类)
4、当要加载一个类的时候发现父类还没加载,那么父类会被触发装载
5、当运行了一个类的main方法时,这个类会强制装载
6、jdk1.7后的动态语言中,解析结果调用了某个类的静态方法或非final静态字段,如果该类未被初始化会被触发
7、反射:(如Class.forName("com.test.Test");但Class.forName("com.test.MyClass1",false,classLoader);不会触发初始化)
那么什么时候一定不会触发以上操作呢?除了以上七种,其他一定为被动引用,不会触发类初始化操作。这里列举几个容易混淆的情况
1、一个第三方类通过子类调用它的父类的静态字段的时候,只会触发父类的初始化,子类会被加载(第一阶段),但是子类不会初始化(执行静态字段);但在子类中调用父类的静态字段,那么两个类都会触发初始化。
2、通过数组来引用一个类的时候,如User[] ss= new User[10];此时不会触发user类初始化
3、引用一个类的编译器期可确定的final常量的时候,前文括号中已经做了解释。形如 public static final String buffer=new String("aa")=>编译器不确定!
那么,具体每一个过程做了些什么呢?我这里简化一下告诉大家:
1. 类加载:Bootstrap Loader—Extended Loader—Application Loader—User Loader(需继承抽象类java.lang.ClassLoader,核心是自定义字节码来源) 通过类全限定名获取类的二进制字节流;将字节流的静态存储结构转化为方法区的运行时护具结构;在内存中生成一个java.lang.Class对象作为这个类的访问入口(方法区中的所有的信息,都是可以通过这个Class类对象反射得到,这便是反射的基础)。
两个类equal那么他们的字节码相同且来自同一个类加载器。
2. 链接:
a) 验证:是否符合java规范 :字节流大体格式验证;元数据验证(java语法);字节码验证(方法体内容,最复杂);符号引用验证(查找类所引用的其他类是否存在,为解析阶段做铺垫)
b) 准备:默认初始值 ,对于编译期可确定的常量,也就对应着字节码中的constantValue属性,在准备阶段会直接赋终值
c) 解析:符号引用转为直接引用(地址或句柄地址),解析地址
3. 初始化 :执行<clinit>():
<clinit>()中包含了一个类中所有类变量的赋值动作和静态块(写在static{ }里)的语句。执行顺序按在源文件中出现的顺序。
<clinit>()不需要调用父类的<clinit>(),这是由虚拟机来确保完成的。
接口的<clinit>()中只包含接口中字段的赋值操作,父接口的<clinit>()不需要在子接口或实现类的<clinit>()之前执行,只有在真正引用到了父接口中定义的字段时,才会执行父接口的<clinit>()。
<clinit>()是线程安全的。如果多个线程同时去初始化一个类,只有一个线程会去执行初始化,其他的线程都会阻塞。
双亲委派模型
一个类加载器收到加载请求,它会把这个请求委派给自己的父类加载器去加载,只有在父加载器反馈无法加载时(搜索后发现这个类不归它管)子加载器才会自己加载
源码很简单:先检查类是否已加载过,然后判断自己有无父加载器,然后调用父类加载器的loadClass()方法,若父类加载器加载失败并返回ClassNotFoundException,调用自己的findClass()加载。