从JDK源码级别剖析JVM类加载机制

1 什么是Java虚拟机

  1. 一个可执行java字节码的虚拟机进程;
  2. 跨平台的是java程序,而不是java虚拟机,java虚拟机在各个操作系统是不兼容的,例如windows、linux、mac都需要安装各自版本的虚拟机,java虚拟机通过jdk实现功能。jvm是用c/c++来写的,它屏蔽了不同操作系统硬件和软件之间的差异;
  3. oracle(原sun公司)虚拟机Sun HotSpot,生产环境使用该jdk。open jdk,使用在linux系统上,开源免费;

2 JVM类加载机制

2.1 类编译

2.1.1 javac

javac Math.java

将Math.java编译成Math.class字节码文件;
字节码本质是一个字节数组byte[](所以被称作字节码文件),它有特定的复杂的内部结构;

2.1.2 javap

javap -v Math.class

将字节码反编译为可读的字节码指令文件;
源代码:

public class SyncCodeBlock {public int i;public void syncTask(){synchronized (this){i++;}}
}

反编译后:

public void syncTask();descriptor: ()Vflags: ACC_PUBLICCode:stack=3, locals=3, args_size=10: aload_01: dup2: astore_13: monitorenter  //注意此处,进入同步方法4: aload_05: dup6: getfield      #2             // Field i:I9: iconst_110: iadd11: putfield      #2            // Field i:I14: aload_115: monitorexit   //注意此处,退出同步方法16: goto          2419: astore_220: aload_121: monitorexit //注意此处,退出同步方法22: aload_223: athrow24: returnException table://省略其他字节码.......

JVM指令手册.pdf

2.1.3 class常量池

class文件包含的信息:类版本号、字段、方法、接口等描述信息、常量池信息;
常量池信息存放了编译器生成的字面量和符号引用;
一个class文件十六进制大体结构:
image.png
对应的含义如下:
image.png
javap -v Math.class如下:
image.png
红框为常量池信息,等号右边字符为“字面量”,左边#为符号引用;
常量池一旦被jvm装载到内存,就是运行时常量池了,对应的符号引用对应被加载到内存代码的直接引用,即动态链接;

2.1.4 字符串常量池

字符串分配,和其他对象分配一样,需要耗费高昂的时间和空间,作为基础数据类型,频繁的创建字符串,极大的耗费性能;
jvm对字符串进行了优化,为字符串开辟常量池,类似缓存区。创建字符串常量池时,先判断是否在该缓存区。存在返回该字符串,不存在,实例化该字符串并放入缓存区;
image.png
包装类常量池,包括:Byte、Short、Integer、Long、Character、Boolean,前5种在数值小于127时才使用对象池;Double没有实现常量池;
字符串常量池,java7之前放在方法区,java7以及以后放在堆区;

2.2 类加载

2.2.1 类加载运行过程

  • 通过java命令运行编译后的class文件,启动类的main函数,通过类加载器将主类加载到JVM内存
