@TryLoveCatch
2022-04-17T11:46:50.000000Z
字数 8669
阅读 771
Java知识体系
一个java文件,会被编译成class文件,然后jvm把class文件加载到内存,并通过一系列处理,最终转化为可以直接使用的java对象的过程,就是类加载。
加载类,没有强制的约束,要看交给虚拟机的具体实现。
初始化类呢,主要有这几种情况:
特别的
加载-连接-初始化-使用-卸载
其中连接阶段包含验证-准备-解析
加载,主要做了三件事:
特别地,第一件事情(通过一个类的全限定名来获取定义此类的二进制字节流)是由类加载器完成的,具体涉及JVM预定义的类加载器、双亲委派模型等内容,后续会介绍。
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,主要有文件格式验证、元数据验证、字节码验证、符号引用验证等。
文件格式验证:验证字节流是否符合Class文件格式的规范
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
字节码验证:主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会产生危害虚拟机安全的事件
符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验
验证阶段很重要,但是不是必须的,可以取消。
准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。
这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。
public static int value1 = 123;
public static final int value2 = 123;
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化阶段,才真正开始执行类中定义的java程序代码(字节码)。
准备阶段,类变量已经被赋值过一次初始值(零值)了;初始化阶段,则会根据代码来给类变量在此赋值。
初始化阶段是执行类构造器clinit()方法的过程。
关于类构造器clinit()方法:
JVM预定义的三种类加载器:
JVM加载类的时候,采取双亲委托机制,简单来说就是加载任务会先委托给父类加载器。整个流程是这样的:
这里面会有一个问题:
加载器会首先代理给其它类加载器来尝试加载某个类,这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。
在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中,一个类用其 全名 和 一个ClassLoader的实例 作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。
Class.forName(String name)默认会使用调用类的类加载器来进行类加载。
1.防止同一个.class文件重复加载
2.对于任意一个类确保在虚拟机中的唯一性.由加载它的类加载器和这个类的全类名一同确立其在Java虚拟机中的唯一性
3.保证.class文件不被篡改,通过委托方式可以保证系统类的加载逻辑不被篡改
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
//1.先检查是否已经加载过--findLoaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
//2.如果自己没加载过,存在父类,则委托父类
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
//3.如果父类也没加载过,则尝试本级classLoader加载
c = findClass(name);
}
}
return c;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
1.防止同一个.class文件重复加载
2.对于任意一个类确保在虚拟机中的唯一性.由加载它的类加载器和这个类的全类名一同确立其在Java虚拟机中的唯一性
3.保证.class文件不被篡改,通过委托方式可以保证系统类的加载逻辑不被篡改
因为它的双亲委派过程都是在loadClass方法中实现的,那么想要破坏这种机制,就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可。
Class.forName(String name)默认会使用调用类的类加载器来进行类加载。
如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass中实现你自己的加载逻辑即可。
文件系统类加载器例子如下:
package classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
// 文件系统类加载器
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
// 获取类的字节码
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name); // 获取类的字节数组
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
// 读取类文件的字节
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
// 读取类文件的字节码
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
// 得到类文件的完全路径
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
Android 中的 Dalvik/ART 无法像 JVM 那样 直接 加载 class 文件和 jar 文件中的 class,需要通过 dx 工具来优化转换成 Dalvik byte code 才行,只能通过 dex 或者 包含 dex 的jar、apk 文件来加载(注意 odex 文件后缀可能是 .dex 或 .odex,也属于 dex 文件),因此 Android 中的 ClassLoader 工作就交给了 BaseDexClassLoader 来处理。
PathClassLoader 在应用启动时创建,从 data/app/… 安装目录下加载 apk 文件。
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
PathClassLoader 其 dexPath 比较受限制,一般是已经安装应用的 apk 文件路径。
在 Android 中,App 安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的。
对比 PathClassLoader 只能加载已经安装应用的 dex 或 apk 文件,DexClassLoader 则没有此限制,可以从 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的 dex 的加载。
DexClassLoader 的源码里面只有一个构造方法
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
String optimizedDirectory : 用来缓存优化的 dex 文件的路径,即从 apk 或 jar 文件中提取出来的 dex 文件。该路径不可以为空,且应该是应用私有的,有读写权限的路径(实际上也可以使用外部存储空间,但是这样的话就存在代码注入的风险),可以通过以下方式来创建一个这样的路径:
File dexOutputDir = context.getCodeCacheDir();
注:后续发现,getCodeCacheDir() 方法只能在 API 21 以上可以使用。
String libraryPath : 存储 C/C++ 库文件的路径集
PathClassLoader 和 DexClassLoader,但这两者都是对 BaseDexClassLoader 的一层简单封装,真正的实现都在 BaseDexClassLoader 内。
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
...
return c;
}
@Override
protected URL findResource(String name) {
return pathList.findResource(name);
}
@Override
protected Enumeration<URL> findResources(String name) {
return pathList.findResources(name);
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
可以看出来,findClass() 、findResource() 均是基于 pathList 来实现的
private final DexPathList pathList
我们来看DexPathList 的 findClass() 方法
public Class findClass(String name, List<Throwable> suppressed) {
// 遍历 dexElements 数组,依次寻找对应的 class,一旦找到就终止遍历
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
// 抛出异常
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
这里有关于热修复实现的一个点,就是将补丁 dex 文件放到 dexElements 数组前面,这样在加载 class 时,优先找到补丁包中的 dex 文件,加载到 class 之后就不再寻找,从而原来的 apk 文件中同名的类就不会再使用,从而达到修复的目的
至此,BaseDexClassLader 寻找 class 的路线就清晰了:
JVM类生命周期概述:加载时机与加载过程
深入理解Java对象的创建过程:类的初始化与实例化
深入理解Java类加载器(一):Java类加载原理解析
一看你就懂,超详细java中的ClassLoader详解
热修复入门:Android 中的 ClassLoader
类加载机制:全盘负责和双亲委托