java类加载机制

最近开通了一个订阅号 gexiaolong 在其中记录一些关于java总是记了又忘,忘了又记的一些知识点,所以还是写一篇日志记录一下吧

老规矩,抄作业,关于java的类加载机制的问题也是看了忘,忘了又在看

在此梳理记录一下

说说目前我对java类加载机制的肤浅理解:

就是将java类编译之后对应的的class(字节码)文件加载进入jvm虚拟机并创建该类的Class对象的过程就叫做类的加载吧

具体细节步骤有哪些?
目前能想到的就是,1.找到对应的class文件,解析并校验文件内容是否符合
2.校验通过之后load进jvm虚拟机内存中为当前的java类创建一个相应的Class对象并初始化

​以下查资料,博客,文章等 详述类加载的各个细节步骤:

在此之前先看一下java程序的执行图序
java类加载机制_第1张图片

JVM内存:
java类加载机制_第2张图片

一、类加载概念

java虚拟机将Class字节码文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型

.class文件有类装载器装载后,在jvm中将形成一份描述Class结构的元信息对象,通过该类的Class对象可以获取该类的结构信息,如构造函数,属性和方法等,java允许通过该类的Class对象的元信息间接调用Class对象的功能

二、类加载过程

工作机制
类装载器寻找字节码文件,并构造出类在jvm内部表示的对象组件
类装载器把一个类装入jvm中,要经过

(1)装载loading:查找和导入相应的class文件
(2)链接linking:把类的二进制数据合并到JRE中
2.1 校验-verification 检查载入class文件数据的正确性
2.2 准备-preparation 给类的静态变量分配存储空间 赋默认值
2.3 解析-resolution 将符号引用转成直接引用
(3)初始化initializing:对类的静态变量,静态代码块执行初始化操作 赋初始值

加载.class文件的方式有:
1.本地系统中直接加载
2.通过网络下载.class文件
3.从zip,jar等归档文件中加载.class文件
4.从专有数据库中提取.class文件
5.将java源文件动态编译为.class文件

2.1装载
jvm在类的加载阶段需要完成以下三件事情:
1.通过一个类的全限定名称来获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

类的加载可以由系统提供的类加载器完成,也可以由用户自定义的类加载器完成(可在自定义的类加载器中控制字节流的获取方式)
加载完成,虚拟机外部的字节流按照虚拟机所需的格式存储在方法区之中,且在java堆中创建一个java.lang.Class类的对象,通过该对象访问方法区中的数据

2.2链接阶段

验证–>准备–>解析–>初始化
验证:
1.文件格式验证 CA FE BA BE
2.元数据验证
3.字节码验证
4.符号引用验证

准备: 为类变量分配内存并设置类变量的初始值【零值】
1.只包含static 修饰的类变量,会分配在方法区
2.不包含使用final修饰的static,因为final在编译的时候就会分配了,

解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用(Symbolic Reference) : 以一组符号来描述所引用的目标,
直接引用(Direct Reference): 可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用与虚拟机实现的内存布局相关,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不相同,若存在了直接引用,则引用的目标必定已经在内存中存在的

1). 类或者接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析
2). 字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,若果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束
3). 类方法解析: 对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,且对类方法的匹配搜索,是先搜索父类,再搜索接口
4). 接口方法解析:与类方法解析步骤类似,只是接口不会有父类,因此只递归向上搜索父接口就行

解析通常主要针对类、接口、字段、类方法、接口方法、方法类型等。对应常量池中的
CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info

2.3初始化
初始化阶段就是开始执行类中定义的java程序代码
执行类构造器方法()
初始化为类的静态变量赋正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化,在java中对类变量进行初始化的两种方式:

1.声明变量时指定初始值
2.使用静态代码块为类变量指定初始值

三、JVM初始化步骤:

1.若该类还没有被加载和连接,则优先加载并连接该类
2.若该类的直接父类还没有被初始化,则先初始化其直接父类
3.若该类中有初始化语句,则系统依次执行这些初始化语句

初始化阶段执行类构造器方法() 的过程

