网上介绍Java类加载的文章不计其数,但大多都千篇一律。之前有打算写一下类加载,一直感觉自己理解不是很透彻,现在感觉可以出锅了,哦,不对,可以出徒了,也不对,可以写博了。废话不多说,上干货。
不加例子的解释都是耍流氓,先来个简单的例子:
public class TestStatic {
private static TestStatic tester = new TestStatic();
private static int count1;
private static int count2 = 2;
public TestStatic() {
count1++;
count2++;
System.out.println("" + count1 + "\t" + count2);
}
static {
System.out.println("静态代码块执行:" + count1);
}
{
System.out.println("构造代码块执行");
}
public static TestStatic getTester() {
System.out.println(count1 + "\t" + count2);
return tester;
}
public static void main(String[] args) {
System.out.println(TestStatic.getTester());
}
}
这段代码的执行结果是:
构造代码块执行
1 1
静态代码块执行:1
1 2
如果答对的同学,可以下树了,答错的同学,听我娓娓道来。
- 首先经过类加载器加载,
TestStatic
对象的二进制字节流已进入内存。 TestStatic
被jvm标记为启动类了(包含main函数),触发类加载, 在链接阶段的准备阶段,变量tester
的初始值为null,count1
为0,count2
也为0,- 链接准备阶段的赋值操作完成,触发类加载的初始化阶段。首先触发类构造器执行,执行到
TestStatic tester = new TestStatic();
,实例构造器开始执行。故构造代码块首先执行,打印出:构造代码块执行。然后执行构造函数,count1和count2此时都是链接准备阶段的赋值结果0,然后自增,故打印出1,1。tester赋值完毕,然后继续执行,count1没有重新赋值,还是1,count2被重新赋值,变成了2。然后执行静态代码块,故打印出静态代码块执行:1。至此,类加载完成。 - 最后调用类的静态方法,会触发类加载,因为此时
TestStatic
已经被jvm加载过,故不在重新加载,直接调用方法getTester
,打印出 1 2
何时触发类的加载
主动引用的类会触发类加载机制,被动引用则不会触发。
主动引用
- 调用类的静态属性
- 调用类的静态方法
- 使用反射加载类
Class.forName("com.demo.Student")
- 使用
new
关键字创建对象。 - 被jvm标记为启动类
main
函数所在的类。 - 初始化子类,若父类未被初始化,则首先初始化父类。
被动引用
- 调用父类的静态变量,不会触发子类加载,只会加载父类
- 通过一个类创建数组引用,
Student[] students = new Student[]{10};
- 调用类的常量
类是怎样被加载
.java源文件被编译成.class文件,通过Java的类加载器,把.class文件加载到jvm的方法区中,在堆中生成代表次类的Class对象,用来封装方法区的数据结构。一个类无论产生多少对象,其Class对象只有这一个。
类加载器
- 系统类加载器(
Bootstrap ClassLoader
)
Java最底层的类加载器,负责加载$JAVA_HOME/jre/lib/rt.jar
中的class,String
,Object
,Integer
等就是由该类加载器加载,由C++实现。 - 扩展类加载器(
Extension ClassLoade
)
加载Java平台扩展的jar包,负责加载$JAVA_HOME/jre/lib/ext/*.jar
中的class,或者-Djava.etx.dirs
指定jar包中的类。 - 应用类加载器(
App Classloader
)
最常用的类加载器,classpath
中的类及目录中的class。 - 自定义类加载器
根据需要,用户自定义的类加载器,按照jvm规范。tomcat,jboss都根据规范实现了自己的classloader
双亲委派模型
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
双亲委派模型保证了Java的安全性,试想,如果你自定义了一个java.lang.Object,通过双亲委派模型,最终会让系统类加载器加载,你自定义的类加载器不会被加载。保证了Object的安全性。另外有些j2ee服务器需要打破这种双亲委派模型。
类加载的三个步骤
1:加载
查找并加载类的二进制数据、将硬盘上的.class文件加载进内存中。
2:链接
链接又分为三个步骤:
- 验证
确保被加载类的正确性,也就是javac编译的class文件的正确性 - 准备(重要)
为类的静态属性赋初始值,int 初始值为0,boolean初始值为false,引用类型初始值为null。特别注意:如果类变量被final
修饰,此时直接赋值为程序猿声明的值。 - 解析
将类的符号引用转化为直接引用,也就是将对象的引用转化为指针。
3:初始化
常规情况:
- 首先初始化类构造器,编译器自动收集类中的静态变量,静态代码块合并产生类构造器,顺序按照代码中出现的顺序
- 然后初始化实例构造器,编译器自动收集类中 实例变量赋值动作,实例代码块,构造函数,合并成实例构造器。
实力初始化并不一定在类构造器之后执行,有可能类初始化包含实例初始化,这句话非常重要,好好体会,便能明白上面的题的原理