一、类的加载过程包括以下几个阶段。
1、加载:在加载阶段,虚拟机需要完成以下3件事情:
(1)通过类的全限定名获取类的二进制字节流。
(2)将该字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在内存中生成一个代表该类的Class对象,作为方法区这个类的各种数据的访问入口
2、验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全。
(1)验证字节流是否符合Class文件格式的规范,并且能被当前版本虚拟机处理。这一阶段可能包括以下验证点:
- 是否以魔术0xCAFEBABE开头
- 主、次版本号是否在当前虚拟机处理范围之内
- 常量池的常量中是否有不被支持的常量类型
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
- 。。。
(2)元数据验证
- 是否有父类(除Object类外,所有类都应该有父类)
- 是否继承了不允许被继承的类(final类)
- 如果不是抽象类,是否实现了父类或接口中要求实现的方法
- 。。。
(3)字节码验证:是最复杂的,主要通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,在第二阶段对元数据信息中的类型做完校验后,这个阶段对类的方法体进行校验分析
3、准备:在方法区中,为static变量进行内存分配,并赋予初始化值。例如:public static int value = 123; value在准备阶段过后的值为0,而不是123,把value赋值为123要在初始化阶段。但public static final int value = 123这种常量; value值就是123。
4、解析:将常量池内的符号引用替换为直接引用。在编译阶段,类不知道引用的类的实际内存地址,所以用符号代替,等到了解析阶段,知道了地址,就用真正的地址替换掉符号。解析阶段在某些情况下可以在初始化开始后与之互相交叉地混合进行,并不一定要解析完成了,才能开始初始化阶段。
5、初始化:该阶段是执行类构造器<clinit>()方法的过程。
(1)<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的。静态代码块只能访问到定义在静态代码块之前的变量,定义在它之后的,只能赋值,不能访问。
public class Test { static { i = 0; //可以赋值 System.out.println(i); //不能访问 } static int i = 1; }(2) 父类的<clinit>方法先执行,所以父类的静态代码块要优先于子类的变量赋值操作
(3)接口中不能使用静态代码块,但仍然有变量初始化的赋值操作,因此接口和类一样都会生成<clinit>方法,但不同的是,执行接口的<clinit>方法不需要先执行父接口的<clinit>方法,除非父接口定义的变量被使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>方法
(4)有且只有以下5种会立即对类进行初始化操作(而加载、验证、准备自然在此之前开始),这5种被称为主动引用,而被动引用则不会
- 在new实例化对象、读取或设置类的静态字段(除了final字段)时
- 调用类的静态方法时,用reflect包的方法进行反射操作且没有进行过初始化时
- 初始化一个类而父类还没有初始化时、先触发父类的初始化
- 指定运行的包含main()方法的类时
- 当使用JDK 1.7的动态语言支持,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化时,则需要先触发其初始化。
被动引用主要有以下3种:
- 子类调用自己未重写的父类的静态变量,静态方法时,不会触发子类初始化
public class Person { protected static int value = 123; static { System.out.println("Person"); } public Person() { System.out.println("Person构造方法"); } protected static String demo() { return "demo"; } } public class Student extends Person{ static { System.out.println("Student"); } } public class Demo { public static void main(String[] args) { System.out.println(Student.demo()); } }输出Person demo,不会输出Student
- 创建类数组时,不会触发类的初始化
Student[] s = new Student[10];
- 调用类的常量时,由于常量在编译阶段会存入调用类的常量池中,所以本质上没有直接引用的定义常量的类,所以不会触发定义常量的类的初始化
自我总结:
类中属性的初始化顺序,父类静态 > 子类静态 > 父类成员变量/普通代码块 > 父类构造方法 > 子类成员变量/普通代码块 > 子类构造方法,静态的只会执行一遍
public class MyDemo2 { static{ System.out.println("父类静态代码块"); } { System.out.println("父类动态代码块"); } public MyDemo2(){ System.out.println("父类构造方法"); } public static MyDemo2 mydemo = new MyDemo2(); public void demo(){ System.out.println("父类demo方法"); } } public class MyDemo1 extends MyDemo2{ static{ System.out.println("静态代码块"); } public static MyDemo1 mydemo = new MyDemo1(); { System.out.println("动态代码块"); } public MyDemo1(){ System.out.println("构造方法"); } public static void main(String[] args) { System.out.println("main方法"); } }
public class Test { public static void main(String[] args) { new Outer(); } } class Inner { Inner (int marker) { System.out.println(marker); } } class Outer { Inner i1 =new Inner(1); Outer() { System.out.println("zzz"); in2 = new Inner(22); } Inner in2 = new Inner(2); }输出:1
2
zzz
22
二、类加载器
1、每个类加载器都有一个独立的类名称空间。所以,即使两个类来源于同一个class文件,被同一虚拟机加载,但只要加载它们的类加载器不同,那么两个类就必定不相等。
2、双亲委派机制:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器,因子所有的加载请求最终都应该传送到顶层的启动类加载器中,只有父加载器没有找到类时,子加载器才会自己去加载。
加载器从上到下分为:
(1)启动类加载器:<JAVA_HOME>\lib目录中的jar
(2)扩展类加载器:<JAVA_HOME>\lib\ext目录中的jar
(3)应用程序类加载器:ClassPath上指定的类库,即自己写的类和第三方导入的jar包
问题:可否自己写一个java.lang.String类?
答:不可以,因为自己写的类,被加载时,启动类加载器会加载rt.jar下的System类,不会加载自己写的类。
另外,自己写的类所在的包不能以java开头。因为类加载器ClassLoader在加载时会调用下面这个私有方法,类的全限定名以java开头的会抛出异常。
private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) { if (!checkName(name)) throw new NoClassDefFoundError("IllegalName: " + name); if ((name != null) && name.startsWith("java.")) { throw new SecurityException ("Prohibited package name: " + name.substring(0, name.lastIndexOf('.'))); } if (pd == null) { pd = defaultDomain; } if (name != null) checkCerts(name, pd.getCodeSource()); return pd; }