JVM 基础篇:类加载器

一.了解JVM

1.1什么是JVM

JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

虚拟机可以分为系统虚拟机程序虚拟机

  • 系统虚拟机是一种虚拟化技术,它模拟整个计算机硬件环境,包括处理器、内存、存储和外部设备。它的主要目标是在单个物理计算机上同时运行多个操作系统。每个虚拟机都具有独立的操作系统和应用程序,就像在不同的物理计算机上运行一样。系统虚拟机的例子包括VMware、VirtualBox和Hyper-V。
  • 程序虚拟机是一种虚拟化技术,它仅模拟计算机上的一个单独的应用程序运行环境,而不是整个操作系统。它的主要目标是提供一个独立的运行环境,使应用程序能够在不同的操作系统上运行而无需修改。程序虚拟机通常用于解决跨平台兼容性的问题,模拟一个应用程序的运行环境,使应用程序能够跨平台运行。常见的程序虚拟机包括Java虚拟机(JVM)。

1.2JRE/JDK/JVM

  • JDK(Java Development Kit) 是整个JAVA的核心,包括了Java运行环境JRE(Java Runtime Envirnment),一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API)。
  • JRE(Java Runtime Environment,Java运行环境), 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器)
  • JVM(Java Virtual Mechinal)是JRE的一部分,叫做JAVA虚拟机,它是整个java实现跨平台的最核心的部分,负责解释执行并运行字节码文件(.class)。

1.3JVM的功能

1.4常见的JVM

1.5JVM的整体结构

二.字节码文件

2.1字节码文件的组成

这里重点看一下基本信息、常量池和方法

2.1.1基本信息

Magic魔数

主副版本号

主版本号不兼容会引发以下错误:

2.1.2常量池

字节码文件中常量池的作用:避免相同的内容重复定义,节省空间。

我们看下面一个案例

public class ConstantPoolTest2 {public static final String a1 = "abc";public static final String a2 = "abc";public static final String abc = "abc";public static void main(String[] args) {ConstantPoolTest2 constantPoolTest = new ConstantPoolTest2();}
}

首先看字段,我们看a1的常量值实际上指向了常量池中的8号

这实际上是常量池中的一个String_info,但是里面并没有存储真正的字符串字面量,而是一个10号常量的索引

10号常量存储的就是真正的字符串字面量

为什么字段不直接存储常量池里字符串字面量的索引?而要先找String_info,然后再找字符串字面量

因为字节码文件被加载的时候会把常量池中String_info加载到字符串常量池中,所以String_info需要存一份引用

那为什么String_info里不直接存储字符串字面量,而是存一份索引?

字段中的变量名也可能要引用常量池里的字符串字面量,如果用String_info存储字符串字面量则不合理,因为字段中的变量名并不是一个字符串的对象

符号引用

2.1.3方法

字节码中的方法区域是存放字节码指令的核心位置,字节码指令的内容存放在方法的Code属性中。

注意:

  • iload是将值复制一份,局部变量表中的值还在
  • istore是将操作数栈的栈顶弹出,并放入局部变量表的某个位置,此时操作数栈的值是不存在的
i=i++的执行流程

iinc x by y:将局部变量表的x号位置增加y

很明显操作数栈中的值一直都是0,只要istore,那么局部变量表中的值也会被覆盖,所以最终i为0

i=++i的执行流程

由于iinc指令在iload指令之前,所以i的最终值是1

2.2字节码文件常用工具

2.2.1 Java 字节码的字节码查看器:javap -v

2.2.2 阿里arthas

Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,大大提升线上问题排查效率。

详细请看官网:简介 | arthas (aliyun.com)

dashboard显示当前系统的实时数据面板,-i刷新实时数据的时间间隔 (ms),-n刷新实时数据的次数

dump已加载类的字节码文件到特定目录:

dump -d 特定目录  类的全限定名(即包名+类名) 

反编译已加载类,得到源码:

jad 类的全限定名(即包名+类名) 

三.类的生命周期

3.1加载阶段

  1. 类加载器ClassLoader根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息
  2. 类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到内存的方法区中。生成一个InstanceKlass对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息。
  3. 同时,Java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.Class对象。作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中)

对于开发者来说,只需要访问堆中的Class对象而不需要访问方法区中所有信息。这样Java虚拟机就能很好地控制开发者访问数据的范围。

