JVM学习之类加载子系统

类加载子系统

在这里插入图片描述
类加载子系统负责从文件或者网络中加载Class文件,class文件在开头有特定的标识

ClassLoader只负责class文件的加载,是否可运行是执行引擎决定的

加载的类信息放在方法区。除了类信息之外,方法区也会放运行时常量池,可能放置字符串字面量和数字字面量(这部分常量信息是Class文件中常量池部分内存映射)

加载

  1. 通过一个类的全限定名获取此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区(JDK8以前是永久代,之后是元数据区)的运行时数据结构
  3. 在内存中生成一个java.lang.Class对象,作为方法区这个类的各种数据访问入口
加载的.class来自哪里
  • 本地系统
  • 网络,如Web Applet
  • 从zip包获取(jar,war都属于此类)
  • 运行时动态计算,大多来自动态代理
  • 其他文件生成,如JSP
  • 从专用数据库中读取出.class文件,比较少见
  • 从加密文件中获取,典型应用场景是防止Class文件被反编译

链接

1.验证
  • 目的在于确保Class文件中包含的信息符合当前虚拟机要求,保证被加载类的正确性,不会危及虚拟机的安全
  • 主要包括四种验证:
    • 文件格式验证:比如开头是CAFABABE
    • 元数据验证
    • 字节码验证
    • 符号引用验证
2.准备
  • 为类变量分配内存并且设置该类变量的默认初始值,即零值。
  • 这里不包含用final修饰的static常量,因为final在编译的时候就分配了,准备阶段会显式的初始化
  • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着类的实例化分配在Java堆中
3. 解析
  • 将常量池中的符号引用转换为直接引用

  • 事实上,解析操作往往会伴随着JVM执行完初始化后再执行

  • 符号引用就是一组符号来描述所引用的目标,直接引用就是直接指向目标的指针、相对偏移量或者一个间接定位到目标的句柄

  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对常量池中的CONSTANT_class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info

  • 符号引用/直接引用理解

    public class Main{private static int a = 1;public static void main(String[] args){System.out.println(a);}
    }
    

    上面的代码编译成字节码,常量池部分:

    #1 = Methodref          #6.#24         // java/lang/Object."<init>":()V
    #2 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
    #3 = Fieldref           #5.#27         // com/example/demo/Main.a:I
    #4 = Methodref          #28.#29        // java/io/PrintStream.println:(I)V
    #5 = Class              #30            // com/example/demo/Main
    #6 = Class              #31            // java/lang/Object
    #7 = Utf8               a
    #8 = Utf8               I
    #9 = Utf8               <init>
    ... //省略其余部分
    

    这些#数字就是符号引用,直接引用就是内存的真实地址或者偏移量或者句柄

初始化

  • 初始化过程就是执行类的构造器方法<clinit()>过程
  • 此方法不需要定义,是javac编译器自动收集类中所有变量的赋值动作和静态代码块中的语句合并而来
  • 构造器方法中指令按语句在源文件中出现的顺序执行
  • clinit()不同于类的构造器。关联:构造器是虚拟机视角下的init()而非clinit()
  • 若该类具有父类,JVM会保证子类的clinit()执行之前,父类的clinit()已经执行完成
  • 虚拟机必须保证同一个类的clinit()在多线程下被同步加锁
public class Main{//以下这个变量在准备阶段会赋零值,也就是a=0//在初始化阶段才会被赋值1private static int a = 1;public static void main(String[] args){System.out.println(a);}
}

以上代码块编译以后,使用bytecode-viewer看下clinit()方法的字节码:

static  { // <clinit> //()VL0 {iconst_1putstatic com/example/demo/Main.a:intreturn}
}

然后上面代码修改成:

public class Main{private static int a = 1;static{a = 2;}public static void main(String[] args){System.out.println(a);}
}

重新编译后查看clinit()方法字节码:

static  { // <clinit> //()VL0 {iconst_1putstatic com/example/demo/Main.a:int}L1 {iconst_2putstatic com/example/demo/Main.a:int}L2 {return}
}

可以看到先赋值为1,后面又赋值为2。

如果我们改变一下static代码块和声明变量a的顺序,代码如下:

public class Main {static{a = 2;}private static int a = 1;public static void main(String[] args){System.out.println(a);}
}