1.类构造器方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块) 中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序所决定

2.类构造器方法与类的构造函数不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的类构造器方法执行之前,父类的类构造器方法已经执行完毕,因此在虚拟机中第一个执行的类构造器方法的类是java.lang.Object

3.由于父类的类构造器方法先执行,所以父类中定义的静态语句块要优先于子类的变量赋值操作

4.类构造器方法对于类或者接口 不是必须的,若一个类中没有静态语句块 也没有对变量的赋值操作,那么编译器可以补位该类生成类构造器方法

5.接口中可能会有变量的赋值操作,因此接口也会生成类构造器方法,但是接口与类不同,执行接口的类构造器方法不需要先执行父接口的类构造器方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另:接口实现类在初始化时也不会执行接口的类构造器方法

6.虚拟机会保证一个类的类构造器方法在多线程环境中被正确地加锁和同步。若有多个线程同时初始化一个类,那么只会有一个线程去执行这个类的类构造器方法,其他线程都需要阻塞等待,直到活动线程执行类构造器方法完毕。
? 思考,如果 这个线程已经执行完该类构造器方法构建类完成,还需要其他线程再调用该类构造器方法吗?

类的初始化
虚拟机规范并未强行约束,初始化具体细节交由虚拟机的具体实现自由把握。但是对于初始化阶段虚拟机严格规范了以下的几种情况,如果类未初始化 则会对类进行初始化

  • 1.创建类的实例
    2.访问类的静态变量(除常量[被final修饰的静态变量]) 原因:常量是一种特殊的变量,在编译器把他们当做值(value) 【直接初始化了变量名和对应的值在字节码中】 而不是域(field)来对待。 若代码中用到了常变量(constant
    variable),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种有用的优化,但是如果需要改变final域的值,那么每一块用到这个域的代码都需要重新编译

    3.访问类的静态方法
    4.反射如(Class.forName(“my.gexl.Test”))
    5.当初始化一个类时,发现其父类还未初始化,则先触发父类的初始化
    6.虚拟机启动时,定义了main()方法的那个类先初始化

以上为类的“主动引用”,均会触发类的初始化,还有一些“被动引用”的情况如:接口的加载过程和类的加载过程中,接口中不能使用static{}块,当一个接口初始化时,并不要求其父接口全部都完成初始化,只有在真正使用到父接口时(引用接口中定义的常量)才会初始化。

  • 被动引用如下:
    1.子类调用父类的静态变量,子类不会被初始化。只有父类被初始化,对于静态字段,只有直接定义这个字段的类才会被初始化
    2.通过数组定义来引用类,不会触发类的初始化
    3.访问类的常量[被final修饰的类的常量],不会初始化类
    若非被final修饰的类的常量,使用时还是会触发类的初始化
    以下为测试代码:
public class ClassLoadTest {

    public static void main(String[] args) {
     	ConstClass [] subs = new ConstClass[10];
        System.out.println(ConstClass.v);
        System.out.println(ConstClass.HELLOWORLD);
    	// 输出结果为:
    	// 21
		// ConstClass init
		// helloworld
    }
}
class ConstClass {

    public static final int v = 21;
    static {
        System.out.println("ConstClass init");
    }
    public static String HELLOWORLD = "helloworld";
}

*类的初始化顺序 * [感觉这块的表达太书面化,写的有点晦涩,跟上面初始化的主动引用和被动引用所触发的初始化,表达的意思结果差不多,运行上面的代码便可理解的事情,个人建议此处可略过,感兴趣的可以读一读]

在java语言规范中, 域(fields,静态的或是非静态的)、块(block静态的或是非静态的)、不同类(子类和超类) 和不同的接口(子接口,实现类和超接口)的初始化顺序也很重要。
需要遵循如下规则

  1. 类从顶至底的顺序初始化,所以声明在顶部的字段要早于底部的字段初始化
  2. 超类早于子类和衍生类的初始化
  3. 如果类的初始化是由于访问静态域而触发,那么只有声明静态域的类才被初始化,而不会触发超类或者子类的初始化
  4. 初始化即使静态域被子类或子接口或者它的实现类所引用
  5. 接口初始化不会导致父接口的初始化
    6.静态域的初始化是在类的静态初始化期间,非静态域的初始化是在类的实例创建期间,静态域的初始化在非静态域之前
    7.非静态域通过构造函数初始化,子类在做任何初始化之前构造器会隐含地调用父类的构造器,它保证了非静态域或实例变量(父类) 初始化早于子类

