详解类生到死的来龙去脉

news2024/12/23 23:00:15

类生命周期和加载过程

image-20231029213414544

一个类在 JVM 里的生命周期有 7 个阶段,分别是加载(Loading)、校验(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)

前五个部分(加载,校验,准备,解析,初始化)统称为类加载

1)加载 加载阶段也可以称为“装载”阶段。 这个阶段主要的操作是: 根据明确知道的 class 完全限定名, 来获取二进制 classfile 格式的字节流,简单点说就是找到文件系统中/jar 包中/或存在于任何地方的“class 文件”。 如果找不到二进制表示形式,则会抛出 NoClassDefFound 错误。

装载阶段并不会检查 classfile 的语法和格式。 类加载的整个过程主要由 JVM 和 Java 的类加载系统共同完成, 当然具体到 loading 阶段则是由 JVM 与具体的某一个类加载器(java.lang.classLoader)协作完成的。

2)校验 链接过程的第一个阶段是 校验,确保 class 文件里的字节流信息符合当前虚拟机的要求,不会危害虚拟机的安全。

校验过程检查 classfile 的语义,判断常量池中的符号,并执行类型检查, 主要目的是判断字节码的合法性,比如 magic number, 对版本号进行验证。 这些检查过程中可能会抛出 VerifyErrorClassFormatErrorUnsupportedClassVersionError

因为 classfile 的验证属是链接阶段的一部分,所以这个过程中可能需要加载其他类,在某个类的加载过程中,JVM 必须加载其所有的超类和接口。

如果类层次结构有问题(例如,该类是自己的超类或接口,死循环了),则 JVM 将抛出 ClassCircularityError。 而如果实现的接口并不是一个 interface,或者声明的超类是一个 interface,也会抛出 IncompatibleClassChangeError

3)准备

然后进入准备阶段,这个阶段将会创建静态字段,并将其初始化为标准默认值(比如null或者0 值),并分配方法表,即在方法区中分配这些变量所使用的内存空间。

请注意,准备阶段并未执行任何 Java 代码。

例如:

public static int i = 1;

在准备阶段i的值会被初始化为 0,后面在类初始化阶段才会执行赋值为 1;但是下面如果使用 final 作为静态常量,某些 JVM 的行为就不一样了:

public static final int i = 1; 对应常量 i,在准备阶段就会被赋值 1

4)解析 然后进入可选的解析符号引用阶段。 也就是解析常量池,主要有以下四种:类或接口的解析、字段解析、类方法解析、接口方法解析。

编写的代码中,当一个变量引用某个对象的时候,这个引用在 .class 文件中是以符号引用来存储的(相当于做了一个索引记录)。

解析阶段就需要将其解析并链接为直接引用(相当于指向实际对象)。如果有了直接引用,那引用的目标必定在堆中存在。

加载一个 class 时, 需要加载所有的 super 类和 super 接口。

5)初始化 JVM 规范明确规定, 必须在类的首次“主动使用”时才能执行类初始化

初始化的过程包括执行:

  • 类构造器方法
  • static 静态变量赋值语句
  • static 静态代码块

如果是一个子类进行初始化会先对其父类进行初始化,保证其父类在子类之前进行初始化。所以其实在 java 中初始化一个类,那么必然先初始化过 java.lang.Object 类,因为所有的 java 类都继承自 java.lang.Object。

只要尊重语言的语义,在执行下一步操作之前完成 装载,链接和初始化这些步骤,如果出错就按照规定抛出相应的错误,类加载系统完全可以根据自己的策略,灵活地进行符号解析等链接过程。 为了提高性能,HotSpot JVM 通常要等到类初始化时才去装载和链接类。 因此,如果 A 类引用了 B 类,那么加载 A 类并不一定会去加载 B 类(除非需要进行验证)。 主动对 B 类执行第一条指令时才会导致 B 类的初始化,这就需要先完成对 B 类的装载和链接。

