java的类加载
类加载机制
JVM类加载机制分为五个部分: 加载,验证,准备,解析,初始化
加载
加载是类加载过程中的一个阶段,会在内存中生成一个代表这个类的java.lang.Class 对象,作为方法区这个类的各种数据的入口
加载可以是从Class文件中获取,也可以从jar,war包读取获取,也可以在运行时生成(动态代理),以及JSP文件转换为Class类
验证
这个阶段主要是为了 确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求
,并且不会危害虚拟机自身
准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,在方法区分配这些变量所使用的内存空间
例如:
public static int v = 666;
复制
此时会先给v分配内存,初始化变量为0值,在编译后,会将赋值指令存放与类构造器
client
方法中
但是,如果增加了final关键字:
public static final int a = 666;
复制
将会在编译阶段生成ConstantValue属性,在准备阶段会根据ConstantValue赋值为666
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程
CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info
等常量
复制
- 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
- 直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
初始化
初始化阶段是类加载的最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器之外,其他操作都有JVM主导
到了初始化阶段,才开始真正执行类中定义的java程序代码
初始化阶段是 执行类构造器<client>方法的过程
<client>方法是由编译器自动收集类中的变量赋值操作,静态语句块中的语句 合并而成的
虚拟机会保证 子<client>
方法执行之前它的父类<client>
方法已经执行完毕
如果一个类中没有对静态变量赋值,也没有静态语句快,则不会生成
<client>
方法
以下情况不会执行类的初始化:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触 发定义常量所在的类
- 通过类名获取 Class 对象,不会触发类的初始化。
- 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初 始化,其实这个参数是告诉虚拟机,是否要对类进行初始化
- 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。
什么时候触发类加载
什么情况需要开始类加载过程的第一阶段(加载)呢? Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。
但是对于初始化阶段,虚拟机规范则严格规定了以下几种情况必须立即对类进行初始化,如果类没有进行过初始化,则需要先触发其初始化。
- new一个对象的时候
- 访问类的静态变量(注意上面的,如果是访问父类的静态字段,不会触发子类的初始化)
- 访问类的静态方法
- 反射 Class.forName
- 初始化一个类的子类(会先初始化父类)
- 虚拟机启动时,定义了main方法的那个类
类加载器
虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类,JVM 提 供了 3 种类加载器:
启动类加载器(Bootstrap ClassLoader)
负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被 虚拟机认可(按文件名识别,如 rt.jar)的类。
扩展类加载器(Extension ClassLoader)
负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类 库。
应用程序类加载器(Application ClassLoader)
负责加载用户路径(classpath)上的类库
双亲委派
JVM通过 双亲委派模型进行类的加载,我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器
当一个类收到了类加载请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成
没一个层次的类加载器都是如此,因此所有的加载请求都会传送到启动类加载器中
只有当父类加载器反馈自己无法完成这个请求的时候
,子类加载器才会尝试自己加载.
双亲委派机制可以保证不同地方引用的类,都是同一个.
自定义类加载器
在之前的文章中,我们有一个MathServiceImpl类,以这个类作为demo,进行演示,我们 先将add方法改为错误的减法,同时保存在target编译后的class文件:
public Double add(double a, double b) {
Double result=a-b;
return result;
}
复制
将编译后的class文件放到当前项目目录中:
再将add改为正确的加号
此时,我们存在2个MathServiceImpl
- 重新编译项目后,target会存在一个加号的MathServiceImpl
- 项目当前目录下,一个减号的MathServiceImpl
在正常情况下,根据双亲委派机制,将加载编译目录中的MathServiceImpl,为正确的写法,现在我们需要实现一个ClassLoader类,重写findClass和loadClass方法:
package org.example;
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String codePath;
public MyClassLoader(ClassLoader parent, String codePath) {
super(parent);
this.codePath = codePath;
}
public MyClassLoader(String codePath) {
this.codePath = codePath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
BufferedInputStream bis = null;
ByteArrayOutputStream bos = null;
codePath = codePath + name.replace(".", File.separator) + ".class";
byte[] bytes = new byte[1024];
int line = 0;
try {
//读取编译后的文件
bis = new BufferedInputStream(new FileInputStream(codePath));
bos = new ByteArrayOutputStream();
while ((line = bis.read(bytes)) != -1) {
bos.write(bytes, 0, line);
}
bos.flush();
bytes = bos.toByteArray();
return defineClass(null, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
bis.close();
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return super.findClass(name);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t1 = System.nanoTime();
//如果包名是org.example.MathServiceImpl开头的,调用自定义类的findClass方法,否则调用父类的loadClass方法
if (name.startsWith("org.example.MathServiceImpl")) {
c = this.findClass(name);
} else {
c = this.getParent().loadClass(name);
}
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
复制
main方法进行调用:
package org.example;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
//通过默认的类加载器加载的类
MathServiceImpl mathService = new MathServiceImpl();
System.out.println(mathService.add(10,23));
//类加载器加载的类
String codePath = "/Users/tioncico/IdeaProjects/test-maven/test-maven/src/main/java/";
MyClassLoader myClassLoader = new MyClassLoader(codePath);
Class<?> aClass = myClassLoader.loadClass("org.example.MathServiceImpl");
System.out.println("测试字节码是由" + aClass.getClassLoader().getClass().getName() + "加载的。。");
//利用反射实例化对象,和调用TwoNum类里面的twoNum方法
Object o = aClass.newInstance();
Method add = aClass.getDeclaredMethod("add", double.class, double.class);
Object invoke = add.invoke(o, 10, 23);
System.out.println(invoke);
//重新mathService
MathServiceImpl mathServiceNew = new MathServiceImpl();
System.out.println(mathServiceNew.add(10,23));
}
}
复制
输出:
自己实现类加载器之后,可以违反双亲委派机制,强制要求自定义加载,所以出现了2个类的方法返回结果不一致的问题
- 点赞
- 收藏
- 关注作者
评论(0)