JAVA进阶之路JVM-2:类加载机制,类的生命周期,类加载过程,类加载时机,类加载器,双亲委派模型,对象创建过程

JVM类加载机制

类加载

​ 在JVM虚拟机实现规范中,通过ClassLoader类加载把*.class字节码文件(文件流)加载到内存,并对字节码文件内容进行验证,准备,解析和初始化,最终形成可以被虚拟机直接使用的java.lang.Class对象,这个过程被称作类加载

​ 类是在运行期间第一次使用时,被类加载器动态加载至JVM。JVM不会一次性加载所有类。因为如果一次性加载,那么占用很多的内存

类的生命周期

在这里插入图片描述

类的声明周期包括一下7个阶段:

  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化
  • 使用
  • 卸载

结束类生命周期的几种场景:

  • 执行System.exit()方法
  • 程序正常执行结束
  • 程序执行中遇到了异常或错误而异常终止
  • 操作系统出现错误或强制结束程序而导致JVM虚拟机进程终止

类加载过程

类加载过程包括:加载、验证、准备、解析和初始化

可以通过一句谐音来记忆:“家宴准备了西式菜”,家(加载)宴(验证)准备(准备)了西(解析)式(初始化)菜

加载

​ 在加载阶段,JVM主要完成以下三件事:

  • 通过类的完全限定名称获取定义该类的 *.class 字节码文件的二进制字节流
  • 将该字节流表示的静态存储结构转换为Metaspace元空间区的运行时存储结构。
  • 在内存中生成一个代表该类的 class对象,作为元空间区中该类各种数据的访问入口。

由于 JVM 虚拟机对加载*.class字节码文件的来源并未做限制,所以出现了以下的 *.class 字
节码文件加载方式:

  1. 本地文件系统直接读取

  2. 从网络中通过服务器响应读取。例如: web Applet 技术

  3. 从 JAR 、 EAR 、 WAR 等压缩文件中读取

  4. 运行时通过动态代理技术生成字节码文件。例如: 在 java.lang.reflect.Proxy 使用

    ProxyGenerator.generateProxyClass 的代理类的二进制字节流

  5. 由其他文件或容器生成。例如: 由tomcat将*.jsp文件翻译成 *.java 文件后,编译生成对应的.Class字节码文件


​ 在加载阶段完成之后,*.class字节码文件的类信息数据就会存储在元空间,同时在JVM虚拟机堆中生成一个该类的Class对象

验证

在验证阶段,JVM主要确保*.class字节码文件中包含的信息复合当前虚拟机的要求,并不会危害虚拟机的安全

​ 验证阶段会完成下面四个阶段的检验:

  1. **文件格式验证:**验证字节流,是否符合*.class字节码文件 格式的规范,且能被当前版本的虚拟机处理
    • 是否以魔法数0xCAFEBABE开头
    • 主、次版本号是否在当前虚拟机处理范围之内
    • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
    • 指向常量的各种索引值中是否有指向不存在的常量或不符合装型的常量
    • CONSTANT Utf8 info 型的常量中是否有不符合 UTF8编码的数据
    • *.class文件中各个部分及文件本身是否有被删除的或附加的其他信息
  2. **元数据验证:**对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
    • 这个类是否有父类(除了java.lang.Object之外,所有的类都应该有父类)
    • 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
    • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
    • 类中的字段,方法是否与父类产生了矛盾,例如:覆盖了父类的final字段,出现不符合规定的方法重载,例如:方法参数都一致,但返回值类型却不同等
  3. **字节码验证:**通过数据流和控制流分析,确保程序语义是合法的,符合逻辑的
    • 保证跳转指令不会跳转到方法体以外的字节码指令上
    • 保证方法体中的类型转换是有效的,例如: 可以把子类对象赋值给父类数据装型,这是安全的;但把父类对象意赋值给子类数据类型,甚至把对象赋值给毫无继承关系、完全不相千的一个数据类型,则是危险和不合法的
    • 保证任意时刻操作数栈的数据装型与指令代码序列都能配合工作,例如: 不会出现在操作栈中放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中
  4. **符号引用验证:**生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段一一解析阶段中发生。确保解析动作能正常执行。
    • 符号引用中通过字将串描述的全限定名是否能找到对应的类
    • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
    • 符号引用中的类、字段和方法的访问性( privatedefaultprotectedpublic)是否可被当前类访问

