JVM 类加载器

字节码的结构

魔数u4 cafe babe

版本u4 52 = java8

常量池计数器u2 从1开始,0索引留给不需要的情况

常量池 表 #1 -> #计数器-1

类标识符 u2 public final abstrat class annotion interface 之类

类索引u2 名字

父类索引u2 父类名字

接口计数器 u2 接口数组长度

接口集合 表 接口索引数组

字段计数器 u2

字段 表

-访问标识符 u2

-字段名索引 u2

-字段描述符索引 u2

-字段属性计数器 u2

-字段属性 表

方法计数器 u2

方法 表

-访问标识符 u2

-方法名索引 u2

-方法描述符索引 u2

-方法属性计数器 u2

-方法属性 表

前段编译器

将高级语言源文件编译成Class文件的过程就是前端编译的过程。这是java跨平台执行的关键。

前段编译器只负责将高级语言,编译成字节码,不负责具体的性能优化之类的,这些要在执行引擎中的JIT及时编译器中负责。

前段编译器编译的流程如下。

词法分析

检查关键字,引用等等有没有错误。

语法分析

根据具体的高级语言的语法分析,是否出现语法错误。

语义分析

根据逻辑关系,分析是否有可能出现的逻辑问题。比如未初始化啊,数组越界之类的。这部分的功能有限,只能发现较为明显的逻辑错误。

生成字节码

类加载过程

字节码文件并不存放在内存中,而是在内存外(可能是在磁盘中,也可能在网络中,也可能是动态生成的)。当需要用到的时候再由类加载去寻找并载入内存到运行时数据区才可以被JVM使用。

类加载的过程有7个阶段。

加载------验证------准备------解析------初始化------使用------卸载

加载

1 通过某种方式找到对应的Class文件,获取到二进制数据流。

2 解析二进制数据流,并根据来建立对应方法区中的数据结构。

3 创建java.lang.Class类对象实例,用来作为方法区访问类数据的入口。(也就是给一个索引到方法区数据结构的对象)

Class类的构造方法是私有的,只有JVM可以创建。然后这个Class实例对象,是元空间的入口,也是实现反射的关键数据。通过Class类提供的接口方法,可以获得这个描述类的种种信息。

验证

属于链接中的第一步。

加载到内存之后,我们就可以快速的对字节码进行验证,保证字节码是合法,合规合理的。

字节码格式验证,字节码语义验证,字节码验证(逻辑),符号引用验证。

这和前端编译器,编译流程很像,词法分析对应格式验证,语法分析对应语义验证,语义分析对应字节码验证,以及符号引用验证。

准备阶段

这个时候,字节码通过验证了,那么可以开始完善类的静态部分了,这样一个Class对象算是可用。

也就是静态的成员变量,分配内存,初始化值。

1 分配内存,并进行初始化内存。

对于类的静态成员变量,都会先对内存空间初始化一个值,根据类型不同,初始化的也不同。

java不支持boolean原生类型,内部实现实际上是通过int实现。默认int 0对应false。

这个初始化默认值对于static final修饰的基本数据类型 无用,所以没有,因为其不需要多这么个初始为默认值的步骤,而可以直接确定最终值,在准备阶段直接进行显式赋值。(另外对于String类型 在显式赋值中不涉及 方法或构造器调用,其初始化是在链接阶段的准备环节进行)

对于实例变量,在准备阶段不会进行初始化,因为实例变量会随着实例对象分配到堆中,你不能提前知道要创建实例对象了并提前给创建好。

仅仅是初始化内存空间,相当于清理垃圾 ,并不会执行任何代码来赋值,这一步是后面初始化阶段做的事情)

解析阶段

将符号引用,转换为实际的直接引用。在实际的运行环境中,寻找符号引用的直接引用,替换到类对象中。

这个阶段有可能在初始化之后再进行,不确定。

初始化阶段

这是类装载的最后一个阶段。这个阶段,JVM才会执行初始化代码,根据类的初始化代码进行初始化类对象。

最重要的工作就是执行<cinit>()方法。(类的初始化方法),这个方法只能由java编译器生成,并只能被JVM调用(我们无法调用)。