类加载(初始化)时机

了解了类的加载过程,再看看类的初始化何时会被触发呢?JVM 规范枚举了下述多种触发情况:

  • 当虚拟机启动时,初始化用户指定的主类,就是启动执行的 main 方法所在的类;
  • 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类,就是 new 一个类的时候要初始化;
  • 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
  • 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
  • 子类的初始化会触发父类的初始化
  • 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化
  • 使用反射 API 对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化;
  • 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

同时以下几种情况不会执行类初始化:

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化
  • 定义对象数组,不会触发该类的初始化。
  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
  • 通过类名获取 Class 对象,不会触发类的初始化,Hello.class 不会让 Hello 类初始化。
  • 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,这个参数是告诉虚拟机,是否要对类进行初始化。Class.forName(“jvm.Hello”)默认会加载 Hello 类。
  • 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作(加载了,但是不初始化)。

Class.forName(), classLoader.loadClass() 等 Java API, 反射API, 以及 JNI_FindClass 都可以启动类加载。 JVM 本身也会进行类加载。 比如在 JVM 启动时加载核心类,java.lang.Object, java.lang.Thread 等等。

类加载器机制

类加载过程可以描述为“通过一个类的全限定名 a.b.c.XXClass 来获取描述此类的 Class 对象”,这个过程由“类加载器(ClassLoader)”来完成。这样的好处在于,子类加载器可以复用父加载器加载的类

系统自带的类加载器分为三种:

  • 启动类加载器(BootstrapClassLoader)
  • 扩展类加载器(ExtClassLoader)
  • 应用类加载器(AppClassLoader)

启动类加载器是由 JVM 内部实现的,在 Java 的 API 里无法拿到,但是可以侧面看到和影响它。后 2 种类加载器在 Oracle Hotspot JVM 里,都是在中sun.misc.Launcher定义的,扩展类加载器和应用类加载器一般都继承自URLClassLoader类,这个类也默认实现了从各种不同来源加载 class 字节码转换成 Class 的方法

image-20231029221110032

  1. 启动类加载器(bootstrap class loader): 用来加载 Java 的核心类,是用原生 C++ 代码来实现的,并不继承自 java.lang.ClassLoader(负责加载JDK中jre/lib/rt.jar里所有的class)。可以看做是 JVM 自带的,在代码层面无法直接获取到启动类加载器的引用,所以不允许直接操作它, 如果打印出来就是个 null。举例来说,java.lang.String 是由启动类加载器加载的,所以 String.class.getClassLoader() 就会返回 null
  2. 扩展类加载器(extensions class loader)负责加载 JRE 的扩展目录,lib/ext 或者由 java.ext.dirs 系统属性指定的目录中的 JAR 包的类,代码里直接获取它的父类加载器为 null(因为无法拿到启动类加载器)。
  3. 应用类加载器(app class loader)负责在 JVM 启动时加载来自 Java 命令的 -classpath 或者 -cp 选项、java.class.path 系统属性指定的 jar 包和类路径。在应用程序代码里可以通过 ClassLoader 的静态方法 getSystemClassLoader() 来获取应用类加载器。如果没有特别指定,则在没有使用自定义类加载器情况下,用户自定义的类都由此加载器加载

此外还可以自定义类加载器。如果用户自定义了类加载器,则自定义类加载器都以应用类加载器作为父加载器。应用类加载器的父类加载器为扩展类加载器。这些类加载器是有层次关系的,启动加载器又叫根加载器,是扩展加载器的父加载器。

image-20231029222105968