为什么需要验证?

Java语言本身是相对安全的语言,但*.class字节码文件并不一定要求用Java源码编译而来,可以使用任何途径,甚至可以用十六进制编译器直接编写来产生*.class字节码文件

​ 类的加载是 JVM 针对 *.class 字节码文件的读取加载机制,所以虚拟机如果不检查输入的字节流,可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。
​ 另外,通过类加载机制的验证环节,可以增强解释器的运行期执行性能。因为,解释器在运行期间无需再对每条执行指令进行检查。

准备

  • 类变量是被static修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是元空间区的内存
  • 实例变量不会在这阶段分配内存,它会在对象实例化时,随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,且类加载只执行一次,实例化可以进行多次
  • 初始值一般为0值

例如:下面的类变量value被初始化为0而不是123

public static int value =123;

如果类变量是常量,那么它将初始化为表达式所定义的值不是0

例如:下面的常量value被初始化为123而不是0

public static final int value =123;

解析

​ 将常量池的符号引用替换为直接引用。(就是把常量池中值的地址直接替换为值)

初始化

初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器<clinit>()方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序指定的主观计划去初始化类变量和其他资源

<clinit>()是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。所以,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问

例如:以下代码中静态变量i只能赋值,不能访问,因为i定义在静态代码块的后开你

public class Test{
  static{
    i=0;							 // 给变量赋值可以正常编译通过
    System.out.print(1);// 这句编译器会提示“非法向前引用”
  }
  static int i = 1;
}

由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如下面代码:

public class Parent{
  public static int A =1 ;
 	static {
    A=2;
  }
  static class Sub extends Parent{
    public static int B=A;
  }
  public static void main(String[] args){
    System.out.println(Sub.B);    // 2
  }
}

<clinit>线程安全

​ 虚拟机会保证一个类<clinin>()方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类<clinit>()方法,其他线程都会阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中,该阻塞非常隐蔽,几乎不会被察觉

类加载的时机

主动引用

虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了只有下列六种情况必须对类进行加载:

  • 当遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,比如new一个对象,读取一个静态字段(未被final修饰)、或调用一个类的静态方法时

    • 当 jvm 执行 new指令时会加载类。即: 当程序创建一个类的实例对象
    • 当 jvm 执行 getstatic 指今时会加载类。即: 程访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
    • 当 jvm 执行 putstatic 指令时会加载类。即: 程序给类的静态变量赋值
    • 当 jvm 执行invokestatic指令时会加载类。即: 程序调用类的静态方法
  • 使用 java.lang.reflect 包的方法对类进行反射调用时如Class.forname("...") ,或

    new Instance()等等。如果类没初始化,需要触发类的加载

  • 加载一个类,如果其父类还未加载,则先触发该父类的加载

  • 当虚拟机启动时,用户需要定义一个要执行的主类(包含 main() 方法的类),虚拟机会先加载这个类

  • 当一个接口中定义了 JDK8 新加入的默认方法 (被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了加载,则该接口要在实现类之前被加载

被动引用

​ 除主动引用之外,所有引用类的方式都不会触发加载,成为被动引用

被动引用常见的例子包括:

  1. 通过子类引用父类的静态字段,不会导致子类加载

    System.out.println(SubClass.value);  // value字段在SubClass类的父类中定义
    
  2. 通过数组定义引用类,不会触发此类的加载。该过程会对数组进行加载,数组类是一个由虚拟机自动生成的,直接继承自Object的子类,其中包含了数组的属性和方法

    SuperClass[] sca = new SuperClass[10];
    
  3. 常量在编译阶段会存入调用类的常量池,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的加载

    System.out.println(ConstClass.HELLOWORLD);
    

类加载器

类加载器

在类加载过程的加载阶段,通过类的完全限定名,获取描述 类的二进制流的实现类,被称为“类加载器”

类加载器分类

从JVM虚拟机的角度来讲,只存在两种不同的类加载器:

  1. **启动类加载器:**使用C++实现,是虚拟机的一部分
  2. **其他类的加载器:**使用java实现,独立于虚拟机,继承自抽象类java.lang.ClassLoader

从Java开发人员的角度来看,类架子啊其可以划分得更细致一些:

  1. 启动类加载器:

    类加载器负责将存放在 <JRE_HOME> \ lib目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar ,名字不符合的类库即使放在 ib 目录中也不会被加载)类库加载到虚拟机内存中。例如java.util.*,java.io.**java.lang.* 类等常用基础库都是由启动类加载器加载。启动类加载器无法被 java程序直接引用。

  2. 扩展类加载器:

    该类加载器是由 ExtClassLoader ( sun.misc.Launcher$ExtclassLoader ) 实现,负责将 <JRE HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,例如 swing 系列、内置的 js引擎、xm 解析器等以 javax 开头的扩展类库都是由扩展类加载器加载,开发者可以直接使用扩展类加载器。

  3. 应用程序类加载器:

    类加载器是由 AppclassLoader(sun.misc.Launcher$AppclassLoader ) 实现。由于这个类加载器是 ClassLoader 中的 getSystemclassLoader() 方法的返回值,因此也被称为系统类加载器。它负责加载用户类路径 ( ClassPath ) 上所指定的类库,比如: 我们自己编写的自定义类或第三方 jar 包。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