<cinit>()是根据类的静态成员赋值语句以及static代码块合并形成的。但是需要记住,在加载子类之前,JVM总会试图先加载其父类,所以父类的《cinit》一定先于子类的《cinit》执行。

如果没有静态赋值语句和静态代码块,那么编译器就不会产生《cinit》方法

cinit()方法只能在类加载的时候被调用一次,后续不能再调用,所以当JVM内部多个线程同时加载一个类的时候就需要保证线程安全,cinit方法自带同步锁,只有一个线程能执行cinit,其他线程都阻塞。等待执行完毕后会通知其他线程返回这个结果。

如果cinit中出现耗时长操作,会导致线程阻塞,难以排查。

初始化时机(cinit调用时机)

因为要执行代码,所以无疑初始化需要时间开销 。所以并不是任何时候都能随时进行初始化的。初始化的时机就变得非常重要。

有两种使用类的方式,主动使用会导致初始化发生,被动使用不需要初始化。

1 主动使用(核心就是涉及用到Class对象

也就是当我们需要用到类的静态变量了,那么一定会进行初始化。

  • new实例对象,或者反序列化得到实例对象

  • 反射获取到Class对象

  • 调用静态方法,静态方法可能涉及到静态变量的访问,所以可能静态成员变量需要初始化。

  • 访问未被初始化阶段赋值的静态成员变量(如果访问static final修饰的在准备阶段就赋值的静态变量,那不需要初始化就可以,但是是static final String NAME = new String("123:")这种就不可以)

对于接口来说,所有的静态字段都是static final修饰的,效果和类一样。

初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化 。JVM虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。在初始化一个类时,并不会先初始化它所实现的接口;在初始化一个接口时,并不会先初始化它的父接口 。因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化

  • 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类在初始化之前需要实现接口的初始化

  • JVM启动时,用户需要指定一个要执行的主类[包含main()方法的那个类],JVM会先初始化这个主类。这个类在调用main()方法之前被链接和初始化,main()方法的执行将依次加载,链接和初始化后面需要使用到的类。

  • 初次创建MethodHandle实例时,初始化该MethodHandle实例时指向的方法所在的类

2 被动使用

并不是在代码中出现的类,就一定会被加载或者初始化

  • 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。当通过子类引用父类的静态变量,不会导致子类初始化,而如果访问子类的,就会导致父类进行初始化,以及子类的初始化

  • 通过数组定义类引用,不会触发此类的初始化。直到给具体的数组中的元素赋予对象才会。数组定义的引用,只是从编译阶段确定,所以并不会导致初始化。

  • 引用常量不会触发此类或接口的初始化,因为常量在链接阶段已经被显式赋值

  • 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。通过反射获取到Class对象,才会导致初始化,例如Class.forName()会导致初始化。

类的卸载

类的卸载,涉及到类加载,类Class对象,类实例之间的引用关系。

**类加载器和加载的类对象,相互关联。**类加载内部Java集合存放了加载过的类对象引用,而类对象也引用加载他的类加载器。

类实例总是引用代表这个类的Class对象getClass(),类中都有一个静态属性class(通过类名.class可获得),引用着这个类的Class对象。

所以条条大路通Class对象。

所以什么时候卸载类,要等到Class对象不在被引用的时候。

所以类被卸载的三个条件,都是围绕Class对象被引用

  1. 所有类的实例对象都被回收

  2. Class对象没有直接被引用

  3. 类的加载器被GC回收(只有用户自定义加载器能够被回收)

这样图中Order.class实例的三个方向的引用都断了,那么可以卸载类了。

类加载器

实际上属于类加载过程的细节。类加载过程中第一个加载阶段就是类加载器负责的。

类加载器在整个装载阶段,只能影响到类的加载,而无法改变类的链接和初始化行为(存疑)

类加载器必要性

了解类加载器机制,可以解决以下问题。

(1)避免在开发中遇到java.lang.ClassNotFoundException异常或java.lang.NoClassDefFoundError异常时手足无措。

(2)只有了解类加载器的加载机制,才能够在出现异常的时候快速地根据错误异常日志定位并解决问题。

(3)需要支持类的动态加载或需要对编译后的class文件进行加解密操作时,就需要与类加载器打交道。

(4)开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑

也就是异常处理,动态或者自定义加载

类加载器的命名空间

每一个类是通过加载它的类加载器加上类本身的名字来确定其在JVM中的唯一性!而不仅仅是通过类的名字。

每个类加载器都有自己的命名空间,命名空间由该类加载器(实例)及所有的父类加载器组成,在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;

这样就保证了类的唯一性。

在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类;在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。

也就是通过不同的类加载器,加载一个类的不同版本。

类加载的基本特征

通常有三大特征:双亲委派,可见性,单一性

双亲委派实际上就是优先交给上一级加载器加载。这样是为了避免类在其他地方重复加载,第二个是恶意代码不能通过重复加载来替换核心类库。

可见性是下级加载器,可以访问上级加载器加载了哪些类型,反过来是不行的。也就是说高级加载器加载的类,只能看到自己和更高级的类的存在,如果和同级或者下级类进行交互,是ClassNotFound的。而下级加载器加载的类就可以看到上级加载器加载的类的存在。(就像是父类和子类的关系一样,但是只能是访问关系一样)

单一性,因为父加载器加载过的类型对于子加载器是可见的,所以父加载器加载的类型就不会在子加载器中重复加载 。但是在同一级的加载器中(兄弟加载器)相互是不可见的,所以同一个类可以被同级别的加载器加载多次

类加载器的分类

实际上就是类加载器分级,是根据什么分级的,不同级别的类加载器有什么职责。

从最本质的来分,JVM有两种类加载器,启动类加载器和自定义类加载器。

自定义类加载器通常是指由开发人员自定义的一类类加载器,但是Java虚拟机规范中规定的更为广泛,凡是从抽象类ClassLoader派生而来的类加载器都是自定义类加载器。那么不是从ClassLoader派生而来的类加载器自然就是启动类加载器了。

无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构如图20-1所示,其中扩展类加载器和应用程序类由抽象类ClassLoader派生而来

实际上不同加载器之间是聚合关系,也就是下级加载器,有上级加载器的引用。而不是继承关系。

只是在ClassLoader这个抽象类中,有一个成员变量引用上一级的加载器,叫做parent。所以上级加载器才被叫做父加载器。

引导类加载器(启动类加载器)

这两种称呼,一种是基于职责负责引导程序运行,一种是指是启动程序的类加载器。

引导类加载器(BootstrapClassLoader,又称启动类加载器)使用C/C++语言实现,嵌套在JVM内部。

引导类加载器不继承java.lang.ClassLoader,没有父类加载器 。出于安全考虑,引导类加载器主要用来加载Java的核心库,也就是"JAVA_HOME/jre/lib/rt.jar"或"sun.boot.class.path"路径下的内容,指定为扩展类和应用程序类加载器的父类加载器

所以引导类加载器作用有限,主要用来加载核心类库。

扩展类加载器

扩展类加载器(ExtensionClassLoader)由Java语言编写,间接继承与ClassLoader

扩展类加载器主要负责从java.ext.dirs系统属性所指定的目录或者JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的类放在上述目录下,也会自动由扩展类加载器加载。简言之扩展类加载器主要负责加载Java的扩展库。

应用程序类加载器

也叫做系统加载器,也是由Java语言编写,间接继承于ClassLoader类父类加载器为扩展类加载器。

负责加载环境变量classpath或系统属性java.class.path指定路径下的类库,应用程序中的类加载器默认是应用程序类加载器。(在IDE中,可以查看到JDK的CLASSPATH。在命令行执行的时候CLASSPATH就是当前路径)

它是用户自定义类加载器的默认父类加载器,通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器。

自定义加载器

自定义加载器有很多好处。

  1. 实现插件效果,即插即用。通过启用自定义加载器,加载额外功能。不需要的时候直接回收自定义加载器,就拔除功能。

  2. 隔离加载类。同级类加载器之间相互隔离。所以我们可以通过将不同的类簇通过不同的同级类加载器加载实现隔离。

  3. 修改类加载方式,除了启动类加载器之外,其他的类加载器并非一定引入。所以我们可以改变类加载器的加载。

  4. 扩展加载源。如果需要加载从咔咔郭郭来的类,可以通过自定义加载器载入。

  5. 提高程序的安全性。在一般情况下,使用不同的类加载器去加载不同的功能模块,会提高应用程序的安全性。但是,如果涉及Java类型转换,则加载器反而容易产生不美好的事情。在做Java类型转换时,只有两个类型都是由同一个加载器所加载,才能进行类型转换,否则转换时会发生异常。

获取常见类的加载器

方式 加载器类型
class对象.getClassLoader() 获取class对象的类加载器
Thread.currentThread().getContextClassLoader() 当前线程上下文的类加载器
ClassLoader.getSystemClassLoader() 获取系统类加载器
classLoader对象.getParent() 获取父类加载器

这些加载器,大多都是由应用类加载器来充当。

可以看到都是应用类加载器加载的。

引导类加载器结果为null,原因是引导类加载器是C++语言编写,并不是一个java对象,所以这里用null展示

数组类特殊

数组类的Class对象,不是由类加载器创建的,而是在Java运行期JVM根据需要自动创建的。数组类的类加载器可以通过Class.getClassLoader()方法返回,如果数组元素是引用数据类型,类加载器与数组当中元素类型相同,如果数组元素类型是基本数据类型,就没有类加载器

复制代码
 System.out.println(int[][].class.getClassLoader());
 //输出null
 System.out.println(ClassLoaderTest[][].class.getClassLoader());
 //输出sun.misc.Launcher$AppClassLoader@18b4aac2

源码分析

有必要学习类加载器的源码。

ClassLoader主要方法

抽象类ClassLoader的主要方法(内部没有抽象方法)如下。

1)public final ClassLoader getParent()该方法作用是返回该类加载器的父类加载器。

