Class文件结构

暂略

类的加载

在Java中,类的加载是指将类的相关信息加载到内存。Java中,类是动态加载的,当第一次使用这个类的时候才会加载(也可采用预加载机制来加载某个类)。加载一个类时,会查看其父类是否已加载,如果没有,则会加载其父类。

加载过程

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或类初始化。

isq.png

加载

将类的class文件读入内存,并为之创建一个java.lang.Class对象

类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象。即,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:

  1. 从本地文件系统加载class文件,这是绝大部分示例程序的类加载方式;
  2. 从jar包加载class文件,这种方式也是很常见的,如JDBC编程时用到的数据库驱动类就放在jar文件中,JVM可以从jar文件中直接加载该class文件;
  3. 通过网络加载class文件;
  4. 把一个Java源文件动态编译,并执行加载。


类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

连接

类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中,具体分为验证、准备、解析 三步。

验证

检验被加载的类是否有正确的内部结构,并和其他类协调一致。

主要包括四种验证,文件格式验证,源数据验证,字节码验证,符号引用验证。

准备

为类变量分配内存,并设置默认初始值。

  • 此处的默认值,基本数据为Java默认值,引用数据类型为null。
  • 此阶段不包含final修饰的static变量,因为final在编译的时候就会分配了,准备阶段会显式初始化。
  • 该阶段不会为实例变量分配初始化, 因为,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

注意如下代码:

  1. class Tess {
  2. private static int num;
  3. static {
  4. num = 1;
  5. number = 2; //1
  6. System.out.println(num);
  7. System.out.println(number); //2
  8. }
  9. private static int number;
  10. }

注释1处是可以对定义在下面的类变量number进行赋值的,因为,在类加载的第二阶段 连接-准备 阶段已经给类变量number分配内存空间,并设置了默认初始化值0,而在静态代码块中的赋值操作发生在是类加载的 第三阶段 初始化阶段,所以注释1处是能正常执行的。
注释2处编译时就直接报错(Illegal forward reference),因为注释2处,虽然发生在类加载的第三阶段 类变量的初始化阶段,且在第二阶段 连接阶段 已经为该类变量分配了内存,但声明变量的地方在后面,所以此时引用该变量是非法的,因为此时只分配了内存空间,并未声明变量。

解析

将类的二进制数据中的符号引用替换成直接引用。

  • 符号引用:就是一组符号来描述所引用的目标。在编译阶段编译器并不知道引用对象的地址所以只能用符号引用来定位目标。
  • 直接引用:就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

举个形象的例子。 符号引用:一野的家 ;直接引用:重庆市Xxx_具体的地址

初始化

一句话描述这个阶段就是执行类构造器< clinit >()方法的过程。
该阶段,java程序代码才开始真正执行,虚拟机负责对类进行初始化,主要就是对类变量进行赋初值。在准备阶段已经为类变量赋过一次默认值。在初始化阶端,程序员可以根据自己的需求来赋值了。 作用:虚拟机负责对类进行初始化,主要就是对类变量进行赋初值

  • 对类变量指定初始值的两种方式
    • 声明类变量时指定初始值;
    • 使用静态初始化块为类变量指定初始值。

声明变量时指定初始值和静态初始化块,都将被当成类的初始化语句,JVM会按这些语句在程序中的排列顺序依次执行它们。

  • jvm初始化对象的步骤
  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类;
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类;
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句。
    • 首先 类变量和static块的初始化
    • 其次 实例变量和非static块的初始化
    • 最后 构造器初始化

当执行第2个步骤时,系统对直接父类的初始化步骤也遵循此步骤1-3;如果该直接父类又有直接父类,则系统再次重复这三个步骤来先初始化这个父类……依此类推,所以JVM最先初始化的总是java.lang.Object类。当程序主动使用任何一个类时,系统会保证该类以及所有父类(包括直接父类和间接父类)都会被初始化。
字节码&类加载 - 图2

  • 类的初始化时机

java中对类加载的时机没有强制的要求,但是对类的初始化时机有具体的要求,当Java程序首次通过下面6种方式来使用某个类或接口时,系统就会初始化该类或接口。

  • 创建类的实例;
    • 为某个类创建实例的方式包括:使用new操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。
  • 调用某个类的类方法(静态方法);
  • 访问某个类或接口的类变量,或为该类变量赋值;
  • 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象;
    • 例如代码:Class.forName(”Person”),如果系统还未初始化Person类,则这行代码将会导致该Person类被初始化,并返回Person类对应的java.lang.Class对象。
  • 初始化某个类的子类;
    • 当初始化某个类的子类时,该子类的所有父类都会被初始化。
  • 直接使用java.exe命令来运行某个主类。
    • 当运行某个主类时,程序会先初始化该主类。