最终输出的值是什么?答案是 1;

原因:

在链接中的准备阶段,会给a申请内存并赋0值,在初始化阶段指令按语句出现的顺序执行,static代码块和声明赋值a=1都会被收集到clinit()中,按照收集顺序,先执行到a=2,再执行a=1,所以最终输出的值是1。

如果此时:

public class Main {static{a = 2;System.out.println(a);//编译错误 Illegal forward reference  非法前向引用错误 static里可以赋值但不能调用}private static int a = 1;public static void main(String[] args){System.out.println(a);}
}

static域中不能调用声明在它下面的变量。

注意:没有static修饰的变量,没有static域的话,是没有clinit()方法的

每个类必然存在一个默认构造器(当然也可以我们显式提供),编译完成后必然有一个init()方法

上面的Main类没有显式的构造方法,编译时会自动加一个,编译后如下:

0 aload_0
1 invokespecial #1 <java/lang/Object.<init> : ()V>
4 return

来看一个有继承的例子:

package com.example.demo;public class Main {public static void main(String[] args) {System.out.println(B.b);}
}class A{public static int a = 1;static {a = 2;}
}class B extends A{public static int b = a;
}

运行Main.main,输出什么? 答案:2

分析:执行Main.main,Main类被加载,执行main函数时引用了B类,此时加载B类,发现B类继承自A类,那么先加载A类,加载完A类后执行链接,初始化步骤,初始化时执行A类的clinit()方法,此时先执行了a = 1,后执行a = 2A类初始化完成后A.a的值是2。A类初始化完成后B类执行链接和初始化,B类初始化过程中执行它的clinit(),此时B.b 赋值为 2,然后Main.main读到的值是2。

接下来验证下多线程情况下,clinit()是否是只会执行1次:

package com.example.demo;public class Main {public static void main(String[] args) {Runnable runnable = () ->{System.out.println(Thread.currentThread().getName() + "开始执行");Test test = new Test();System.out.println(Thread.currentThread().getName() + "执行完成");};Thread thread1 = new Thread(runnable,"线程1");Thread thread2 = new Thread(runnable,"线程2");thread1.start();thread2.start();}
}class Test{static {System.out.println(Thread.currentThread().getName() + ": Test被加载");if(true){while(true){}}}
}

执行日志为:

线程2开始执行
线程1开始执行
线程2: Test被加载

启动了2个线程去new Test()触发加载,只有线程2成功了,线程1阻塞在了Test test = new Test();这一行。

类加载器概述

  • Java支持两种类型的类加载器,分别为:引导类加载器(Bootstrap Classloader)自定义类加载器(User-Defined ClassLoader)
  • 从概念上来讲,自定义类加载器一般是指程序中由开发人员自定义的一类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
  • 无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个:

在这里插入图片描述

注意:上面的ClassLoader是类似等级关系,不是继承关系。Bootstrap ClassLoader不是Java语言实现的。

我们来看几个典型类加载器:

public class Main {public static void main(String[] args) {//获取系统类加载器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();//输出:sun.misc.Launcher$AppClassLoader@18b4aac2 看的出来是应用类加载器System.out.println(systemClassLoader);//获取SystemClassLoader的上一层(这里不是继承关系,只是上一层)类加载器ClassLoader extClassLoader = systemClassLoader.getParent();//输出:sun.misc.Launcher$ExtClassLoader@3b22cdd0 AppClassLoader的上一层是ExtClassLoaderSystem.out.println(extClassLoader);//获取ext类加载器的上一层ClassLoader bootstrapClassLoader = extClassLoader.getParent();//输出:null ExtClassLoader的上一层是null,其实是BootstrapClassLoaderSystem.out.println(bootstrapClassLoader);//获取当前类的加载器ClassLoader classLoaderOfMain = Main.class.getClassLoader();//输出:sun.misc.Launcher$AppClassLoader@18b4aac2和系统类加载器一模一样System.out.println(classLoaderOfMain);//看下String类的类加载器ClassLoader classLoaderOfString = String.class.getClassLoader();//输出:null,是BootstrapClassLoader加载的System.out.println(classLoaderOfString);}
}

注意:Java的核心库都是BootstrapClassLoader加载的

类加载器分类-虚拟机自带的加载器

启动类加载器(引导类加载器)
  • 启动类加载器使用C/C++语言实现的,嵌套在JVM内部。