2)public Class<?>loadClass(String name)throws ClassNotFoundException该方法作用是加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,则抛出"ClassNotFoundException"异常。

该方法中的逻辑就是双亲委派模型的实现

具体操作就是:

  1. sychronized保证同步没问题

  2. 查看自己是否加载过 通过findLoadedClass ,也就是protected final Class<?>findLoadedClass(String name) )

  3. 调用父加载器进行加载(如果父为空,调用findBootstrapClassOrNull加载)

  4. 父加载器失败,自己加载 通过findClass(name)

  5. 进行链接操作 resolveClass (也就是 验证,准备,解析 这三个操作)

这里的findClass方法就很重要了,最为加载器的兜底逻辑,负责查找二进制名为name的类,返回的是Class类实例。

protected Class<?>findClass(String name)throws ClassNotFoundException

在jdk1.2之后,官方已经不建议我们重写loadClass方法,因为loadClass方法中实现保证了双亲委派机制的逻辑。我们自己的逻辑建议写在findClass方法中。当loadClass()方法中父类加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模型。

在findClass()中,应该调用defineClass(),将找到的二进制流转换成Class对象。

protected final Class<?>defineClass(String name,byte [] b,int off,int len)

通过这个方法不仅能够通过class文件实例化Class实例对象,也可以通过其他方式实例化Class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象

