欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JVM之类的加载和类装载器ClassLoader

程序员文章站 2022-05-12 12:01:40
...

一个类从加载到new一个对象的大致过程


想知道一个一个写好的类,这些朴实的小生命是如何*营业?如何被不法机构消费的吗?那就来看看吧【该文章是借鉴糅杂了各方面博客和资源输出的,包括其中个别字段和图片均非本人原创输出,仅为记录和分享】

什么是类的加载?

Java代码——>字节码文件——>JVM。JVM将指定的class文件读取到内存里,并运行该class文件里的Java程序的过程,就称之为类的加载;反之,将某个class文件的运行时数据从JVM中移除的过程,就称之为类的卸载。

定义:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制

类加载的时机(什么时候开始加载类?):

什么时候开始加载,虚拟机规范并没有强制性的约束,对于其它大部分阶段究竟何时开始虚拟机规范也都没有进行规范,这些都是交由虚拟机的具体实现来把握。所以不同的虚拟机它们开始的时机可能是不同的。但是对于初始化却严格的规定了有且只有四种情况必须先对类进行“初始化”(加载,验证,准备自然需要在初始化之前完成):
1.遇到new、getstatic、putstatic和invokestatic这四条指令时,如果对应的类没有初始化,则要对对应的类先进行初始化。这四个指令对应到我们java代码中的场景分别是,new关键字实例化对象的时候;读取或设置一个类的静态字段(读取被final修饰,已在编译器把结果放入常量池的静态字段除外) ;调用类的静态方法时。
2.使用java.lang.reflect包方法时对类进行反射调用的时候。
3.初始化一个类的时候发现其父类还没初始化,要先初始化其父类。
4.当虚拟机开始启动时,用户需要指定一个主类,虚拟机会先执行这个主类的初始化。
原作者存疑:虚拟机规范用了有且只有这个非常强烈的限定词,把这四种场景称为主动引用,其它引用方式称为被动引用。这是指其它方式的引用都不允许触发初始化么?存疑。

类卸载时机【延展知识】

类的卸载跟采用的垃圾收集算法有关,在CMS中有两种方法卸载不必要的类,一种是等到元空间(Metaspace)满了的时候触发FGC,另一种是使用跟CMS并发收集算法类似的方式,不过对于元空间的阈值和触发CMS并发收集的阈值是独立的。更具体的可以参考之前的文章:CMS学习笔记。在这里,我们只需要记住,JVM中一个类的卸载要满足下面这3个条件:
1.该类所有的实例对象都已被回收;
2.该类的类加载器对象已经被回收;
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

类加载的过程?

一个类加载的过程:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
例图:
JVM之类的加载和类装载器ClassLoader我们扩大场景连接到jvm内存模型 纵观全局得去了解事物从头到尾的流程更能够清晰的感受到各个之间的交互,功能和定位JVM之类的加载和类装载器ClassLoader

刚刚说了一个类加载的过程 接来下先理清一下一个项目加载的过程:

项目运行过程:多个java文件经过编译打包生成可运行jar包,最终由java命令运行某个主类的main函数启 动程序,这里首先需要通过类加载器把主类加载到JVM。 主类在运行过程中如果使用到其它类,会逐步加载这些类。 注意,jar包里的类不是一次性全部加载的,是使用到时才加载
上图JVM之类的加载和类装载器ClassLoader

类加载器加载类的具体过程


类加载到使用整个过程有如下几步: 加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

具体的讲解在此链接中:jvm类加载过程吹毛求疵事无巨细究极讲解(个人吹爆)
上图:JVM之类的加载和类装载器ClassLoader

步骤解析【浅薄版本】:
加载:
1 通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个二进制字节流所代表的静态存储结构导入为方法区的运行时数据结构。
3.在java堆中生成一个java.lang.Class对象,来代表的这个类,作为方法区这些数据的入口。
加载阶段完成之后二进制字节流就按照虚拟机所需的格式存储在方区去中

概述:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用 类的main()方法,new对象等等

个人认为加载最强的总结和思想

