【温故知新-Java虚拟机篇】4.类加载机制

该系列博客暂且定义为《深入理Java解虚拟机》的笔记,有些坑等后续看完书再填,有不对的地方多指教。

千辛万苦,把前三部分枯燥的内容结束了,终于迎来了类加载机制。


1.类加载的时机

类从被加载到虚拟机内存,到卸载出内存,这个声明周期包含以下几个阶段:


1 )其中加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,解析阶段为了支持Java语言的动态绑定,可能会在初始化后开始。

2)对于类的加载时间,Java虚拟机规范没有强制约束,但对于“初始化”阶段有且只有以下5种情况进行,而类的加载、验证、准备自然也在此前完成:

a.遇到new、getstatic、putstatic和invokestatic这四条字节码指令的时候,如果类没有进行过初始化,则需要先出发初始化。即通过new关键字实例化对象的时候、读取或者设置一个静态字段的时候,以及调用一个类的静态方法的时候。

1.静态字段只有直接定义的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会出发子类初始化。
2.对象数组实例化,不会触发类的初始化,而会出发对象数组类的初始化。
3.类中常量的获取,不会触发初始化,因为编译的时候常量已经存入常量池中。

package com.adiaixin.classloader;
//父类
public class SuperClass {
    static{
        System.out.println("Super Class init!");
    }

    public static void staticInvoke(){
        System.out.println("invoke static method!");
    }

    public static int value = 123;
}

package com.adiaixin.classloader;
//子类
public class SubClass extends SuperClass {
    static {
        System.out.println("Sub Class init!");
    }
}
package com.adiaixin.classloader;
//常量类
public class ConstClass {
    static {
        System.out.println("Const Class init !");
    }

    public static final String HELLO_JVM = "hello jvm !";
}
package test;

import com.adiaixin.classloader.ConstClass;
import com.adiaixin.classloader.SubClass;
import com.adiaixin.classloader.SuperClass;

public class ClassLoadTimeTest {
    public static void main(String[] arr) {

        /**特例1:静态字段设置和调用、静态方法调用,只会直接定义的类才初始化*/
        System.out.println(SubClass.value);
        // 1.获取父类静态字段值,只初始化父类

        //SubClass.value = 234;
        // 2.设置父类静态字段值,只初始化父类

        //SubClass.staticInvoke();
        // 3.调用父类静态方法,只初始化父类

        //SubClass subClass = new SubClass();
        // 4.子类实例化,初始化父类子类


        /**
         * 特例2:通过数组定义来引用类,不会出发次类的初始化,会出发一个对象数组类的初始化。
         * */
        SuperClass[] scArr = new SuperClass[10];
        System.out.println(SuperClass.class);
        //class com.adiaixin.classloader.SuperClass

        System.out.println(scArr.getClass());
        //[Lcom.adiaixin.classloader.SuperClass   [表示数组,L表示对象


        /**特例3:常量(static final)在编译的时候会存入常量池中,本质上并没有引用到定义常量的类,因此不会触发类的初始化*/
        System.out.println(ConstClass.HELLO_JVM);
    }
}

b.使用java.lang.reflect包的方法对类进行反射调用的时候。
c.当初始化一个类,发现父类没有被初始化,则需要先出发父类的初始化。
d.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
e.当使用JDK1.7的动态语言支持,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先初始化(MethodHandle还不熟悉,待填坑)。


2.类的加载过程

1)加载

a.使用javac(compile)编译过后,会生成.class结尾的字节码文件,类加载的过程实际是将字节码文件“加载到”内存中方法区的过程:

1.通过一个类的全限定名(即包含包名的全路径类名如com.adiaixin.classload.Test)来获取定义此类的二进制字节流
2.将字节流代表的静态存储结构转化为方法区的运行时数据结构。

3.在内存的方法区内生成一个代表这个类的java.lang.Class对象(Hot Spot)作为方法区这个类的各种数据访问入口。

b.通过全限定名获取二进制字节流,来源可以有:ZIP包如jar,war等、网络中获取、运行时计算生成、文件读取、数据库读取等方式。开发人员可以通过重写loadClass()方法来控制节流的获取方式。

