原文链接 https://juejin.cn/post/7292955856545972275
JVM(Java虚拟机)的类加载主要分为以下几个阶段:加载、验证、准备、解析、初始化,以及卸载。
加载阶段,主要完成了以下三个步骤:
验证阶段是为了确保加载的类信息符合Java虚拟机的规范。这个过程要确定Class文件中的字节流代码是符合JVM规格的,主要完成以下检查:
在准备阶段,JVM为类静态变量分配存储空间(主要是类变量和静态变量)并设置默认初始化值。注意:这里的初始化值是指数据类型的初始值,而不是用户自定义的初始值。但是如果是 static final 的话,这里赋值的是 final 定义的值
解析阶段的主要任务是将类的符号引用(常量池中的符号)替换为内存中的直接引用。简单来说,就是将类似于字符串的引用地址,转换成实际内存地址。
在初始化阶段,JVM对类的变量进行赋值以及静态代码块的执行。与准备阶段的区别是,这里会将用户自定义的初始值进行赋值,同时执行静态代码块。
当一个类不再被使用时,JVM会将其卸载,回收内存空间。这一过程由垃圾回收(Garbage Collection)进行。
了解类加载阶段,我们还需要了解Java中的类加载器(ClassLoader)机制。主要有以下几类:
类加载器之间具有双亲委派(Parent Delegation)模型。简单来说,就是在加载一个类时,会首先委托父加载器进行加载。如果父加载器无法完成加载任务,子加载器才会尝试进行加载。这个机制保证了子类加载器不会重复加载已经被父类加载器加载过的类。
当需要加载一些JDK提供的扩展库时,扩展类加载器就会被用到。扩展类加载器主要负责加载JRE(Java Runtime Environment)扩展目录(jre/lib/ext)下的类库,例如Java的一些扩展API功能。
扩展类加载器的使用场景包括但不限于:
需要注意的是,使用扩展类加载器加载的扩展库在程序中必须遵循双亲委派模型。另外,应谨慎使用扩展类加载器,因为扩展库通常是全局共享的,可能导致类库版本冲突等问题。如果有需要,也可以考虑使用自定义类加载器进行加载。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
Java提供了获取类的类加载器的方法(getClassLoader()
),而不是直接实例化一个类加载器来加载类,主要有以下原因:
综上所述,Java为类提供了获取它的类加载器的方法,以维护类加载器层次结构,避免类加载冲突,提高资源和内存管理效率,以及确保程序的安全性。与直接实例化一个类加载器相比,这种方法对于处理类加载和管理方面更为灵活且高效。
是的,类加载器具有一个称为“类缓存”的内部数据结构,用于存储已经加载过的类。当加载一个新类时,类加载器会首先查找类缓存以确定是否已经加载过这个类。如果已经加载过这个类,类加载器就会直接从类缓存中返回这个类。这样做可以避免重复加载类,提高加载性能。
不一定。Java程序中每种类加载器类型通常只有一个实例,特别是对于引导类加载器、扩展类加载器和应用程序类加载器。但这并不是一个确定的规则。有时,你可能需要创建多个自定义类加载器的实例,以满足特定的需求(例如,加载不同路径或不同版本的类库等)。
主动加载一个类通常用于以下几个场景:
总之,当你需要控制类加载的时间和顺序,执行静态代码块,或者动态加载某个类时,需要主动加载类。
加载一个类并不意味着一定会实例化这个类。类加载是指将类的.class文件(字节码文件)读取到内存中,并生成相应的 Class
对象。实例化类是指通过调用类的构造器创建类的对象。
加载类主要涉及类的字节码文件的读取、解析和验证等过程。而实例化类涉及类的对象的创建、字段的初始化以及构造方法的调用等过程。加载类主要由类加载器(ClassLoader)负责,实例化类由 Java 虚拟机负责。
一个类被加载之后,它的产物是一个与该类对应的 Class
对象。这个 Class
对象包含了类的元数据(如类名、方法、字段等信息),同时也是类对象的创建和类的静态方法、静态字段的访问入口。你可以通过 Class.forName("com.example.MyClass")
或者 ClassLoader.loadClass("com.example.MyClass")
等方法获取类的 Class
对象,然后通过这个 Class
对象来实例化类、访问静态方法及静态字段等操作。
Class.forName(String)
和 ClassLoader.loadClass(String)
都用于加载类,但它们有以下区别:
Class.forName(String)
在加载类之后会立即对类进行初始化(执行静态代码块和静态变量的赋值操作),而 ClassLoader.loadClass(String)
方法只会加载类,但不会执行初始化操作,只有在实际使用时,才会触发类的初始化。Class.forName(String)
方法默认使用调用者所在类的类加载器,可以通过 Class.forName(String, boolean, ClassLoader)
指定类加载器。ClassLoader.loadClass(String)
由调用者显式指定类加载器。使用场景:
Class.forName(String)
方法。ClassLoader.loadClass(String)
方法。是的,类加载的最后一步流程是初始化。但ClassLoader.loadClass(String)方法在加载类的时候,不会立即执行类的初始化操作。
Java的类加载器在加载类时,会遵循"延迟加载"(Lazy Loading)原则,即只在实际需要时才进行加载和初始化。ClassLoader.loadClass(String) 方法在加载类时,只会完成加载、验证、准备、解析这四个阶段,而类的初始化操作会被推迟到实际使用时进行。
当你用ClassLoader.loadClass(String)加载一个类时,只有在以下情况之一发生时,才会触发类的初始化:
可以在子线程中加载类。类加载器有一个内部机制来确保多线程环境下类加载的线程安全。当一个类被加载时,类加载器会获取一个与请求的类关联的内部锁。这意味着,当多个线程试图加载相同的类时,只有一个线程能够获得锁并进行加载。其他线程将等待,直到类加载完成并释放锁。因此,类加载过程是线程安全的。
类加载过程的初始化(Initialization
)主要包括执行静态代码块和为静态变量赋值。在这个阶段,JVM会确保类的静态代码块只被执行一次,静态变量只会被赋值一次。类加载过程的初始化主要涉及类级别的操作,且只会在类第一次使用时进行。
实例化过程的初始化是指创建类的对象(实例)时,调用类的构造器和为对象的实例变量赋值。实例化过程的初始化是针对对象(实例)级别的操作,对于每个新创建的对象,都会进行实例化过程的初始化。
总结一下,类加载过程的初始化关注类的静态部分(静态变量、静态代码块),而实例化过程的初始化关注类的对象部分(实例变量、构造器)。
class A {
public static int value = B.value + 1;
}
class B {
public static int value = A.value + 1;
}
以上代码并不会导致循环依赖,A.value 输出 2
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。