当前位置: 首页 > news >正文

JVM字节码与类的加载(一):类的加载过程详解

一、类的加载过程详解

我们知道class文件是存放在磁盘上的,如果想要在JVM中使用class文件,需要将其加载至内存当中。

前面我们已经讲解了class文件的结构,本章将详细介绍class文件加载到内存中的过程。

(一)概述

在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由JVM预先定义,可以直接被用户使用,引用数据类型则需要执行类的加载才可以被用户使用。

Java虚拟机规范中规定,class文件加载到内存,再到类卸载出内存会经历7个阶段,分别是加载、验证、准备、解析、初始化、使用和卸载,其中,验证、准备和解析3个阶段统称为链接(Linking),整个过程称为类的生命周期,如图所示。

在这里插入图片描述

(二)加载(Loading)阶段

1.加载完成的操作

所谓加载,简而言之就是将Java类的class文件加载到机器内存中,并在内存中构建出Java类的原型,也就是类模板对象。

所谓类模板对象,其实就是Java类在JVM内存中的一个快照,JVM将从class文件中解析出的常量池、类字段、类方法等信息存储到类模板对象中

JVM在运行期可以通过类模板对象获取Java类中的任意信息,能够访问Java类中的成员变量,也能调用Java方法,反射机制便是基于这一基础,如果JVM没有将Java类的声明信息存储起来,则JVM在运行期也无法使用反射。

在加载类时,JVM必须完成以下3件事情。

  • (1)通过类的全名,获取类的二进制数据流。
  • (2)解析类的二进制数据流为方法区内的数据结构(Java类模型)​。
  • (3)创建java.lang.Class类的实例,作为方法区中访问类数据的入口。
(1)定义一个简单的 Java 类(作为被加载的目标)

首先创建一个Person类,它包含字段、方法等信息,这些信息会被存储到类模板对象中:

// Person.java
public class Person {// 类字段信息private String name;private int age;// 构造方法public Person() {}public Person(String name, int age) {this.name = name;this.age = age;}// 类方法信息public void sayHello() {System.out.println("Hello, I'm " + name);}public String getName() { return name; }public int getAge() { return age; }
}
(2) 演示类加载与Class对象的获取

类加载的最终结果是在 JVM 中生成java.lang.Class对象(类模板对象的访问入口)。以下代码展示获取Class对象的三种常见方式,这些方式本质上都是触发类加载并获取类模板对象的入口:

