聊聊Java的类加载

网上介绍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:初始化

常规情况:

  • 首先初始化类构造器,编译器自动收集类中的静态变量,静态代码块合并产生类构造器,顺序按照代码中出现的顺序
  • 然后初始化实例构造器,编译器自动收集类中 实例变量赋值动作,实例代码块,构造函数,合并成实例构造器。

实力初始化并不一定在类构造器之后执行,有可能类初始化包含实例初始化,这句话非常重要,好好体会,便能明白上面的题的原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章