一言以蔽之,加载阶段就是把类的二进制字节流加载到方法区中,并设置了一个访问它们的入口。java虚拟机规范对这三点的要求并不具体,它只说通过“类的全限定名”来获取“二进制字节流”,并没有说要如何获取,以及去哪里获取。相当于它只定义了一个接口,具体的实现可以任由虚拟机的实现来发挥。虚拟机的设计团队在加载阶段搭建了一个相当开发、广阔的舞台。java的发展历程中,许多举重轻重的java技术都是建立在这一基础上的,例如:
1.从ZIP包中读取,从JAR,EAR,WAR格式中读取。
2.从网络中获取,这种场景典型的引用就是Applet。
3.运行时自动生成,这种场景运用最多的是动态代理技术,在java.lang.reflect.proxy中,就是运用了ProxyGenerator.generateProxyClass来为特定接口生成*Proxy的代理类的二进制字节流。
4.由其它文件生成,比如jsp。
5.从数据库中读取。
相对于类加载过程的其它阶段,加载阶段(准确的说,是加载阶段中获取类的二进制字节流的过程)是开发期可控性最强的过程,因为加载阶段既可以使用系统定义的类加载器进行加载,也可以自定义类加载器来控制二进制字节流的加载方式。
方法区中的数据存储格式虚拟机规范也没有具体规范,由虚拟机的实现自行定义

-连接

  • 验证:确保被加载的类的正确性

  • 准备:为类的静态变量分配内存,并将其初始化为默认值

  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链 接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接 引用

初始化:对类的静态变量初始化为指定的值,执行静态代码块

总体概述:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。类加载和连接的过程都是在运行期间完成的。

类和类加载器的关系


那么你想过在jvm中是如何去确定一个类的唯一性的呢?难道通过一个类的全限定名就真的可以唯一确定吗? 如果一个人的名字相同但不是一个母亲所生,他们是同一个人吗?

对于任意一个类,都需要加载它的类加载器和这个类本身来确定这个类在Java虚拟机中的唯一性,每一个类加载器都有一个独立的类名称空间。也就是说,如果比较两个类是否是同一个类,除了这比较这两个类本身的全限定名是否相同之外,还要比较这两个类是否是同一个类加载器加载的。即使同一个类文件两次加载到同一个虚拟机中,但如果是由两个不同的类加载器加载的,那这两个类仍然不是同一个类。

不多bb 代码伺候

package com.csdn.demo_ds.jvm;

/**
 * @author Administrator
 * @ClassName ClassLoaderTest
 * @Description
 * @date 2020-11-11 11:50
 */


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

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception{
        ClassLoader loader=new ClassLoader() {
            @Override
            public Class<?> loadClass(String name)throws ClassNotFoundException{
                try{
                    //组装成 ClassLoaderTest.class
                    String filename=name.substring(name.lastIndexOf(".")+1)+".class";
                    //getClass()得到类  getResourceAsStream(filename)通过文件名得到类信息放入到inputsrteam中去
                    InputStream is=getClass().getResourceAsStream(filename);
                    if(is==null){
                        //双亲加载机制
                        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);
                }
            }
        };
        Object obj=loader.loadClass("com.csdn.demo_ds.jvm.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());//类名
        System.out.println(obj.getClass().getClassLoader());//类加载器
        System.out.println(ClassLoaderTest.class.getClassLoader());//应用类加载器
        System.out.println(obj instanceof com.csdn.demo_ds.jvm.ClassLoaderTest);
    }
}

类加载器和双亲委派机制


java类加载器有以下几种
1.启动类加载器

负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等. 换句话说负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。

2.扩展类加载器

负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中 的JAR类包 .这个加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

3.应用程序类加载器

负责加载ClassPath路径下的类包,主要就是加载你自己写 的那些类这个类加载器是由sun.misc.Launcher$AppClassLoader实现的。由于这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,所以也叫系统类加载器。它负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

4.自定义加载器:负责加载用户自定义路径下的类包

双亲委派机制上图:
JVM之类的加载和类装载器ClassLoader
说明:

这里类加载其实就有一个双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不 到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则 在自己的类加载路径中查找并载入目标类。 比如我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类 加载器加载,扩展类加载器再委托启动类加载器,顶层启动类加载器在自己的类加载路径里 找了半天没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加 载,在自己的类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应 用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就 自己加载了。。 双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载

那为什么要有双亲加载机制呢?

1.沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止 核心API库被随意篡改
2.避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加 载一次,保证被加载类的唯一性

如果没有这个机制。在我们心目中所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过双亲委派模型,对于 Java 核心库的类的加载工作由启动类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间

具体关于双亲委派深入探讨参照这位神人的博客:学术研究大神

相关标签: 类加载器 jvm