四、类加载器

由同一个类加载器加载 且来源于同一个Class文件的两个类才是相等的

双亲委派模型
jvm虚拟机角度来说,一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分;另外一种是由其他类加载器,是由java语言实现的,独立于jvm外部,并且全部继承自抽象类java.lang.ClassLoader

BootStrap ClassLoader 由C++语言实现 启动 加载 /lib/rt.jar
Extension ClassLoader 扩展类加载器 jre/lib/ext.jar
Application ClassLoader 应用类加载器 加载classpath:
Custom ClassLoader 自定义类加载器

  public class Test {
  		public static void main(String[] args) {
			// Test的类加载器
        	System.out.println(Test.class.getClassLoader());
			// Test的类加载器的父 类加载器
        	System.out.println(Test.class.getClassLoader().getParent().getClass());
			// Test的类加载器的父类加载器的 加载器
        	System.out.println(Test.class.getClassLoader().getParent().getClass().getClassLoader());
			// Test的类加载器的父 类加载器的父 类加载器
        	System.out.println(Test.class.getClassLoader().getParent().getParent());
    }
输出结果为:
sun.misc.Launcher$AppClassLoader@644d46    --> 对应 AppClassLoader 
sun.misc.Launcher$ExtClassLoader@ed1f14   ---> 对应 ExtClassLoader
null									 ---> 对应 BootStrapClassLoader
null									 ---> 对应 BootStrapClassLoader

双亲委派类加载模型工作过程
关于双亲委派的说明:[基于安全因素的考虑 比如再写一个名字为java.lang.String的类]
该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器(parent属性)。子类加载器和父类加载器不是以继承关系来实现的,而是通过组合(Composition)关系来复用父加载器的代码

双亲委派类加载机制:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载器请求都会传给顶层的启动类加载器,只有当父类加载器反馈自己无法完成该加载请求(父类加载器未找到对应的类),子加载器才会尝试自己去加载

在java源码中基于类加载的具体实现如下
rt.jar 包中的java.lang.ClassLoader类中

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 查找加载 class文件
            Class<?> c = findLoadedClass(name);
            // 
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 优先由 父加载器加载
                        c = parent.loadClass(name, false);
                    } else {
                    	// parent == null 时由 BootstrapClassLoader 加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    // 若父类加载器 未加载成功 则由自身查找加载
                    
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

由代码看出,双亲委派模型通过locadClass() 方法来实现,
先检查是否已经被加载过,如果没有则调用父加载器的loadClass() 方法,如果父类加载器为空则默认使用启动类加载器加载,如果父类加载器加载失败,则抛出ClassNotFoundException,然后调用自己的findClass()方法进行加载

自定义类加载器
若要实现自定义的类加载器,需要继承自 java.lang.ClassLoader 类,并且重写其中的findClass() 方法即可。
java.lang.ClassLoader类的基本职责就是根据一个指定的类名称,找到或者生成与其对应的字节代码,然后从这些字节代码中定义一个Java类,即java.lang.Class类【对应该java】的一个Class实例。除此之外ClassLoader 还负责加载Java应用所需要的资源,如图像文件和配置文件等,ClassLoader中与加载类相关的方法如下:
方法说明
getParent() 返回该类加载器的父 类加载器

loadClass(String name) 加载名称为二进制名称为name的类 返回的结果是java.lang.Class类的实例

findClass(String name) 查找名称为name的类,返回的结果为java.lang.Class类的实例

findLoadedClass(String name) 查找名称为name的已经加载过的类,返回结果是java.lang.Class类的实例

resolveClass(Class c) 连接指定的Java类

自jdk1.2 之后双亲委派模型已经被引入到类加载体系中,自定义的类加载器只需要重写findClass() 方法即可

示例代码:
java类加载机制_第3张图片

public class ClassLoaderTest {

    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("aaaa");
		Class cl = myClassLoader.loadClass("com.gexl.bean.User2");
        System.out.println(myClassLoader.getClass().getClassLoader());
        System.out.println(myClassLoader.getParent());
        Object user2 = cl.newInstance();
        System.out.println("user2=="+user2);
        System.out.println(user2.getClass().getClassLoader());
        // 调用 User2 中的 say方法 输出
        cl.getMethod("say",null).invoke(user2,null);
    }
		// 输出结果为:
		进入自定义的类加载器name=com.gexl.bean.User2
		defineClassName==com.gexl.bean.User2
		sun.misc.Launcher$AppClassLoader@644d46
		sun.misc.Launcher$AppClassLoader@644d46
		user2==com.gexl.bean.User2@19d7047
		MyClassLoader@29724f
		Hello User2..
		---------------
}

