准备

我是一个java文件如何实现我的功能呢?需要去JVM(Java Virtual Machine)这个地方旅行。

变身

我高高兴兴的来到JVM,想要开始JVM之旅,它确说:“现在的我还不能进去,需要一次转换生成class文件才行”。为什么这样呢?

JVM不能直接加载java文件的原因:

编译

知道原因后,我又问JVM,我怎么才能变成class文件呢,JVM告诉我可以通过javac命令

javac

javac 是 Java 编译器命令用于将 Java 源代码文件编译字节码文件(.class 文件)。

如果最早学习编程的话 肯定在命令行窗口里敲过 javac 转换class字节码文件。

命令格式

javac [options] [source files]

需要注意的是,`javac` 命令需要正确配置 JDK 环境后才能使用。JDK(Java Development Kit)是 Java 开发工具包的缩写,是 Java 应用程序开发核心组件之一。

具体实现

编译器编译源文件时,需要源文件进行语法分析语义分析类型检查操作

执行

知道怎么变身后,我立即通过javac命令,让自己变成可以被JVM执行的class文件。

加载

变成class文件后,我怎么能进入JVM内部呢,是走着去还是坐车去呢?JVM告诉我要通过类加载器进入

类加载器

Java类加载器是Java虚拟机(JVM)中的一个重要组件,它负责将类文件(.class文件)加载到JVM中。

分类

Java 中的类加载器是按照其加载类的特点进行分类的,主要有以下几种类型:

查看使用的类加载器

public class ClassLoaderTest {
    public static void main(String[] args) {
        //启动类加载器
        System.out.println(String.class.getClassLoader());
        //扩展类加载器
        System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());
        //应用程序类加载器
        System.out.println(ClassLoaderTest.class.getClassLoader());
        //扩展类加载器的父加载器
        System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getParent());
        //应用程序类加载器的父加载器
        System.out.println(ClassLoaderTest.class.getClassLoader().getParent());
    }
}

运行结果如下

自定义类加载器

自定义类加载器主要包括两种类型:

重载ClassLoader

代码

public class CustomClassLoader extends ClassLoader {
    private String basePath;

    public CustomClassLoader(String basePath) {
        this.basePath = basePath;
    }

    @Override
    public Class<?&gt; findClass(String name) throws ClassNotFoundException {
        byte[] data = getClassData(name);
        if (data == null) {
            throw new ClassNotFoundException();
        } else {
            // 使用 defineClass 方法将 byte 数组转换为 Class 对象
            return defineClass(name, data, 0, data.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = basePath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
        try (InputStream inputStream = new FileInputStream(path);
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, length);
            }
            return outputStream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}


说明

上述代码继承ClassLoader类,并重写了其中的findClass()方法实现从指定目录中加载类文件的功能

findClass()方法中,首先通过getClassData()方法读取返回类文件的字节数组,如果获取的字节数组为空,则抛出ClassNotFoundException异常;否则,使用defineClass()法将字节数组转换为 Class 对象,并返回该对象

getClassData()方法中,根据传入的类名生成类文件路径,并使用FileInputStream将类文件读入字节数组中。

使用:

public class CustomClassLoaderTest {
    public static void main(String[] args) throws Exception {
        // 创建自定义类加载器,指定类文件所在的目录
        CustomClassLoader classLoader = new CustomClassLoader("F:\classes");

        // 使用自定义类加载器加载 Hello 类
        Class<?> clazz = classLoader.loadClass("com.example.something.Hello");
        Object obj = clazz.getDeclaredConstructor().newInstance();
        System.out.println(obj);
    }
}


基于 URLClassLoader

代码

public class CustomURLClassLoader extends URLClassLoader {
    public CustomURLClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 调用父类 loadClass 方法进行委托加载
            Class<?> clazz = super.findClass(name);
            return clazz;
        } catch (ClassNotFoundException e) {
            // 如果父类无法加载,则尝试在 URL 中加载
            byte[] data = getClassData(name);
            if (data == null) {
                throw new ClassNotFoundException();
            } else {
                // 使用 defineClass 方法将 byte 数组转换为 Class 对象
                return defineClass(name, data, 0, data.length);
            }
        }
    }

    private byte[] getClassData(String className) {
        String path = className.replace('.', '/') + ".class";
        URL[] urls = getURLs();
        for (URL url : urls) {
            try {
                URL classUrl = new URL(url, path);
                // 使用 URLConnection 检查类文件是否存在
                try (InputStream is = classUrl.openStream();
                     ByteArrayOutputStream os = new ByteArrayOutputStream()) {
                    byte[] buffer = new byte[1024];
                    int length;
                    while ((length = is.read(buffer)) != -1) {
                        os.write(buffer, 0, length);
                    }
                    return os.toByteArray();
                }
            } catch (IOException e) {
                // ignore and try next URL
            }
        }
        return null;
    }
}


说明