package com.firechou.test.testjava.jvm;public class Math {public static final int initData = 666;public static User user = new User();public int compute() {  //一个方法对应一块栈帧内存区域int a = 1;int b = 2;int c = (a + b) * 10;return c;}public static void main(String[] args) {Math math = new Math();int result = math.compute();System.out.println(result);}
}
  • 注意,通过java命令执行带package的类时,需要先进入classes根目录,执行class文件时带上包名
zhouyan@MacBook-Pro classes % pwd
/Users/zhouyan/projects/IdeaProjects/test-group/test-java/target/classes
zhouyan@MacBook-Pro classes % java com.firechou.test.testjava.jvm.Math
30
  • java命令执行代码流程

  • 其中loadClass的类加载过程如下

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

加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
验证:校验字节码文件的正确性;
准备:给类的静态变量分配内存,并赋予默认值;
解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用;
初始化:对类的静态变量初始化为指定的值,执行静态代码块;

2.2.2 类加载到方法区信息

加载到jvm方法区主要包括:运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等;

类加载器的引用:这个类到类加载器实例的引用;
对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
主类在运行过程中用到的其他类加载为懒加载,使用到时才会加载;
可手动执行类加载:

Class.forName(...);

2.3 类加载器

2.3.1 类加载器分类

  • 启动类加载器

BootStrapClassLoader;
也叫引导类加载器,c++编写,负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等;

  • 扩展类加载器

ExtensionCLassLoader;
负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包,比如swing系列、内置js引擎、xml解析器等,通常以javax开头;

  • 应用程序类加载器

AppClassLoader;
负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类;

  • 自定义类加载器

负责加载用户自定义路径下的类包;
继承java.lang.ClassLoader,该类有两个核心方法:loadClass(String, boolean)和findClass(),loadClass实现了双亲委派机制,findClass为空由子类实现;
自定义类加载器就是重写findClass方法;
打破双亲委派机制是重写loadClass方法,修改双亲委派机制的逻辑;

2.3.2 类加载器初始化过程

执行java命令时,虚拟机会创建JVM启动器实例sun.misc.Launcher;

// sun.misc.Launcher的构造方法
public Launcher() {ExtClassLoader var1;try {// 构造扩展类加载器,在构造的过程中将其父加载器设置为null// 使用到了单例模式var1 = Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);}try {// 构造应用类加载器,在构造的过程中将其父加载器设置为ExtClassLoader// Launcher的loader属性值是AppClassLoader,我们一般都是用这个类加载器来加载我们自己写的应用程序this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);} catch (IOException var9) {throw new InternalError("Could not create application class loader", var9);}Thread.currentThread().setContextClassLoader(this.loader);String var2 = System.getProperty("java.security.manager");if (var2 != null) {SecurityManager var3 = null;if (!"".equals(var2) && !"default".equals(var2)) {try {var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();} catch (IllegalAccessException var5) {} catch (InstantiationException var6) {} catch (ClassNotFoundException var7) {} catch (ClassCastException var8) {}} else {var3 = new SecurityManager();}if (var3 == null) {throw new InternalError("Could not create SecurityManager: " + var2);}System.setSecurityManager(var3);}}

其中,各个类加载器的父类为URLClassLoader,将某个类的父类加载器值为某个类加载器,实际上是将父类URLClassLoader的构造方法参数parent值置为某个类加载器;
比如将ExtClassLoader的父类加载器置为null,就是将URLClassLoader构造方法中的parent参数值为null;

// java.net.URLClassLoader
public URLClassLoader(URL[] urls, ClassLoader parent,URLStreamHandlerFactory factory) {super(parent);// this is to make the stack depth consistent with 1.1SecurityManager security = System.getSecurityManager();if (security != null) {security.checkCreateClassLoader();}acc = AccessController.getContext();ucp = new URLClassPath(urls, factory, acc);}

2.4 双亲委派机制

2.4.1 双亲委派机制

  • 什么是双亲委派机制?

双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载;
比如加载自己写的Math类,先由应用程序类加载器委托扩展类加载器加载,再由扩展类加载器委托启动类加载器加载,启动类加载器在自己的类加载路径没找到该Math类,于是退回扩展类加载器加载,扩展类加载器同样在自己的类加载路径没找到该Math类,于是退回应用程序类加载器加载,应用程序类加载器在自己的类加载路径找到了Math类,于是开始执行加载逻辑;

  • AppClassLoader实现双亲委派机制
// java.lang.ClassLoader,实现双亲委派机制
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loaded// 检查当前类加载器是否已经加载了该类Class<?> c = findLoadedClass(name);if (c == null) {// 没有加载该类long t0 = System.nanoTime();try {if (parent != null) {// 如果当前加载器父加载器不为空则委托父加载器加载该类c = parent.loadClass(name, false);} else {// 如果当前加载器父加载器为空则委托引导类加载器加载该类c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.// 如果没有找到则退回下级类加载器加载,findClass由对应的类加载器实现long t1 = System.nanoTime();// 都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) { // 为false不会执行resolveClass(c);}return c;}}

其中,AppClassLoader和ExtClassLoader都继承了URLClassLoader类,URLClassLoader类实现了ClassLoader类的findClass方法;

  • 双亲委派机制作用

沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改,保证安全;
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
提高类的使用效率:一般程序中大部分代码都是自己写的类,通过双亲委派机制AppClassLoader加载这些类,在再次使用到该类时,明显提高了性能;

  • 全盘负责委托机制

“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入。

2.4.2 自定义类加载器

自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法。

package com.firechou.test.testjava.jvm;import lombok.Data;@Data
public class User1 {private String name;public void print(){System.out.println("com.firechou.test.testjava.jvm.User.print");}
}
package com.firechou.test.testjava.jvm;import java.io.FileInputStream;
import java.lang.reflect.Method;public class MyClassLoaderTest {static class MyClassLoader extends ClassLoader {private String classPath;public MyClassLoader(String classPath) {this.classPath = classPath;}private byte[] loadByte(String name) throws Exception {name = name.replaceAll("\\.", "/");FileInputStream fis = new FileInputStream(classPath + "/" + name+ ".class");int len = fis.available();byte[] data = new byte[len];fis.read(data);fis.close();return data;}protected Class<?> findClass(String name) throws ClassNotFoundException {try {byte[] data = loadByte(name);//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。return defineClass(name, data, 0, data.length);} catch (Exception e) {e.printStackTrace();throw new ClassNotFoundException();}}}public static void main(String args[]) throws Exception {//初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoaderMyClassLoader classLoader = new MyClassLoader("/Users/zhouyan/projects/IdeaProjects/");//如上目录下再创建 com/firechou/test/testjava/jvm 子目录(对应类的包名),将User类的复制类User1.class丢入该目录// com.firechou.test.testjava.jvm.User.print()Class clazz = classLoader.loadClass("com.firechou.test.testjava.jvm.User1");Object obj = clazz.newInstance();Method method = clazz.getDeclaredMethod("print", null);method.invoke(obj, null);System.out.println(clazz.getClassLoader().getClass().getName());/*** com.firechou.test.testjava.jvm.User.print* com.firechou.test.testjava.jvm.MyClassLoaderTest$MyClassLoader*/}
}

注意:com.firechou.test.testjava.jvm.User同级的User1.java类要删除掉,否则程序运行时会在target目录下生成对应的User1.class,根据双亲委派机制,最终得到类加载器仍然是AppClassLoader;

2.4.3 tomcat打破双亲委派机制

  • 打破双亲委派机制

实现方案:
自定义类加载器,**重写loadClass()**方法,判断如果是自定义的类则使用自己的类加载器加载,如果是其他类还是遵行双亲委派机制,也必须遵行双亲委派机制,否则报错。

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();// 注释此处逻辑,不走双亲委派机制// try {//     if (parent != null) {//         c = parent.loadClass(name, false);//     } else {//         c = findBootstrapClassOrNull(name);//     }// } catch (ClassNotFoundException e) {//     // ClassNotFoundException thrown if class not found//     // from the non-null parent class loader// }if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}
  • tomcat为什么要打破双亲委派机制?

web容器可以部署多个应用,需支持加载同一个类库的不同版本。默认的类加载器不会识别版本,只认类的全限定名,所以同一个类库的不同版本默认加载器只会加载一次;
web容器也有自己的类库,容器的类库应该与程序的类库分开;
需要支持jsp修改热加载。jsp编译后也是class文件,class文件修改了但是类名没修改,默认类加载器不会重新加载该类,需要卸载该类加载器,重新创建类加载器,才可以重新加载jsp文件,每一个jsp文件对应一个类加载器;

  • tomcat的几个类加载器


CommonClassLoader,tomcat最基本的类加载器,加载路径中的class可被tomcat容器和所有webapp访问;
CatalinaClassLoader,tomcat容器私有类加载器,对webapp不可见;
SharedClassLoader,webapp共享类加载器,tomcat容器不可见,对所有webapp可见;
WebappClassLoader,各个webapp私有类加载器,只对自己的webapp可见;
JsperLoader,加载范围为当前jsp文件编译后的.class文件,当tomcat监测到jsp文件被修改,就会删除该JsperLoader实例,再创建新的JsperLoader实例,从而实现了jsp的热加载;

  • tomcat这种类加载机制违背了java推荐的双亲委派模型了吗?

违背了。很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个WebappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。

  • 多个相同全限定名类对象可以共存

同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个。

  • 模拟实现Tomcat的JasperLoader热加载

原理:后台启动线程监听jsp文件变化,如果变化了找到该jsp对应的servlet类的加载器引用(gcroot),重新生成新的JasperLoader加载器赋值给引用,然后加载新的jsp对应的servlet类,之前的那个加载器因为没有gcroot引用了,下一次gc的时候会被销毁。

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

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

相关文章

docker-compose实现微服务jar+mysql的容器服务发布(经典版)

一 安装mysql服务 1.1 拉取镜像 1.拉取&#xff1a; docker pull mysql:5.7.29 2.查看镜像&#xff1a; docker images 1.2 在宿主机创建文件存储mysql 1.创建映射目录&#xff1a;mysql-c5 在/root/export/dockertest 目录下&#xff0c;mkdir -p mysql-c5 &#…

Elasticsearch介绍和安装

ELK简介 Elastic Stack核心产品包括Elasticsearch、Logstash、Kibana&#xff08;也称为ELK&#xff09;和Beats等等。能够安全可靠地获取任何来源、任何格式的数据&#xff0c;然后实时地对数据进行搜索、分析和可视化 Kibana是一个免费且开放的用户界面&#xff0c;能…

LVS +Keepalived高可用群集

文章目录 一、Keepalived概述二、Keepalived服务重要功能1.管理 LVS 负载均衡软件2.支持故障自动切换&#xff08;Failover&#xff09;3.实现 LVS 集群中节点的健康检查&#xff08;Health Checking&#xff09;4.VRRP通信原理 三、keepalived体系主要模块及作用四、keepalive…

LogicFlow 在HTML中的引入与使用

LogicFlow 在HTML中的引入与使用 LogicFlow的引入与使用&#xff0c;相较于BPMNJS相对容易一些&#xff0c;更加灵活一些&#xff0c;但是扩展代码可能写得更多一些。 示例展示 示例代码 github: https://github.com/iotzzh/origin-examples/blob/main/%E6%B5%81%E7%A8%8B%E5%9…

ADB 命令结合 monkey 的简单使用,超详细

一&#xff1a;ADB简介 1&#xff0c;什么是adb&#xff1a; ADB 全称为 Android Debug Bridge&#xff0c;起到调试桥的作用&#xff0c;是一个客户端-服务器端程序。其中客户端是用来操作的电脑&#xff0c;服务端是 Android 设备。ADB 也是 Android SDK 中的一个工具&#…

C++中的继承/虚继承原理

C中的继承 文章目录 C中的继承1.继承的概念和定义1.1 继承定义1.12 继承关系和访问限定符2.基类和派生类对象的复制转换3.继承中的作用域4.派生类的默认成员函数继承与友元 6.**继承与静态成员****复杂的菱形继承及菱形虚拟继承**7.虚继承解决数据冗余和二义性的原理 1.继承的概…

亚马逊云科技Zero ETL数据库,助力企业走向数据驱动的业务增长之路

据Forrester研究&#xff0c;相对于数据应用不够成熟的公司&#xff0c;那些有效获取业务洞察的公司&#xff0c;有高达8.5倍的可能性实现至少20%的收入增长。然而&#xff0c;要实现这一增长&#xff0c;需要简化一项流程——在数据分析前管理和准备好数据。这就是为什么亚马逊…

Java动态规划LeetCode1137. 第 N 个泰波那契数

方法1&#xff1a;通过动态规划解题&#xff0c;这道题也是动态规划的一道很好的入门题&#xff0c;因为比较简单和容易理解。 代码如下&#xff1a; public int tribonacci(int n) {//处理特殊情况if(n0){return 0;}if(n1||n2){return 1;}//定义数组int[]dpnew int[n1];//初…

随机数检测(三)

随机数检测&#xff08;三&#xff09;- 块内最大游程检测、二元推导检测、自相关检测、矩阵秩检测 3.8 块内最大游程检测方法3.9 二元推导检测方法3.10 自相关检测3.11 矩阵秩检测 如果商用密码产品认证中遇到问题&#xff0c;欢迎加微信symmrz或13720098215沟通。 3.8 块内最…

uniapp - [全端兼容] 多选弹框选择器,弹框形式的列表多选选择器组件插件(底部弹框式列表多选功能,支持数据回显、动态数据、主题色等配置)

前言 网上的教程都太乱了,各种不兼容且 BUG 太多,注释也没有很难进行改造。 本文 实现了 uniapp 全端兼容的弹框多选选择器,从底部弹出列表项进行多选(可回显已选中和各种主题色、样式配置), 您可以直接复制代码,稍微改改样式就能用了。 如下图所示,数据列表(支持接口…

23 | MySQL是怎么保证数据不丢的?

以下内容出自《MySQL 实战 45 讲》 23 | MySQL是怎么保证数据不丢的&#xff1f; binlog 的写入机制 1、事务执行过程中&#xff0c;先把日志写到 binlog cache&#xff0c;事务提交的时候&#xff0c;再把 binlog cache 写到 binlog 文件中。 2、一个事务的 binlog 是不能被…

C++的范围for语句详解 附易错实例

&#x1f4af; 博客内容&#xff1a;C读取一行内个数不定的整数的方式 &#x1f600; 作  者&#xff1a;陈大大陈 &#x1f680; 个人简介&#xff1a;一个正在努力学技术的准前端&#xff0c;专注基础和实战分享 &#xff0c;欢迎私信&#xff01; &#x1f496; 欢迎大家&…