类加载


类加载

类加载过程

  • 验证:是否符合jvm规范
  • 准备:分配足够的内存空间
  • 解析: 符号信息和引用的转换
  • 初始化:默认初始化,再赋值
  • 使用:构建对象
  • 卸载:几率比较少,也会被GC

以下按照顺序进行

加载分析

加载过程:加载、验证、准备、解析、初始化

解析开始的顺序不确定,其他开始的时间按照顺序执行,同时这四个阶段也不一定按照顺序完成

基本步骤

类加载过程

  • 通过类的全限定名来获取定义的二进制字节流
  • 将字节流代表的静态存储结构转化为方法区的运行时数据结构
  • 再JAVA堆中生成一个java.lang.Class对象,作为访问这些数据的入口

加载路径

从三个地方加载

  • JDK类库的类
  • 第三方类库(spring、maven等)
  • 应用程序类库(自己写的类,依赖前二者)

加载方式以及时机

  • 隐式加载

    • 访问类的静态成员(成员变量、静态方法)
    • 构建类的实例对象(使用new关键字 和 反射构建)
    • 构建子类实例对象(双亲委派模式,会先行加载父类类型)
  • 显示加载

    • ClassLoader.loadClass(….)
    • Class.forName(….)

显示加载

package ex.java.jvm.loader
class A ....
    Static {sout("classA") }

main....
ClassLoader loader = TestClassLoader.class.getClassLoader
loader.loadClass("ex.java.jvm.loader")     

以上classA被加载,静态代码块A不加载

//替换main的内容
Class.forName("ex.java.jvm.loader")

以上classA被加载,静态代码块A加载

//替换main的内容
ClassLoader loader = TestClassLoader.class.getClassLoader
Class.forName("ex.java.jvm.loader",false,loader)

以上classA被加载,静态代码块A不加载

隐式加载

class A...
    static int cap = 100;
	static{ sout("A静态块") }
	static void doPrint(){ sout("A消息")}

main...
    sout(A.cap)
    //A.doPrint();
    //new 一个继承A的类

以上三种方式classA都被加载,静态代码块执行

连接分析

验证

确保Class文件的字节流包含的信息符合虚拟机的要求,不会危害虚拟机的安全

通过四个阶段完成校验

  • 文件格式验证
  • 元数据验证
  • 字节码合法性验证
  • 符号引用验证

验证阶段是非常重要却又非必须,如果引用的类经过反复的验证,可以采用-Xverify:none参数来关闭大部分的类验证,缩短虚拟机类加载的时间

准备

为类变量分配内存并设置类变量默认值(在方法区中)

默认值:0、null、false…….

如果是常量,在准备阶段就会赋值(代码给定的值)

解析

将常量池的符号引用替换成直接引用

当一个java文件编译成class之后,方法都是以符号引用的方式保存。而在加载类时,部分符合条件的符号引用会被转换成“直接引用”,这个过程我们称之为“解析(Resolution)”。而符合这种条件的方法有:静态方法、私有方法、构造方法、父类方法。它们在类加载的的解析阶段就会将符号引用解析为该方法的直接引用。

  • 符号引用:一组符号,可以是任何字面量,用文本形式来表示引用关系
  • 直接引用:直接指向目标的指针、相对偏移量或者一个间接定位到目标的句柄(jvm可以直接使用的数据形式)

初始化分析

此阶段为类加载的最后一个阶段,让我们的自定义加载器加入进来

因为系统的ClassLoader只会加载指定目录下的class文件,如果想加载自己的class文件,那么就可以自定义一个ClassLoader,甚至通过远程热更新配置文件,让自定义加载器加载不同的类等等功能

java中对类变量初始值的设定方法

  • 声明类变量时指定初始值
  • 使用静态代码块为类变量指定初始值

java程序对类的使用方式为两种

  • 主动使用:会执行加载、连接、初始化静态域
  • 被动使用:会执行加载、连接,不会初始化静态域

例如:通过子类引用父类的静态字段,为子类的被动使用,不会导致子类初始化

类加载器概要

负责将类读到内存的对象,常见方法有

  • getParent() 返回类加载器的父类对象
  • loadClass(String name) 加载名称为name的类
  • findClass(String name) 查找名称为name的类
  • findLoadedClass(String name) 查找名称为name的已加载类
  • defineClass(String name,byte[] b,int off,int len)把字节数组b种的内容转换为java
  • 等等

