前言:在很长的一段时间里,我对类加载机制一直弃之不顾,直到碰到下面的两段代码时,促使我去学习类加载的机制。
疑惑1:
public class Test {
class A{
public void print(){
}
}
//代码1
private void methodA(){
try {
Class cls = Class.forName("A");//"A"是类的路径,由于在本类中使用,可以直接写成"A"
A a = (A)cls.newInstance();
a.print();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
//代码2
private void methodB(){
A a = new A();
a.print();
}
}
有很长那么一段时间,我都在想methodA()和methodB()有什么区别,直接使用methodB()方法new一个A对象出来不就好了吗?
疑惑2:
public class Test {
public static void main(String[] args) {
A a = new A();
}
}
class A {
public static int count = 0;
static {
count = 3;
}
public A() {
System.out.println("count = " + count);
}
}
输出结果是:
count = 3
为啥是3呢?内部是怎么给count赋值的?
带着疑惑1和疑惑2,开始今天的类加载分析;
类加载机制
还记得第一次接触java的时候,当时还不知道eclipse,编写完一个.java类后,在cmd终端用javac对类进行编译,然后使用java运行这个程序;
javac指令是将一个java文件编译成class文件,java指令是启动虚拟机,将class文件加载到内存,并对数据进行校验,解析,和初始化,最终形成虚拟机可以直接使用的java类型;
javac指令就不分析了,我们主要分析java这个指令;
类加载过程的划分:
类加载的过程从大的方面可以分3个部分,分别是加载,链接,初始化;
如果细分就是:加载,验证,准备,解析,初始化,使用,卸载;
其中验证,准备,解析属于链接的过程;
1.加载
通过类加载器,将class文件字节码加载到内存中,并将这些静态数据转化成方法区中的二进制的运行时数据结构,同时在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口;
点评:①在堆中生成的这个class对象指向方法区中的运行时数据结构,外部程序要想访问方法区中的运行时数据结构,只能通过这个class对象进行访问,所以,这个class,是方法区类数据的访问入口;比如反射,就是通过访问某个类的class对象,从而对该类的信息进行操作的;
②字节码的来源很丰富,比如硬盘上的class文件,也可以是网络上别人传递过来的字节码数据,也可以是jar包;
2.链接
将java类的二进制代码合并进JVM的运行状态之中的过程;
(1)验证
确保加载的信息符合JVM规范,没有安全方面的问题;
(2)准备
正式为类变量(static变量)分配内存并设置类变量的初始值,这些内存都将在方法区中进行分配;
点评:举个例子,代码中有static count = 3 ,此时设置类变量的初始值,即count = 0 ,这个3,是该类在初始化的时候被赋值给count的;
(3)解析
虚拟机常量池内的符号引用替换为直接引用的过程;
3.初始化
(1)初始化阶段是执行类构造器<clinit>()方法的过程,类构造器<clinit>()方法是由编辑器自动收集类中所有的类变量的赋值动作和静态语句块(static变量)中的语句合并产生的;
点评:①类构造器并不是类的构造函数,类构造器是由static变量的赋值动作和static块合并这个得到的;
也就是说,当我们new一个对象的时候,要先保证类的加载,链接和初始化已经完成,也就是先执行的类的构造器<clinit>()方法,自上而下的执行类中的static变量的赋值动作和static块代码,然后再去执行构造函数;
②类只会被初始化一次,也就是说,类构造器<clinit>()方法只会被调用一次,下次再使用A时,比如再次new该类的对象,就不会再执行类中的static变量的赋值动作和static块代码;
这样是不是就解决了疑惑2中的疑问呢?
(2)当初始化一个类的时候,如果发现它的父类还没有进行初始化,则需要先进行父类的初始化;
点评:①假如class A继承自class A_Father,当我们要使用A的时候,要先A进行加载,链接和初始化,在对A进行初始化之前,发现A的父类class A_Father还没有进行初始化,那就先进行class A_Father的初始化,也就是执行的class A_Father中的static变量的赋值动作和static块代码,class A_Father初始化结束之后,才会进行class A的初始化,再接下来,就可以调用A了;
注意:如果class A_Father还有父类,那就得继续向上初始化直到Object;
②上面已经说过,类的加载和初始化只有一次,一旦A,class A_Father都被初始化过了,下次再碰到他们的时候,是不会再执行类中的static变量的赋值动作和static块代码;
(3)虚拟机会保证一个类的构造器<clinit>()方法在多线程环境中被正确加锁和同步;
(4)当访问一个java类的静态域时,只有这个声明这个域的类才会被初始化;
上面分析完了类的初始化过程,现在有个疑惑,类在使用的时候,一定要被被初始化吗?如果不是,那在什么情况下类才需要被初始化呢?
1.类的主动引用(一定会发生初始化)
(1)new一个对象;
(2)调用类的静态成员(除了final常量)和静态方法;
(3)使用java.lang.reflect包的方法对类进行反射调用;
(4)当虚拟机启动,java Hello,则一定会初始化Hello类,说白了就是先启动main方法所在的类;
(5)当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类;
2.类的被动引用(不会发生类的初始化)
(1)当访问一个java类的静态域时,只有这个声明这个域的类才会被初始化;
(2)通过数组定义类引用,不会触发此类的初始化;
(3)引用常量不会触发此类的初始化(常量在编辑阶段就存入调用类的常量池中了)
现在再来回答下文章开头的疑惑1和疑惑2;
疑惑2:我们在讲解类的初始化时就已经知道它的答案;
当我们new一个对象的时候,要先保证类的加载,链接和初始化已经完成,也就是先执行的类的构造器<clinit>()方法,自上而下的执行类中的static变量的赋值动作和static块代码,然后再去执行构造函数;
疑惑1:Class cls = Class.forName("A");A a = (A)cls.newInstance()和A a = new A()有啥区别?
Class cls = Class.forName("A");A a = (A)cls.newInstance()是反射机制,而反射机制使用了类加载,所以疑惑1的问题也可以理解成类加载和new一个对象有啥区别?
1.首先要知道,使用new的时候,这个要new的类可以没有加载;但使用newInstance时候,就必须保证这个类已经加载并且已经被链接了。class的静态方法forName()方法完成了上面的加载和链接两个步骤。
2.从上面1中我们得知,newInstance实际上是把new这个方式分解为两步,即,首先调用class的加载方法加载某个类,然后实例化。这样分步有什么好处呢?
之前的Class cls = Class.forName("A");A a = (A)cls.newInstance()可以替换成下面的代码:
String className = readfromXMlConfig;//从xml配置文件中获得字符串
Class c = Class.forName(className);
AInterface factory = (AInterface)c.newInstance();
上面代码就消灭了A类名称,无论A类来自于字符串还是怎么变化,上述代码不变,甚至可以更换A的兄弟类B , C , D….等,只要他们继承Ainterface就可以。
这样,我们调用class的静态加载方法forName时获得更好的灵活性,提供给了我们降耦的手段。
注意,类加载机制和new还有以下区别:
newInstance只能调用无参构造,new能调用任何public构造;
newInstance()是通过这个类的默认构造函数构建了一个对象,如果没有默认构造函数或者没有访问默认构造函数的权限就会抛出异常;
常见题型:
1.下面程序的输出结果是什么?
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);
}
}
请类加载的流程来分析:
SingleTon singleTon = SingleTon.getInstance();
访问类的静态方法,需要初始化SingleTon类:
①加载:将SingleTon的class文件加载进内存;
②验证;
③准备:给SingleTon类中的static静态域赋默认值:
singleTon = null ;
count1 = 0 ;
count2 = 0 ;
④解析;
⑤初始化:给SingleTon类中的静态变量赋值和执行静态代码块;
先是SingleTon singleTon = new SingleTon():调用了类的构造方法后count = 1; count2 = 1 ;
继续为count1与count2赋值,此时count1没有赋值操作,所有count1为1,但是count2执行赋值操作就变为0;
所以,输出结果为:1, 0 ;
2.下面程序的输出结果是:
public class Base{
private String baseName = "base";
public Base(){
callName();
}
public void callName(){
System. out. println(baseName);
}
static class Sub extends Base{
private String baseName = "sub";
public void callName(){
System. out. println (baseName) ;
}
}
public static void main(String[] args){
Base b = new Sub();
}
}
类的初始化顺序是:
①执行父类的静态域和静态代码块;
Base无静态域和静态代码块;
②执行子类的静态域和静态代码块;
Sub无静态域和静态代码块;
③执行父类的非静态域和非静态代码块;
private String baseName = "base";
④执行父类的构造函数;
调用Base的callName()方法;
由于Sub重写覆盖了Base的callName(),所以执行Sub的callName()方法:System. out. println (baseName) ;
但此时Sub的baseName还没有赋值,所以,输出为null;
⑤执行子类的非静态域和非静态代码块;
private String baseName = "sub";
⑥执行子类的构造函数;
输出结果为:null。