  • 它用来加载Java核心类库(${JAVA_HOME}/jre/lib/rt.jar,${JAVA_HOME}/jre/lib/resource.jarsun.boot.class.path下的内容)用于提供JVM自身需要的类

  • 并不继承自java.lang.ClassLoader,没有父类加载器

  • 加载扩展类加载器和应用类加载器,并指定他们的父类加载器

  • 出于安全考虑,Bootstrap启动类加载器只加载包名为javajavaxsun等开头的类

    我们看下这个加载器都加载哪些类:

    URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
    Arrays.stream(urLs).map(URL::toExternalForm).forEach(System.out::println);
    输出:
    file:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/lib/resources.jar
    file:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/lib/rt.jar
    file:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/lib/jsse.jar
    file:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/lib/jce.jar
    file:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/lib/charsets.jar
    file:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/lib/jfr.jar
    file:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/classes
    //这个正常情况下不应该有 只是我的IDEA添加了这一项
    file:/Users/xxx/Library/Caches/JetBrains/IdeaIC2022.3/captureAgent/debugger-agent.jar
    
扩展类加载器
  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于ClassLoader
  • 父类加载器为启动类加载器
  • java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载

看下它能加载哪些类:

String extDirs = System.getProperty("java.ext.dirs");
//根据 ; 分割字符串
Arrays.stream(extDirs.split(";"))//每个分割的字符串再根据 : 分割 并合并成一个流.flatMap(extDir -> Stream.of(extDir.split(":"))).forEach(System.out::println);
输出结果:(大多都是MacOs自己加上去的,只有${JAVA_HOME}/jre/lib/ext和/usr/lib/java是默认有的)
/Users/xxx/Library/Java/Extensions
/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/lib/ext
/Library/Java/Extensions
/Network/Library/Java/Extensions
/System/Library/Java/Extensions
/usr/lib/java
应用程序类加载器(系统类加载器 ApplicationClassLoader)
  • Java语言编写,由sun.misc.Launcher$ApplicaitonClassLoader实现
  • 派生于ClassLoader
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或者系统属性java.class.path指定路径下的类库
  • 该类加载器是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
  • 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器

类加载器分类-用户自定义的加载器

在Java的日常莹莹程序开发中,累的加载几乎是由上述3中类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。

为什么要自定义类加载器

  • 隔离加载类,比如特定的中间件
  • 修改类加载的方式
  • 扩展加载源
  • 防止源码泄露

用户自定义类加载器的实现步骤:

  1. 开发人员可以通过继承抽象类java.lang.ClassLoader的方式
  2. JDK1.2以后不再建议用户覆盖loadClass()方法,而是吧自定义类加载逻辑写在findClass()方法中
  3. 在编写自定义类加载器是,如果没有太过复杂的需求,可以直接继承URLClassLoader类,这样可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加高效

双亲委派机制

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把任务交由父类处理,他是一种任务委派模式。

工作原理:

在这里插入图片描述

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

优势:

  1. 避免类重复加载
  2. 保护程序安全,防止核心库类被篡改

类加载器补充

  • 在JVM中表示两个class对象是否为同一个类存在两个必要条件

    • 类的全限定名必须一致
    • 加载这个类的ClassLoader实例对象必须相同

    换句话说,在JVM中,及时两个类对象(class对象)来源于同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。

  • JVM必须知道一个类型是由启动类加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

类的主动使用和被动使用

Java程序对类的使用方式分为主动使用和被动使用,区别就是会不会导致类的初始化

  • 主动使用的七种情况

    • 创建类的实例

    • 访问某个类或接口的静态变量,或对该静态变量赋值

    • 调用类的静态方法

    • 反射(比如:Class.forName(“com.xxx.Test”))

    • 初始化一个类的子类

    • Java虚拟机启动时被标明为启动类

    • JDK7开始提供的动态语言支持:

      java.lang.invoke.MethodHandler实例的解析结果REF_getStatucREF_putStaticREF_invokeStatic句柄对应的类没有初始化,则初始化

