类加载过程示例


本篇的目的,是通过具体代码解释jvm类加载的详细过程。再结合之前的理论说明,完全可以应对所有类加载的相关问题

类加载的整体说明,请参看 《JVM类加载总结》

1、准备试验用代码

父类:

public class ParentClass {

    public static final String PARENT_FINAL_P = "父类常量";
    public static String parentP = "父类成员变量";

    static{
        System.out.println("父类静态代码块");
    }

    {
        System.out.println("父类动态代码块");
    }

    public ParentClass() {
        System.out.println("父类构造方法");
    }

    public static void parentStaticMethod(){
        System.out.println("父类静态方法");
    }

    private String propertityA;

    public String getPropertityA() {
        return propertityA;
    }

    public void setPropertityA(String propertityA) {
        this.propertityA = propertityA;
    }
}

子类:

public class ChildClass extends ParentClass{

    public static final String CHILD_FINAL_P = "子类常量";
    public static String childP = "子类成员变量";

    static{
        System.out.println("子类静态代码块");
    }

    {
        System.out.println("子类动态代码块");
    }

    static{
        System.out.println("子类静态代码块2");
    }

    public ChildClass() {
        System.out.println("子类构造方法");
    }

    public static void childStaticMethod(){
        System.out.println("子类静态方法");
    }

    private String propertityB;

    public String getPropertityB() {
        return propertityB;
    }

    public void setPropertityB(String propertityB) {
        this.propertityB = propertityB;
    }

}

测试类:
(测试用的main()方法不能写在父类或者子类里面。原因:jvm会强制加载main()方法所在的类,这样会影响测试效果)

public class TestClass {

    static {
        System.out.println("测试类静态代码块");
    }

    {
        System.out.println("测试类动态代码块");
    }

    public static void main(String[] args) throws ClassNotFoundException {
        这里的内容下面分情况来写
    }

}
2、引用常量不会引发类加载
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(ChildClass.PARENT_FINAL_P);
    }
    
测试类静态代码块
父类常量    

可以看到,引用的父类的常量(final),也不会触发子类的任何加载行为(当然父类也没有被加载)

3、引用父类的成员变量或是静态方法会执行初始化
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(ChildClass.parentP);
    }
    
测试类静态代码块
父类静态代码块
父类成员变量    
    

子类引用父类的成员变量时,触发了父类的初始化(“父类静态代码块”被执行),但是子类本身没有被初始化

    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(ChildClass.childP);
    }
    
测试类静态代码块
父类静态代码块
子类静态代码块
子类静态代码块2
子类成员变量    

子类引用自身成员变量时,根据“虚拟机会保证首先执行父类的()方法”的理论,父类先被初始化(“父类静态代码块”),然后再初始化子类(“子类静态代码块1、2”)

另外,关于静态变量、静态代码块初始化的执行顺序,代码中先定义的先执行(变量、动态代码块同理)

4、使用new关键字实例化对象,会初始化类,并触发对象的一系列初始化动作
    public static void main(String[] args) throws ClassNotFoundException {
        ChildClass child = new ChildClass();
    }
    
    
测试类静态代码块
父类静态代码块
子类静态代码块
子类静态代码块2
父类动态代码块
父类构造方法
子类动态代码块
子类构造方法    

可以看到,与第3节相比,不但执行了类加载(clinit()方法调用静态代码),而且先创建了父类对象(父类动态代码块、父类构造方法),然后再创建的子类对象(子类动态代码块、子类构造方法 )

所以在一些文章中,给出了这样的对象初始化顺序:

静态变量/静态代码块(先父类后子类) > main方法 > 变量/动态代码块(先父类后子类) > 构造器(先父类后子类)

其实只要弄明白类的加载原理(包含一点最基础的java内存模型知识),这种顺序完全不用死记硬背

5、ClassLoader.loadClass
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        classLoader.loadClass("com.demo.ChildClass");
    }
    
测试类静态代码块    

loadClass什么也没有做……

因为这个public的loadClass,调用的是ClassLoader的

protected Class<?> loadClass(String name, boolean resolve)

方法,并且resolve固定传false,意思是只进行加载和链接,不执行初始化过程。

6、Class.forName
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("com.demo.ChildClass");
    }
    
测试类静态代码块
父类静态代码块
子类静态代码块
子类静态代码块2    
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        Class.forName("com.demo.ChildClass", false, classLoader);
    }
测试类静态代码块    

Class.forName的情况与ClassLoader.loadClass不同,有两个重载方法。

只指定一个参数时,调用的是一个native方法

forName0(className, true, ClassLoader.getClassLoader(caller), caller);

其中第二个参数true,指定了要对类进行初始化;

而使用重载的三个参数的方法时,其方法定义是这样的

public static Class<?> forName(String name, boolean initialize,ClassLoader loader)

第二个参数指定是否初始化,然后调用了同样的native方法,将boolean变量传递了进去,传false的话就不执行初始化过程了

7、类的加载过程只执行一次
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(ChildClass.CHILD_FINAL_P);
        System.out.println(">>><<<");
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        classLoader.loadClass("com.demo.ChildClass");
        System.out.println(">>><<<");
        Class.forName("com.demo.ChildClass");
        System.out.println(">>><<<");
        ChildClass child = new ChildClass();
    }
    
测试类静态代码块
子类常量
>>><<<
>>><<<
父类静态代码块
子类静态代码块
子类静态代码块2
>>><<<
父类动态代码块
父类构造方法
子类动态代码块
子类构造方法 

最开始只引用了一下子类成员常量,因为常量在方法区的常量池里面,没有触发类的任何动作;

第二步 classLoader.loadClass 只做了类的加载和链接,不进行初始化,并没有任何输出;

第三步 Class.forName ,前面已经完成了加载和链接,所以跳过(输出中无法清晰显示出这个结论),只做了类的初始化;

第四步进行对象的创建和初始化,由于类的加载全过程已经完成,所以new关键字并没有触发各种静态资源的初始化(因为已经完事了)

简单总结一下
  • 编译时把常量都放进常量池,引用时不会涉及到类

  • 类加载时,除了非静态成员变量不会被加载,其它的都会被加载(包括非静态方法——这一结论无法用示例来说明),加载完毕初始化

  • 创建实例时,先执行动态方法块,再执行构造方法

  • 先父类后子类

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值