在这里插入图片描述

在这里插入图片描述

什么情况下需要自定义类加载器

  1. 隔离加载类。在某些框架内进行中间件与应用的模块之间进行隔离,把类加载到不同的环境
  2. 修改类加载方式
  3. 扩展加载源。比如: 从数据库、网络、电视机顶盒进行类加载
  4. 防止源码泄漏。比如: 编译时字节码进行加密,需要通过自定义类加载器对字节码进行解密还原

双亲委派模型

应用程序是由三种类加载器相互配合,从而实现类加载,除此之外还可以加入自己定义的类加载器

类加载器之间的层级关系,称之为双亲委派模型。该模型要求除了顶层的启动类加载器外,其他的类加载器都要有自己的父类加载器,这里的父子关系一般通过组合关系来实现,而不是继承关系

在这里插入图片描述

双亲委派工作机制

一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载

双亲委派的作用

  1. 每个类只会加载一次,解决了各个类加载器加载基础类的统一问题(基础类库由上层的加载器进行加载)
  2. 防止恶意破坏的类加载,内存中不会出现多份同样的字节码的系统类,保证Java程序安全稳定运行

例如:java.lang.Object存放在 rt.jar 中,如果编写另外一个java.lang.Object 并放到ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在ClassPath 中的 Obiect 优先级更高,因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。 rt.jar 中的 object 优先级更高,那么程序中使用的所有的Object,都是由启动类加载器所加载的Object。

双亲委派的实现源码

​ 以下是抽象类java.lang.ClassLoadr的代码片段,其中的loadClass()方法允许过程如下:先检查是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出ClassNotFoundException,此时尝试自己去加载

在这里插入图片描述

SPI打破双亲委派

SPI是一种服务发现机制,它通过ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类

如下图:SPI核心类定义在rt.jar中(例如:java.lang.Driver接口),所以java.lang.Driver接口本身是由启动类加载器加载,调用java.lang.Driver接口的实现时,启动类加载器无法加载实现类,这个时候就提供了线程上下文类加载器(Thread Context ClassLoader)加载实现类,ThreadContextClassLoader是可以通过java.lang.Thread#setContextClassLoader方法设置类加载器,这样就打破了双亲委派的类加载模式

在这里插入图片描述

对象的创建过程

Step1:类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程

Step2:分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。内存分配的查找方式有“指针碰撞”和“空闲列表” 两种

在这里插入图片描述

选择以上两种方式中的哪一种,取决于Java堆内存是否规整,而Java堆内存是否规整,取决于GC收集器的算法是‘标记-清楚’,还是‘标记-整理’