注意:

  1. 如final型的类变量,只要该类变量的值能在编译时就可以确定下来,那么这个类变量相当于“宏变量”。Java编译器会在编译时直接把这个类变量出现的地方替换成它的值,因此即使程序使用该静态类变量,也不会导致该类的初始化。
  2. 反之,如果final修饰的类变量的值不能在编译时确定下来,则必须等到运行时才可以确定该类变量的值,如果通过该类来访问它的类变量,则会导致该类被初始化。
  3. 当使用ClassLoader类的loadClass()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用Class的forName()静态方法才会导致强制初始化该类。

类加载机制

加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。

那么,怎么样才算是“同一个类呢”?
正如一个对象有一个唯一的标识一样,一个载入JVM中的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包名+类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为唯一标识。

例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person、pg、kl)。这意味着两个类加载器加载的同名类:(Person、pg、kl)和(Person、pg、kl2)是不同的,它们所加载的类也是完全不同、互不兼容的。

类的三种加载机制

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将她的class文件加载到内存生成的class对象。而且加载某个类的class文件时,java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派 模式。

全盘负责

当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。

双亲委派模型⭐️

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

  • 注意:类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系。
  • 优点
    • 可以避免重复加载,父类已经加载了,子类就不需要再次加载更加安全;
    • 很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。

缓存机制

缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

类的加载时机

主动使用

主动使用会导致类的初始化,有以下八种情况。

  1. 通过命令行启动应用时由JVM初始化加载含有main()方法的主类;
  2. 创建类的实例;
  3. 访问某个类或接口的静态变量,或对该静态变量赋值;
  4. 调用类的静态方法;
  5. 反射(如,Class.forName(…)方法);
  6. 初始化一个类的子类;
  7. JVM启动时被标明为启动类的类;
  8. JDK7 开始提供的动态语言支持。

注:Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。

被动使用

非主动使用的8种情况外的所有使用都是被动使用。被动使用是不会导致 类的初始化的,仅执行类的加载、连接。
例如:通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。

四种类加载器

站在JVM的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
puf.png
注意上图中的加载器划分关系为包含关系,并不是继承关系,通过组合实现。

loadClass--方法的执行步骤.svg
其中,两个蓝色框的步骤中,允许重写ClassLoader的findClass()方法来实现自己的载入策略。甚至重写整个loadClass()方法来实现自己的载入过程。

Bootstrap ClassLoader

Bootstrap ClassLoader:
引导(也称原始或)类加载器,由C++编写,用于加载Java的核心类,即,%JAVA_HOME%/lib目录下的类,也就是我们用到的基本的像String什么的都是这个加载器加载的。也可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。

  • 由于根类加载器并不是Java实现的,所以并不继续自java.lang.ClassLoader,而且程序通常无须访问根类加载器,因此访问扩展类加载器的父类加载器时返回null;
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器;
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为Java、Javax、sun等开头的类

    Extension ClassLoader

    扩展类加载器,用于加载%JAVA_HOME%\lib\ext中的类,这个目录下的类不常用到。还可以加载-D java.ext.dirs选项指定的目录。

  • 派生于ClassLoader类;

  • 位于 sun.misc.Launcher$ExtClassLoader;
  • 父类加载器为根类加载器;

System ClassLoader

系统类加载器,也称App ClassLoader,加载当前应用的classpath的所有类,即所有用户自定义的类。

  • 派生于ClassLoader类;
  • 位于sun.misc.Launcher$AppClassLoader;
  • 父类加载器为扩展类加载器;
  • 该类加载器是程序默认的类加载器,一般来说,Java应用的类都是由它来完成加载;
  • 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

User ClassLoader

用户类加载器,即 ,自定义的类加载器,可以有多个。

  • 适用场景
    • 隔离加载类;
    • 修改类加载的方式;
    • 拓展加载源;
    • 防止源码泄漏
  • 实现步骤

自定义类加载器的实现步骤:

  1. 创建一个类继承ClassLoader抽象类;
  2. 重写findClass()方法;
  3. 在findClass()方法中调用defineClass()。

注意:如果没有过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

JVM中除根类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写ClassLoader所包含的方法来实现自定义的类加载器。ClassLoader中包含了大量的protected方法——这些方法都可被子类重写。常见方法如下:
xuk.png

获取类加载器的途径

bpw.png
说明:此处的clazz指定是类的实例对象

自定义类加载器

JVM中除根类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写ClassLoader所包含的方法来实现自定义的类加载器。ClassLoader中包含了大量的protected方法——这些方法都可被子类重写。常见方法如下:

方法名 描述


