<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 类的方式都被看作是对类的被动使用,都不会导致类的初始化。


相关文章
|
23天前
|
Arthas 监控 Java
Arthas memory(查看 JVM 内存信息)
Arthas memory(查看 JVM 内存信息)
62 6
|
2月前
|
缓存 算法 Java
JVM实战—4.JVM垃圾回收器的原理和调优
本文详细探讨了JVM垃圾回收机制,包括新生代ParNew和老年代CMS垃圾回收器的工作原理与优化方法。内容涵盖ParNew的多线程特性、默认线程数设置及适用场景,CMS的四个阶段(初始标记、并发标记、重新标记、并发清理)及其性能分析,以及如何通过合理分配内存区域、调整参数(如-XX:SurvivorRatio、-XX:MaxTenuringThreshold等)来优化垃圾回收。此外,还结合电商大促案例,分析了系统高峰期的内存使用模型,并总结了YGC和FGC的触发条件与优化策略。最后,针对常见问题进行了汇总解答,强调了基于系统运行模型进行JVM参数调优的重要性。
132 10
JVM实战—4.JVM垃圾回收器的原理和调优
|
2月前
|
消息中间件 Java 应用服务中间件
JVM实战—2.JVM内存设置与对象分配流转
本文详细介绍了JVM内存管理的相关知识,包括:JVM内存划分原理、对象分配与流转、线上系统JVM内存设置、JVM参数优化、问题汇总。
JVM实战—2.JVM内存设置与对象分配流转
|
2月前
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
2月前
|
消息中间件 存储 算法
JVM实战—3.JVM垃圾回收的算法和全流程
本文详细介绍了JVM内存管理与垃圾回收机制,涵盖以下内容:对象何时被垃圾回收、垃圾回收算法及其优劣、新生代和老年代的垃圾回收算法、Stop the World问题分析、核心流程梳理。
JVM实战—3.JVM垃圾回收的算法和全流程
|
2月前
|
存储 Java 编译器
JVM简介—3.JVM的执行子系统
本文详细介绍了Java类的加载、执行及其相关机制,涵盖Class文件结构、字节码指令、类加载器、双亲委派模型、栈桢和方法调用等内容。
JVM简介—3.JVM的执行子系统
|
2月前
|
消息中间件 算法 Java
JVM实战—5.G1垃圾回收器的原理和调优
本文详细解析了G1垃圾回收器的工作原理及其优化方法。首先介绍了G1通过将堆内存划分为多个Region实现分代回收,有效减少停顿时间,并可通过参数设置控制GC停顿时长。接着分析了G1相较于传统GC的优势,如停顿时间可控、大对象不进入老年代等。还探讨了如何合理设置G1参数以优化性能,包括调整新生代与老年代比例、控制GC频率及避免Full GC。最后结合实际案例说明了G1在大内存场景和对延迟敏感业务中的应用价值,同时解答了关于内存碎片、Region划分对性能影响等问题。
|
2月前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
203 29
JVM简介—1.Java内存区域
|
4月前
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
759 166
|
6月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
1167 1