JVM学习之类加载子系统

news2024/9/22 1:29:32

类加载子系统

在这里插入图片描述
类加载子系统负责从文件或者网络中加载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
  //在初始化阶段才会被赋值1
  private static int a = 1;
  
  public static void main(String[] args){
    System.out.println(a);
  }
}

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

static  { // <clinit> //()V
	L0 {
		iconst_1
		putstatic com/example/demo/Main.a:int
		return
	}
}

然后上面代码修改成:

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

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

static  { // <clinit> //()V
  L0 {
    iconst_1
      putstatic com/example/demo/Main.a:int
  }
  L1 {
    iconst_2
      putstatic 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的上一层是ExtClassLoader
        System.out.println(extClassLoader);

        //获取ext类加载器的上一层
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        //输出:null ExtClassLoader的上一层是null,其实是BootstrapClassLoader
        System.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.coloradmin.cn/o/1316425.html

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

相关文章

漏刻有时数据可视化Echarts组件开发(43)纹理填充和HTMLImageElement知识说明

在 ECharts 中&#xff0c;纹理填充可以通过自定义系列&#xff08;series&#xff09;的 itemStyle 属性来实现。itemStyle 属性用于设置系列中每个数据项的样式&#xff0c;包括填充颜色、边框颜色、边框线宽等。 纹理填充 // 纹理填充 {image: imageDom, // 支持为 HTMLIm…

VM虚拟机打不开原来保存的虚拟机文件夹ubuntu

VMWare虚拟机打不开原来保存的虚拟机文件夹ubuntu 换了电脑把之前的虚拟机克隆的文件夹直接拿来用 报这个错&#xff1a; 指定的文件不是虚拟磁盘 打不开磁盘“D:\ubuntu_iso\ubuntu_location\Ubuntu 64 位-s002.vmdk”或它所依赖的某个快照磁盘。 模块“Disk”启动失败。 未…

HiveSql语法优化三 :join优化

前面提到过&#xff1a;Hive拥有多种join算法&#xff0c;包括Common Join&#xff0c;Map Join&#xff0c;Bucket Map Join&#xff0c;Sort Merge Buckt Map Join等&#xff1b;每种join算法都有对应的优化方案。 Map Join 在优化阶段&#xff0c;如果能将Common Join优化为…

PAT 乙级 1008 数组元素循环右移问题

解题思路:这种循环题有一个经典的O(N)解法&#xff0c;就是前后对称交换&#xff0c;举例&#xff0c;我要循环右移 123456 的后俩个&#xff0c;我们的算法是将56&#xff0c;变成65&#xff0c;把前面的1234变成4321,然后将432165 对称交换就变成了561234 c语言代码如下&…

【Proteus仿真】【51单片机】电子称重秤

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真51单片机控制器&#xff0c;使LCD1602液晶&#xff0c;矩阵按键、蜂鸣器、HX711称重模块等。 主要功能&#xff1a; 系统运行后&#xff0c;LCD1602显示HX711称重模块检测重量&…

Python基础06-异常

零、文章目录 Python基础06-异常 1、异常的基本概念 &#xff08;1&#xff09;异常是什么 当检测到一个错误时&#xff0c;解释器就无法继续执行了&#xff0c;反而出现了一些错误的提示&#xff0c;这就是所谓的"异常"。 &#xff08;2&#xff09;异常演示 …

持续集成交付CICD:Jenkins使用GitLab共享库实现基于SaltStack的CD流水线部署前后端应用

目录 一、实验 1.Jenkins使用GitLab共享库实现基于SaltStack的CD流水线部署前后端应用 2.优化共享库代码 二、问题 1.Jenkins手动构建后端项目流水线报错 一、实验 1.Jenkins使用GitLab共享库实现基于SaltStack的CD流水线部署前后端应用 &#xff08;1&#xff09;GitLa…

MySQL,分组order by

一、创建分组 ## 创建分组 -- 返回每个发布会的参会人数 SELECT event_id,COUNT(*) as canjia_num FROM sign_guest GROUP BY event_id; 1、group by子句可以包含任意个列&#xff0c;但是但指定的所有列都是一起计算的。 group by 后2个字段一起计算的 2、group by后面可以跟…

Kafka-日志索引

Kafka的Log日志梳理 Topic下的消息是如何存储的&#xff1f; 在搭建Kafka服务时&#xff0c;在server.properties配置文件中通过log.dir属性指定了Kafka的日志存储目录。 实际上&#xff0c;Kafka的所有消息就全都存储在这个目录下。 这些核心数据文件中&#xff0c;.log结尾…

某60内网渗透之frp实战指南2

内网渗透 文章目录 内网渗透frp实战指南2实验目的实验环境实验工具实验原理实验内容frp实战指南2 实验步骤(1)确定基本信息。(2)查看frp工具的基本用法(3)服务端frp的配置(4)客户端frp的配置(5)使用frp服务 frp实战指南2 实验目的 让学员通过该系统的练习主要掌握&#xff1a…

方差分析实例

目录 方差分析步骤 相关概念 基本思想 随机误差 系统误差 组内方差 组间方差 方差的比较 方差分析的前提 1.每个总体都应服从正态分布 2.各个总体的方差必须相同 3.观察值是独立的 原假设成立 备择假设成立 单因素方差分析 提出假设 检验的统计量 水平的均值…

云原生之深入解析Linkerd Service Mesh的功能和使用

一、简介 Linkerd 是 Kubernetes 的一个完全开源的服务网格实现&#xff0c;它通过为你提供运行时调试、可观测性、可靠性和安全性&#xff0c;使运行服务更轻松、更安全&#xff0c;所有这些都不需要对代码进行任何更改。Linkerd 通过在每个服务实例旁边安装一组超轻、透明的…

【卡塔尔世界杯数据可视化与新闻展示】

卡塔尔世界杯数据可视化与新闻展示 前言数据获取与处理可视化页面搭建功能实现新闻信息显示详情查看登录注册评论信息管理 创新点结语 前言 随着卡塔尔世界杯的临近&#xff0c;对于足球爱好者来说&#xff0c;对比赛的数据分析和新闻报道将成为关注的焦点。本文将介绍如何使用…

Ubuntu安装蓝牙模块pybluez以及问题解决方案【完美解决】

文章目录 简介问题及解决办法总结 简介 近期因工程需要在Ubuntu中使用蓝牙远程一些设备。安装Bluetooth的Python第三方软件包pybluez时遇到很多问题&#xff0c;一番折腾后完美解决。此篇博客进行了梳理和总结&#xff0c;供大家参考。 问题及解决办法 pip install pybluez安…

nodejs微信小程序+python+PHP技术下的音乐推送系统-计算机毕业设计推荐

音乐推送系统采取面对对象的开发模式进行软件的开发和硬体的架设&#xff0c;能很好的满足实际使用的需求&#xff0c;完善了对应的软体架设以及程序编码的工作&#xff0c;采取MySQL作为后台数据的主要存储单元&#xff0c;  本文设计了一款音乐推送系统&#xff0c;系统为人…

解决vue3+ts打包,ts类型检查报错导致打包失败

最近拉的开源大屏项目goview&#xff0c;在打包的过程中一直报Ts类型报错导致打包失败&#xff0c;项目的打包命令为&#xff1a; "build": "vue-tsc --noEmit && vite build" 是因为 vue-tsc --noEmit 是 TypeScript 编译器&#xff08;tsc&#…

054:vue工具 --- BASE64加密解密互相转换

第054个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 &#xff08;1&#xff09;提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使…

正态总体的假设检验

一、三种情况 1.均值μ的假设检验 (1)σ已知 (2)σ未知 2.方差σ的假设检验 二、例题

【MySQL】数据库基础入门 安装MySQL

目录 介绍&#xff1a; 安装MySQL: 设置 root 账号密码 2.配置环境变量 2.找到 Path 系统变量, 点击 "编辑" 介绍&#xff1a; MySQL是一个开源的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;它是一种用于管理和存储数据的软件。 安装MySQL: …

车规MCU应用场景及国产替代进展

目录 1.车规MCU应用场景 1.1 车身域 1.2 动力底盘域 1.3 座舱域和智驾域 1.4 网联域 2.国产替代进展 3.小结 前面一篇文章征途漫漫:汽车MCU的国产替代往事-CSDN博客对车规MCU国产替代的背景与一些往事进行了简单叙述&#xff0c;今天来聊聊车规MCU具体会在汽车哪些地方用…