c.数组类本身不通过类加载器创建,它是Java虚拟机直接创建的,遵循以下原则:

1.如果数组的组件类型为引用类型(SuperClass[]),就递归得去加载(未初始化)这个组件类,这个组件类将在类加载器的命名空间被标记(不同的加载器,加载同一个类,两个类用equals()是不想等的,后面会将)。
2.如果数组的组件类型不是引用类型(int[]),Java虚拟机将会把数组类标记为与引导类加载器关联。

3.数组类的可见性与它的组件类可见性一致,如果组建类不是引用类型,那么可见性默认为public。

d.加载阶段与连接阶段的部分内容(字节码文件格式校验)是交叉进行的,即有一部分加载完成后直接校验,与加载其他字节码并行。

2)验证

验证时连接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。主要包含:
a.文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前虚拟机处理,以下部分验证:
1.是否以魔术0xCAFFEBABE开头。
2.主、次版本号是否在当前虚拟机处理范围内。
3.常量池中的常量中是否有不被支持的常量类型(检查常量tag标志,是否在)
4.指向常量的各种索引值中是否有指向不存在的常量或者不符合类型的常量
5.CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
6.Class文件中各个部分及文件本体是否有被删除的或附加的其他信息

b.元数据验证:对于字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范要求,部分验证:

1.这个类是否有父类(除了java.lang.Object外,所有类都应当有父类)
2.这个类的父类是否继承了不允许被继承的类(被final修饰的类)
3.如果这个类不是抽象类,是否实现了父类或接口中需要实现的所有方法。

4.类中的字段、方法是否与父类产生矛盾(如覆盖父类的final字段、方法参数一直,返回类型不同等)

c.字节码验证:通过对方法的数据流和控制流分析,确定程序语义是否合法、符合逻辑,部分如下:

1.保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。

2.保证跳转指令不会跳转到方法体以外的字节码指令上。

3.保证方法中的类型转换时有效的。

d.符号引用验证:发生在符号引用转换为直接引用的时候,即在解析阶段发生,可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,即类中的引用校验,通常如下:

1.符号引用中通过字符串描述的全限定名是否能找到对应的类。

2.在指定类中是否存在符合方法的字段描述符以及简单名所描述的方法和字段。

3.符号引用中的类、字段、方法的访问性是否可被当前类访问。

3)准备

准备阶段是正式为类变量(static修饰的变量)分配内存并设置变量初始值的阶段。
a.这些类变量使用的内存都在方法区中分配,实例变量随对象分配到Java堆中。
b.设置变量初始值是指数据类型的零值,如public static int value=123,经过准备阶段,value赋值为0,初始化后为123

4)解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

a.符号引用(Symbolic References):

1.符号引用以一组符号来描述引用的目标,符号可以是任何形式的字面量,只要能无歧义定位到目标即可。

2.符号引用与虚拟机内存布局无关,引用的目标并不一定已经加载到内存中。

3.各种虚拟机实现的内存布局可以不同,但是他们能接受的符号引用必须一致。

b.直接引用(Direct Refefrences):

1.直接引用是指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

2.它与内存布局相关,同一个符号引用在不同虚拟机上翻译出来的直接引用一般不同。

3.如果直接引用存在,那么引用的目标必定已经存在内存中。

c.虚拟机规范未规定解析阶段发生具体时间,即虚拟机可以根据需要来判断到底在类加载时解析,还是引用被使用前解析

d.虚拟机规范要求在以下16个指令前,必须要执行符号引用解析:anewarray、multianewarray、checkcast、getfield、pufield、getstatic、putstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、ldc、ldc_w、new。

e.解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

1.类或接口的解析:假设类D要解析一个符号引用N,N的类型是一个类或者接口C,那么解析步骤如下:

1)如果C不是一个数组,那么虚拟机将会把代表N的全限定名传递给D的类加载器去加载C。

2)如果C是数组类型,并且数组的元素类型为对象(TestClass[]),那么按照1)加载元素类型(只加载,链接、不初始化)。

3)前两步没有异常,那么将检查D是否具备对C的访问权限。