上述代码继承了 `URLClassLoader` 类,并重写了其中的 `findClass()` 方法,实现尝试使用父类加载器进行加载,如果无法加载,则尝试使用 URL 加载类文件的功能。在 `getClassData()` 方法中,会遍历 `URLClassLoader` 中定义的 URL,检查类文件是否存在,并返回类文件的字节数组,如果无法找到类文件,则返回 `null`。

使用:

public class CustomURLClassLoaderTest {
    public static void main(String[] args) throws Exception {
        // 创建 URL 数组,指定类文件所在的 URL
        URL[] urls = { new URL("file:F:\classes") };

        // 创建父类加载器,使用系统类加载器
        ClassLoader parent = ClassLoader.getSystemClassLoader();

        // 创建自定义 URL 类加载器
        CustomURLClassLoader classLoader = new CustomURLClassLoader(urls, parent);

        // 使用自定义 URL 类加载器加载 Hello 类
        Class<?> clazz = classLoader.loadClass("com.example.something.Hello");
        Object obj = clazz.getDeclaredConstructor().newInstance();
        System.out.println(obj);
    }
}


双亲委派

加载器那么多,我具体是哪个类进行加载得呢?双亲委派机制告诉我答案.

定义

双亲委派是一种Java类加载器的工作机制,它将类加载请求委派给父类加载器,直到顶级系统类加载器。基本思想是,除非有特殊需求,否则所有类的加载任务应该由父类加载器完成,从而保证Java核心库的类型安全和稳定性,并防止恶意代码的自行布置。如果一个没有在父类加载器中发现子类加载器才会尝试加载该类。这种类加载器之间的父子关系被称为“双亲委派模型”.

如图

 

意义

为什么通过双亲委派进行加载呢?

Linking

加载过后,我是否可以被使用了呢?答案是否定的,我还要经历Lingking 阶段,包括Verification、Preparation 和 Resolution。

Verification(验证)

验证阶段,Java虚拟机会进行语法与语义的检查,以保证class文件的完整性和正确性,同时保证被加载的class与虚拟机版本兼容。主要的检查内容包括文件格式、字节码语义、符号引用等。

Preparation(准备)

在准备阶段,Java虚拟机会为类变量分配内存,并且赋予初始值。如果类变量包含静态变量,那么这时也会初始化静态变量。因此,在这个阶段,类变量所使用的空间已经被分配,将其设置默认初始值即可

Resolution(解析)

解析阶段,将类或接口中的符号引用转化为直接引用过程。在 Java 虚拟机加载类时,符号引用是一种指向常量池中某个符号引用,而直接引用则是指向内存中某个位置的直接指针。解析阶段可以理解为是在解决类之间的依赖关系,使各个类之间可以像使用自身成员一样使用别的类中成员

初始化

验证、准备和解析后,我还要经过初始化,才能被使用。

定义

初始化是指在类加载过程最后一步,JVM要对类进行一些初始化的操作,确保类可以安全地使用。在这个阶段,往往包括静态变量显式赋值静态代码块执行。

内容

静态变量显式赋值

当类加载器完成类的加载、验证、准备后,在初始化阶段,JVM对类的静态变量进行显式赋值。如果类定义了多个静态变量,JVM会按照代码中声明顺序进行初始化,并且若发现过程需要访问到其他未初始化的类,JVM会先完成这些类的初始化。

静态代码块的执行

除了静态变量的显式赋值,类的静态代码块也会在初始化阶段执行。当JVM执行类加载的Initializing阶段时,会执行类中所有静态代码块的内容,如果类中没有定义静态代码块,则不执行。这个过程一般用于在使用之前对类进行初始化。

接口初始化

当一个类在初始化时,如果发现其父类还未进行初始化,JVM会先对其父类进行初始化。如果该类实现了接口,也会对这个接口进行初始化操作接口的初始化过程和类一样,都会进行静态变量显式赋值及静态代码块执行,同时还会检查接口中的所有静态方法

功能实现

初始化之后,我才真正的进入JVM中,其它小伙伴需要我的时候,只需要创建我的实例,就可以使用我的功能了,得到我帮助得小伙伴都很感谢我。

GC

在JVM中我过得很开心,也留下了很多足迹。在我走后,如何让我得足迹不对其他小伙伴有影响呢?GC可以帮我解决这个问题

定义

GC(Garbage Collection)是JVM提供的垃圾回收机制。在Java中,对象动态分配的,内存是由JVM自动管理,而不是由程序员手动分配和释放。当一个对象不再被程序引用时,就应该垃圾回收回收占用的内存,这样可以防止内存泄漏和提高内存的。

小结

通过我的旅行,你知道JVM是怎么加载一个类的了么?我们通过加载、Linking、初始化和使用等各个阶段,将Java类完整地载入内存并执行其中定义的方法和变量。这个过程中,每个阶段都扮演着不同的角色,并为类的正常运行提供了必要的支持

参考来源

作者:京东物流 陈昌浩

来源:京东云开发者社区 自猿其说Tech 转载请注明来源

原文地址:https://blog.csdn.net/qq_39535439/article/details/134708457

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_18395.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注