class Animal {
    public void say() {
        System.out.println("Hello world");
    }
}

class MyClassLoader extends ClassLoader{

     // 类加载器的名字
    private String name;
    // 类存放的路径
    private String path = "D:\\temp";

    MyClassLoader(String name) {
        this.name = name;
    }

    MyClassLoader(ClassLoader parent,String name) {
        super(parent);
        this.name = name;
    }

    /**
     * 重写findClass 方法
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("进入自定义的类加载器name="+name);
        String fileName = path.concat(File.separator).concat(name.replace(".",File.separator)).concat(".class");
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(new File(fileName));
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
                int b = 0;
                while ((b = fis.read()) != -1) {
                    bos.write(b);
                }
            byte[] data = bos.toByteArray();
            // 此处defineClass 传入的name为类的全限定名com.gexl.bean.User2
            System.out.println("defineClassName=="+name);
            return this.defineClass(name,data,0,data.length);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

建议大家还是要手动撸一遍上面的代码,还是有些地方需要注意的点

1.User2.class 文件不能放在当前项目的目录下,我放在了D:\temp\com\gexl\bean 文件夹下
如果放在当前项目的目录下会被否则会被sun.misc.Launcher$AppClassLoader 加载到jvm中
不会通过我们自定义的类加载器来加载
2.defineClass 方法 传递的name文件名需要是类的全限定性名称,即com.gexl.bean.User2格式的,因为 defineClass 方法是按这种格式进行处理的 否则会报错:: Exception in thread “main” java.lang.NoClassDefFoundError: D:\temp\com\gexl\bean\User2/class (wrong name: com/gexl/bean/User2)
at java.lang.ClassLoader.defineClass1(Native Method)

动态加载Jar && ClassLoader隔离问题

动态加载Jar:

URL[] urls = new URL[]{new URL("file:lib\jar1.jar)}
URLClassLoader loader = new URLClassLoader(urls,parentLoader);

加载libs下面的jar1.jar , 其中parentLoader 就是上面1中的parent,可以为当前的ClassLoader

此处我暂时也是有点不太清楚是怎么回事的。先暂时略过吧,写了这么多现在脑子有点不太转圈了

ClassLoader 的隔离问题
如果有两个类的包名和类名都相同的情况 PackageName+ClassName.
都相同的话,但是由两个不同的ClassLoader 加载【ClassLoader id】进入虚拟机
那么这两个类之间是无法强行转换的,这个就叫做ClassLoader 隔离

解决办法,只保留一个类,指定同一个parentClassLoader 来加载此类就ok了

关于类的加载机制,细节的内容还是挺多的,有些东西也是不太容易记住的,建议还是要自己手撸一遍代码,跑一跑测试一下,写了将近1万字了,就先这样吧,若有一些疏漏错误,希望可以与大家一起讨论

具体在往jvm虚拟机内部的一些native方法相关的cpp源码文件,暂时还未来的及学习分析,后续有时间的话在接着深入学习之后,再来与大家分享吧。

你可能感兴趣的:(java,开发语言,java-ee)