文章目录
👉🏿 共识:
.java 经过编译后生成 .class 文件 - - 位于磁盘中
JVM需要把这些文件load到内存中,才能够进一步使用!
问:JVM把这些文件load到内存,都经历了哪些过程?
1. 类的生命周期
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
1.1 类的加载有哪些场景?
- 使用 new 关键字实例化对象的时候
- 读取或设置一个类型的静态字段(final 修饰的除外,因为final修饰的字段会在编译期进入常量池中)
- 调用一个类的静态方法时
- 使用java.lang.reflect包下的方法对类型进行反射调用的时候,如果类型没有进行过初始化,需要先触发初始化
- 当初始化某个类的时候,如果发现其弗雷没有进行初始化,需要先初始化其父类
- 当虚拟机启动的时候,用户需要制定一个要执行的主类,虚拟机会先初始化这个主类
2. JVM的规范要求
2.1 加载
- 通过类的权限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
2.2 验证
确保Class文件的字节流中包含的信息符合约束
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
2.3 准备
针对类变量分配内存,并初始化(零值)
public static int x = 123; // 初始化为0
public static final int x = 123; // 初始化为123
数据类型 | 初始值(零值) |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | ‘\u0000’ |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
2.4 解析
- 类或接口的解析
- 字段解析
- 方法解析
- 接口方法解析
2.5 初始化
加载、验证、准备、解析,这几个动作,除加载外,均由JVM进行主导控制
初始化,是根据程序员通过程序编码制定的初始计划,去初始化类变量和其他资源
2.6 过程概述
总过程:Loading → Linking(verification → preparation → resolation) → Initializing
- Loading:将Class文件Load到内存中
- Linking
- Vertification:验证class文件的内容格式是否标准
- Preparation(important):将class文件的静态变量赋默认值
- resolation:class常量池中的符号引用转换为内存地址
- Initializing:静态变量赋初始值,调用静态代码块
3. ClassLoader
3.1 理解
JVM中自带有一个类加载器(ClassLoader),也是一个Class
一个Class文件load到内存中,同时生成一个Class类对象(MetaSpace),这个对象指向类这个Class文件(本质上,通过Class类对象访问了Class文件)
👉🏿 e.g - 反射
可以通过Class对象获取某个方法,还能调用这个方法;
方法信息存储在Class对象中
真正在执行到时候,是从CLass文件中,获取方法的二进制码,翻译成java指令,才进行执行
我们不能直接访问class文件,因为这需要我们自己解析,所以通过Class对象进行访问
3.2 类加载器的层次
不同的类加载器负责加载不同的Class文件
- Bootstrap
- jvm内部由C++实现的一个模块,java中没有一个Class与之对应,所以获取的时候,会返回null
- 加载JDK中的核心类(runtime.jar(lib/rt.jar)、charset.jar)
- Extension
- 加载扩展包中的类(jre/lib/ext/*.jar)
- 由-Djava.ext.dirs指定
- Application
- 加载classpath制定的内容
- 我们自己写的java编译生成的class文件
- Custom
- 自定义的类加载器
委托关系:Bootstrap → Extension → Application → Custom
几个类加载器之间,是业务委托关系,没有语法层面的继承关系!
父加载器不是类加载器的加载器,也不是加载器的父类加载器!
所有的ClassLoader,都是由Bootstrap(用C++实现的)加载的,不是由它的parent加载的!
源码中:ClassLoader有一个parent属性,用来生命自己的父加载器!
3.3 加载过程:双亲委派机制
双亲委派:从父到子,从子到父
1.当需要执行一个class文件时,是怎么load到内存的?
- 自下而上从ClassLoader的缓存(对戏那个内部自己维护的一个容器)中查找这个class是否已经被加载
- Custom → Application → Extension → Bootstrap
- 如果缓存中没有找到,然后自上而下从自己负责的区域中查找目标类,并完成加载
- Bootstrap → Extension → Application → Custom
- 如果还是没能找到,无法完成加载,则抛出异常 ClassNotFoundException
2.类加载为什么要使用双亲委派机制?
- 主要是为了业务安全!
- 要求根据一个类的权限定名来加载目标类,且该类只能被加载一次!
- 好处,防止核心类库被覆盖重写
- 好处,防止资源被重复加载,造成浪费
- 双亲委派机制已经被内部写死
- 类加载器的JVM规范:
- "通过一个类的权限定名来获取描述该类的二进制字节流"这个动作放到JVM外部去实现,以便让应用程序自己决定如何获取所需的类
- 源码:是一个递归执行的方法 findLoadedClass()
3.4 类加载器的作用范围
sun.misc.Launcher.class的源码 – ClassLoader的包装类、启动类
Bootstrap:sun.boot.class.path
Extension:java.ext.dirs
App:java.class.path
3.5 自定义类加载器
-
继承ClassLoader
-
实现findClass(String name)方法
public class My_ClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { File f = new File("e:/test/", name.replace(".", "/").concat(".class")); // 定位class文件 try { FileInputStream fis = new FileInputStream(f); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int b = 0; while ((b=fis.read()) !=0) { baos.write(b); } byte[] bytes = baos.toByteArray(); // class文件转数组 baos.close(); fis.close();//可以写的更加严谨 return defineClass(name, bytes, 0, bytes.length); // 将二进制Class文件,转换为Class对象 -- ClassLoader提供的方法 } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); //throws ClassNotFoundException } public static void main(String[] args) throws Exception { ClassLoader l = new T006_MSBClassLoader(); Class clazz = l.loadClass("com.ipds.jvm.Hello"); Class clazz1 = l.loadClass("com.ipds.jvm.Hello"); System.out.println(clazz == clazz1); Hello h = (Hello)clazz.newInstance(); h.m(); System.out.println(l.getClass().getClassLoader()); System.out.println(l.getParent()); System.out.println(getSystemClassLoader()); } }
-
扩展(字节码文件加密)
- 声明一个seed;(相当与salt)
- 使用这个seed与原始的每一个字节进行异或操作,将输出的byte输出到新的class文件(二进制文件)
- 使用自定义的ClassLoader,先对这个二进制文件进行解密操作(使用seed与此文件的每个字节进行异或)
- 对解密后的内容进行加载
public class ClassLoaderWithEnc extends ClassLoader {
public static int seed = 0B10110110; // salt
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File f = new File("c:/test/", name.replace('.', '/').concat(".ipdsclass"));
try {
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
while ((b=fis.read()) !=0) {
baos.write(b ^ seed);// 解密
}
byte[] bytes = baos.toByteArray();
baos.close();
fis.close();
return defineClass(name, bytes, 0, bytes.length); // 将字节码文件,转换为内存中的Class对象
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name); //throws ClassNotFoundException
}
public static void main(String[] args) throws Exception {
encFile("com.ipds.jvm.hello");// 加密
ClassLoader l = new T007_MSBClassLoaderWithEncription();
Class clazz = l.loadClass("com.ipds.jvm.Hello");
Hello h = (Hello)clazz.newInstance();
h.m();
System.out.println(l.getClass().getClassLoader());
System.out.println(l.getParent());
}
private static void encFile(String name) throws Exception {
File f = new File("c:/test/", name.replace('.', '/').concat(".class"));
FileInputStream fis = new FileInputStream(f);
FileOutputStream fos = new FileOutputStream(new File("c:/test/", name.replaceAll(".", "/").concat(".ipdsclass")));
int b = 0;
while((b = fis.read()) != -1) {
fos.write(b ^ seed);
}
fis.close();
fos.close();
}
}