提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
初学Java的时候,你应该用命令行编译过Java文件。Java代码通过javac编译成class文件,而类加载器的作用,就是把class文件装进虚拟机。
加载过程
类加载过程主要分为三个步骤:加载、链接、初始化。
而其中链接过程又分为三个步骤:
- 校验
- 准备
- 解析
加上卸载、使用两个步骤统称为类的生命周期。
- Loading(加载)
把一个一个class文件的二进制loading到内存中 - Linking(链接)
- Verification(校验)
校验加载文件符不符合class规范
- 校验你加载进来的文件符不符合class规范
- Preparation(准备)
准备阶段会为类的静态变量分配内存、赋初值
- 把class的静态变量赋一个初值 如果代码中的static静态变量为8,(static int = 8;)在这里他会把static赋默认值0。初值的规则如下:
- 值得注意的是实例变量是在创建对象的时候完成赋值的,不在准备阶段进行赋值。
- final修饰的常量在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步
- 把class的静态变量赋一个初值 如果代码中的static静态变量为8,(static int = 8;)在这里他会把static赋默认值0。初值的规则如下:
- Resolution(解析)
将常量池当中二进制数据当中的符号引用转化为直接引用的过程(转换为计算机熟悉的地址)
- 在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,多数是以就用符号引用来代替,即(全限定名+描述)等,然后转换为直接引用,就是计算机熟悉的指向对象,类变量和类方法的指针与相对偏移量(指向实例的变量,方法的指针)。
- Resolution会把类、方法属性等等符号引用解析为直接引用
- 常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
- jvm的loadClass方法的resolve就是决定是不是需要解析,为ture代表解析
- Verification(校验)
- initializing
根据程序员程序编码制定的主观计划去初始化类变量和其他资源。
在初始化的时候才会对一个实例变量才赋值一个初始值
类的加载
什么是类的加载?
类的加载指的是
在 Java 中,类的加载过程包括以下步骤:
- 通过编译器将.java文件编译为.class文件: Java 源代码经过编译器编译生成对应的字节码文件(.class 文件),其中包含了类的结构信息和字节码指令。
- 将类的字节码文件读入到内存中: 类加载器负责将类的字节码文件从磁盘读入到内存中,这是类加载的第一步。
- 将内存中的字节码文件放在运行时数据区的方法区内: 类加载器将类的字节码文件加载到方法区(在 Java 8 及之前是方法区,Java 8 后是元空间 Metaspace)中,方法区存储类的结构信息、静态变量、常量池等数据。
- 在 Metaspace 中创建一个 java.lang.Class 对象: 在类加载完成后,JVM 会在 Metaspace 中创建一个 java.lang.Class 对象,用来表示这个类在内存中的数据结构,包括类的结构信息、方法信息等。这个 java.lang.Class 对象是 Java 反射机制的基础,通过它可以访问类的结构信息。
总的来说,类加载是 Java 程序启动时必不可少的过程,它负责将类的字节码文件加载到内存中,并创建对应的数据结构,使得程序能够访问和使用这些类。类加载过程是 Java 虚拟机实现动态性和灵活性的基础之一。
关于 java.lang.Class 对象存储位置的情况:
java.lang.Class 对象的存储位置:
java.lang.Class 对象用来表示类的元数据信息,包括类的结构信息、方法信息等。
在 Java 中,java.lang.Class 对象本身是存储在堆内存中的,用来描述类的元数据信息。
每个类在内存中都有一个对应的 java.lang.Class 对象,这个对象存储在堆内存中,而不是存储在方法区或元空间中。
类的元数据信息:
类的元数据信息,如类的结构信息、常量池、方法信息等,会被加载到方法区(Java 8 之前)或元空间(Java 8 后)中。
java.lang.Class 对象存储在堆内存中,用来表示这些类的元数据信息。
总结:
java.lang.Class 对象存储在堆内存中,用来表示类的元数据信息。
类的元数据信息存储在方法区(Java 8 之前)或元空间(Java 8 后)中。
因此,java.lang.Class 对象和类的元数据信息存储在不同的内存区域
在 Java 8 之前,类的元数据信息(如类的结构信息、常量池、方法信息等)存储在方法区中。而在 Java 8 及之后,这些元数据信息被移至了元空间(Metaspace)中。
方法区(Java 8 之前):用来存储类的元数据信息,包括类的结构信息、常量池、方法信息等。在 Java 8 及之前的版本,方法区是存储这些信息的地方。
元空间(Java 8 及之后):Java 8 开始引入了元空间(Metaspace),用来存储类的元数据信息。元空间是堆内存的一部分,但不同于传统的堆空间,它具有更灵活的内存管理方式。
java.lang.Class 对象:java.lang.Class 对象本身是在堆内存中分配的,用来表示类的元数据信息。这个对象包含了指向类的元数据信息(存储在方法区或元空间中)的引用。
因此,类的元数据信息存储在方法区(Java 8 之前)或元空间(Java 8 及之后),而 java.lang.Class 对象存储在堆内存中,用来表示类的元数据信息。感谢您的提醒,希望这次能更清楚地解释这个概念。
类的加载的最终产品是位于metaspace中的 Class对象, Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。(即我们访问这个类的时候都是通过class对象访问)
其次,类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它
,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。(只有调用到有bug的方法才会保错)
类加载器
当任何一个class他被loading到内存后,他会生成2个内容。
- 第一个内容是他会把二进制的loading到内存,原封不动的放在内存上面的一块区域
- 第二个他生成一个class类的对象,封装类在方法区内的数据结构。以后我们如果去访问class对象,都是通过class对象指向的内存中的class文件,去访问class类的文件内容
- (
class对象在metaspace里面,class对象不是new出来的是hostpot里面loading过程中带出来的
)
内存分区
像存常量、存class这样存各种各样信息的时候他这块内容实际上叫Method Area叫方法区
在1.8之前这个方法区落地在永久代里,1.8之后在metaspace
一个对象的ClassLoader是谁通过下方法:
public class ClassLoaderLevel {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(ClassLoaderLevel.class.getClassLoader());
}
}
类加载器他有一个过程,是分成不同的层次去加载,不同的类加载器负责加载不同的class
类加载器的分类
- 启动类加载器:BootstrapClassLoader
- 扩展类加载器:ExtentionClassLoader
- 应用类加载器:AppClassLoader (也叫做“系统类加载器”)
- 自定义类加载器:Costom ClassLoader
既然只是把class文件装进虚拟机,为什么要用多种加载器呢?
因为Java虚拟机启动的时候,并不会一次性加载所有的class文件(内存会爆),而是根据需要去动态加载。
最顶层的类加载器为Bootstrap,当我们调getClassLoader为null的时候他就到达该层
Bootstrap为什么返回空,因为他是有C++来进行实现的,Java里面并没有一个class进行对应所以返回为null(Bootstrap是c++实现的一个模块)
父加载器
父加载器不是类加载器的加载器,也不是类加载器的父类加载器
- 父加载器不要和其他的语法上混淆在一起,
类加载器的继承关系,这里的继承关系不是Java语法中的子父类extends的继承关系,而是语法上的继承关系
这里找不到我就委托给父加载器去寻找
所有的类加载器往上面找,到最后还是被我们的Bootstrap类加载器给loading进来
双亲委派
一个Class文件需要被loading到内存的时候,指执行的过程
- 这里有任何一个class文件,如果你自定义了 ClassLoader
- 先尝试着去自定义的ClassLader(Custom ClassLoader)里面去找,它内部维护了一个缓存,(已经加载进来的class文件就不需要在进行加载)
- 如果缓存中没有找到的话,他并不是直接加载进内存中,它会去它的父亲,父加载器App ClassLoader的缓存里面找这个class文件
- 如果有返回
- 如果没有,父加载器App也没有,委托给App的父亲Extension类加载器去找
- 如果Extension也没有就委托Extension的父加载器Bootstrap加载器里寻找。
如果到顶层了都没有这个class的缓存,Bootstrap就往回在进行委托给Extension加载器进行加载(但是extension加载器只加载扩展包等等不能加载该class文件)于是在进行向下委托直至委托到达Costom ClassLoader来进行加载。如果加载不成功抛异常Class Not Found
这个就叫做双亲委派 (其实双亲委派指的是由一个从子到父的过程又有一个从父到子的过程)
为什么要搞双亲委派?
主要是为了安全
、安全
、还是TM的安全
- 假如没有这种机制?
- 你自定义的ClassLoader都可以把class文件loading到内存的话,那么存在这么一种class<java.lang.String>直接把Oracle自己写的内部核心String给覆盖掉交给我的自定义的ClssLader来把这个String,loading到内存,
- 接下来把这个整个一个模块打包成一个类库交给我的客户,客户再输入密码的时候要输入String这个对象(自己定义的一个String),这样只要使用了你这个类库我就可以把你的密码偷偷摸摸发送邮件给自己。
- 但是使用双亲委派就不会出现这种问题,
- 还是加载<java.lang.String>的时候,他就产生了警惕
- 他会去先去父加载器查询有没有加载过,查到上面的时候发现已经加载过来,然后就直接给你返回。
类加载器的范围
类加载器的加载路径
打印一个class文件ToString方法,它默认显示的是类的名字加上后面的HashCode码
为了更好的理解,我们可以查看源码,sun.misc.Launcher:
- BootstrapClassLoader
它是一个 java虚拟机 的入口应用:
-
Launcher 初始化了 ExtClassLoader 和 AppClassLoader类加载器。
-
Launcher 中并没有看见 BootstrapClassLoader,但通过 System.getProperty(“sun.boot.class.path”) 【System.getProperty()方法获取系统变量】得到了字符串 bootClassPath,这个应该就是 BootstrapClassLoader 加载的jar包路径。
我们可以先代码测试一下 sun.boot.class.path 是什么内容。
package JVM;
public class ClassLoaderT003 {
public static void main(String[] args) {
System.out.println("Bootstrap");
String pathBoot = System.getProperty("sun.boot.class.path"); //拿到这个属性
System.out.println(pathBoot.replaceAll(";",System.lineSeparator()));//把分号替换成换行符
}
}
得到的结果是: 可以看到,这些全是JRE目录下的jar包或者是class文件。
BootstrapClassLoader 是JAVA中最顶层的ClassLoader, 负责加载JAVA的核心类库,例如如 rt.jar,charset.jar,可以看出基础类型(int.class,String.class)均由它加载
BootstrapClassLoader加载的类库路径为System.getProperty(“sun.boot.class.path”);
- ExtClassLoader源码
可以指定 -D java.ext.dirs 参数来添加和改变 ExtClassLoader 的加载路径。这里我们通过可以编写测试代码:
package JVM;
public class ClassLoaderT003 {
public static void main(String[] args) {
System.out.println("Extension");
String pathExt = System.getProperty("java.ext.dirs"); //拿到这个属性
System.out.println(pathExt.replaceAll(";",System.lineSeparator()));//把分号替换成换行符
}
}
结果如下:ExtClassLoader类加载器的加载路径为java.ext.dirs
- AppClassLoader源码
package JVM;
public class ClassLoaderT003 {
public static void main(String[] args) {
System.out.println("App");
String pathApp = System.getProperty("java.class.path"); //拿到这个属性
System.out.println(pathApp.replaceAll(";",System.lineSeparator()));//把分号替换成换行符
}
}
执行上述代码可以可以看到 AppClassLoader 加载的就是 java.class.path 下的路径。我们同样打印它的值
App
C:\Program Files\Java\jdk1.8.0_333\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\deploy.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\access-bridge-64.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\cldrdata.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\dnsns.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\jaccess.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\jfxrt.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\localedata.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\nashorn.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\sunec.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\sunjce_provider.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\sunmscapi.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\sunpkcs11.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\ext\zipfs.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\javaws.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\jfxswt.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\management-agent.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\plugin.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_333\jre\lib\rt.jar
D:\Learn\LearnDemoProject\Java\out\production\Java
D:\Learn\Softs\IntelliJ IDEA 2022.2.2\lib\idea_rt.jar
结果如下:AppClassLoader类加载器的加载路径为java.class.path
如何自定义类加载器
如果想把一个class类加载进内存的时候,你应该怎么做?
package com.tudeen.study;
public class T005LoaderClassByHand {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = T005LoaderClassByHand.class.getClassLoader().loadClass("com.tudeen.study.ClassLoaderLevel");
System.out.println(aClass.getName());
}
}
这个时候我们会拿到我们的App类加载器,拿到这个类加载器之后我只需要调loadClass方法,把需要加载的类名告诉他,它就会帮我们加载到内存,然后返回一个class类的对象
- 它会先从硬盘上,找到这个类的源码
- 找到class文件源码之后,把他loading到内存
- 于此同时,生成一个class类的对象,把这个class类的对象返回给你
- com.tudeen.study.ClassLoaderLevel返回我们类的名字,说明已经正常加载了
- (这里和反射不一样,loadClass是反射的基石,所谓的反射无非是使用class类的这个对象访问二进制的代码)
loadClass执行的核心逻辑
所以要自定义类加载器,就需要重写findClass方法
所以说Java在初始化对象的时候class对象是Hostpot虚拟机源码执行带出来的
自定义类加载器
package com.tudeen.study;
import java.io.*;
public class T006MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//protected 允许同包下的类和不同包的子类访问,所以继承ClassLoader重写findClass方法
//创建一个文件
File f = new File("D:/Learn/Project/untitled01", name.replaceAll(".", "/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(f);//转换成流
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int b = 0;
while ((b = fis.read()) != 0) { //从文件里面读出来写进字节数组
byteArrayOutputStream.write(b);
}
//转换成为二进制的字节数组
byte[] bytes = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.close();
fis.close();
//这里已经把class文件转换为二进制字节数组了,怎么把这个二进制字节数组转换成Class类的对象呢?
/**
* defineClass
* 这个方法是通过jni实现的,属于jvm的代码,其功能就是通过字节码定义类,所以是通过调用C/C++实现的
* ,该方法根据一段字节流和一个给定的类名字字符串,返回一个表示该类型的Class的实例.
* */
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoader loader = new T006MyClassLoader();
Class<?> aClass = loader.loadClass("com.tudeen.study.HelloWorld");
HelloWorld h = (HelloWorld) aClass.newInstance();
h.main() ;
System.out.println(loader.getClass().getClassLoader());
System.out.println(loader.getParent());
}
}
- 运行结果如下
HelloWorld
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
Process finished with exit code 0
加密
package com.tudeen.study;
import java.io.*;
public class T006MyClassLoader extends ClassLoader {
private static int seed = 0B10110110;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//protected 允许同包下的类和不同包的子类访问,所以继承ClassLoader重写findClass方法
//创建一个文件
File f = new File("D:/Learn/Project/untitled01", name.replaceAll(".", "/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(f);//转换成流
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int b = 0;
while ((b = fis.read()) != 0) { //从文件里面读出来写进字节数组
// 对二进制进行异或加密
byteArrayOutputStream.write(b^seed);
}
//转换成为二进制的字节数组
byte[] bytes = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.close();
fis.close();
//这里已经把class文件转换为二进制字节数组了,怎么把这个二进制字节数组转换成Class类的对象呢?
/**
* defineClass
* 这个方法是通过jni实现的,属于jvm的代码,其功能就是通过字节码定义类,所以是通过调用C/C++实现的
* ,该方法根据一段字节流和一个给定的类名字字符串,返回一个表示该类型的Class的实例.
* */
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoader loader = new T006MyClassLoader();
Class<?> aClass = loader.loadClass("com.tudeen.study.HelloWorld");
HelloWorld h = (HelloWorld) aClass.newInstance();
h.main() ;
System.out.println(loader.getClass().getClassLoader());
System.out.println(loader.getParent());
}
}
通过自己知道的seed去异或二进制字节码文件进行加密与解密。