<JVM上篇:内存与垃圾回收篇>02-类加载子系统(下)

简介: <JVM上篇:内存与垃圾回收篇>02-类加载子系统

2.3.2. 用户自定义类加载器


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


隔离加载类(比如项目中的中间件和使用到的框架,有可能会有相同的类路径也一样,可能出现冲突问题,需要类加载器进行冲裁.)

修改类加载的方式(修改处理Bootstrap加载器外其他类加载器的使用方式,需要时进行加载.)

扩展加载源(比如:数据库,机顶盒…)

防止源码泄漏(利用自定义加载类对加密的字节码文件 加载并进行解密)

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


开发人员可以通过继承抽象类 java.lang.ClassLoader 类的方式,实现自己的类加载器,以满足一些特殊的需求

在 JDK1.2 之前,在自定义类加载器时,总会去继承 ClassLoader 类并重写 loadClass() 方法,从而实现自定义的类加载类,但是在 JDK1.2 之后已不再建议用户去覆盖 loadclass() 方法,而是建议把自定义的类加载逻辑写在 findClass()方法中

在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免自己去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。


代码演示:

/**
 * 自定义用户类加载器
 * @author shkstart
 * @create 2019 下午 12:21
 */
public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] result = getClassFromCustomPath(name);
            if(result == null){
                throw new FileNotFoundException();
            }else{
                return defineClass(name,result,0,result.length);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        throw new ClassNotFoundException(name);
    }
  //获取字节码文件的字节流
    private byte[] getClassFromCustomPath(String name){
        //从自定义路径中加载指定类:细节略
        //如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
        return null;
    }
    public static void main(String[] args) {
        CustomClassLoader customClassLoader = new CustomClassLoader();
        try {
            Class<?> clazz = Class.forName("One",true,customClassLoader);
            Object obj = clazz.newInstance();
            System.out.println(obj.getClass().getClassLoader());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.4. ClassLoader 的使用说明

ClassLoader 类是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器)

252bb992cb6107568ed2de2bfbee8bfe.png

常见方法如下:

1090b99fa4191151246cc6c778b1e826.png

sun.misc.Launcher 它是一个 java 虚拟机的入口应用

a22114b608dffe484041b591d486a7fd.png


获取 ClassLoader 的途径

  • 方式一:获取当前 ClassLoader
clazz.getClassLoader()


方式二:获取当前线程上下文的 ClassLoader

Thread.currentThread().getContextClassLoader()


方式三:获取系统的 ClassLoader

ClassLoader.getSystemClassLoader()


方式四:获取调用者的 ClassLoader

DriverManager.getCallerClassLoader()


代码演示:

/**
 * @author shkstart
 * @create 2020 上午 10:59
 */
public class ClassLoaderTest2 {
    public static void main(String[] args) {
        try {
            //1.
            ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
            System.out.println(classLoader);//null
            //2.
            ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
            System.out.println(classLoader1);//sun.misc.Launcher$AppClassLoader@18b4aac2
            //3.
            ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
            System.out.println(classLoader2);//sun.misc.Launcher$ExtClassLoader@1b6d3586
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}


2.5. 双亲委派机制


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


工作原理


1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;

2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;

3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。


6e113e1da9f9c305afd3c0fe517af9c9.png

代码演示:

项目下建立java.lang包,里面创建String类

public class String {
    static{
        System.out.println("我是自定义的String类的静态代码块");
    }
}

在java1包下创建StringTest类

public class StringTest {
    public static void main(String[] args) {
        java.lang.String str = new java.lang.String();
        System.out.println("hello,atguigu.com");
        StringTest test = new StringTest();
        System.out.println(test.getClass().getClassLoader());
    }
}

执行StringTest方法


a1ae499b2d00aabbda304d609712ef64.png


在String类中增加Main方法,执行观察结果:


19c66a19fd8d8bbd0e3d414a005af58b.png


举例


当我们加载 jdbc.jar 用于实现数据库连接的时候,首先我们需要知道的是 jdbc.jar 是基于 SPI 接口进行实现的,所以在加载的时候,会进行双亲委派,最终从根加载器(启动类)中加载 SPI 核心类.然后在加载 SPI 接口类,接着在进行反向委派,通过线程上下文类加载器进行实现类 jdbc.jar 的加载。


**补充:**spi是引导类加载器加载,第三方的类库实现是系统类加载器加载。当使用spi时,是无法访问系统类加载器加载,所以引入了线程上下文类加载器,由线程上下文类加载器来加载第三方的库,这样就能使用到第三方的库了


62a9ee37c13333bba5ee92330af0cbc5.png


优势


避免类的重复加载

保护程序安全,防止核心 API 被随意篡改

自定义类:java.lang.String

自定义类:java.lang.ShkStart(报错:阻止创建 java.lang 开头的类)


a64ff4d8f6717dba26674d0675198d1c.png


沙箱安全机制


自定义 String 类,但是在加载自定义 String 类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载 jdk 自带的文件(rt.jar 包中 java\lang\String.class),报错信息说没有 main 方法,就是因为加载的是 rt.jar 包中的 string 类。这样可以保证对 java 核心源代码的保护,这就是沙箱安全机制。


2.6. 其他


如何判断两个 class 对象是否相同


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


类的完整类名必须一致,包括包名。

加载这个类的 ClassLoader(指 ClassLoader 实例对象)必须相同。

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


对类加载器的引用


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


类的主动使用和被动使用


Java 程序对类的使用方式分为:主动使用和被动使用。


主动使用,又分为七种情况:


创建类的实例


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


调用类的静态方法


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


初始化一个类的子类


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


JDK 7 开始提供的动态语言支持:


java.lang.invoke.MethodHandle 实例的解析结果


REF_getStatic、REF_putStatic、REF_invokeStatic 句柄对应的类没有初始化,则初始化


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


相关文章
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
1136 55
|
Arthas 监控 Java
Arthas memory(查看 JVM 内存信息)
Arthas memory(查看 JVM 内存信息)
1125 6
|
11月前
|
存储 缓存 Java
我们来说一说 JVM 的内存模型
我是小假 期待与你的下一次相遇 ~
657 5
|
11月前
|
存储 缓存 算法
深入理解JVM《JVM内存区域详解 - 世界的基石》
Java代码从编译到执行需经javac编译为.class字节码,再由JVM加载运行。JVM内存分为线程私有(程序计数器、虚拟机栈、本地方法栈)和线程共享(堆、方法区)区域,其中堆是GC主战场,方法区在JDK 8+演变为使用本地内存的元空间,直接内存则用于提升NIO性能,但可能引发OOM。
|
存储 Java 编译器
JVM简介—3.JVM的执行子系统
本文详细介绍了Java类的加载、执行及其相关机制,涵盖Class文件结构、字节码指令、类加载器、双亲委派模型、栈桢和方法调用等内容。
JVM简介—3.JVM的执行子系统
|
存储
阿里云轻量应用服务器收费标准价格表:200Mbps带宽、CPU内存及存储配置详解
阿里云香港轻量应用服务器,200Mbps带宽,免备案,支持多IP及国际线路,月租25元起,年付享8.5折优惠,适用于网站、应用等多种场景。
3617 0
|
存储 缓存 NoSQL
内存管理基础:数据结构的存储方式
数据结构在内存中的存储方式主要包括连续存储、链式存储、索引存储和散列存储。连续存储如数组,数据元素按顺序连续存放,访问速度快但扩展性差;链式存储如链表,通过指针连接分散的节点,便于插入删除但访问效率低;索引存储通过索引表提高查找效率,常用于数据库系统;散列存储如哈希表,通过哈希函数实现快速存取,但需处理冲突。不同场景下应根据访问模式、数据规模和操作频率选择合适的存储结构,甚至结合多种方式以达到最优性能。掌握这些存储机制是构建高效程序和理解高级数据结构的基础。
1208 1
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
1221 0
|
存储 弹性计算 固态存储
阿里云服务器配置费用整理,支持一万人CPU内存、公网带宽和存储IO性能全解析
要支撑1万人在线流量,需选择阿里云企业级ECS服务器,如通用型g系列、高主频型hf系列或通用算力型u1实例,配置如16核64G及以上,搭配高带宽与SSD/ESSD云盘,费用约数千元每月。
1730 0