java中的类加载器ClassLoader和类初始化
一般来说,类加载器是按照树形的层次结构组织的,每个加载器都有一个父类加载器。另外,每个类加载器都支持代理模式,即可以自己完成Java类的加载工作,也可以代理给其它类加载器。
ClassLoader中的几个实现类
1、Bootstrap ClassLoader 这个是JVM加载自身工作需要的类,完全由JVM自己来控制,外部无法访问到这个;
2、ExtClassLoader比较特殊的,服务的特定目标在System.getProperty("java.ext.dirs");
3、AppClassLoader,父类是ExtClassLoader,java中参数-classpath中的类都可以被这个类加载器加载;
4、URLClassLoader,一般这个类帮我们实现了大部分的工作,自定义可以继承这个类,这样仅仅在需要的地方做修改就行了;
类加载器的加载顺序有两种,一种是父类优先策略,一种是是自己优先策略,父类优先策略是比较一般的情况(如JDK采用的就是这种方式),在这种策略下,类在加载某个Java类之前,会尝试代理给其父类加载器,只有当父类加载器找不到时,才尝试子类加载器去加载,如果找到了,自己就不用加载。自己优先的策略与父类优先相反,它会首先尝试自己加载,如果找到了就不用父类加载器去加载,只有找不到的时候才要父类加载器去加载,这种在web容器(如tomcat)中比较常见。
不管使用什么样的类加载器,类都是在第一次被用到时,动态加载到JVM的。这句话有两层含义:
- Java程序在运行时并不一定被完整加载,只有当发现该类还没有加载时,才去本地或远程查找类的.class文件并验证和加载(赖加载);
- 当程序创建了第一个对类的静态成员的引用(如类的静态变量、静态方法、构造方法——构造方法也是静态的)时,才会加载该类。Java的这个特性叫做:动态加载。
JVM加载clas文件到内存的方式
1、显示加载:不通过代码里的ClassLoader调用,而是JVM来自动加载类到内存中的方式;
1.1、通过Class中的forName;
1.2、通过ClassLoader中的loadClass
1.3、通过ClasLoader中的findSystemClass
2、隐身加载:通过代码中ClassLoader来加载的方式;
如何加载class文件
1)加载(Loading),由类加载器执行,查找字节码,并创建一个Class对象(只是创建);
a)通过类的全名产生对应类的二进制数据流。(注意,如果没找到对应类文件,只有在类实际使用时才抛出错误。)
b)分析并将这些二进制数据流转换为方法区(JVM 的架构:方法区、堆,栈,本地方法栈,pc 寄存器)特定的数据结构(这些数据结构是实现有关的,不同 JVM 有不同实现)。这里处理了部分检验,比如类文件的魔数的验证,检查文件是否过长或者过短,确定是否有父类(除了 Obecjt 类)。
c)创建对应类的 java.lang.Class 实例(注意,有了对应的 Class 实例,并不意味着这个类已经完成了加载链链接!)。
2)链接(Linking),验证字节码,为静态域分配存储空间(只是分配,并不初始化该存储空间),解析该类创建所需要的对其它类的应用;
a)验证(verification)
链接的第三部解析会把类中成员方法、成员变量、类和接口的符号引用替换为直接引用,而在这之前,需要检测被引用的类型正确性和接入属性是否正确(就是 public ,private 的的问题),诸如检查 final class 又没有被继承,检查静态变量的正确性等等。(注意到实际上有一部分验证过程已经在加载的过程中执行了。)
b)准备(preparation)
对类的成员变量分配空间。虽然有初始值,但这个时候不会对他们进行初始化(因为这里不会执行任何 Java 代码)。具体如下:所有原始类型的值都为 0。如 float: 0f, int: 0, boolean: 0(注意 boolean 底层实现大多使用 int),引用类型则为 null。值得注意的是,JVM 可能会在这个时期给一些有助于程序运行效率提高的数据结构分配空间。比如方发表(类似与 C++中的虚函数表,参见另一篇博文《Java:方法的虚分派和方法表》)。
c)解析(Resolution)
首先,为类、接口、方法、成员变量的符号引用定位直接引用(如果符号引用先到常量池中寻找符号,再找先应的类型,无疑会耗费更多时间),完成内存结构的布局。
然后,这一步是可选的。可以在符号引用第一次被使用时完成,即所谓的延迟解析(late resolution)。但对用户而言,这一步永远是延迟解析的,即使运行时会执行 early resolution,但程序不会显示的在第一次判断出错误时抛出错误,而会在对应的类第一次主动使用的时候抛出错误!
最后,这一步与之后的类初始化是不冲突的,并非一定要所有的解析结束以后才执行类的初始化。不同的 JVM 实现不同。详情见另一篇博文《Java 类加载的延迟初始化》。
3)初始化(Initialization)。
注 意:static{}子句是在类第一次被java程序主动使用时才执行且执行一次,也就是执行Class.forName()方法时。如果用类.class并不会执行static{}子句,因为它是在编译时就已经存在类的.class文件,不然会编译失败。代码可以说明问题,注意类A的输出结果:
动态加载类:
public class BeanUtilsTest { public static void main(String[] args) throws Exception { Class clz = Class.forName("com.ai.redis.A"); } } class A { public static int VALUE; static { System.out.println("run parent static code."); } }
输出结果:打印run parent static code.
类.class:
public class BeanUtilsTest { public static void main(String[] args) throws Exception { Class clz1 = A.class; } } class A { public static int VALUE; static { System.out.println("run parent static code."); } }
输出结果:啥也没有。
通过以上比较,下面这段代码应该知道打印什么了吧。
public class BeanUtilsTest { public static void main(String[] args) throws Exception { System.out.println(A.VALUE); } } class A { public static final int VALUE = 10; static { System.out.println("run parent static code."); } }
输出结果:10
有人要问了,为什么不打印run parent static code.因为VALUE变量是在编译时就已经确定的一个常量值跟类.class文件是一个道理,所以不打印。
注:编译时常量必须满足3个条件:static的,final的,常量。
而下面的代码都会导致打印run parent static code.
<pre class="html" name="code"> static int a; final int b; static final int c = Math.abs(10); static final int d; static { d = 5; }
PS:
根据java虚拟机规范,所有java虚拟机实现必须在每个类或接口被java程序首次主动使用时才初始化。
主动使用有以下6种:
1) 创建类的实例
2) 访问某个类或者接口的静态变量,或者对该静态变量赋值(如果访问静态编译时常量(即编译时可以确定值的常量)不会导致类的初始化)
3) 调用类的静态方法
4) 反射(Class.forName(xxx.xxx.xxx))
5) 初始化一个类的子类(相当于对父类的主动使用),不过直接通过子类引用父类元素,不会引起子类的初始化(参见示例6)
6) Java虚拟机被标明为启动类的类(包含main方法的)
类与接口的初始化不同,如果一个类被初始化,则其父类或父接口也会被初始化,但如果一个接口初始化,则不会引起其父接口的初始化。
为什么接口不能定义成员变量,而只能定义 final static 变量。
- 1.接口是不可实例化,它的所有元素都不必是实例(对象)层面的。static 满足了这一点。
- 2.如果接口的变量能被修改,那么一旦一个子类实现了这个接口,并修改了接口中的非 final 变量,而该子类的子类再次修改这个非 final 的变量后,造成的结果就是虽然实现了相同的接口,但接口中的变量值是不一样的。
综上述,static final 更适合于接口。
参考:
1、《通过类字面常量解释接口常量为什么只能定义为 static final,类加载过程—Thinking in java》
2、http://blog.csdn.net/biaobiaoqi/article/details/6909141