2.字段解析:首先将字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是按照1流程解析类或者接口。我们用C表示解析成功的类或接口,按照以下步骤对C进行后续字段的搜索:

1)如果C类本身包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用。

2)否则,如果C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口查找引用。

3)否则,如果C不是java.lang.Object,按照继承关系从下往上递归搜索其父类查找引用。

4)否则,查找失败,抛出java.lang.NoSuchFieldError异常。

3.类方法解析:与字段解析相同,先解析出类方法表中的class_index项中索引的方法所属类或接口的引用,然后:

1)类和接口方法符号引用的常量类型定义是分开的(CONSTANT_Methodref_info与CONSTANT_InterfaceMethodref_info),如果发现类方法中class_index的索引C是个接口,抛异常。

2)否则,在类C中查找是否有简单名和描述符都与目标匹配的方法,如果有则直接返回方法的直接引用。

3)否则,在C的父类中递归查找,如果存在返回。

4)否则,在C实现的接口列表以及它们的父接口中递归查找,如果存在匹配方法,说明类C是一个抽象类,这是结束查找,抛出java.lang.AbstaractMethodError异常。

5)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。

4.接口方法解析:解析接口方法表中的class_index项中的类或接口符号引用,然后按照以下顺序查找:

1)如果class_index中的索引C是个类,那么抛出java.lang.IncompatibleClassError异常。

2)否则,在接口C中查找是否有简单名和描述符都与目标匹配的方法,如果有则直接返回方法的直接引用。

3)否则,在接口C的父接口中查找,如果存在则返回。

4)否则,宣告失败,抛出java.lang.NoSuchMethodError。


5)初始化

类初始化是类加载过程的最后一步,前面的过程中,除了加载阶段,用户可以自定义类加载器,其余动作都为虚拟机主导和控制。到了初始化阶段,才真正开始执行勒种定义的Java程序代码(或者说是字节码)。初始化阶段是执行类构造器<clinit>()方法的过程,虚拟机会隐式调用这个方法。


a.<clinit>()方法说明:

1.<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(satic{})中语句合并产生的。


2.虚拟机收集顺序是由语句在源文件中出现的顺序决定的。


3.静态语句块只能访问定义在其之前的类变量,对于定义在其之后的类变量,可以赋值,不能访问。



4.与实例构造器(<init>())方法不同,类构造器不显式调用其父类构造器,虚拟机会保证在执行子类构造器之前,父类的构造器方法已经执行完毕。因此虚拟机中第一个被执行<clinit>()方法的肯定是java.lang.Object


5.父类的<clinit>()方法先执行,那么父类中的静态语句块要优先于子类的变量赋值操作

public class ClassInit {

    static class Parent {
        public static int A = 1;
        static {
            A = 2;
            System.out.println("Parent <clinit>() process ");
        }
    }
    
    static class Sub extends Parent {
        public static int B = A;
    }

    public static void main(String[] arr) {
        System.out.println(Sub.B);
        //结果为:2
    }
}

6.如果类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。


7.与类构造器不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法只有当父接口中定义的变量使用时,才会初始化接口的实现类在初始化时,也一样不会执行接口中的<clinit>()方法


8.虚拟机保证一个类的<clinit>()方法在多线程环境中被正确加锁、同步,如果有多个线程去初始化一个类,那么只有一个线程能执行,其他线程阻塞,知道活动线程执行<clinit>()方法完毕。