loadClass(String name, boolean resolve)
为ClassLoader的入口点,根据指定名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。 通过重写这两个方法之一可以实现自定义的ClassLoader。通常推荐重写findClass()方法。loadClass()方法的执行步骤如下:
① 用findLoadedClass(String) 来检查是否已经加载类,如果已经加载则直接返回;
② 缓存区中未找到已经加载的该类,则,在父类加载器上调用loadClass()方法。如果父类加载器为null,则使用根类加载器来加载;
③ 调用findClass(String)方法查找类。
从上面步骤中可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更为复杂。
findClass(String name) 根据指定名称来查找类。
Class defineClass(String name, byte[] b, int off, int len) 负责将指定类的字节码文件(即Class文件,如Hello.class)读入字节数组byte[] b内,并把它转换为Class对象,该字节码文件可以来源于文件、网络等。
defineClass() 管理JVM的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等。不过不用担心,程序员无须重写该方法。实际上该方法是final的,即使想重写也没有机会。
findSystemClass(String name) 从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass()方法将原始字节转换成Class对象,以将该文件转换成类。
static getSystemClassLoader() 静态方法,用于返回系统类加载器。
getParent() 获取该类加载器的父类加载器。
resolveClass(Class<?> c) 链接指定的类。类加载器可以使用此方法来链接类c。无须理会关于此方法的太多细节。
findLoadedClass(String name) 如果此Java虚拟机已加载了名为name的类,则直接返回该类对应的Class实例,否则返回null。该方法是Java类加载缓存机制的体现。

实现步骤
1. 创建一个类继承ClassLoader抽象类
2. 重写findClass()方法
3. 在findClass()方法中调用defineClass()

示例

以下代码是一个自定义的ClassLoader,该ClassLoader通过重写findClass()方法来实现自定义的类加载机制。这个ClassLoader可以在加载类之前先编译该类的源文件,从而实现运行Java之前先编译该程序的目标,这样便可通过该ClassLoader直接运行Java源文件。

字节码&类加载 - 图7
字节码&类加载 - 图8

上面程序中的粗体字代码重写了findClass()方法,通过重写该方法就可以实现自定义的类加载机制。在本类的findClass()方法中先检查需要加载类的Class文件是否存在,如果不存在则先编译源文件,再调用ClassLoader的defineClass()方法来加载这个Class文件,并生成相应的Class对象。
注意:上面程序的main()方法中的粗体字代码使用了反射来调用方法。

接下来可以随意提供一个简单的主类,该主类无须编译就可以使用上面的CompileClass Loader来运行它。
字节码&类加载 - 图9

无须编译该Hello.java,可以直接使用如下命令来运行该Hello.java程序。
字节码&类加载 - 图10

运行结果如下:
字节码&类加载 - 图11

示例程序提供的类加载器功能比较简单,仅仅提供了在运行之前先编译Java源文件的功能。实际上,使用自定义的类加载器,可以实现如下常见功能:
➢ 执行代码前自动验证数字签名。
➢ 根据用户提供的密码解密代码,从而可以实现代码混淆器来避免反编译*.class文件。
➢ 根据用户需求来动态地加载类。
➢ 根据应用需求把其他数据以字节码的形式加载到应用中。

  • URL ClassLoader类

Java为ClassLoader提供了一个URLClassLoader实现类,该类也是系统类加载器和扩展类加载器的父类(此处的父类,就是指类与类之间的继承关系)。URLClassLoader功能比较强大,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。

在应用程序中可以直接使用URLClassLoader加载类,URLClassLoader类提供了如下两个构造器:

  1. URLClassLoader(URL[ ] urls):

使用默认的父类加载器创建一个ClassLoader对象,该对象将从urls所指定的系列路径来查询并加载类。

  1. URLClassLoader(URL[ ] urls, ClassLoader parent):

使用指定的父类加载器创建一个ClassLoader对象,其他功能与前一个构造器相同。

一旦得到了URLClassLoader对象之后,就可以调用该对象的loadClass()方法来加载指定类。

示例:
下面程序示范了如何直接从文件系统中加载MySQL驱动,并使用该驱动来获取数据库连接。通过这种方式来获取数据库连接,可以无须将MySQL驱动添加到CLASSPATH环境变量中。
字节码&类加载 - 图12

  1. 上面程序中的前两行粗体字代码创建了一个URLClassLoader对象,该对象使用默认的父类加载器,该类加载器的类加载路径是当前路径下的mysql-connector-java-8.0.13.jar文件,将MySQL驱动复制到该路径下,这样保证该ClassLoader可以正常加载到com.mysql.jdbc.Driver类。

程序的第三行粗体字代码使用ClassLoader的loadClass()加载指定类,并调用Class对象的newInstance()方法创建了一个该类的默认实例——也就是得到com.mysql.jdbc.Driver类的对象,当然该对象的实现类实现了java.sql.Driver接口,所以程序将其强制类型转换为Driver。程序的最后一行粗体字代码通过Driver而不是DriverManager来获取数据库连接,关于Driver接口的用法读者可以自行查阅API文档。

正如前面所看到的,创建URLClassLoader时传入了一个URL数组参数,该ClassLoader就可以从这系列URL指定的资源中加载指定类,这里的URL可以以file:为前缀,表明从本地文件系统加载;可以以http:为前缀,表明从互联网通过HTTP访问来加载;也可以以ftp:为前缀,表明从互联网通过FTP访问来加载……功能非常强大。