美文网首页
类加载过程示例

类加载过程示例

作者: 白花蛇草可乐 | 来源:发表于2019-09-28 16:15 被阅读0次

    本篇的目的,是通过具体代码解释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
    子类成员变量    
    

    子类引用自身成员变量时,根据“虚拟机会保证首先执行父类的<clinit>()方法”的理论,父类先被初始化(“父类静态代码块”),然后再初始化子类(“子类静态代码块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关键字并没有触发各种静态资源的初始化(因为已经完事了)

    简单总结一下

    • 编译时把常量都放进常量池,引用时不会涉及到类

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

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

    • 先父类后子类

    相关文章

      网友评论

          本文标题:类加载过程示例

          本文链接:https://www.haomeiwen.com/subject/vguayctx.html