加载阶段过后,字节码文件就已经被读取到了内存中,并且会创建一个代表该类的Class对象。

3.2连接阶段

  1. 第一个环节是验证,验证的主要目的是检测Java字节码文件是否遵守了《Java虚拟机规范》中的约束。这个阶段一般不需要程序员参与。
  2. 准备阶段为静态变量(static)分配内存并设置初始值。准备阶段只会给静态变量赋初始值,而每一种基本数据类型和引用数据类型都有其初始值。但注意如果字段是final修饰的基本类型或者字符串常量(经过编译器优化),则会在准备阶段直接赋予最终值。
  3. 解析阶段主要是将常量池中的符号引用替换为直接引用。 符号引用就是在字节码文件中使用编号来访问常量池中的内容。 直接引用不再使用编号,而是使用内存中地址进行访问具体的数据。

3.3初始化阶段

初始化阶段会执行字节码文件中 clinit 部分的字节码指令。

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

以下几种方式会导致类的初始化:

  1. 访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化
  2. 调用Class.forName(String className),注意 Class<?> forName(String name, boolean initialize, ClassLoader loader) 这个构造器可以指定不初始化
  3. new一个该类的对象时。
  4. 执行Main方法的当前类。

我们可以在JVM设置里添加 -XX:+TraceClassLoading 参数,这样可以看到有哪些类被加载

通过测试以下程序可以发现

类被加载时不一定会被初始化,而是在需要初始化的时候才初始化。

类加载但不初始化的情况

  • Class<?> forName(String name, boolean initialize, ClassLoader loader) 这个构造器可以指定不初始化
  • ClassLoader的loadClass(String className);方法也只会加载并编译某类,并不会对其执行初始化
  • 类名.class

面试题分析

clinit指令在以下情况下不会出现

  1. 无静态代码块且无静态变量赋值语句。
  2. 有静态变量的声明,但是没有赋值语句。如 public static int a;
  3. 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。如 public final static int a = 1;

当出现继承关系时

  • 直接访问父类的静态变量,不会触发子类的初始化。
  • 子类的初始化clinit调用之前,会先调用父类的clinit初始化方法。

如果把new B02()去掉会怎么样呢?

数组的创建不会导致数组中元素的类进行初始化。

四.类加载器

类加载器的设计JDK8和8之后的版本差别较大,JDK8及之前的版本中默认的类加载器有如下几种:

4.1启动类加载器(引导类加载器,根加载器,Bootstrap)

启动类加载器是最底层的类加载器,是虚拟机的一部分,它是由C++语言实现的,无法在Java代码中直接获取到,且没有父加载器(这里形容的是父子关系的层次结构,并非继承关系),也没有继承java.lang.lassLoader类。

它主要负责加载由系统属性 “sun.boot.cass.path” 指定的路径下的核心类库(即<JAVA_HOME>/jre/lib),包含了Object、String、Math、装箱类型、日期类等核心类

public class Demo {public static void main(String[] args) {//Bootstrap 引导类加载器//打印为null,是因为Bootstrap是C++实现的ClassLoader classLoader = Object.class.getClassLoader();System.out.println(classLoader);//查看引导类加载器会加载那些jar包URL[] urLs = Launcher.getBootstrapClassPath().getURLs();for (URL urL : urLs) {System.out.println(urL);}}
}

4.2扩展类加载器(ExtClassLoader)

  • 全类名:sum.misc.Launch$ExtClassLoader,Java语言实现。  
  • 扩展类加载器的父加载器是Bootstrap启动类加载器 (注:不是继承关系)  
  • 扩展类加载器负责加载<JAVA_HOME>\jre\lib\ext目录下的类库。

注: JDK9是jdk.internal.loader.ClassLoaders$PlatformClassLoader类

4.3应用程序类加载器(系统类加载器,AppClassLoader)

  • 全类名: sun.misc.Launcher$AppClassLoader  
  • 系统类加载器的父加载器是ExtClassLoader扩展类加载器 (注:不是继承关系)  
  • 系统类加载器负责加载 classpath环境变量所指定的类库,包括项目中自己编写的类文件以及第三方jar包中的类文件,是用户自定义类的默认类加载器。

注: JDK9是jdk.internal.loader.ClassLoaders$AppClassLoader类

4.4三者之间的关系