protected final void resolveClass(Class<?>c)

使用该方法可以使用类的Class对象创建完成的同时也被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将class文件中的符号引用转换为直接引用

也就是

SecureClassLoader与URLClassLoader

ClassLoader中有很多方法没有实现。SecureClassLoader新增了对Class源的验证,权限之类的方法。

他的子类URLClassLoader给ClassLoader众多没实现的方法提供了实现。例如findClass(),findResouce()方法。新增了通过URLClassPath类来协助获取Class字节码流的功能。

我们可以通过继承自URLClassLoader来避免实现过于复杂的findClass()和字节码获取代码。

ExtClassLoader与AppClassLoader

这两个类加载器都继承自URLClassLoader。是sun.misc.lanucher的内部静态类。sun.misc.lanucher主要用来启动主应用程序。

类加载调用方法

一般通过Class.forName(全限定名),或者classLoader实例.loadClass(全限定名)加载一个类。

不同的是Class方法会在加载到内存同时进行初始化。

而classLoader实例方法,只会加载到内存,不会触发实例化。

让自定义类加载器加载类的办法

如果在findClass中不做什么改变,那么类一般都会让父加载器也就是AppClassLoader给加载了。所以我们需要在loaderClass的时候传入类名,而不是全限定名,这样父加载器就找不到类,会交给自定义加载器加载。在findClass中我们再拼接出类的路径,找到类文件,然后进行加载。