Step3:初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

Step4:设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。另外根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

Step5:执行init构造方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 构造方法还没有执行,目前所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <int> 构造方法,把对象按照程序逻辑的意愿进行初始化,这样一个真正可用的对象才算完整创建出来

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

易基因: MeRIP-seq等从m6A RNA甲基化角度揭示NFATc1对破骨细胞的调控机制|研究速递

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。 双膦酸盐类药物是强效骨吸收抑制剂&#xff0c;是治疗骨质疏松症、多发性骨髓瘤、骨转移等疾病的首选药物。这些药物通过抑制甲羟戊酸通路和促进破骨细胞凋亡来促进骨吸收。双膦酸盐类药…

计算机网络:快速了解网络框架

文章目录 前言一、什么是Internet&#xff1f;1.从具体构成角度什么是协议&#xff1f; 2.从服务角度3小结 二、网络边缘1.采用网络设施面向连接服务&#xff08;TCP&#xff09;2.采用基础设施的无连接服务&#xff08;UDP&#xff09; 三、网络的核心1.电路交换2.分组交换3.分…

如何在外远程访问本地NAS威联通QNAP?

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;数据结构、Cpolar杂谈 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. 威联通安装cpolar内网穿透二. 内网穿透2.1 创建隧道2.2 测试公网远程访问 三.…

DDD全网最通俗易懂讲解(二)

领域事件相关案例 我来给你介绍一个保险承保业务过程中有关领域事件的案例。 一个保单的生成&#xff0c;经历了很多子域、业务状态变更和跨微服务业务数据的传递。这个过程会产生很多的领域事件&#xff0c;这些领域事件促成了保险业务数据、对象在不同的微服务和子域之间的…

数据库基础入门 — 关联查询

我是南城余&#xff01;阿里云开发者平台专家博士证书获得者&#xff01; 欢迎关注我的博客&#xff01;一同成长&#xff01; 一名从事运维开发的worker&#xff0c;记录分享学习。 专注于AI&#xff0c;运维开发&#xff0c;windows Linux 系统领域的分享&#xff01; 本…

docker容器运维操作命令

docker exec &#xff1a;在运行的容器中执行命令 docker exec [OPTIONS] CONTAINER COMMAND [ARG...] OPTIONS说明&#xff1a; -d :分离模式: 在后台运行 -i :即使没有附加也保持STDIN 打开 -t :分配一个伪终端docker ps : 列出容器 docker ps [OPTIONS] OPTIONS说明&#…

P8安全基本理论A001-CIA安全模型-使用PGP描述网络安全CIA模型之私密性、完整性案例

【教学资源名称】 CIA安全模型-使用PGP描述网络安全CIA模型之私密性、完整性案例 【预备知识】 在信息安全等级保护工作中&#xff0c;根据信息系统的机密性&#xff08;Confidentiality&#xff09;、完整性&#xff08;Integrity&#xff09;、可用性&#xff08;Availabilit…

MySQL的索引

&#x1f4d1;打牌 &#xff1a; da pai ge的个人主页 &#x1f324;️个人专栏 &#xff1a; da pai ge的博客专栏 ☁️宝剑锋从磨砺出&#xff0c;梅花香自苦寒来 目录 &#x1f324;️概念 &#x1f…

vue3安装eslint和prettier,最简单的步骤

第1步&#xff1a; 安装eslint yarn add eslint -D 第2步&#xff1a; 在根文件夹中&#xff0c;创建.eslintrc.js文件 第3步&#xff1a; 在package.json文件中新增命令 "lint": "eslint --fix --ext .ts,.tsx,.vue src --quiet","prettier"…

Day49:647. 回文子串、516.最长回文子序列

文章目录 647. 回文子串思路代码实现 516.最长回文子序列思路代码实现 647. 回文子串 题目链接 思路 确定dp数组&#xff08;dp table&#xff09;以及下标的含义 布尔类型的dp[i][j]&#xff1a;表示区间范围[i,j] &#xff08;注意是左闭右闭&#xff09;的子串是否是回文…