类加载机制有三个特点

  1. 双亲委托:当一个自定义类加载器需要加载一个类,比如 java.lang.String,不会一上来就直接试图加载它,而是先委托自己的父加载器去加载,父加载器如果发现自己还有父加载器,会一直往前找,这样只要上级加载器,比如启动类加载器已经加载了某个类比如 java.lang.String,所有的子加载器都不需要自己加载了。如果几个类加载器都没有加载到指定名称的类,那么会抛出 ClassNotFountException 异常。(将加载类的任务委托给父类加载器来加载
  2. 负责依赖:如果一个加载器在加载某个类的时候,发现这个类依赖于另外几个类或接口,也会去尝试加载这些依赖项。
  3. 缓存加载:为了提升加载效率,消除重复加载,一旦某个类被一个类加载器加载,那么它会缓存这个加载结果,不会重复加载。

自定义类加载器

自行实现类加载器来加载其他格式的类,对加载方式、加载数据的格式进行自定义处理,只要能通过 classloader 返回一个 Class 实例即可。这就大大增强了加载器灵活性。

可以试着实现一个可以用来处理简单加密的字节码的类加载器,用来保护class 字节码文件不被使用者直接拿来破解。

希望加载的一个 Hello 类:

package jvm;

public class Hello {
    static {
        System.out.println("Hello Class Initialized!");
    }
}

这个 Hello 类非常简单,就是在初始化的时候,打印出来一句“Hello Class Initialized!”。

假设这个类的内容非常重要,我们不想把编译到得到的 Hello.class 给别人,但是我们还是想别人可以调用或执行这个类,应该怎么办呢?一个简单的思路是,把这个类的 class 文件二进制作为字节流先加密一下,然后尝试通过自定义的类加载器来加载加密后的数据。为了演示简单,使用 jdk 自带的 Base64 算法,把字节码加密成一个文本。在下面这个例子里,实现一个 HelloClassLoader,它继承自 ClassLoader 类,但是希望它通过我们提供的一段 Base64 字符串,来还原出来,并执行Hello 类里的打印一串字符串的逻辑。

package jvm;

import java.util.Base64;

// 继承 ClassLoader 类
public class HelloClassLoader extends ClassLoader {

    public static void main(String[] args) {
        try {
            // 直接通过class实例newInstance创建对象
            new HelloClassLoader().findClass("jvm.Hello").newInstance(); // 加载并初始化Hello类
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

    // 查找具有指定二进制名称(全限定名)的类,重写ClassLoader方法
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        String helloBase64 = "yv66vgAAADQAHwoABgARCQASABMIABQKABUAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2N" +
                "hbFZhcmlhYmxlVGFibGUBAAR0aGlzAQALTGp2bS9IZWxsbzsBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAHAAgHABkMABoAGwEAGEhlb" +
                "GxvIENsYXNzIEluaXRpYWxpemVkIQcAHAwAHQAeAQAJanZtL0hlbGxvAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2" +
                "YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAAUABgAAAAAAAgABAAcACA" +
                "ABAAkAAAAvAAEAAQAAAAUqtwABsQAAAAIACgAAAAYAAQAAAAMACwAAAAwAAQAAAAUADAANAAAACAAOAAgAAQAJAAAAJQACAAAAAAAJsgACEgO2AASxAAAAAQAK" +
                "AAAACgACAAAABgAIAAcAAQAPAAAAAgAQ";

        byte[] bytes = decode(helloBase64);	
        // 将字节数组转换为类class的实例
        return defineClass(name,bytes,0,bytes.length);
    }

    // 直接将Base64加密字段解析为字节数组	
    public byte[] decode(String base64){
        return Base64.getDecoder().decode(base64);
    }

}

自定义类加载器继承自 ClassLoader,并覆盖了 findClass 方法,该方法用于根据类名查找和加载类的字节码

defineClass方法:将字节数组转换成class的实例,拿到class实例之后可以直接调用反射方法创建对象

直接执行main方法,控制台会输出Hello Class Initialized!字样。

成功执行了Hello类的代码,但是完全不需要有Hello这个类的class文件。此外,需要说明的是两个没有关系的自定义类加载器之间加载的类是不共享的(只共享父类加载器,兄弟之间不共享),这样就可以实现不同的类型沙箱的隔离性,可以用多个类加载器,各自加载同一个类的不同版本,大家可以相互之间不影响彼此,从而在这个基础上可以实现类的动态加载卸载,热插拔的插件机制等,具体信息可以参考OSGi等模块化技术。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1148880.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ 内存

内存分区模型 代码区&#xff1a;存放函数体的二进制代码&#xff0c;由操作系统进行管理全局区&#xff1a;存放全局变量和静态变量以及常量栈区&#xff1a;由编译器自动分配释放&#xff0c;存放函数的参数值、局部变量等堆区&#xff1a;由程序员分配和释放&#xff0c;若…

C#,数值计算——分类与推理Svmlinkernel的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { public class Svmlinkernel : Svmgenkernel { public int n { get; set; } public double[] mu { get; set; } public Svmlinkernel(double[,] ddata, double[] yy) : base(yy, ddata) …

SSM游戏购物商城

摘 要 信息化爆炸的时代&#xff0c;互联网技术的指数型的增长&#xff0c;信息化程度的不断普及&#xff0c;社会节奏在加快&#xff0c;每天都有大量的信息扑面而来&#xff0c;人们正处于数字信息化世界。数字化的互联网具有便捷性&#xff0c;传递快&#xff0c;效率高&am…

二叉树问题——前/中/后/层遍历(递归与栈)

摘要 博文主要介绍二叉树的前/中/后/层遍历(递归与栈)方法 一、前/中/后/层遍历问题 144. 二叉树的前序遍历 145. 二叉树的后序遍历 94. 二叉树的中序遍历 102. 二叉树的层序遍历 二、二叉树遍历递归解析 // 前序遍历递归LC144_二叉树的前序遍历 class Solution {publi…

一键转换:将mp4视频批量转换为mov格式

在视频编辑和后期制作领域&#xff0c;不同的视频格式往往有着各自的优势和适用场景。其中&#xff0c;MP4和MOV是两种常见的视频格式&#xff0c;它们都具有广泛的应用。有时候&#xff0c;为了满足特定的需求或兼容性&#xff0c;我们需要将MP4视频批量转换为MOV格式。为了实…

力扣第738题 单调递增的数字 c++ 暴力超时 贪心优化

题目 738. 单调递增的数字 中等 相关标签 贪心 数学 当且仅当每个相邻位数上的数字 x 和 y 满足 x < y 时&#xff0c;我们称这个整数是单调递增的。 给定一个整数 n &#xff0c;返回 小于或等于 n 的最大数字&#xff0c;且数字呈 单调递增 。 示例 1: 输入: n 1…

在虚拟环境中,通过pip安装tensorflow

目录 激活python虚拟环境&#xff0c;更新pip 通过pip 安装tensorflow 确定python版本&#xff1a; ​编辑安装tensorflow: ​编辑 为什么使用pip安装tensorflow? 激活python虚拟环境&#xff0c;更新pip 命令为python -m pip install --upgrade pip 通过pip 安装tensorf…

WLAN的组网架构和工作原理

目录 WLAN的组网架构 FAT AP架构 AC FIT AP架构 敏捷分布式AP 下一代园区网络&#xff1a;智简园区&#xff08;大中型园区网络&#xff09; WLAN工作原理 WLAN工作流程 1.AP上线 &#xff08;1&#xff09;AP获取IP地址&#xff1b; &#xff08;2&#xff09;AP发…

网络协议--TCP的保活定时器

23.1 引言 许多TCP/IP的初学者会很惊奇地发现可以没有任何数据流通过一个空闲的TCP连接。也就是说&#xff0c;如果TCP连接的双方都没有向对方发送数据&#xff0c;则在两个TCP模块之间不交换任何信息。例如&#xff0c;没有可以在其他网络协议中发现的轮询。这意味着我们可以…

应用开发平台集成工作流系列之17——流程建模功能前端设计与改造回顾

背景 对于流程设置不友好的问题&#xff0c;国内钉钉另行设计与实现了一套流程建模模式&#xff0c;跟bpmn规范无关&#xff0c;有人仿照实现了下&#xff0c;并做了开源&#xff08;https://github.com/StavinLi/Workflow-Vue3&#xff09;&#xff0c;效果图如下&#xff1a…

python设计模式笔记1:创建型模式 工厂模式和抽象工厂模式

1.工厂模式 (1) 导入所需的模块&#xff08; json 和 ElementTree &#xff09;。 (2) 定义 JSON数据提取器类&#xff08; JSONDataExtractor &#xff09;。 (3) 定义 XML数据提取器类&#xff08; XMLDataExtractor &#xff09;。 (4) 添加工厂函数 dataextraction_factor…

【错误解决方案】ModuleNotFoundError: No module named ‘PeptideBuilder‘

1. 错误提示 在python程序中&#xff0c;试图导入一个不存在的模块PeptideBuilder导致的错误&#xff1a; 错误提示&#xff1a;ModuleNotFoundError: No module named PeptideBuilder 2. 解决方案 解决方案是确保你已经正确安装了PeptideBuilder模块。你可以通过pip来安装它…

操作系统章节练习

第5章 存储器管理 一. 多选题&#xff08;共8题&#xff0c;64分&#xff09; 1. (多选题, 8分)为什么在页式存储器中实现程序共享时&#xff0c;必须对共享程序给出相同的页号&#xff1f; A. 共享页号相同方便地址转换。B. 实现程序共享时&#xff0c;由于页式存储结构要求…

多线程---线程池

文章目录 什么是线程池&#xff1f;线程池的实现标准库中的线程池&#xff08;四种&#xff09;自己实现一个线程池 线程池支持的参数在实际的开发中&#xff0c;线程池的线程数如何确定&#xff1f; 什么是线程池&#xff1f; 线程诞生的原因就是进程太“重量”了。虽然线程的…

计算机网络——第一章体系结构相关习题及详细解析

1-1 在OSI参考模型中&#xff0c;自下而上第一个提供端到端服务的层次是&#xff1a; A.数据链路层 B.传输层 C.会话层 D.应用层 答案选择&#xff1a;B.传输层 即&#xff0c;在OSI参考模型中&#xff0c;自下而上第一个提供端到端服务的层次是传输层。…

【数据结构】 队列详解!庖丁解牛般细致讲解!

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; 数据结构解析 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言&#x1f324;️队列的概念剖析☁️什么是队列☁️队列的特性☁️队列的图解 &#x1…

【鸿蒙软件开发】ArkTS容器组件之Badge

文章目录 前言一、Badge组件1.1 子组件1.2 接口接口1参数 接口2参数 BadgePosition枚举说明BadgeStyle对象说明 1.3 示例代码 总结 前言 Badge组件&#xff1a;可以附加在单个组件上用于信息标记的容器组件。 一、Badge组件 可以附加在单个组件上用于信息标记的容器组件。 说…

光强的检测与控制系统设计

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、实习内容二、实习方法2.1 proteus仿真部分2.2 使用Altium designer软件绘制原理图2.2.1 工程创建2.2.2 绘制封装以及链接封装与原件原理图2.2.3检查原件原理…

python不同版本的下载安装和配置

python下载和安装 1 基础软件安装 sudo apt update sudo apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev libsqlite3-dev wget2 python压缩文件下载 我这里下载的是3.9.9,各位也可以根据自己需要下…

精品Python的定制化图书借阅推荐引擎设计与实现

《[含文档PPT源码等]精品基于Python的定制化图书推荐引擎设计与实现》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 软件开发环境及开发工具&#xff1a; 开发语言&#xff1a;python 使用框架&#xff1a;Django 前端技…