层次

类加载层级

从下往下依次

  • 启动类加载器:底层为C、C++,加载启动相关类。在java中 getParent() 拿不到该类,返回null即表示由此类加载加载
  • 扩展类加载器:拓展的资源
  • 应用程序加载器:自己写的类加载
  • 自定义加载器 :spring、tomcat等容器都有自己定义的加载器

双亲委派机制

保证了同一个类只被一个加载器加载

当一个加载器加载某个类时

  • 首先查看自己是否加载过该类,有则返回,没有则委托给父类加载,依次递归
  • 父类无法加载时,自己加载
  • 自己无法加载,抛出异常

自定义类加载器

为什么要自定义类加载器?

  • 隔离加载类
  • 修改类加载的方式
  • 扩展加载源
  • 防止源码泄漏

直接继承classLoader,重写 findClass(String name) 方法,运行自定义加载器过程

  • 会先去寻找父类加载器,加载指定当前类路径下的包名和类名
  • 当前类路径下如果没有,就从重写的 findClass(String name) 方法中去查找,通过自定义加载器加载

基于URLClassLoader继承ClassLoader,从指定目录、jar包、网络中加载指定的类资源

在类加载器中的构造方法可以指定父类加载器为null,取消双亲委派

热替换

再以上指定路径下的class文件进行文件替换,实现在线更新

要想实现Java类的热替换,首先必须要让系统中同名类的不同版本实例的共存,要想实现同一个类的不同版本的共存,必须要通过不同的类加载器来加载该类的不同版本。另外,为了能够绕过Java类的既定加载过程,需要实现自己的类加载器。

源代码

public class CustomClassLoader extends ClassLoader {

private String basedir; // 需要该类加载器直接加载的类文件的基目录
private HashSet className; // 需要由该类加载器直接加载的类名

public CustomClassLoader(String basedir, String[] clazns) throws Exception {
	super(null); // 指定父类加载器为 null 
	this.basedir = basedir;
	className = new HashSet();
	loadClassByMe(clazns);
}

//获得所有文件的完整路径以及类名,刷入缓存    
private void loadClassByMe(String[] clazns) throws Exception {
	for (int i = 0; i < clazns.length; i++) {
		loadDirectly(clazns[i]);
		className.add(clazns[i]);
	}
}

//拼接文件路径及文件名
private Class loadDirectly(String name) throws Exception {
	Class cls = null;
	StringBuffer sb = new StringBuffer(basedir);
    //路径中的 .  替换为 /
	String classname = name.replace('.', File.separatorChar) + ".class";
	sb.append(File.separator + classname);
	System.out.println(sb.toString());
	File classF = new File(sb.toString());
	cls = instantiateClass(name, new FileInputStream(classF), classF.length());
	return cls;
}

//读取并加载类
private Class instantiateClass(String name, InputStream fin, long len) throws Exception {
	byte[] raw = new byte[(int) len];
	fin.read(raw);
	fin.close();
	return defineClass(name, raw, 0, raw.length);
}

//    
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
	Class cls = null;
    //判断类是否已经加载过
	cls = findLoadedClass(name);
	if (!this.className.contains(name) && cls == null)
		cls = getSystemClassLoader().loadClass(name);
	if (cls == null)
		throw new ClassNotFoundException(name);
	if (resolve)
		resolveClass(cls);
	return cls;
}

public static void main(String[] args) throws FileNotFoundException, IOException {
	new Timer().schedule(new TimerTask() {

		@Override
		public void run() {
			try {
				// 每次都创建出一个新的类加载器
				CustomClassLoader customClassLoader = new CustomClassLoader(
						CustomClassLoader.class.getResource("\").getFile(), new String[] { "Foo" });
				Class<?> cls = customClassLoader.loadClass("Foo");
				Object foo = cls.newInstanc e();

				Method m = foo.getClass().getMethod("sayHi", new Class[] {});
				m.invoke(foo, new Object[] {});
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}
	}, 0, 1000L);
}}

需要在基目录下创建一个类

public class Foo implements FooInterface {
    @Override
    public void sayHi() {
        System.out.println("hi\t我是第一次");
    }

}

接口任意创建

public interface FooInterface {
     void sayHi();
}

重新编译一份Foo.class文件,替换到目标路径之下,实现自动更新


文章作者: hyy
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hyy !
  目录