前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM类加载机制与双亲委派模型

JVM类加载机制与双亲委派模型

原创
作者头像
Joseph_青椒
修改2023-08-06 12:09:50
2180
修改2023-08-06 12:09:50
举报
文章被收录于专栏:java_josephjava_joseph

目标概述

这篇文章主要认识jvm和懂得类加载机制,双亲委派模型,分为基础认识,和类加载两大模块,

JVM基础认识

什么是JVM虚拟机

我们知道,java可以做到平台无关性,一次编译四处运行,其实依靠的就是虚拟机,对.class字节码文件来说,不论是Liunx、windows

运行在一个软件上,就是jvm虚拟机,他提供了java编译运行的条件,而同一个jvm,在不同操作系统上,是根据不同操作系统编写的,

相当于做了一层过度,通过jvm,来让java具有跨平台的特性,是java语言的核心

User.java--->编译器----->User.class------>虚拟机------->Window系统

|

|----->虚拟机----->mac系统

|

|

类加载过程

|--->加载--->验证---->准备----->解析---->初始化

操作系统-虚拟机-JRE-JDK的关系

操作系统<虚拟机<JRE<JDK

JRE是java运行的环境,而JDK则是多了一些调试的工具,如编译器,调试器,和一些开发工具

生产环境部署的时候,一般选择JRE而不是JDK,

JRE安装包更小,打包镜像更省空间,但是缺少调试开发工具,如编译器与调试器,

特殊情况,需要编译调试的程序,就需要安装JDK

JVM内存结构

基于JDK8的HotSpot虚拟机,不同虚拟机不同版本会有不一样)

名称

作用

特点

程序计数器

也叫PC寄存器,用于记录当前线程执行的字节码指令位置,以便线程在恢复执行时能够从正确的位置开始

线程私有

Java虚拟机栈

用于存储Java方法执行过程中的局部变量、方法参数和返回值,以及方法执行时的操作数栈

线程私有

本地方法栈

用于存储Java程序调用本地方法的参数和返回值等信息。

线程私有

用于存储Java程序创建的对象,所有线程共享一个堆,堆中的对象可以被垃圾回收器回收,以便为新的对象分配空间

线程共享

元数据区

用于存储类的元数据信息,如类名、方法名、字段名等,以及动态生成的代理类、动态生成的字节码等 元空间是位于本地(直接)内存中的,而不是像JDK8之前方法区位于堆内存中的。

线程共享

image-20230805185808957
image-20230805185808957

运行时常量池就时类加载之后,把类中常量的信息移动到元数据区,

字符串常量池jdk1.7以后哦才能够元数据区移动到堆中

常量池时编译后的概念,属于字节码class文件中,而运行时常量池是类加载之后的概念,存活时间不一样

方法区---可以看成一个接口,永久代和元数据区看成实现,元数据区是代替了永久代

方法区/元空间存储类元数据信息的区域,包括类的结构、方法、字段信息等

JVM类加载机制与双亲委派模型

User.java--->编译器----->User.class------>虚拟机------->Window系统

|

|----->虚拟机----->mac系统

|

|

类加载过程

|--->加载--->验证---->准备----->解析---->初始化

类加载子系统

类加载器ClassLoador

jvm面试必问内容,负责将类的字节码加载的jvm中,具体在下面的双亲委派机制细说

类加载器指的是类加载过程中的加载

链接器(Linker)

  • 负责将Java类的二进制代码链接到Java虚拟机中,并生成可执行的Java虚拟机代码,包括 验证、准备和解析
  • 验证操作主要是验证类的字节码是否符合JVM规范
  • 准备操作主要是为类的静态变量分配内存并设置默认值
  • 解析操作主要是将符号引用转换为直接引用

初始化器(Initializer)

  • 负责执行Java类的静态初始化,包括静态变量的赋值、静态代码块的执行等
  • 初始化器是类加载子系统的最后一个阶段

类加载的三个特点

双亲委派模型、

延迟加载(在需要某个类的时候再加载,减少启动时间)、

动态加载(在jvm运行的时候动态加载和卸载类(反射机制))

双亲委派机制

为何需要双亲委派机制

比如定义一个与jdk中重名的类,比如java.long.Object,与jdk同名同包

恶意注入的话,为了防止这种错乱,采用双亲委派模型

image-20230805194414164
image-20230805194414164

应用程序加载代码的时候,不止直接加载,而是先看父类加载器--拓展类加载器-,这里加载不了,再看启动类加载器,没有的话才会区加载应用程序类加载器,

上面Object的例子,再启动类的时候能加载出来,就不会报错了,在这里可以找到

JDK9模块化系统

随着jdk9模块化系统的引入,类加载器也发生了变化

(至于为何引入模块化系统,这里不做谈论,有人说没有意义,只是新的管理方式,每个模块有独立的开发测试部署环境)

大家可以看IDEA中External Libraies库中查看区别