  • 除了以上7种情况,其他使用Java类的方式都被看做是对类的被动使用,都不会导致类的初始化

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

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

相关文章

TCP报文头(首部)详解

本篇文章基于 RFC 9293: Transmission Control Protocol (TCP) 对TCP报头进行讲解&#xff0c;部分内容会与旧版本有些许区别。 TCP协议传输的数据单元是报文段&#xff0c;一个报文段由TCP首部&#xff08;报文头&#xff09;和TCP数据两部分组成&#xff0c;其中TCP首部尤其重…

基于EasyExcel的数据导入导出

前言&#xff1a; 代码复制粘贴即可用&#xff0c;主要包含的功能有Excel模板下载、基于Excel数据导入、Excel数据导出。 根据实际情况修改一些细节即可&#xff0c;最后有结果展示&#xff0c;可以先看下结果&#xff0c;是否是您想要的。 台上一分钟&#xff0c;台下60秒&a…

【数据在内存中的存储】

目录 整数在内存中的存储大小端字节序和字节序判断浮点数在内存中的存储 1. 整数在内存中的存储 整数的二进制表示方法有三种: 原码、反码和补码 三种表示方法均有符号位和数值位两部分&#xff0c;符号位0表示“正”,1表示"负",而数值位最高的一位被当做符号位,剩…

《工程数值计算Python教程》笔记

文章目录 [toc]第一章&#xff1a;绪论 1.1 1.1 1.1|数值计算在工程科学中的重要性 1.2 1.2 1.2|数值计算方法 1.3 1.3 1.3|程序设计盒图计算方法的选取减少运算次数避免相近的数相减 1.4 1.4 1.4|误差的来源、表示及传递误差的来源和分类模型误差观测误差截断误差舍入误差 误差…

RabbitMQ消息顺序性保障

RabbitMQ 没有属性设置消息的顺序性&#xff0c;只能设置消息的优先级&#xff0c;因此消息顺序性保障只能在 consumer 上实现 场景分析&#xff1a; 生产者向 RabbitMQ 里发送了三条数据&#xff0c; 顺序依次是 data1-> data2 -> data3&#xff0c;压入的是一个内存…

基于vue+element-plus+echarts制作动态绘图页面(柱状图,饼图和折线图)

前言 我们知道echarts是一个非常强大的绘图库&#xff0c;基于这个库&#xff0c;我们可以绘制出精美的图表。对于一张图来说&#xff0c;其实比较重要的就是配置项&#xff0c;填入不同的配置内容就可以呈现出不同的效果。 当然配置项中除了样式之外&#xff0c;最重要的就是…

Stable Diffusion 微调及推理优化实践指南

随着 Stable Diffsuion 的迅速走红&#xff0c;引发了 AI 绘图的时代变革。然而对于大部分人来说&#xff0c;训练扩散模型的门槛太高&#xff0c;对 Stable Diffusion 进行全量微调也很难入手。由此&#xff0c;社区催生了一系列针对 Stable Diffusion 的高效微调方案&#xf…

Qt之QNetworkAccessManager 从本地和内存中上传数据到Http服务器

简述 接连做了好几个服务器的项目&#xff0c;例如文件传输用的Ftp和对象存储服务器(Object Storage Service)&#xff0c;简单的信息传输用的WebServer&#xff0c;之前也有用过HttpServer不过都和WebServer一样简单的调用接口提交数据并没有上传过文件&#xff0c;正好趁这次…

力扣LCR 130. 衣橱整理(DFS 解法)

Problem: LCR 130. 衣橱整理 文章目录 题目描述思路解题方法复杂度Code 题目描述 思路 首先该问题可以归纳为一类遍历二维矩阵的题目&#xff0c;此类中的一部分题目可以利用DFS来解决&#xff0c;具体到本题目&#xff1a; 我们可以利用一个布尔类型的二维数组记录我们已经访…

no module named ‘xxx‘

目录结构如下 我想在GCNmodel的model里引入layers的GraphConvolution&#xff1a;from GCNmodel.layers import GraphConvolution&#xff0c;但这样却报错no module named GCNmodel&#xff0c;而且用from layers import GraphConvolution也不行。然后用sys.path.appen(xxx)…

selenium-grid4.3.0两种模式记录

selenium-grid4.3.0两种模式记录 本文运行&#xff0c;需要提前配置好Java11以及安装好Chrom、Firefox、Safari其中一个浏览器&#xff0c;如果是Chrom、Firefox需要下载对应版本的驱动&#xff0c;并给 webdriver 配置环境变量&#xff0c;Safari浏览器Mac系统会自带&#xf…

使用下载代替物理串口输出-STM32 Debug (printf) Viewer

使用下载代替物理串口输出-STM32 Debug 硬件要求配置方法代码要求打印输出结果 硬件要求 STM32的PB9、PB10引脚的串口1通常用作其他功能使用后&#xff0c;无法通过printf()函数打印输出想要调试输出查看变量或调试信息。现已使用另外一种方法实现printf()函数打印输出。 ST…

Mr. Cappuccino的第67杯咖啡——MacOS通过PD安装Win11

MacOS通过PD安装Win11 下载ParallelsDesktop安装ParallelsDesktop激活ParallelsDesktop下载Windows11安装Windows11激活Windows11 下载ParallelsDesktop ParallelsDesktop下载地址 安装ParallelsDesktop 关闭上面的窗口&#xff0c;继续操作 激活ParallelsDesktop 关闭上面的…

【LeetCode刷题】-- 161.相隔为1的编辑距离

161.相隔为1的编辑距离 方法&#xff1a;一次遍历 首先&#xff0c;我们要确认字符串的长度不会相差太远。如果长度差了2个或更多字符&#xff0c;那么 s 和 t 就不可能是一次编辑之差的字符串。 接下来&#xff0c;我们假设 s 的长度总是短于或等于 t 的长度。如果不是这样&…

【MATLAB第82期】基于MATLAB的季节性差分自回归滑动平均模型SARIMA时间序列预测模型含预测未来

【MATLAB第82期】基于MATLAB的季节性差分自回归滑动平均模型SARIMA时间序列预测模型含预测未来 一、模型介绍 1、模型简介 季节性差分自回归移动平均模型&#xff08;Seasonal Autoregressive Integrated Moving Average Model, SARIMA&#xff09;&#xff0c;又称为周期性…

uniapp的uni-im 即时通信使用教程【用户与商家对话、聊天 / 最新 / 最全 / 带源码 / 教程】

目录 使用场景用户图片商家图片 官方文档官方文档地址插件地址 项目创建uniCloud开发环境申请开发环境申请完后 概括开始使用步骤1App.vue 步骤2找到软件登录图片找到软件登录接口登录源码如下 步骤3找到软件注册图片注册源码如下 步骤4找到index.vue首页图片 index.vue源码如下…

佛山IBM System x3550 M4服务器维修检查

案例背景&#xff1a; 一家位于东莞的制造公司&#xff0c;在其佛山分厂中安装了一台IBM X3550 M4服务器作为其关键业务设备。该服务器负责管理和存储公司的生产数据、ERP系统和供应链数据。在生产过程中&#xff0c;该服务器突然发生了故障&#xff0c;导致佛山分厂的生产中断…

LT7911D是TYPE-C/DP或者EDP转2 PORT MIPI和LVDS加音频

1.概述&#xff1a; T7911D是一款高性能TYPE-C/DP/EDP转2 PORT MIPI或者LVDS的芯片&#xff0c;目前主要在AR/VR或者显示器上应用的很多&#xff0c;对于DP1.2输入&#xff0c;LT7911D可配置为1/2/4车道。自适应均衡化使其适用于长电缆应用&#xff0c;最大带宽可达21.6Gbps。…

编译android的C版本Lua库

本文讲述如何使用android studio 编译最新版本的Lua开源库),请自行下载。 我们提供的Demo,可以自行下载,工程结构如下: 本文编译的是Lua 5.4.6的版本,编译采用cmake的方式,我们支持编译静态库和动态库(我们在这一讲里:“Lua与***C在Android上的互调”是使用静态库)…

管理类联考——数学——真题篇——按知识分类——几何——解析几何

文章目录 解析几何2023真题&#xff08;2023-07&#xff09;-几何-解析几何-最值-画图求最值-两线相减求最大-联想三角形的“两边差小于第三边”&#xff0c;当为第三边为最大真题&#xff08;2023-19&#xff09;-几何-解析几何-最值-画图求最值-圆方程画出圆的形状-两点间距离…