  • AppClassLoader的父加载器是ExtClassLoader
  • ExtClassLoader的父加载器是Bootstrap
  • Bootstrap是根加载器  
  • AppClassLoader和ExtClassLoader都实现了抽象类ClassLoader

三者之间是没有继承关系的,而是一种组合关系

  • 抽象类ClassLoader有一个字段parent, AppClassLoader和ExtClassLoader通过设置该字段引用,指定父加载器。(是组合关系)
  • AppClassLoader 的parent指向 ExtClassLoader
  • ExtClassLoader 的parent指向 null,(null的原因是因为Bootstrap是C++实现的,通过代码中逻辑判断来转向Bootstrap)

4.5双亲委派机制

双亲委派机制指的是:自底向上查找是否加载过,再由顶向下进行加载。

双亲委派机制的好处

  • 避免类的重复加载:当父加载器已经加载该类时,就没有必要子加载器再加载一遍,保证被加载类的唯一性。
  • 避免核心类篡改:通过双亲委派机制,让顶层的类加 载器去加载核心类,避免恶意代码 替换JDK中的核心类库,比如 java.lang.String,确保核心类 库的完整性和安全性。

我们可以看看下面的程序

这里我自定义了一个String类,并且类的全限定名和JDK内置的String类完全一样。但运行结果如下:

异常提示:在类 java.lang.String 中找不到 main 方法。

why?这里程序在执行时识别的是src中的java.lang.String,src就是classpath,因此会调用系统加载器。但根据双亲委派机制,系统加载器会逐层委派上层加载器来加载此类,在委派的时候,最上层的加载器是根加载器,即根加载器优先级最高。而根加载器能够在jre\lib\rt.jar包中找到一个重名的java.lang.String(即jdk自带的String),因此根据双亲委派最终会由最顶层的根加载器来执行jdk自带的java.lang.String。显然,jdk中的String并没有main()方法,因此报错找不到main()

4.6自定义类加载器

Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。

loadClass默认实现如下:

再看看loadClass(String name, boolean resolve)函数,双亲委派机制的核心代码就位于这里

从上面代码可以明显看出,loadClass(String, boolean)函数即实现了双亲委派模型!整个大致过程如下:

  1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
  2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);)或者是调用bootstrap类加载器来加载。
  3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。

整个调用过程如下图所示

在自定义ClassLoader的子类时候,我们常见的会有两种做法:

  • 重写loadClass()方法:这样会打破双亲委派模型,可能会导致一些Java的核心类无法加载,不建议重写
  • 重写findClass()方法:是在双亲委派模型的框架下进行小范围的改动,建议重写

实战代码如下:

public class MyClassLoader extends ClassLoader {private String root;@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] classData = loadClassData(name);if (classData == null) {throw new ClassNotFoundException();} else {return defineClass(name, classData, 0, classData.length);}}private byte[] loadClassData(String className) {String fileName = root + File.separatorChar+ className.replace('.', File.separatorChar) + ".class";try {InputStream ins = Files.newInputStream(Paths.get(fileName));ByteArrayOutputStream baos = new ByteArrayOutputStream();int bufferSize = 1024;byte[] buffer = new byte[bufferSize];int length = 0;while ((length = ins.read(buffer)) != -1) {baos.write(buffer, 0, length);}return baos.toByteArray();} catch (IOException e) {e.printStackTrace();}return null;}public String getRoot() {return root;}public void setRoot(String root) {this.root = root;}public static void main(String[] args)  {MyClassLoader classLoader = new MyClassLoader();classLoader.setRoot("D:\\");Class<?> testClass = null;try {//需要为com.字节码文件.classloader.A 格式,否则defineClass方法会抛异常testClass = classLoader.loadClass("com.字节码文件.classloader.A");Object object = testClass.newInstance();System.out.println(object.getClass().getClassLoader());} catch (Exception e) {e.printStackTrace();}}
}

注意