image-20230805194854957
image-20230805194854957

类加载器ClassLoader源码解读

主要就是loadClass方法

双亲委派就是在这里实现的

代码语言:javascript
复制
 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //首先检查这个class是否已经加载过了
?
            Class<?> c = findLoadedClass(name);
            //c==null表示没有加载
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    //如果父类的加载器为空 则递归到bootStrapClassloader,这里就是双亲委派模型的实现
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
?
                if (c == null) {
                    //这里就是启动类加载不到的情况,会调用findClass法
                    long t1 = System.nanoTime();
                    //findClass默认是未实现的,找不到,就拿应用程序类加载器加载了
                    c = findClass(name);
?
                    // this is the defining class loader; record the stats
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

由此可见,findClass是留给我们程序员实现的,实现这个,就会去找新的其他的Class而不是应用程序中的Class,重写这个方法,就能不走Application,走我们指定生成的类,就能实现自定义类加载器

defineClass适用于定义类的方法,将字节数组转化为Class对象,将其添加到类加载器的命名空间中。这个是我们直接调用就可以了

resloveClass:解析类的方法,将类的引用转换为实际的类,进行链接和初始化

值得注意的是

image-20230805203855356
image-20230805203855356
代码语言:javascript
复制
  private static class PlatformClassLoader extends BuiltinClassLoader {
        static {
            if (!ClassLoader.registerAsParallelCapable())
                throw new InternalError();
        }
        
        
  private static class AppClassLoader extends BuiltinClassLoader {
        static {
            if (!ClassLoader.registerAsParallelCapable())
                throw new InternalError();
        }

说的是继承关系,其实这些类加载器,其实是组合关系,通过组合实现父子属性

自定义类加载器

为何要实现类加载器?

比如需要网络传来的字节码文件,应用程序加载不到,就需要自定义类加载器,拓展加载源

还有就是可以自定义加密的类文件,通过加密,只有这个类加载器加载,保护机密类的安全

做重要的就是:实现类隔离(tomcat有大量的应用)

注意点:

通过上面ClassLoader的解析,可以知道,

我们实现类加载器,要先继承ClassLoader,然后重写findClass而不是loadClass(loadClass会破坏类的双亲委派模型)

重写的时候,获取加载源的流,调用defindClass方法,加载到自定义的命名空间就可以了

可能看到这里,还是不太理解为什么重写findClass方法能实现自定义类加载器了。

重写前

image-20230805210739250
image-20230805210739250

重写后

image-20230805205456231
image-20230805205456231

这样就实现了自定义类加载器了!

编码

代码语言:javascript
复制
public class JosephClassLoader extends ClassLoader {
?
    private String codePath;
?
    public JosephLoader(String codePath) {
        this.codePath = codePath;
    }
?
?
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = codePath + name + ".class";
        System.out.println(fileName);
        //获取输入流
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName));
             ByteArrayOutputStream bos = new ByteArrayOutputStream();) {
            int len;
            byte[] data = new byte[1024];
            while ((len = bis.read(data)) != -1) {
                bos.write(data, 0, len);
            }
            //获取内存中字节数组,因为要调用defineClass方法,获得Class类对象
            byte[] byteCode = bos.toByteArray();
            //执行 defineClass 将字节数组转成Class对象
            Class<?> defineClass = defineClass(null, byteCode, 0, byteCode.length);
            return defineClass;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
?
    }
}

注意!:实现类隔离(tomcat有大量的应用)

自定义类加载器的这个用途,很多人不能理解,

个面试题:

不同类加载器是否会重复加载同个class类

答案是:不同类加载器会加载同名的,同路径,的类,即使时同一份字节码文件,被不同类加载器加载

对于同一份字节码文件,自定义类加载器去加载为对象实例,这些对象时不一样的,也就是说class字节码一样,但是Class对象时不一样的

可以这么理解,字节码文件,是编译好的,.class文件,还没经过jvm,不同的类加载器生成不同的Class类对象,可以类比为类和对象的关系。

这是tomcat常用的

但是!!!对于不自定义类加载器的场景

重复加载一个类,比如类静态代码的重复执行,引发难以意料的问题

为了避免上述情况的发生,采用双亲委派机制来处理

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JVM基础认识
    • 什么是JVM虚拟机
      • 操作系统-虚拟机-JRE-JDK的关系
        • JVM内存结构
        • JVM类加载机制与双亲委派模型
          • 类加载子系统
            • 类加载器ClassLoador
            • 链接器(Linker)
            • 初始化器(Initializer)
          • 类加载的三个特点
            • 双亲委派机制
              • 为何需要双亲委派机制
              • JDK9模块化系统
            • 类加载器ClassLoader源码解读
              • 自定义类加载器
                • 为何要实现类加载器?
                • 编码
            • 注意!:实现类隔离(tomcat有大量的应用)
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
            http://www.vxiaotou.com