// ClassLoadingDemo.java
public class ClassLoadingDemo {public static void main(String[] args) throws Exception {// 方式1:通过Class.forName("全类名")获取(会触发类加载)Class<?> clazz1 = Class.forName("Person");System.out.println("通过Class.forName()获取的Class对象:" + clazz1);// 方式2:通过"类名.class"获取(不会触发类初始化,但会完成加载)Class<?> clazz2 = Person.class;System.out.println("通过类名.class获取的Class对象:" + clazz2);// 方式3:通过对象.getClass()获取(对象所属类已加载)Person person = new Person("Alice", 25);Class<?> clazz3 = person.getClass();System.out.println("通过对象.getClass()获取的Class对象:" + clazz3);// 验证:同一个类的Class对象是唯一的(对应同一个类模板对象)System.out.println("clazz1 == clazz2: " + (clazz1 == clazz2)); // trueSystem.out.println("clazz1 == clazz3: " + (clazz1 == clazz3)); // true}
}

运行结果如下:

通过Class.forName()获取的Class对象:class Person
通过类名.class获取的Class对象:class Person
通过对象.getClass()获取的Class对象:class Person
clazz1 == clazz2: true
clazz1 == clazz3: true

三种方式获取的Class对象是同一个,证明 JVM 对每个类只生成一个类模板对象,Class对象是访问这个模板的唯一入口(对应类加载步骤 3)。

(3) 反射机制基于类模板对象工作

反射的核心是通过Class对象访问类模板中存储的信息(常量池、字段、方法等)。以下代码展示如何通过反射获取类信息并操作:

// ReflectionDemo.java
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Constructor;public class ReflectionDemo {public static void main(String[] args) throws Exception {// 获取Class对象(类模板的入口)Class<?> personClass = Class.forName("Person");// 1. 访问类的基本信息(来自类模板中的常量池)System.out.println("类全名:" + personClass.getName()); // 输出:PersonSystem.out.println("类加载器:" + personClass.getClassLoader()); // 输出加载该类的类加载器// 2. 访问类的字段信息(来自类模板中的字段表)System.out.println("\n--- 字段信息 ---");Field[] fields = personClass.getDeclaredFields(); // 获取所有声明的字段for (Field field : fields) {System.out.println("字段名:" + field.getName() + ",类型:" + field.getType());}// 3. 访问类的方法信息(来自类模板中的方法表)System.out.println("\n--- 方法信息 ---");Method[] methods = personClass.getDeclaredMethods(); // 获取所有声明的方法for (Method method : methods) {System.out.println("方法名:" + method.getName() + ",返回值类型:" + method.getReturnType());}// 4. 通过反射创建对象并调用方法(基于类模板中的构造器和方法信息)System.out.println("\n--- 反射操作示例 ---");// 利用构造器创建对象Constructor<?> constructor = personClass.getConstructor(String.class, int.class);Person person = (Person) constructor.newInstance("Bob", 30);// 调用方法Method sayHelloMethod = personClass.getMethod("sayHello");sayHelloMethod.invoke(person); // 输出:Hello, I'm Bob// 访问私有字段(需要设置可访问性)Field nameField = personClass.getDeclaredField("name");nameField.setAccessible(true); // 突破访问权限System.out.println("反射获取name字段值:" + nameField.get(person)); // 输出:Bob}
}

反射能获取Person类的字段、方法等信息,本质是通过Class对象访问 JVM 在类加载时存储的类模板数据。如果没有类模板对象(即类未加载),这些操作都无法完成。

(4) 自定义类加载器(模拟类加载步骤 1 和 2)

类加载的第一步是 “通过类全名获取二进制数据流”,第二步是 “解析为方法区的数据结构”。以下自定义类加载器模拟这两个步骤:

// CustomClassLoader.java
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;public class CustomClassLoader extends ClassLoader {@Overrideprotected Class<?> findClass(String className) throws ClassNotFoundException {try {// 步骤1:通过类全名获取二进制数据流(这里从本地文件读取.class字节)String path = className.replace(".", "/") + ".class";InputStream is = new FileInputStream(path);ByteArrayOutputStream baos = new ByteArrayOutputStream();int b;while ((b = is.read()) != -1) {baos.write(b);}byte[] classData = baos.toByteArray(); // 类的二进制数据流// 步骤2:将二进制数据流解析为方法区中的类数据结构(JVM内部完成)// defineClass方法会将字节数组转换为方法区的类模板return defineClass(className, classData, 0, classData.length);} catch (Exception e) {throw new ClassNotFoundException();}}public static void main(String[] args) throws Exception {// 使用自定义类加载器加载Person类CustomClassLoader loader = new CustomClassLoader();Class<?> clazz = loader.loadClass("Person");System.out.println("自定义类加载器加载的类:" + clazz);System.out.println("类加载器:" + clazz.getClassLoader()); // 输出自定义类加载器}
}

findClass方法中,classData的获取对应 “步骤 1:获取二进制数据流”;defineClass方法对应 “步骤 2:解析为方法区数据结构”。之后 JVM 会自动完成 “步骤 3:创建Class对象”。

2.二进制流的获取方式

JVM可以通过多种途径产生或获得类的二进制数据流,下面列举了常见的几种方式。

  • (1)通过文件系统读入一个后缀为.class的文件(最常见)​。
  • (2)读入jar、zip等归档数据包,提取类文件。
  • (3)事先存放在数据库中的类的二进制数据。
  • (4)使用类似于HTTP之类的协议通过网络加载。
  • (5)在运行时生成一段Class的二进制信息。

在获取到类的二进制信息后,JVM就会处理这些数据,并最终转为一个java.lang.Class的实例。

如果输入数据不是JVM规范的class文件的结构,则会抛出“ClassFormatError”异常。

(1) 通过文件系统读入.class文件

这是最常见的方式,自定义类加载器从文件系统加载.class文件:

import java.io.*;public class FileSystemClassLoader extends ClassLoader {private String rootDir;public FileSystemClassLoader(String rootDir) {this.rootDir = rootDir;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 1. 从文件系统读取.class文件byte[] classData = getClassData(name);if (classData == null) {throw new ClassNotFoundException();}// 2. 将字节数组转换为Class对象return defineClass(name, classData, 0, classData.length);}private byte[] getClassData(String className) {String path = rootDir + File.separator + className.replace('.', File.separatorChar) + ".class";try (InputStream is = new FileInputStream(path);ByteArrayOutputStream baos = new ByteArrayOutputStream()) {int bufferSize = 4096;byte[] buffer = new byte[bufferSize];int bytesNumRead;// 读取.class文件的二进制数据while ((bytesNumRead = is.read(buffer)) != -1) {baos.write(buffer, 0, bytesNumRead);}return baos.toByteArray();} catch (IOException e) {e.printStackTrace();}return null;}public static void main(String[] args) throws Exception {// 假设Person.class文件在指定目录下FileSystemClassLoader loader = new FileSystemClassLoader("./classes");Class<?> clazz = loader.loadClass("com.example.Person");System.out.println("加载的类: " + clazz.getName());System.out.println("类加载器: " + clazz.getClassLoader());}
}
(2)从jar/zip归档中提取类文件

演示从JAR包中读取类的二进制数据:

import java.io.*;
import java.util.jar.JarFile;
import java.util.jar.JarEntry;public class JarClassLoader extends ClassLoader {private String jarPath;public JarClassLoader(String jarPath) {this.jarPath = jarPath;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try (JarFile jarFile = new JarFile(jarPath)) {// 1. 构建JAR中的类路径String entryName = name.replace('.', '/') + ".class";JarEntry entry = jarFile.getJarEntry(entryName);if (entry == null) {throw new ClassNotFoundException("类 " + name + " 不在JAR文件中");}// 2. 从JAR中读取类的二进制数据try (InputStream is = jarFile.getInputStream(entry);ByteArrayOutputStream baos = new ByteArrayOutputStream()) {byte[] buffer = new byte[4096];int bytesRead;while ((bytesRead = is.read(buffer)) != -1) {baos.write(buffer, 0, bytesRead);}byte[] classData = baos.toByteArray();return defineClass(name, classData, 0, classData.length);}} catch (IOException e) {throw new ClassNotFoundException("从JAR加载类失败", e);}}public static void main(String[] args) throws Exception {// 从指定的JAR文件加载类JarClassLoader loader = new JarClassLoader("app.jar");Class<?> clazz = loader.loadClass("com.example.utils.StringUtils");System.out.println("从JAR加载的类: " + clazz.getName());}
}
(3)从数据库读取类的二进制数据

模拟从数据库获取类的二进制数据(实际项目中会使用JDBC):

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.sql.*;public class DatabaseClassLoader extends ClassLoader {private String dbUrl;private String username;private String password;public DatabaseClassLoader(String dbUrl, String username, String password) {this.dbUrl = dbUrl;this.username = username;this.password = password;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {Connection conn = null;PreparedStatement stmt = null;ResultSet rs = null;try {// 1. 连接数据库Class.forName("com.mysql.cj.jdbc.Driver");conn = DriverManager.getConnection(dbUrl, username, password);// 2. 查询类的二进制数据stmt = conn.prepareStatement("SELECT class_data FROM classes WHERE class_name = ?");stmt.setString(1, name);rs = stmt.executeQuery();if (rs.next()) {// 3. 读取二进制数据try (InputStream is = rs.getBinaryStream("class_data");ByteArrayOutputStream baos = new ByteArrayOutputStream()) {byte[] buffer = new byte[4096];int bytesRead;while ((bytesRead = is.read(buffer)) != -1) {baos.write(buffer, 0, bytesRead);}byte[] classData = baos.toByteArray();return defineClass(name, classData, 0, classData.length);}}} catch (Exception e) {throw new ClassNotFoundException("从数据库加载类失败", e);} finally {// 关闭资源try { if (rs != null) rs.close(); } catch (SQLException e) {}try { if (stmt != null) stmt.close(); } catch (SQLException e) {}try { if (conn != null) conn.close(); } catch (SQLException e) {}}throw new ClassNotFoundException("类 " + name + " 不在数据库中");}public static void main(String[] args) throws Exception {// 从数据库加载类DatabaseClassLoader loader = new DatabaseClassLoader("jdbc:mysql://localhost:3306/classdb", "user", "password");Class<?> clazz = loader.loadClass("com.example.service.UserService");System.out.println("从数据库加载的类: " + clazz.getName());}
}
(4) 通过网络加载类

使用URLClassLoader通过HTTP协议从网络加载类:

import java.net.URL;
import java.net.URLClassLoader;public class NetworkClassLoaderDemo {public static void main(String[] args) throws Exception {// 1. 创建URL数组,指向网络上的类资源URL[] urls = new URL[] {new URL("http://example.com/classes/")};// 2. 创建URLClassLoader加载网络上的类URLClassLoader networkLoader = new URLClassLoader(urls);// 3. 加载网络上的类Class<?> remoteClass = networkLoader.loadClass("com.example.RemoteClass");System.out.println("从网络加载的类: " + remoteClass.getName());System.out.println("网络类加载器: " + remoteClass.getClassLoader());// 4. 实例化并使用远程类Object instance = remoteClass.getDeclaredConstructor().newInstance();remoteClass.getMethod("doSomething").invoke(instance);}
}
(5) 运行时生成Class二进制信息

使用ASM库在运行时动态生成类的二进制数据:

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;public class DynamicClassGenerator {public static Class<?> generateClass() {// 1. 创建ClassWriter用于生成类的字节码ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);// 2. 定义类的基本信息:public class DynamicClasscw.visit(Opcodes.V11, Opcodes.ACC_PUBLIC, "DynamicClass", null, "java/lang/Object", null);// 3. 生成默认构造函数MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);mv.visitCode();mv.visitVarInsn(Opcodes.ALOAD, 0);mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);mv.visitInsn(Opcodes.RETURN);mv.visitMaxs(1, 1);mv.visitEnd();// 4. 生成一个简单的方法mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "sayHello", "()V", null, null);mv.visitCode();mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("Hello from dynamically generated class!");mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);mv.visitInsn(Opcodes.RETURN);mv.visitMaxs(2, 1);mv.visitEnd();cw.visitEnd();// 5. 获取生成的类的字节数组byte[] classData = cw.toByteArray();// 6. 使用类加载器加载生成的类return new ClassLoader() {protected Class<?> findClass(String name) throws ClassNotFoundException {return defineClass(name, classData, 0, classData.length);}}.loadClass("DynamicClass");}public static void main(String[] args) throws Exception {// 生成并加载动态类Class<?> dynamicClass = generateClass();System.out.println("动态生成的类: " + dynamicClass.getName());// 实例化并调用方法Object instance = dynamicClass.getDeclaredConstructor().newInstance();dynamicClass.getMethod("sayHello").invoke(instance);}
}

注意:运行此示例需要添加ASM库依赖。

(6) 演示ClassFormatError异常

当输入的数据不符合class文件规范时,JVM会抛出ClassFormatError

public class InvalidClassFormatDemo {public static void main(String[] args) {// 1. 创建一个无效的类二进制数据(不是有效的class文件格式)byte[] invalidClassData = "这不是一个有效的class文件".getBytes();// 2. 尝试加载无效的类数据ClassLoader loader = new ClassLoader() {@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {// 尝试将无效数据转换为Class对象return defineClass(name, invalidClassData, 0, invalidClassData.length);} catch (ClassFormatError e) {throw new ClassNotFoundException("无效的类格式", e);}}};try {loader.loadClass("InvalidClass");} catch (ClassNotFoundException e) {System.out.println("加载失败: " + e.getMessage());e.printStackTrace();// 会输出: java.lang.ClassFormatError: Incompatible magic value 2303745250 in class file InvalidClass}}
}

运行结果会显示ClassFormatError,因为我们尝试加载的是普通字符串而非有效的class文件格式。

3.类模型与Class实例的位置

(1)类模型的位置

加载的类在JVM中创建相应的类结构,类结构会存储在方法区中。

(2)Class实例的位置

类加载器将class文件加载至方法区后,会在堆中创建一个Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象。类模型和Class实例的位置对应关系如图所示。

在这里插入图片描述
外部可以通过访问代表Order类的Class对象来获取Order类的数据结构。

java.lang.Class类的构造方法是私有的,只有JVM能够创建。

java.lang.Class实例是访问类型元数据的入口,也是实现反射的关键数据。

通过Class类提供的接口,可以获得目标类所关联的class文件中具体的数据结构、方法、字段等信息。

如代码清单所示,展示了如何通过java.lang.Class类获取方法信息。

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;/*** 通过 Class 类,获得 java.lang.String 类的所有方法信息,并打印方法访问标识符、描述符*/
public class LoadingTest {public static void main(String[] args) {try {Class clazz = Class.forName("java.lang.String");// 获取当前运行时类声明的所有方法Method[] ms = clazz.getDeclaredMethods();for (Method m : ms) {// 获取方法的修饰符String mod = Modifier.toString(m.getModifiers());System.out.print(mod + " ");// 获取方法的返回值类型String returnType = m.getReturnType().getSimpleName();System.out.print(returnType + " ");// 获取方法名System.out.print(m.getName() + "(");// 获取方法的参数列表Class<?>[] ps = m.getParameterTypes();if (ps.length == 0) System.out.print(')');for (int i = 0; i < ps.length; i++) {char end = (i == ps.length - 1) ? ')' : ',';// 获取参数的类型System.out.print(ps[i].getSimpleName() + end);}System.out.println();}} catch (ClassNotFoundException e) {e.printStackTrace();}}
}

通过上面的代码可以直接获取到String类的方法信息,运行结果如下。

public boolean equals(Object)
public String toString()
public int hashCode()
public volatile int compareTo(Object)
public int compareTo(String)
public int indexOf(String,int)
static int indexOf(char[],int,int,String,int)
static int indexOf(char[],int,int,char[],int,int,int)
public int indexOf(int)
public int indexOf(String)
public int indexOf(int,int)
public static String valueOf(char)
public static String valueOf(Object)
public static String valueOf(boolean)
public static String valueOf(char[],int,int)
public static String valueOf(char[])
public static String valueOf(double)
public static String valueOf(float)
public static String valueOf(long)
public static String valueOf(int)
private static void checkBounds(byte[],int,int)
public int length()
public boolean isEmpty()
public char charAt(int)
public int codePointAt(int)
public int codePointBefore(int)
public int codePointCount(int,int)
public int offsetByCodePoints(int,int)
public void getChars(int,int,char[],int)void getChars(char[],int)
public byte[] getBytes()
public byte[] getBytes(String)
public void getBytes(int,int,byte[],int)
public byte[] getBytes(Charset)
public boolean contentEquals(StringBuffer)
public boolean contentEquals(CharSequence)
private boolean nonSyncContentEquals(AbstractStringBuilder)
public boolean equalsIgnoreCase(String)
public int compareToIgnoreCase(String)
public boolean regionMatches(int,String,int,int)
public boolean regionMatches(boolean,int,String,int,int)
public boolean startsWith(String)
public boolean startsWith(String,int)
public boolean endsWith(String)
private int indexOfSupplementary(int,int)
public int lastIndexOf(int,int)
static int lastIndexOf(char[],int,int,char[],int,int,int)
static int lastIndexOf(char[],int,int,String,int)
public int lastIndexOf(String,int)
public int lastIndexOf(int)
public int lastIndexOf(String)
private int lastIndexOfSupplementary(int,int)
public String substring(int)
public String substring(int,int)
public CharSequence subSequence(int,int)
public String concat(String)
public String replace(char,char)
public String replace(CharSequence,CharSequence)
public boolean matches(String)
public boolean contains(CharSequence)
public String replaceFirst(String,String)
public String replaceAll(String,String)
public String[] split(String,int)
public String[] split(String)
public static transient String join(CharSequence,CharSequence[])
public static String join(CharSequence,Iterable)
public String toLowerCase(Locale)
public String toLowerCase()
public String toUpperCase()
public String toUpperCase(Locale)
public String trim()
public char[] toCharArray()
public static transient String format(Locale,String,Object[])
public static transient String format(String,Object[])
public static String copyValueOf(char[],int,int)
public static String copyValueOf(char[])
public native String intern()

4.数组类的加载

创建数组类的情况稍微有些特殊,数组类由JVM在运行时根据需要直接创建,所以数组类没有对应的class文件,也就没有二进制形式,所以也就无法使用类加载器去创建数组类。

但数组的元素类型仍然需要依靠类加载器去创建。创建数组类的过程如下。

  • (1)如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组的元素类型,JVM使用指定的元素类型和数组维度来创建新的数组类。
  • (2)如果数组的元素是基本数据类型,比如int类型的数组,由于基本数据类型是由JVM预先定义的,所以也不需要类加载,只需要关注数组维度即可。

如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为public

以下代码示例将详细演示JVM创建数组类的特殊机制,包括引用类型数组、基本类型数组的创建过程,以及数组类的可访问性规则:

(1) 引用类型数组的创建(元素类型需类加载)

当数组的元素是引用类型时,JVM会先递归加载元素类型,再创建数组类(数组类由JVM直接生成,无对应class文件):

// 定义一个普通的引用类型
class Person {private String name;public Person(String name) {this.name = name;}
}public class ReferenceTypeArrayDemo {public static void main(String[] args) {// 创建Person类型的数组(引用类型数组)Person[] persons = new Person[3];// 1. 查看数组类的信息(JVM动态生成)Class<?> arrayClass = persons.getClass();System.out.println("数组类的名称: " + arrayClass.getName()); // 输出: [LPerson;System.out.println("数组类的类型: " + arrayClass.getSimpleName()); // 输出: Person[]// 2. 查看数组类的类加载器(与元素类型的类加载器一致,但数组类本身由JVM创建)ClassLoader arrayClassLoader = arrayClass.getClassLoader();ClassLoader elementClassLoader = Person.class.getClassLoader();System.out.println("数组类的类加载器: " + arrayClassLoader);System.out.println("元素类型(Person)的类加载器: " + elementClassLoader);System.out.println("是否为同一类加载器: " + (arrayClassLoader == elementClassLoader)); // true// 3. 验证数组类没有对应的class文件(通过尝试获取资源验证)String className = arrayClass.getName().replace('.', '/') + ".class";System.out.println("尝试查找数组类的class文件: " + className);System.out.println("资源是否存在: " + (arrayClassLoader.getResource(className) == null)); // true(不存在)}
}

运行结果说明

  • 数组类的名称为[LPerson;(JVM对引用类型数组的命名规则:[表示一维,L表示引用类型,后面跟元素类型全限定名)。
  • 数组类的类加载器与元素类型(Person)的类加载器相同,但这只是JVM的关联,数组类本身并非由类加载器创建(无对应class文件)。
  • 通过getResource方法验证,数组类没有对应的.class文件(返回null)。
(2) 基本类型数组的创建(无需类加载)

基本类型由JVM预定义,其数组类直接由JVM根据维度创建,无需加载元素类型:

public class PrimitiveTypeArrayDemo {public static void main(String[] args) {// 创建基本类型数组(int[], double[])int[] intArray = new int[5];double[] doubleArray = new double[10];// 1. 查看基本类型数组的类信息System.out.println("int[]的类名称: " + intArray.getClass().getName()); // 输出: [ISystem.out.println("double[]的类名称: " + doubleArray.getClass().getName()); // 输出: [D// 2. 基本类型数组的类加载器(通常为null,因基本类型由JVM内置)System.out.println("int[]的类加载器: " + intArray.getClass().getClassLoader()); // 输出: nullSystem.out.println("double[]的类加载器: " + doubleArray.getClass().getClassLoader()); // 输出: null// 3. 验证基本类型数组无对应class文件String intArrayClassName = intArray.getClass().getName().replace('.', '/') + ".class";System.out.println("int[]的class文件路径: " + intArrayClassName);System.out.println("int[]的class文件是否存在: " + (ClassLoader.getSystemResource(intArrayClassName) == null)); // true}
}

运行结果说明

  • 基本类型数组的类名称遵循JVM规则([I表示int数组,[D表示double数组,无L前缀)。
  • 类加载器为null,因为基本类型及其数组类是JVM内置的,无需类加载器参与。
  • 同样没有对应的.class文件,证明数组类由JVM直接创建。
(3) 数组类的可访问性规则
  • 引用类型数组的可访问性由元素类型决定;
  • 基本类型数组默认是public
public class ArrayAccessibilityDemo {// 定义一个私有内部类(可访问性为private)private static class PrivateElement {private String value;}public static void main(String[] args) {// 1. 引用类型数组(元素类型为private)PrivateElement[] privateArray = new PrivateElement[2];Class<?> privateArrayClass = privateArray.getClass();System.out.println("PrivateElement[]的可访问性(isPublic): " + privateArrayClass.isPublic()); // false(继承元素类型的private)// 2. 基本类型数组(默认public)int[] intArray = new int[3];System.out.println("int[]的可访问性(isPublic): " + intArray.getClass().isPublic()); // true// 3. 公共元素类型的数组(可访问性为public)Person[] publicArray = new Person[2]; // Person是公共类System.out.println("Person[]的可访问性(isPublic): " + publicArray.getClass().isPublic()); // true}
}// 公共类(可访问性为public)
class Person {}

运行结果说明

  • PrivateElement是私有类,其数组PrivateElement[]的可访问性也是非公共的(isPublic()返回false)。
  • 基本类型数组(如int[])默认是publicisPublic()返回true)。
  • 元素类型为公共类(如Person)时,其数组Person[]也是public
(4) 多维数组的创建(递归加载元素类型)

多维数组的创建遵循递归规则:先加载最内层元素类型,再逐层创建外层数组类。

public class MultiDimensionalArrayDemo {public static void main(String[] args) {// 创建二维引用类型数组(Person[][])Person[][] twoDArray = new Person[2][3];// 1. 查看多维数组的类信息Class<?> twoDClass = twoDArray.getClass();System.out.println("二维数组的类名称: " + twoDClass.getName()); // 输出: [[LPerson;(两个[表示二维)// 2. 查看数组的"元素类型"(二维数组的元素是一维数组)Class<?> componentType = twoDClass.getComponentType();System.out.println("二维数组的元素类型: " + componentType.getName()); // 输出: [LPerson;(一维Person数组)// 3. 递归查看最内层元素类型Class<?> innerComponentType = componentType.getComponentType();System.out.println("最内层元素类型: " + innerComponentType.getName()); // 输出: Person// 4. 验证多维数组的创建过程:先加载Person,再创建[LPerson;,最后创建[[LPerson;System.out.println("最内层元素类型是否已加载: " + (innerComponentType.getClassLoader() != null)); // true}
}

运行结果说明

  • 二维数组Person[][]的类名称是[[LPerson;[[表示二维)。
  • 多维数组的创建是递归的:先确保最内层元素类型(Person)已加载,再创建一维数组类([LPerson;),最后创建二维数组类([[LPerson;)。

(三)链接(Linking)阶段

1.链接阶段之验证(Verification)

类加载到机器内存后,就开始链接操作,验证是链接操作的第一步。

验证的目的是保证加载的字节码是合法、合理并符合规范的。

验证的步骤比较复杂,实际要验证的项目也很繁多,如图所示,验证的内容涵盖了类数据信息的格式检查、语义检查、字节码验证、符号引用验证,其中格式检查会和加载阶段一起执行。
在这里插入图片描述

验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中。

格式检查之外的验证操作将会在方法区中进行。如果不在链接阶段进行验证,那么class文件运行时依旧需要进行各种检查,虽然链接阶段的验证拖慢了加载速度,但是却提高了程序执行的速度,正所谓“磨刀不误砍柴工”​。

(1)格式检查

主要检查是否以魔数OxCAFEBABE开头,主版本和副版本号是否在当前JVM的支持范围内,数据中每一个项是否都拥有正确的长度等。

例如,编写一个简单的 Java 类并编译为ValidClass.class

public class ValidClass {}

使用二进制编辑器打开ValidClass.class,将文件开头的魔数0xCAFEBABE修改为其他值(如0xCAFEBAB0)。

修改魔数后,JVM 在格式检查阶段会直接抛出ClassFormatError,提示 “不兼容的魔数”,证明格式检查是类加载的第一道防线。

(2)字节码的语义检查

JVM会进行字节码的语义检查,但凡在语义上不符合规范的,JVM也不会验证通过,比如JVM会检查下面4项语义是否符合规范。

  • (1)是否所有的类都有父类的存在(Object除外)​。
  • (2)是否一些被定义为final的方法或者类被重写或继承了。
  • (3)非抽象类是否实现了所有抽象方法或者接口方法。
  • (4)是否存在不兼容的方法,比如方法的签名除了返回值不同,其他都一样。

Java 中所有类默认继承Object,若通过字节码强行创建一个无父类的类(非 Object),会触发语义检查失败。

正常 Java 代码无法写出这样的类(编译器会自动添加extends Object),但通过字节码工具修改后,加载时会报错:java.lang.VerifyError: Class does not have a parent class

// 定义一个final类
final class FinalClass {}// 错误:尝试继承final类(编译时即报错,模拟JVM语义检查)
class InvalidSubClass extends FinalClass {} // 编译错误:Cannot inherit from final 'FinalClass'// 定义一个含final方法的类
class Parent {public final void finalMethod() {}
}// 错误:尝试重写final方法(编译时即报错)
class Child extends Parent {public void finalMethod() {} // 编译错误:Cannot override the final method from Parent
}

重写 final 方法或继承 final 类后,编译器提前模拟了 JVM 的语义检查,直接报错。若通过字节码强行生成这样的类,JVM 加载时会抛出VerifyError

interface MyInterface {void requiredMethod(); // 抽象方法
}// 错误:非抽象类未实现接口的抽象方法
class UnimplementedClass implements MyInterface {} // 编译错误:Class 'UnimplementedClass' must implement the inherited abstract method 'MyInterface.requiredMethod()'

非抽象类必须实现所有抽象方法,否则编译器报错(JVM 加载时也会因语义检查失败而拒绝)。

class MethodConflict {// 方法1:返回intpublic int sameSignature() { return 0; }// 错误:仅返回值不同的"相同签名"方法public String sameSignature() { return ""; } // 编译错误:'sameSignature()' is already defined in 'MethodConflict'
}

Java 中方法签名不包含返回值,仅返回值不同的方法视为重复定义,编译器(及 JVM 语义检查)会拒绝此类定义。

(3)字节码验证

JVM还会进行字节码验证,字节码验证也是验证过程中最为复杂的一个过程。它试图通过对字节码流的分析,判断字节码是否可以被正确地执行,比如JVM会验证字节码中的以下内容。

  • (1)在字节码的执行过程中,是否会跳转到一条不存在的指令。
  • (2)函数的调用是否传递了正确类型的参数。
  • (3)变量的赋值是不是给了正确的数据类型等。
  • (4)检查栈映射帧的局部变量表和操作数栈是否有着正确的数据类型。

遗憾的是,百分之百准确地判断一段字节码是否可以被安全执行是无法实现的,因此,该过程只是尽可能地检查出可以预知的明显的问题。

如果在这个阶段无法通过检查,JVM也不会正确装载这个类。

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;public class BytecodeVerificationDemo {public static void main(String[] args) throws Exception {// 生成一段非法字节码:跳转到不存在的指令ClassWriter cw = new ClassWriter(0);cw.visit(Opcodes.V11, Opcodes.ACC_PUBLIC, "BadBytecode", null, "java/lang/Object", null);// 生成一个方法,包含非法跳转指令MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "badMethod", "()V", null, null);mv.visitCode();mv.visitJumpInsn(Opcodes.GOTO, 999); // 跳转到不存在的指令位置999mv.visitInsn(Opcodes.RETURN);mv.visitMaxs(0, 1);mv.visitEnd();cw.visitEnd();byte[] badClassData = cw.toByteArray();// 尝试加载非法类ClassLoader loader = new ClassLoader() {@Overrideprotected Class<?> findClass(String name) {return defineClass(name, badClassData, 0, badClassData.length);}};try {loader.loadClass("BadBytecode");} catch (Exception e) {// 字节码验证失败,抛出VerifyErrorSystem.out.println("字节码验证失败:" + e.getMessage()); // 提示跳转指令无效}}
}

JVM 在字节码验证阶段会检测到非法跳转指令,抛出VerifyError,拒绝加载此类。这一步确保了字节码的执行逻辑不会出现明显错误。

但是,如果通过了这个阶段的检查,也不能说明这个类是完全没有问题的。

在前面3次检查中,已经排除了文件格式错误、语义错误以及字节码的不正确性。但是依然不能确保类是没有问题的。

(4)符号引用验证

class文件中的常量池会通过字符串记录将要使用的其他类或者方法。

因此,在验证阶段,JVM就会检查这些类或者方法是否存在,检查当前类是否有权限访问这些数据,如果一个需要使用的类无法在系统中找到,则会抛出“NoClassDefFoundError”错误,如果一个方法无法被找到,则会抛出“NoSuchMethodError”错误。

注意,这个过程发生在链接阶段的解析环节。

//引用不存在的类
public class MissingClassReference {// 引用一个不存在的类private MissingClass obj; // MissingClass未定义public static void main(String[] args) {// 触发类加载(解析符号引用)new MissingClassReference();}
}//引用不存在的方法
public class MissingMethodReference {public static void main(String[] args) {// 调用一个不存在的方法new MissingMethodReference().nonExistentMethod();}
}//访问无权限的方法
class PrivateMethodClass {private void privateMethod() {} // 私有方法,外部不可访问
}
public class IllegalAccessReference {public static void main(String[] args) throws Exception {// 尝试通过反射访问私有方法(模拟符号引用权限检查)PrivateMethodClass obj = new PrivateMethodClass();obj.getClass().getDeclaredMethod("privateMethod").invoke(obj);}
}

2.链接阶段之准备(Preparation)

当一个类验证通过时,JVM就会进入准备阶段。准备阶段主要负责为类的静态变量分配内存,并将其初始化为默认值。JVM为各类型变量默认的初始值如表所示。
在这里插入图片描述
Java并不直接支持boolean类型,对于boolean类型,内部实现是int,int的默认值是0,对应的boolean类型的默认值是false。

注意,这个阶段不管类的静态变量是否有显式赋值,都会赋予默认值。

但不会为使用static final修饰的基本数据类型初始化为0,因为final在编译的时候就会分配将常量值写入类的常量池中,准备阶段会再将常量值显式直接赋值。

不会为实例变量分配初始化,因为实例变量会随着对象一起分配到Java堆中。

这个阶段并不会像初始化阶段那样会有初始化或者代码被执行。

代码清单展示了static final修饰的基本数据类型不会被初始化为0。

public class LinkingTest {// 定义静态变量 id ,并且赋值为默认值0Lprivate static long id;// 定义静态常量 num,并且赋值为 1private static final int num = 1;
}

如果类字段的字段属性表中存在ConstantValue属性,那么在准备阶段该类字段value就会被显式赋值,也就是说在准备阶段,num的值是1,而不是0。

仅被static修饰的类变量,在准备阶段初始化为默认值。

在这里插入图片描述

3.链接阶段之解析(Resolution)

在准备阶段完成后,类加载进入解析阶段。

解析阶段主要负责将类、接口、字段和方法的符号引用转为直接引用。

符号引用就是一些字面量的引用,和JVM的内部数据结构及内存布局无关。

比如class文件中,常量池存储了大量的符号引用。

在程序实际运行时,只有符号引用是不够的,比如当println()方法被调用时,系统需要明确知道该方法的位置。

以方法为例,JVM为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。

通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。代码清单演示了方法在解析阶段的调用过程。

public class ResolutionTest {public void print(){System.out.println("Hello world");}
}
Compiled from "ResolutionTest.java"
public class ResolutionTest {public ResolutionTest();Code:0: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnpublic void print();Code:0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc           #3                  // String Hello world5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: return
}

invokevirtual #4 <java/io/PrintStream.println>方法的符号引用指向常量池中第四个选项,如图所示。

在这里插入图片描述
方法调用的常量是类中方法的符号引用,包含类名和方法以及方法参数,解析阶段就是获取这些属性在内存中的地址,具体过程如图所示,通过第4项常量找到第21项类名常量和第22项方法的名称描述符即可。
在这里插入图片描述

不过Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行。在HotSpot虚拟机中,加载、验证、准备和初始化会按照顺序有条不紊地执行,但解析操作往往会在JVM在执行完初始化之后再执行。

(四)初始化(Initialization)阶段

类的初始化是类装载的最后一个阶段。

如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中,然后JVM才会开始执行Java字节码,也就是说到了初始化阶段,JVM才真正开始执行类中定义的Java程序代码。

初始化阶段的重要工作是执行类的<clinit>()方法(即类初始化方法)​,该方法仅能由Java编译器生成并被JVM调用,程序开发者无法自定义一个同名的方法,也无法直接在Java程序中调用该方法。

<clinit>()方法是由类静态成员的赋值语句以及static语句块合并产生的。

通常在加载一个类之前,JVM总是会试图加载该类的父类,因此父类的<clinit>()方法总是在子类<clinit>()方法之前被调用,也就是说,父类的static语句块优先级高于子类,简要概括为由父及子,静态先行。

Java编译器并不会为所有的类都产生<clinit>()方法。以下情况class文件中将不会包含<clinit>()方法。

  • (1)一个类中并没有声明任何的类变量,也没有静态代码块时。
  • (2)一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时。
  • (3)一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式。
public class InitializationTest {// 场景 1:对应非静态的字段,不管是否进行了显式赋值,都不会生成 <clinit>() 方法public int num = 1;// 场景 2:静态的字段,没有显式的赋值,不会生成 <clinit>() 方法public static int num1;// 场景 3:比如对于声明为 static final 的基本数据类型的字段,不管是否进行了显式赋值,都不会生成 <clinit>() 方法public static final int num2 = 1;
}

查看该类对应的方法信息,如图所示,可以看到不存在<clinit>()方法。

在这里插入图片描述

1.staticfinal搭配

前面提到staticfinal定义的变量在准备阶段完成赋值,但是并不是所有的变量都在链接阶段的准备阶段完成赋值,下面通过代码案例说明不同情况下的不同阶段赋值。

public class InitializationTest1 {public static int a = 1;public static final int INT_CONSTANT = 10;public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000);public static final String s0 = "helloworld0";public static final String s1 = new String("helloworld1");
}

对应字节码在指令在<clinit>()方法中。

 0 iconst_11 putstatic #2 <InitializationTest1.a : I>4 bipush 1006 invokestatic #3 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>9 putstatic #4 <InitializationTest1.INTEGER_CONSTANT1 : Ljava/lang/Integer;>
12 sipush 1000
15 invokestatic #3 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
18 putstatic #5 <InitializationTest1.INTEGER_CONSTANT2 : Ljava/lang/Integer;>
21 new #6 <java/lang/String>
24 dup
25 ldc #7 <helloworld1>
27 invokespecial #8 <java/lang/String.<init> : (Ljava/lang/String;)V>
30 putstatic #9 <InitializationTest1.s1 : Ljava/lang/String;>
33 return

从字节码指令中看到只有定义类成员变量a、INTEGER_CONSTANT1、INTEGER_CONSTANT2s1时是在初始化阶段的<clinit>()方法中完成。

那么另外两个类变量是怎么赋值的呢?通过jclasslib查看字段属性表,如图所示,可以看到只有INT_CONSTANThelloworld0两个常量拥有ConstantValue,说明INT_CONSTANT = 10String s0="helloworld0"是在链接阶段的准备阶段完成的。

我们得出的结论就是,基本数据类型和String类型使用static和final修饰,并且显式赋值中不涉及方法或构造器调用,其初始化是在链接阶段的准备环节进行,其他情况都是在初始化阶段进行赋值。

在这里插入图片描述

2.<clinit>()方法的线程安全性

对于<clinit>()方法的调用,JVM会在内部确保其多线程环境中的安全性。

JVM会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。

正是因为方法<clinit>()带锁线程安全的,如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,导致死锁,这种死锁是很难发现的,因为并没有可用的锁信息。

如果之前的线程成功加载了类,则等在队列中的线程就没有机会再执行<clinit>()方法了,当需要使用这个类时,JVM会直接返回给它已经准备好的信息。

3.类的初始化时机:主动使用和被动使用

初始化阶段是执行类构造器<clinit>()方法的过程。虽然有些类已经存在<clinit>()方法,但是并不确定什么时候会触发执行,可以触发<clinit>()方法的情景称为主动使用,不能触发<clinit>()方法执行的情景称为被动使用。

主动使用可以触发类的初始化,被动使用不能触发类的初始化。

(五)类的使用(Using)

任何一个类在使用之前都必须经历过完整的加载链接初始化3个步骤。一旦一个类成功经历这3个步骤之后,便“万事俱备,只欠东风”​,就等着开发者使用了。

开发人员可以在程序中访问和调用它的静态类成员信息(比如静态字段、静态方法等)​,或者使用new关键字创建对象实例。

下面总结一下各种情况下变量赋值的时机。

public class VariableInitializationExample {// 1. 实例变量 - 无显式初始化public int instanceVarDefault;// 2. 实例变量 - 显式初始化public int instanceVarExplicit = 10;// 3. 静态变量 - 无显式初始化public static int staticVarDefault;// 4. 静态变量 - 显式初始化public static int staticVarExplicit = 20;// 5. 编译期常量public final static int COMPILE_TIME_CONSTANT = 30;// 6. 运行时常量(非编译期确定)public final static int RUNTIME_CONSTANT = getRuntimeValue();// 7. 实例final变量 - 声明时初始化public final int instanceFinalDeclared = 40;// 8. 实例final变量 - 构造器中初始化public final int instanceFinalInConstructor;// 9. 静态代码块赋值public static int staticVarInBlock;static {staticVarInBlock = 50;System.out.println("静态代码块执行");}// 10. 实例代码块赋值public int instanceVarInBlock;{instanceVarInBlock = 60;System.out.println("实例代码块执行");}// 11. 方法赋值(运行时)public int methodAssignedVar;// 构造器public VariableInitializationExample() {this.instanceFinalInConstructor = 70;System.out.println("构造器执行");}// 静态方法private static int getRuntimeValue() {System.out.println("getRuntimeValue() 执行");return 100;}// 实例方法public void assignInMethod(int value) {this.methodAssignedVar = value;}
}

测试类验证时机

public class TestInitialization {public static void main(String[] args) {System.out.println("=== 开始测试 ===");// 访问静态常量(不会触发类初始化)System.out.println("编译期常量: " + VariableInitializationExample.COMPILE_TIME_CONSTANT);System.out.println("--- 创建第一个对象 ---");// 创建对象会触发实例初始化流程VariableInitializationExample obj1 = new VariableInitializationExample();System.out.println("--- 访问静态变量 ---");// 访问静态变量System.out.println("静态变量: " + VariableInitializationExample.staticVarExplicit);System.out.println("--- 创建第二个对象 ---");VariableInitializationExample obj2 = new VariableInitializationExample();System.out.println("--- 方法赋值 ---");obj1.assignInMethod(80);System.out.println("方法赋值后: " + obj1.methodAssignedVar);}
}

下面执行时机详细分析

1.类加载阶段(第一次使用类时)

(1) 加载 (Loading)
  • 读取Class文件到内存
  • 创建Class对象
(2) 链接 - 准备 (Preparation)
// 静态变量赋予默认值
staticVarDefault = 0;           // 默认值
staticVarExplicit = 0;          // 默认值(不是20)
staticVarInBlock = 0;           // 默认值// 编译期常量直接赋值
COMPILE_TIME_CONSTANT = 30;     // 直接赋终值// 运行时常量还是默认值
RUNTIME_CONSTANT = 0;           // 默认值(不是100)
(3) 初始化 (Initialization)

执行<clinit>方法(静态变量赋值 + 静态代码块):

// 按代码顺序执行
staticVarExplicit = 20;         // 显式静态赋值
staticVarInBlock = 50;          // 静态代码块赋值
System.out.println("静态代码块执行");// 运行时常量赋值
RUNTIME_CONSTANT = getRuntimeValue();  // 调用静态方法
System.out.println("getRuntimeValue() 执行");

2.对象实例化阶段 (new Object())

(1)分配内存 + 默认初始化
// 所有实例变量赋默认值
instanceVarDefault = 0;
instanceVarExplicit = 0;
instanceFinalDeclared = 0;
instanceFinalInConstructor = 0;
instanceVarInBlock = 0;
methodAssignedVar = 0;
(2)实例代码块执行
{instanceVarInBlock = 60;System.out.println("实例代码块执行");
}
(3)构造器执行
public VariableInitializationExample() {// 1. 先执行实例变量显式初始化instanceVarExplicit = 10;instanceFinalDeclared = 40;// 2. 执行构造器代码this.instanceFinalInConstructor = 70;System.out.println("构造器执行");
}

3.运行时赋值

// 通过方法赋值(任意时间)
obj1.assignInMethod(80);  // methodAssignedVar = 80

输出结果预测

运行TestInitialization,输出大致如下:

=== 开始测试 ===
编译期常量: 30                    // 不触发类初始化
--- 创建第一个对象 ---
getRuntimeValue() 执行           // 类初始化时执行
静态代码块执行                   // 类初始化时执行
实例代码块执行                   // 对象实例化时执行
构造器执行                      // 对象实例化时执行
--- 访问静态变量 ---
静态变量: 20
--- 创建第二个对象 ---
实例代码块执行                   // 对象实例化时执行
构造器执行                      // 对象实例化时执行
--- 方法赋值 ---
方法赋值后: 80

关键总结:

变量类型准备阶段初始化阶段实例化阶段运行时
静态变量(无显式)默认值0保持0--
静态变量(显式)默认值0显式赋值--
编译期常量直接赋终值---
运行时常量默认值0调用方法赋值--
实例变量(无显式)--默认值0 → 保持0-
实例变量(显式)--默认值0 → 显式赋值-
实例final(声明处)--默认值0 → 显式赋值-
实例final(构造器)--默认值0 → 构造器赋值-
方法赋值变量--默认值0方法调用时赋值

(六)类的卸载(Unloading)

和前面讲过对象的生命周期类似,对象在使用完以后会被垃圾收集器回收,那么对应的类在使用完成以后,也有可能被卸载掉。

在了解类的卸载之前,需要先厘清类、类的加载器、类的Class对象和类的实例之间的引用关系。

1.类、类的加载器、类的Class对象、类的实例之间的引用关系

(1)类加载器和类的Class对象之间的关系。

在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。

// java8中的ClassLoader源码是这样定义的
// The classes loaded by this class loader. The only purpose of this table
// is to keep the classes from being GC'ed until the loader is GC'ed.
private final Vector<Class<?>> classes = new Vector<>();// java17中的ClassLoader源码是这样定义的
// The classes loaded by this class loader. The only purpose of this table
// is to keep the classes from being GC'ed until the loader is GC'ed.
private final ArrayList<Class<?>> classes = new ArrayList<>();

从垃圾回收的角度看,如果类加载器还存在(未被 GC),但它加载的类没有被任何地方引用,这些类可能会被提前回收。而classes集合通过强引用持有这些Class对象,确保只要类加载器还存活,它加载的类就不会被回收(除非主动移除)。

另外,一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。

// 获取当前类的Class对象
Class<?> clazz = TestClass.class;// Class对象获取它的类加载器(Class → ClassLoader)
ClassLoader classLoader = clazz.getClassLoader();

由此可见,代表某个类的Class对象与该类的类加载器之间为双向关联关系。

(2)类、类的Class对象、类的实例对象之间的关系。

一个类的实例总是引用代表这个类的Class对象。

Object类中定义了getClass()方法,这个方法返回代表实例所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。

// MyClass.java - 被加载的测试类,含静态代码块标识加载状态
public class MyClass {// 静态代码块:类加载时执行,用于观察是否被重新加载static {System.out.println("MyClass 被加载了!ClassLoader: " + MyClass.class.getClassLoader());}public void doSomething() {System.out.println("MyClass实例方法执行");}
}
// MyClassLoader.java - 自定义类加载器
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;public class MyClassLoader extends ClassLoader {private String classPath; // 类文件所在路径public MyClassLoader(String classPath) {this.classPath = classPath;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {// 1. 读取类的二进制数据(对应类加载的"获取二进制流"步骤)byte[] classData = loadClassData(name);// 2. 将二进制数据转换为Class对象return defineClass(name, classData, 0, classData.length);} catch (Exception e) {throw new ClassNotFoundException("类加载失败", e);}}// 从文件系统读取类的二进制数据private byte[] loadClassData(String className) throws Exception {String path = classPath + className.replace(".", "/") + ".class";try (InputStream is = new FileInputStream(path);ByteArrayOutputStream baos = new ByteArrayOutputStream()) {byte[] buffer = new byte[1024];int len;while ((len = is.read(buffer)) != -1) {baos.write(buffer, 0, len);}return baos.toByteArray();}}
}

下面演示引用关系(类、类加载器、Class 对象、实例),该示例将清晰展示四者之间的双向引用关系:

// ReferenceRelationshipDemo.java
public class ReferenceRelationshipDemo {public static void main(String[] args) throws Exception {// 1. 创建自定义类加载器MyClassLoader loader = new MyClassLoader("D:/classes/"); // 替换为实际class文件路径// 2. 加载MyClass,获取Class对象Class<?> myClass = loader.loadClass("MyClass");// 3. 创建MyClass的实例Object instance = myClass.newInstance();// 4. 验证引用关系System.out.println("=== 验证引用关系 ===");// (1) 实例 -> Class对象:通过getClass()Class<?> instanceClass = instance.getClass();System.out.println("实例的getClass() == 加载的Class对象? " + (instanceClass == myClass)); // true// (2) Class对象 -> 类加载器:通过getClassLoader()ClassLoader classLoaderFromClass = myClass.getClassLoader();System.out.println("Class对象的类加载器 == 自定义加载器? " + (classLoaderFromClass == loader)); // true// (3) 类加载器 -> 类:类加载器内部持有加载的类的引用(可通过反射验证)// 注意:ClassLoader内部用Vector<Class<?>>保存加载的类,这里简化演示System.out.println("类加载器是否持有类的引用? " + (loader.findLoadedClass("MyClass") == myClass)); // true// (4) 类 -> Class对象:通过静态class属性Class<?> staticClassRef = MyClass.class;System.out.println("类的static class属性 == 加载的Class对象? " + (staticClassRef == myClass)); // true}
}

运行结果:

MyClass 被加载了!ClassLoader: MyClassLoader@6d03e736
=== 验证引用关系 ===
实例的getClass() == 加载的Class对象? true
Class对象的类加载器 == 自定义加载器? true
类加载器是否持有类的引用? true
类的static class属性 == 加载的Class对象? true

说明:

  • 实例通过getClass()引用 Class 对象;
  • Class 对象通过getClassLoader()引用类加载器;
  • 类加载器内部持有已加载类的引用(findLoadedClass可验证);
  • 类通过静态class属性引用 Class 对象。

四者形成完整的双向引用链。

2.类的生命周期

当类被加载、链接和初始化后,它的生命周期就开始了。

当代表类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,类在方法区内的数据也会被卸载,从而结束类的生命周期。

一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。

3.案例

自定义一个类加载器MyClassLoader加载自定义类Order,那么就可以通过Order的Class对象获取到对应的类加载器,再通过Order类的实例对象获取到类Class对象

// 通过类加载器加载Order类 java.lang.Class对象
MyClassLoader myLoader = new MyClassLoader("d:/");
Class clazz = myLoader.loadClass("Order");// 获取 java.lang.Class 对象
Class<Order> orderClass = Order.class;// 获取类加载器
ClassLoader classLoader = orderClass.getClassLoader();// 通过实例对象获取 java.lang.Class 对象
Order order = new Order();
Class<? extends Order> aClass = order.getClass();

类、类的加载器、类的Class对象、类的实例之间的引用关系如图所示。
在这里插入图片描述
myLoader变量和order变量间接引用代表Order类的Class对象,而orderClass变量则直接引用代表Order类的Class对象。

如果程序运行过程中,将图左侧三个引用变量都置为null,此时Order对象结束生命周期,myLoader对象结束生命周期,代表Order类的Class对象也结束生命周期,Order类在方法区内的二进制数据被卸载。

当再次有需要时,会检查Order类的Class对象是否存在,如果存在会直接使用,不再重新加载;如果不存在Order类会被重新加载,在JVM的堆区会生成一个新的代表Order类的Class实例。

4.类的卸载

通过上面的案例可以知道当类对象没有引用时,可能会产生类的卸载,类的卸载需要满足如下三个条件。

  • (1)该类所有的实例已经被回收。
  • (2)加载该类的类加载器的实例已经被回收。
  • (3)该类对应的Class对象没有任何对方被引用。
// ClassUnloadingDemo.java
public class ClassUnloadingDemo {public static void main(String[] args) throws Exception {System.out.println("=== 首次加载MyClass ===");// 1. 第一次加载:创建引用链MyClassLoader loader1 = new MyClassLoader("D:/classes/");Class<?> clazz1 = loader1.loadClass("MyClass");Object instance1 = clazz1.newInstance();// 2. 清除所有引用instance1 = null;   // 实例引用置空clazz1 = null;      // Class对象引用置空loader1 = null;     // 类加载器引用置空// 3. 强制触发GC(提示JVM进行垃圾回收)System.gc();Thread.sleep(1000); // 等待GC完成System.out.println("\n=== 再次加载MyClass ===");// 4. 第二次加载:若静态块重新执行,说明之前的类已被卸载MyClassLoader loader2 = new MyClassLoader("D:/classes/");Class<?> clazz2 = loader2.loadClass("MyClass");Object instance2 = clazz2.newInstance();// 5. 验证两次加载的是不同的Class对象(因第一次可能已卸载)MyClassLoader loader3 = new MyClassLoader("D:/classes/");Class<?> clazz3 = loader3.loadClass("MyClass");System.out.println("两次加载的Class对象是否相同? " + (clazz2 == clazz3)); // false(不同类加载器加载的是不同类)}
}

运行结果:

=== 首次加载MyClass ===
MyClass 被加载了!ClassLoader: MyClassLoader@6d03e736=== 再次加载MyClass ===
MyClass 被加载了!ClassLoader: MyClassLoader@568db2f2
两次加载的Class对象是否相同? false

说明:

  • 首次加载时,静态块执行,打印加载信息;
  • 清除所有引用并 GC 后,再次加载时静态块重新执行,证明之前的类已被卸载(否则同一类加载器不会重复加载,但这里用了新的类加载器,若静态块重新执行,说明旧类已被回收);
  • 不同类加载器加载的相同类会生成不同的 Class 对象(clazz2 != clazz3)。

但是需要注意,并不是所有类加载器下面的类都可以被卸载,Java自带的三种类加载器的实例是不可以被卸载的,所以它们加载的类在整个运行期间是不可以被卸载的,只有被开发者自定义的类加载器实例加载的类才有可能被卸载。

一个已经加载的类被卸载的概率很小,至少被卸载的时间是不确定的。开发者在开发代码的时候,不应该对虚拟机的类卸载做任何假设,在此前提下,再来实现系统中的特定功能。

5.回顾:方法区的垃圾回收

方法区的垃圾收集主要回收两部分内容,分别是常量池中废弃的常量和不再使用的类。

HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。

JVM判定一个常量是否“废弃”还相对简单,而要判定一个类是否属于“不再被使用的类”的条件就比较苛刻了,需要同时满足下面三个条件。

  • (1)该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。

  • (2)加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。

  • (3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

上述三个条件并不是JVM卸载无用类的必要条件,JVM可以卸载类也可以不卸载类,不会像对象那样没有引用就肯定回收。

http://www.dtcms.com/a/473372.html

相关文章:

  • 强军网网站建设网站需要备案才能建设吗
  • 耄大厨——AI厨师智能体(3-工具调用)
  • (二)黑马React(导航/账单项目)
  • SA-LSTM
  • 【Java并发】深入理解synchronized
  • Docker 安装 Harbor 教程
  • Python+Flask+Prophet 汽车之家二手车系统 逻辑回归 二手车推荐系统 机器学习(逻辑回归+Echarts 源码+文档)✅
  • AI_NovelGenerator:自动化长篇小说AI生成工具
  • 济南网站制作开通免费个人简历模板官网
  • 全链路智能运维中的异常检测与根因定位技术
  • 解构 CodexField:创作者经济到模型金融化的代币逻辑与潜力
  • SpringBoot 实现自动数据变更追踪
  • C语言⽂件操作讲解(3)
  • 对网站做数据分析北京市建设工程信息
  • 1.6虚拟机
  • XCP服务
  • Excel - Excel 列出一列中所有不重复数据
  • 如何获取用户右击的Excel单元格位置
  • 昆明企业网站建设公司虹口建设机械网站制作
  • 宁波p2p网站建设黑龙江省建设安全网站
  • Spring Boot 3零基础教程,自动配置机制,笔记07
  • Spring通关笔记:从“Hello Bean”到循环依赖的奇幻漂流
  • 【Spring Security】Spring Security 密码编辑器
  • MCU ADC外设工作原理介绍
  • k8s的ymal文件
  • 杭州公司建设网站网站建设标签
  • 博客系统小笔记
  • 后端开发和软件开发有什么区别
  • 分布式专题——41 RocketMQ集群高级特性
  • 自然语言处理分享系列-词语和短语的分布式表示及其组合性(一)