第20章 多线程

创建线程 继承Thread 类 Thread 类时 java.lang 包中的一个类&#xff0c;从类中实例化的对象代表线程&#xff0c;程序员启动一个新线程需要建立 Thread 实例。 Thread 对象需要一个任务来执行&#xff0c;任务是指线程在启动时执行的工作&#xff0c;start() 方法启动线程&am…

mysql 命令行导入sql 数据,windows导入,强制导入

线上用了polarDB&#xff0c; 本地导入的时候&#xff0c;通过navicat 的备份导入和执行sql文件的方式导入都失败了 用命令行的方式可以导入sql 当我用windows 的cmd 导入的时候&#xff0c;会报一些命令行的错误。 那其实我检查了这个命令是没有问题的。 mysql -uroot -p hu…

SAP_ABAP_编程基础_字符转换_内存表、jsonString 相互转换

SAP ABAP 顾问&#xff08;开发工程师&#xff09;能力模型_Terry谈企业数字化的博客-CSDN博客文章浏览阅读441次。目标&#xff1a;基于对SAP abap 顾问能力模型的梳理&#xff0c;给一年左右经验的abaper 快速成长为三年经验提供超级燃料&#xff01;https://blog.csdn.net/j…

这些汽车托运套路你肯定不知道

这些汽车托运套路你肯定不知道 这些套路你肯定不知道.. 学会这三招 汽车托运不怕吃亏 1 看营业执照 首先确定选择的托运公司是否有保障 要求公司出示营业执照和道路运输经营许可证 如果都没有 那就很有可能是无牌照的小作坊!! 这种出问题就肯定没保障 2 保险跟合同 一车一合同 …

Docker Swarm总结+service创建和部署、overlay网络以及Raft算法(2/5)

博主介绍&#xff1a;Java领域优质创作者,博客之星城市赛道TOP20、专注于前端流行技术框架、Java后端技术领域、项目实战运维以及GIS地理信息领域。 &#x1f345;文末获取源码下载地址&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb;…

第20章多线程

创建线程 继承Thread 类 Thread 类时 java.lang 包中的一个类&#xff0c;从类中实例化的对象代表线程&#xff0c;程序员启动一个新线程需要建立 Thread 实例。 Thread 对象需要一个任务来执行&#xff0c;任务是指线程在启动时执行的工作&#xff0c;start() 方法启动线程&am…

【Linux】cd 命令使用

cd&#xff08;英文全拼&#xff1a;change directory&#xff09;命令用于改变当前工作目录的命令&#xff0c;切换到指定的路径。 ~ 也表示为 home 目录 的意思。. 则是表示目前所在的目录。.. 则表示目前目录位置的上一层目录。 语法 cd [目录] 命令选项及作用 执行令 …

【TiDB】TiDB离线方式部署

目录 1 下载TiDB离线组件包 2 安装TiUP 3 合并离线包 4 TIDB 软件和硬件环境建议配置 5 TiDB环境与系统配置检查 6 生成集群初始化配置文件模板 7 执行部署命令 1 检查就能存在的潜在风险 2 手动修复风险 3 部署 TiDB 集群 8 查看TIUP管理的集群情况 9 检查部署的…

OBS Studio 30.0 正式发布:支持 WebRTC

导读OBS Studio 30.0 已正式发布。此版本移除了对 Ubuntu 20.04、Qt 5 和 FFmpeg 4.4 之前版本的支持。 OBS Studio 30.0 已正式发布。此版本移除了对 Ubuntu 20.04、Qt 5 和 FFmpeg 4.4 之前版本的支持。 主要变化包括&#xff1a; 支持 WebRTC&#xff08;详情查看 OBS Stu…

2023.11.27 关于 Mybatis 增删改操作

目录 引言 增加用户操作 删除用户操作 修改用户操作 阅读下述文章之间 建议点击下方链接先了解 MyBatis 的创建与使用 MyBatis 的创建与使用 建议点击下方链接先了解 单元测试 的创建与使用 Spring Boot 单元测试的创建与使用 引言 为了方便下文实现增、删、改操作我们先…