  • 实例中的 class文件不能放在classpath下,否则根据双亲委派机制会被应用程序类加载器加载,而不会通过我们自定义类加载器来加载。
  • 这里传递的文件名需要是类的全限定性名称,即 com.字节码文件.classloader.A 格式的,因为 defineClass 方法是按这种格式进行处理的。

4.7Launcher类

AppClassLoader和ExtClassLoader是Launcher的静态内部类,在程序启动时JVM会创建Launcher对象,Launcher构造器会同时会创建扩展类加载器和应用类加载器。

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

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

相关文章

nodejs+wasm+rust debug及性能分析

文章目录 背景v8引擎自带的profilelinux的perf采集wasm三方库性能分析编译debug版本wasmrust程序debug调试异常模型正常模型结论优化 参考 Node使用火焰图优化CPU爆涨 - 掘金 【Node.js丨主题周】理解perf 与火焰图-腾讯云开发者社区-腾讯云 Easy profiling for Node.js Applic…

手搭手zabbix5.0监控redis7

Centos7安装配置Redis7 安装redis #安装gcc yum -y install gcc gcc-c #安装net-tools yum -y install net-tools #官网https://redis.io/ cd /opt/ wget http://download.redis.io/releases/redis-7.0.4.tar.gz 解压至/opt/目录下 tar -zxvf redis-7.0.4.tar.gz -C /opt/ #…

ssm+vue的软考系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的软考系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff…

Java反射实体组装SQL

之前在LIS.Core定义了实体特性&#xff0c;在LIS.Model给实体类加了表特性&#xff0c;属性特性&#xff0c;外键特性等。ORM要实现增删改查和查带外键的父表信息就需要解析Model的特性和实体信息组装SQL来供数据库驱动实现增删改查功能。 实现实体得到SQL的工具类&#xff0c…

现在大火的低代码是怎么回事?进来聊聊低代码

一、前言 开发过程中&#xff0c;只是觉得前端后端合起来&#xff0c;有很多冗余信息&#xff0c;被代码一遍遍重复表达&#xff0c;是一件很枯燥、无聊的事情。 这些枯燥的重复工作&#xff0c;完全可以由机器来做&#xff0c;以便解放出我们的时间&#xff0c;来做更有价值的…

【蓝桥每日一题]-动态规划 (保姆级教程 篇11)#方格取数2.0 #传纸条

目录 题目&#xff1a;方格取数 思路&#xff1a; 题目&#xff1a;传纸条 思路&#xff1a; 题目&#xff1a;方格取数 &#xff08;跑两次&#xff09; 思路&#xff1a; 如果记录一种方案后再去跑另一个方案&#xff0c;影响因素太多了&#xff0c;所以两个方案要同时开…

Java Spring 通过 AOP 实现方法参数的重新赋值、修改方法参数的取值

AOP 依赖 我创建的项目项目为 SpringBoot 项目 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.1.3</version></parent><dependency><groupId…

Linux:firewalld防火墙-基础使用(2)

上一章 Linux&#xff1a;firewalld防火墙-介绍&#xff08;1&#xff09;-CSDN博客https://blog.csdn.net/w14768855/article/details/133960695?spm1001.2014.3001.5501 我使用的系统为centos7 firewalld启动停止等操作 systemctl start firewalld 开启防火墙 systemct…

Java面试(基础篇)——解构Java常见的基础面试题 结合Java源码分析

fail-safe 和fail-fast机制分别有什么作用&#xff1f; Fail-fast&#xff1a;快速失败 Fail-fast &#xff1a; 表示快速失败&#xff0c;在集合遍历过程中&#xff0c;一旦发现容器中的数据被修改了&#xff0c;会立刻抛出ConcurrentModificationException 异常&#xff0c…

Jmeter —— jmeter参数化实现

jmeter参数化 在实际的测试工作中&#xff0c;我们经常需要对多组不同的输入数据&#xff0c;进行同样的测试操作步骤&#xff0c;以验证我们的软件的功能。这种测试方式在业界称为数据驱动测试&#xff0c; 而在实际测试工作中&#xff0c;测试工具中实现不同数据输入的过程称…

使用RestSharp和C#编写程序

以下是一个使用RestSharp和C#编写的爬虫程序&#xff0c;用于爬取www.zhihu.com上的视频。此程序使用了https://www.duoip.cn/get_proxy来获取代理IP。 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks…

Elasticsearch实践:ELK+Kafka+Beats对日志收集平台的实现

可以在短时间内搜索和分析大量数据。 Elasticsearch 不仅仅是一个全文搜索引擎&#xff0c;它还提供了分布式的多用户能力&#xff0c;实时的分析&#xff0c;以及对复杂搜索语句的处理能力&#xff0c;使其在众多场景下&#xff0c;如企业搜索&#xff0c;日志和事件数据分析等…