双亲委派模型的改变

双亲委派机制并不是必须的,而是java设计者推荐的一种类加载器实现机制。

他的好处是:

  1. 避免重复加载

  2. 保护程序安全

打破双亲委派机制也无法破坏核心类库的唯一加载

在ClassLoader中的final defineClass方法中,为核心类库提供了一层保护机制。无论是什么类加载器,最终都会调用defineClass这个方法,这个方法final不能重写,在其内部调用preDefineClass方法,对核心类库进行保护。

他的劣势是:

因为我们只设计了父级类加载器的成员变量引用,那么我们的类去寻找其他的类的时候,只能先获取自己的ClassLoader,然后从ClassLoader中获取findLoadedClass(),找不到只能向上找loadedClass。所以无法访问下级类加载器加载的类。

破坏双亲委派机制的三种

为了兼容性

jdk1.2之前,并没有引入双亲委派机制,所以自定义类加载器很多都是通过重写loadClass实现的。在引入之后,为了兼容这些代码,就没有以技术手段防止loadClass被重写。创建了一个prorected findClass来代替重写loadClass。

SPI场景(不同的类加载器加载的类之间的交互)

简单来说就是接口定义在了启动类加载器中,而实现类定义在了其他类加载器中,当启动类加载器需要加载其他子类加载器路径中的类时,需要使用线程上下文类加载器(默认是应用程序类加载器),这样以上下文加载器为中介,使得启动类加载器中的代码也可以访问应用类加载器中的类。

线程上下文类加载器,实际上是一个帽子,主要看我们让哪一个类加载器来带上这个帽子。带上什么帽子起什么作用。原来的findLoadedClass是一条只能向上的线,通过线程上下文类加载器,这样就可以形成一个环。我们通常让APPClassLoader来充当线程上下文类加载器,这样AppClassloader,ExtratClassLoader,BootstrapClassLoader形成了一个环,那么这三个类加载器中加载的类就都相互可查询可见了。

为了热部署

追求程序的动态性,代码热部署,模块热替换等。

java并不天生支持热替换。热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。热替换的关键需求在于服务不能中断,修改必须立即表现正在运行的系统之中。

如果一个类已经加载到系统中,通过修改类文件,并无法让系统再来加载并重定义这个类。

但是如果我们就是想要重新加载并定义这个类呢?为了不影响程序的运行,我们不能卸载这个类,但是我们可以通过加载一个同名类来替换。要做到这件事,首先我们需要防止触发重复加载,我们需要换一个类加载器,这样JVM会认为是不同的类,然后加载进去,不能调用loadClass这样会交由AppClassLoader加载,就是会认为加载过了。

复习路线

JVM 复习1-CSDN博客

相关推荐
Theodore_10226 小时前
7 设计模式原则之合成复用原则
java·开发语言·jvm·设计模式·java-ee·合成复用原则
我是苏苏6 小时前
Web开发:ORM框架之使用Freesql的DbFrist封装常见功能
java·前端·jvm
天草二十六_简村人12 小时前
Java语言编程,通过阿里云mongo数据库监控实现数据库的连接池优化
java·jvm·数据库·mongodb·阿里云·微服务·云计算
老码沉思录14 小时前
Android开发实战班 - 数据持久化 - Room 数据库应用
android·jvm·数据库
起名字真南15 小时前
【C++】深入理解 C++ 中的继承进阶:多继承、菱形继承及其解决方案
java·jvm·c++·chatgpt·aigc
偶尔。53515 小时前
JVM垃圾回收算法详解
jvm
飞滕人生TYF16 小时前
在 for 循环中,JVM可能会将 arr.length 提升到循环外部,仅计算一次。可能会将如何解释 详解
java·jvm
customer0816 小时前
【开源免费】基于SpringBoot+Vue.JS网上订餐系统(JAVA毕业设计)
java·jvm·vue.js·spring boot·spring cloud·java-ee·开源
CAORENZHU20 小时前
JVM垃圾回收详解.②
jvm
Code0cean20 小时前
从零开始学习JVM(九)- 垃圾收集器
jvm