什么是Java的类加载机制
Java 虚拟机一般使用 Java 类的流程为:首先将开发者编写的 Java 源代码(.java文件)编译成 Java 字节码(.class文件),然后类加载器会读取这个 .class 文件,并转换成 java.lang.Class 的实例。有了该 Class 实例后,Java 虚拟机可以利用 newInstance 之类的方法创建其真正对象了。
简单来说类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
堆上的class对象和方法区有什么关系
堆上的class对象和方法区之间有密切的关系。在Java虚拟机中,每个类只有一个class对象,该对象被存储在堆中。同时,该对象中包含了类的所有信息,包括类的名称、父类、接口、成员变量和方法等等。这些信息都存储在方法区中。因此,可以说堆上的class对象和方法区是相互依存的关系。堆上的class对象是方法区中类信息的实体化表示。方法区中的类信息需要通过堆上的class对象来获取,而堆上的class对象依赖于方法区中的类信息来构建完整的类对象。
这个class对象是做什么的
在Java中,每个类的定义都会被编译成一个class文件,这个class文件中包含了该类的基本信息,例如类的名字、方法、变量等。
class对象是一个Java虚拟机在运行时创建的表示类的实例对象,它包含了一个类的完整信息,如类的构造方法、成员变量、成员方法、注解、访问修饰符等等。
class对象在Java程序中被广泛使用。特别是在反射中,class对象被用来获取某个类的信息、创建该类的对象、调用该类的方法等等。通过class对象,Java程序可以在运行时动态地获取类的信息和创建对象实例。因此,class对象在Java程序开发中具有非常重要的作用。
方法区是做什么的
方法区是Java虚拟机的一个重要组成部分,它用来存储类的信息、常量、静态变量等数据。具体来说,方法区的作用包括以下几个方面:
-
存储类信息:方法区中存储了类的定义信息,包括类的名称、继承信息、接口信息、方法和变量等信息,这些信息在JVM启动时就已经被加载到方法区中。
-
存储常量池:方法区中还存储了每个类的常量池,用于存储字面量和符号引用等信息。
-
存储静态变量:在程序运行时,所有的静态变量和类变量都被存储在方法区中,这些变量的值可以在整个程序运行过程中被访问和修改。
-
存储编译后的代码:在JVM解析Java类文件时,会将其中的字节码存储在方法区中,并在需要执行时将其加载到内存中执行。
总之,方法区是Java虚拟机的一部分,它用于存储类的定义、常量、静态变量和编译后的代码等信息。这些信息在程序运行期间都可以被访问和使用。了解方法区的作用可以帮助我们更好地理解Java虚拟机的工作原理。
类的生命周期
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分统称为连接。
类加载过程
Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?
系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。
加载
类加载的第一个阶段,在这个阶段,虚拟机主要完成以下3件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将字节流中所代表的静态数据结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表该类的java.lang.Class对象,作为方法区中这个类各种数据的访问入口。
解释一下这句话:在内存中生成一个代表该类的java.lang.Class对象,作为方法区中这个类各种数据的访问入口。
在Java中,每个类都有一个与之对应的java.lang.Class对象,该对象代表了该类的描述信息和运行时状态。这个Class对象是在解析和加载类文件时在内存中生成的,并作为一个入口,提供了访问方法区中这个类各种数据的接口。
具体来说,Class对象在方法区中存储了该类的信息,包括类的名称、父类、接口、成员变量和方法等。创建了Class对象之后,程序就可以通过这个对象来访问方法区中这个类的各种数据,包括静态变量、方法、构造函数等。此外,在反射中,也可以通过Class对象来获取和修改类的信息,实现动态生成对象和执行方法的功能。
总之,通过在内存中生成一个代表该类的java.lang.Class对象,程序可以访问方法区中这个类的各种数据和信息,从而实现了一种非常重要的功能,即反射。在Java程序开发中,反射是一种常见的编程技术,它可以使程序具有更大的灵活性和可扩展性。
验证
连接阶段的第一步,该阶段主要目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上看,这个阶段大致会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些类变量所使用的内存都将在方法区中分配。
对于该阶段,需要注意以下几点:
- 这个阶段进行内存分配的仅包括类变量( Class Variables ,即静态变量,被 static 关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随对着象一起分配在Java堆中。
- 从概念上讲,类变量所使用的内存都应当在方法区中进行分配。
在JDK 1.7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 1.7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。
具体细节请移步观看我的另一篇博客——JVM面试题详解系列——JVM内存区域详解。 - 这里所设置的初始值通常情况下是数据类型默认的零值(如 0、0L、null、false 等),比如我们定义了public static int test = 6 ,那么 test 变量在准备阶段的初始值就是 0 而不是 6(初始化阶段才会赋值)。特殊情况:如果给 test 变量加上了 final 关键字public static final int test = 6 ,那么准备阶段 test 的值就被赋值为 6。
解析
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。解析动作主要针对接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号应用进行。
在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过 解析操作 符号引用就可以转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。
这些符号引用是什么?什么时候生成的?
符号引用是指在Java类文件中以字符串形式出现的、用于描述被引用的目标(如类、方法、字段)的符号。该符号代表了目标的名称、类型和相关描述符。具体来说,在Java类文件中出现的符号引用包括以下几种:
-
类的符号引用:表示类的名称和路径。
-
字段符号引用:表示字段的名称和类型。
-
方法符号引用:表示方法的名称、参数类型和返回类型。
符号引用的生成是在编译Java源代码时完成的,编译器根据源代码中出现的类型和名称信息生成符号引用,并将其保存到类文件的常量池中。当类文件被加载到JVM中时,VM会根据常量池中的符号引用信息查找类与方法执行,并将符号引用转化成直接引用。
总之,符号引用是Java类文件中一种重要的元数据,在Java源代码编译为字节码文件时生成。在类文件加载到JVM时,常量池中的符号引用会被解析成直接引用,使得程序可以正确地执行类和方法。
符号引用
符号引用是一组用来描述所引用目标的符号,属于编译原理方面的概念,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。直接引用可以帮助程序直接定位到所需的对象。
符号引用和直接引用的详细介绍请移步观看我的另一篇博客——JVM面试题详解系列——Java中几种常量池的区分。
初始化
初始化阶段是执行类构造器 <clinit> ()
方法的过程,是类加载的最后一步,到了这一步,Java虚拟机才开始真正执行类中定义的 Java 程序代码(字节码)。在准备阶段,类变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序制定的主观计划去初始化类变量和其他资源。需要注意的是,<clinit>()
不是程序员在 Java 代码中直接编写的方法,而是由 Javac 编译器自动生成的。
介绍一下 <clinit> ()
方法
<clinit> ()
是Java中的一个特殊方法,它是类的初始化方法,也称为类构造器,用于初始化类的静态变量和静态代码块。该方法在JVM加载一个类时自动被调用,实际上是编译器在编译阶段由编译器自动为类添加的静态方法。
在类的<clinit> ()
方法中,可以包含类的静态变量、静态代码块和静态方法的初始化代码。这些代码会在类加载过程中执行,保证了类的静态变量和静态代码块的正确初始化。
<clinit> ()
方法由编译器自动生成,其代码由静态字段和静态代码块组成,按照在源代码中出现的顺序生成,因此它的执行顺序是严格按照静态代码出现的顺序执行的。
需要注意的是,类的<clinit> ()
方法是线程安全的,它会在类被加载的过程中被调用一次,保证了类的静态成员的正确初始化。
总之,<clinit> ()
是Java中的一个特殊方法,用于初始化类的静态变量、静态代码块和静态方法。该方法由编译器自动生成,保证了类的静态成员在类加载时正确的初始化,是Java中一个很重要的机制。
卸载
卸载类即该类的 Class 对象被 GC。
在Java虚拟机(JVM)中,卸载类需要满足以下三个要求:
-
该类的所有实例对象都已经被GC回收:在垃圾回收期间,JVM会对堆上的对象进行回收。只有当一个类的所有实例对象都已经被GC回收时,才能卸载该类。
-
该类的Class对象不存在引用:在Java虚拟机中,每个类都有一个对应的Class对象。如果该类的Class对象存在引用,比如被其他类的成员变量、方法等持有引用,就不能卸载该类。
-
该类的所有Classloader都已经被GC回收:在Java虚拟机中,每个类的加载器在加载类后会被持有引用。如果该类的加载器存在引用,就不能卸载该类。
只有当这三个要求全部满足时,JVM才能卸载一个类。这个过程是由垃圾回收器自动执行的,无法手动触发。
值得注意的是,类的卸载是一个非常罕见的事件。在大多数情况下,卸载类不是必须的,因为JVM会在内存不足时触发垃圾回收,回收不再使用的对象。只有在特殊的应用场景才需要考虑卸载类的问题,例如在OSGi、动态代码生成等高级应用场合。
简化版:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的
ClassLoader
已经被回收。 - 该类对应的
Java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。