0%

JVM系列之一:Java类加载(ClassLoader)

前言

这两天公司无事,于是自己找了找JVM相关的东西看,这里主要记录一下类加载相关方面的知识,加深自己对ClassLoader的认识,防止看了就忘。

整体概述

首先,一个java程序的运行需要经过两个阶段:编译、运行。
其中,编译就是java编译器(javac.exe)将.java文件编译成JVM可识别的字节码文件(.class)。

而运行,即是JVM虚拟机运行(.class)文件的过程。

类加载,就是把.class文件中的类信息加载进内存,并进行解析生成类对象的过程。
在JVM执行某段代码时,遇到了class A,但内存中并没有这个class A的相关信息,于是JVM就会在相应的class文件中寻找class A的类信息,并将它加载入内存。也就是说,JVM并不是一开始就把所有的类都加载进内存,而是只有遇到内存中没有的类时,才会进行加载。

类加载流程

类加载子过程主要分三个阶段:加载(Loading) 、链接(Linking)、初始化(Initialization)。而链接又细分为三个小阶段:验证(Verification)、准备(Preparation)、解析(Resolution)。

加载(Loading)

通过类的全限定名(包名+类名),获取到该类的.class文件的二进制字节流,将二进制字节流所代表的静态数据存储结构,转化为方法区运行时的数据结构。
并在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

总结: 加载.class文件进内存 -> 将.class文件映射成jvm能识别的结构 -> 在内存中生成Class对象
另外需要注意的是,相对于类加载的其他阶段而言,加载阶段(准确的说,说加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以用自己自定义的类加载器来完成加载。换句话说,ClassLoader就是作用于这个阶段!

链接(Linking)

链接是指将上面创建好的Class类合并到Java虚拟机中,使之能够执行的过程。

验证(Verify)

  • 确保.class文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全。
  • 主要包含四种验证:文件格式验证、元数据验证、字节码验证、符号验证
这个阶段会验证class文件是否以“CA FA BA BE”开头。

准备(Prepare)

  • 为类变量分配内存并且设置该类变量的默认初始值,即零值
  • 这里不包含用final修饰的static(常量),因为final在编译的时候就会分配了,准备阶段会显示地初始化
  • 这里不会为实例变量分配初始值,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
总结: 为类中的静态字段(static)分配内存

解析(Resolve)

  • 将常量池内的符号引用转换为直接引用的过程
  • 通常,解析是在初始化之后完成的
  • 符号解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等,对于常量池中的CONSTANT_Class_info, CONSTANT_Fieldref_info, CONSTANT_Methodref_info等
符号引用就是用一组符号来描述引用的目标,直接引用就是直接指向目标的指针、相对偏移量或者一个间接定位到目标的句柄。

初始化

初始化阶段就是执行类构造器方法<clinit>()的过程,此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来,构造器方法中指令按语句在源文件中出现的顺序执行。另外<client>()区别于<init>。若该类有父类,JVM会保证父类的<clinit>在子类前执行。虚拟机必须保证一个类的<client>方法在多线程下被同步加锁

这里要注意init和clinit的区别:
1、init和clinit方法执行时机不同
init是对象构造器方法,也就是说new 一个对象,调用该对象类的constructor方法时才会执行init方法
clinit是累构造器方法,是jvm类加载子系统中初始化阶段调用。
2、init和clinit方法执行目的不同
init是instance实例构造器,对非静态变量解析初始化。
clinit是class累构造器,对静态变量、静态代码块进行初始化。
3、clinit最多只会有一个,但有几个构造方法就会有几个init(至少一个)。

总结:
只有类中存在static{}静态代码块static描述的变量时。才会存在<clinit>()类构造方法
需要注意的是,类的方法是单独加载的,<clinit>()不会收方法的影响
用jclasslib 查看如下:

没有static代码块则没有client方法。

static代码块则有client方法。

类加载器的分类

类加载器大致分为如下四种:

这四者是包含关系,不是上下层关系,也不是父子类关系。
其中 拓展类加载器(Extension ClassLoader)在jdk9及以后被平台加载器(Platform ClassLoader)替代.
可以通过<classname>.class.getClassLoader() 方法获取其类加载器。

启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 启动类加载器由C/C++实现,嵌套在JVM内部
  • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar、sun.boot.class.path路径下的内容),用于提高JVM自身需要的类
  • 加载拓展类和应用程序类加载器,并指定为他们的父类加载器
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

扩展类加载器(Extension ClassLoader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
  • 派生于ClassLoader类
  • 父加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录,也会自动由扩展类加载器加载

应用程序类加载器(系统类加载器,AppClassLoader)

  • Java语言编写,由sun.misc.Launcher$APPClassLoader实现
  • 派生于ClassLoader类
  • 父加载器为扩展类加载器
  • 负责加载环境变了classpath或系统属性java.class.path指定路径下的类库
  • 是程序中默认的类加载器,一般来说,Java应用的类都是由它完成加载
  • 通过ClassLoader.getSystemClassLoader()方法可以获取到该类加载器

用户自定义类加载器(User Defined Class Loader)

为什么需要自定义类加载器

1、隔离加载类
2、修改类加载的方式
3、扩展加载源
4、防止源码泄漏

实现步骤

  • 通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
  • 在JDK1.2之前,自定义类加载器时,总要去继承ClassLoader类并重写`loadClass()`方法,从而实现自定义的类加载器。但在JDK1.2之后,不再建议用户去覆盖`loadClass()`方法,而是建议把自定义的类加载逻辑写在`findClass()`方法中
  • 在编写自定义的类加载器时,如果没有太过于复杂的需求,可以直接继承`URLClassLoader`类,这样可以避免自己去编写`findClass()`方法及获取字节流的方式,使自定义类加载器编写更加简洁

获取加载器的几种方式

1、获取当前类的加载器。 class.getClassLoader()
2、获取当前线程上下文的加载器。Thread.currentThread.getContextClassLoader()
3、获取系统的加载器。ClassLoader.getSystemClassLoader()
4、获取调用者的加载器。DriverManager.getClassLoader()

双亲委派机制

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成Class对象。而且加载某个类的class文件时,JVM采用的就是双亲委派模式

工作原理如下:
1、如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
2、如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终到达顶层的启动类加载器。
3、如果父类加载器可以完成类的加载任务,就成功返回,如果父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
感觉和职责链模式有点像啊!

总结

最后,在对类加载机制进行一个概括。
java编译器(javac)将.java文件编译成.class文件【编译】。然后类加载器(ClassLoader) 通过全名读取.class文件的二进制流到内存,将其转化成jvm能够识别的结果,并在内存中生成一个java.lang.Class对象【加载】。
接着,JVM对读入的二进制流文件进行验证,判断是否符合当前虚拟机的要求,并确保生成的java.lang.Class对象的正确性【验证】。然后,再为Class对象中的static属性分配内存,并设置零值【准备】。将常量池中的符号应用转化为直接引用【解析】。最后,对类中的静态变量和静态代码块进行初始化,也就是给他们赋指定的值【初始化】。

参考

-------------------本文结束 感谢阅读-------------------