static class DeadLoopClass {
        static {
            if (true) {
                System.out.println(Thread.currentThread() + " 初始化 DeadLoopClass");
                for (int i=0; i<5; ++i) {
                    try {
                        System.out.println(Thread.currentThread() + " 睡眠 " + i + " 秒");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] arr) {
       Runnable initThread = new Runnable() {
           public void run() {
               System.out.println(Thread.currentThread() + " 线程开始");
               DeadLoopClass dlc = new DeadLoopClass();
               System.out.println(Thread.currentThread() + " 线程结束");
           }

       };
        Thread thread1 = new Thread(initThread);
        Thread thread2 = new Thread(initThread);
        thread1.start();
        thread2.start();

       /*
        线程toString格式:Thread[线程名,优先级,线程组名称]

        Thread[Thread-0,5,main] 线程开始
        Thread[Thread-1,5,main] 线程开始
        Thread[Thread-0,5,main] 初始化 DeadLoopClass
        Thread[Thread-0,5,main] 睡眠 0 秒
        Thread[Thread-0,5,main] 睡眠 1 秒
        Thread[Thread-0,5,main] 睡眠 2 秒
        Thread[Thread-0,5,main] 睡眠 3 秒
        Thread[Thread-0,5,main] 睡眠 4 秒
        Thread[Thread-0,5,main] 线程结束
        Thread[Thread-1,5,main] 线程结束*/
    }


3.类加载器

1)介绍

每个类加载器都拥有一个独立的名称空间,比较两个类是否“相等”,在同一个类加载器下才有意义,否则同一个类被两个不同的类加载器加载,它们必然不相等,包括equals()、isAssignableFrom()、inInstance()。

package com.adiaixin.java;

import java.io.IOException;
import java.io.InputStream;

public class CustomClassLoaderTest {

    static class CustomClassLoader extends ClassLoader {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            try {
                String classFileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                if ("java.lang.Integer".equals(name)) {
                    name = "java.lang.Integer";
                }
                InputStream is = getClass().getResourceAsStream(classFileName);
                if (null == is) {
                    return super.loadClass(name);
                }
                byte[] b = new byte[is.available()];
                is.read(b);
                return defineClass(name, b, 0, b.length);
            } catch (IOException e) {
                throw new ClassNotFoundException(name);
            }
        }
    }

    public static void main(String[] arr) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        CustomClassLoader myLoader = new CustomClassLoader();
        Class<CustomClassLoaderTest> clazz = (Class<CustomClassLoaderTest>)myLoader.loadClass("com.adiaixin.java.CustomClassLoaderTest");//注意包名换成自己的

        System.out.println(clazz.getName());
        //com.adiaixin.java.CustomClassLoaderTest

        System.out.println(CustomClassLoaderTest.class.getName());
        //com.adiaixin.java.CustomClassLoaderTest

        System.out.println(clazz.newInstance() instanceof CustomClassLoaderTest);
        //false

        System.out.println(clazz.isAssignableFrom(CustomClassLoaderTest.class));//clazz如果是CustomClassLoaderTest.class相同或者是其父类返回true
        //false

        System.out.println(clazz.equals(CustomClassLoaderTest.class));
        //false

        System.out.println(clazz.getClassLoader().getClass().getName());
        //自定义类加载器:com.adiaixin.java.CustomClassLoaderTest$CustomClassLoader

        System.out.println(CustomClassLoaderTest.class.getClassLoader().getClass().getName());
        //虚拟机默认类加载器:sun.misc.Launcher$AppClassLoader


        System.out.println(myLoader.loadClass("java.lang.Object").equals(Object.class));
        //true  原因见下面双亲委派模型
        System.out.println(myLoader.loadClass("java.lang.Integer").equals(Integer.class));
        //true  原因见下面双亲委派模型

    }
}



2)类加载器分类:

a.启动类加载器(Bootstrap ClassLoader):这个类加载器负责将放在<JAVA_HOME>\lib下目录中的,或者被-Xbootclasspath参数所指定的路径中,并且是虚拟机识别的类库加载到虚拟机内存中。


b.扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录下中的,或者被java.etx.dirs系统变量所指定的路径中所有类库。


c.应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。一般称为系统类加载器,它负责加载用户路径(ClassPath)上所指定的类库。


3)双亲委派模型(Parents Delegation Model):


a.除了顶层的启动类加载器外,其余加载器都应当有自己的父类加载器。


b.工作过程:如果一个类加载器收到了加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的请求最终都应该传送到启动类加载器中,只有当父类反馈无法加载时(它的搜索返回中没有找到所需的类),子加载器才会尝试自己去加载。


c.因为所有的加载请求都会传送到启动类加载器,所以在<JAVA_HOME>\lib目录下定义的类在各种类加载器的环境中都是同一个类,例如java.lang.Object,Integer等等。







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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值