Java 基础(类的加载与ClassLoader的理解)

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化

加载

将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象。这个加载的过程需要类加载器参与。

链接

将Java类的二进制代码合并到JVM的运行状态之中的过程。

  1. 验证: 确保加载的类信息符合JVM规范,例如:以cafe开头,没有安全方面的问题
  2. 准备: 正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
  3. 解析: 虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

初始化

  1. 执行类构造器()方法的过程。类构造器()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
  2. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
  3. 虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。

Java的程序要运行需要将编译好的class文件加载到JVM运行时数据区。

JVM用来存储加载的类信息、常量、静态变量、编译后的代码等数据。在虚拟机规范中,这是一个逻辑区划。具体实现根据不同虚拟机来实现。如:oracle的HotSpot在java7中方法去放在永久代,java8放在元数据空间,并且通过GC机制对这个区域进行管理

那么class文件是怎么加载进去的?

在了解类的加载机制之前,我们需要了解一下类的生命周期。Java类从被加载到JVM内存开始,到卸载出内存为止,它的整个生命周期包括了:加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using)和卸载(Unloading)七个阶段。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GWjNPVbT-1598406411235)(/Users/lipan/app/typora-pic/image-20200826090538368.png)]

Java类的加载需要用到类加载器。类加载器负责装入类,搜索网络,jar,zip,文件夹,二进制数据,内存等指定位置的资源。一个Java程序运行,至少有3个不同的类加载器实例,负责加载不同的类。这三个类加载器分别为,启动类加载器(Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader),应用程序类加载器(Application ClassLoader)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hdzykyqq-1598406411236)(/Users/lipan/app/typora-pic/image-20200826090256929.png)]

通过JDK提供的API:java.lang.Class.getClassLoader() 可以进行类加载器的查看,该API会返回装载类的类加载器,如果这个类是由Bootstrap ClassLoader加载的,那个这个方法会返回null。

代码示例:

复制代码
/**
 * 查看类的加载器实例
 */
public class ClassLoaderView {
    public static void main(String[] args) throws Exception {
        // 加载核心类库的 BootStrap ClassLoader
        System.out.println("核心类库加载器:"
                + ClassLoaderView.class.getClassLoader().loadClass("java.lang.String").getClassLoader());
        // 加载拓展库的 Extension ClassLoader
        System.out.println("拓展类库加载器:" + ClassLoaderView.class.getClassLoader()
                .loadClass("com.sun.nio.zipfs.ZipCoder").getClassLoader());
        // 加载应用程序的
        System.out.println("应用程序库加载器:" + ClassLoaderView.class.getClassLoader());

        // 双亲委派模型 Parents Delegation Model
        System.out.println("应用程序库加载器的父类:" + ClassLoaderView.class.getClassLoader().getParent());
        System.out.println(
                "应用程序库加载器的父类的父类:" + ClassLoaderView.class.getClassLoader().getParent().getParent());
    }
}
复制代码

运行结果:

核心类库加载器:null
拓展类库加载器:sun.misc.Launcher$ExtClassLoader@6ff3c5b5
应用程序库加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
应用程序库加载器的父类:sun.misc.Launcher$ExtClassLoader@6ff3c5b5
应用程序库加载器的父类的父类:null

class信息可以存在不同的地方,那么JVM是如何知道我们的类存在什么地方的哪?通过查看sun.misc.Launcher.AppClassLoader的源码我们可以看到,它会读取java.class.path这个配置来获取那些地址加载类资源。参考以下代码示例,利用jsp和jcmd两个命令可以进行验证。

final String var1 = System.getProperty(“java.class.path”);

代码示例:

public class HelloWord {

  public static void main(String[] args) throws IOException {

    System.out.println("Hello Word");
    System.in.read();
  }
}

jsp命令可以查看本机Java进程,jcmd命令可以查看运行时配置:jcmd 进程号 VM.system_properties

)(/Users/lipan/app/typora-pic/image-20200826093026308.png)]

不会,类具有唯一性:同一个类加载器,类名一样,代表是同一个类。

识别方式: ClassLoader Instance id+PackageName+ClassName

验证方式: 使用类加载器,对同一个Class类的不同版本,进行多次加载,检查是否会加载到最新的代码

JVM中的类不可能一直存在,在满足一定条件的情况下类会被卸载掉。在满足该Class的所有实例都已被垃圾回收,同时加载该类的ClassLoader实例也已经被垃圾回收,那么这个类会被JVM卸载掉。在JVM启动中增加

-verbose:class参数,可以输出类加载和卸载的日志信息。

Java中的类并不会重复加载,同一类加载器,同一类名,代表的是同一个类。而避免类重复加载的主要原因在于JVM在加载类时默认采用的是双亲委派模型。所谓的双亲委派模型,就是某个特定的类加载器在接到类的加载请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。由下到上逐级委托,由上到下逐级查找,双亲委派模型保证了Java核心库的类型安全

在这里插入图片描述

注:类加载器之间不存在父类子类的关系,“双亲”是翻译,可以理解为逻辑上定义的上下级关系

可以通过不断创建新的类加载器,加载类,实现热加载

复制代码
public static void main(String[] args) throws Exception {

        URL classUrl = new URL("xx");
        // 测试双亲委派机制
        // 如果使用此加载器作为父加载器,则下面的热更新会失效,因为双亲委派机制,HelloService实际上是被这个类加载器加载的;
        // URLClassLoader parentLoader = new URLClassLoader(new URL[]{classUrl});

        while (true) {

            // 创建一个新的类加载器,它的父加载器为上面的parentLoader
            URLClassLoader loader = new URLClassLoader(new URL[]{classUrl},LoaderTest1.class.getClassLoader());

            Class clazz = loader.loadClass("HelloService");
            System.out.println("HelloService所使用的类加载器:" + clazz.getClassLoader());
            Object newInstance = clazz.newInstance();
            Object value = clazz.getMethod("test").invoke(newInstance);
            System.out.println("调用getValue获得的返回值为:" + value);

            // help gc
            newInstance = null;
            value = null;

            System.gc();
            loader.close();

            Thread.sleep(3000L); // 1秒执行一次
            System.out.println();
        }
    }
复制代码

 

posted @   民宿  阅读(126)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示