java类加载的加载阶段

1.概述
java类加载又分为加载,连接(连接又分验证,准备,解析三阶段),初始化三个阶段,“加载”只是类加载的一个阶段,不要混淆。下面主要说说加载阶段。
java类加载都发生在运行时。在程序运行时,jvm会根据所用到的类的全限定名获得它的二进制字节流,并根据字节流中定义的静态存储结构在方法区中形成它的数据结构,最后在内存中生成一个Class对象作为方法区中数据结构的入口。
注:根据类的全限定名获得二进制字节流并不局限于通过class文件获取。比如动态代理,运行时并没有代理类的class文件,而是直接由jvm运算生成。
2.加载时机
java虚拟机并没有明确规定什么时候对类进行加载,但是却规定了有且只有5种情况必须马上对类进行初始化。注意:有且只有。
摘自《深入理解java虚拟机》

1.遇到new、getstatic、putstatic、invokestatic这4条字节码指令时。生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

2.使用java.lang.reflect包方法对类进行反射调用的时候。

3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()的那个类),虚拟机会先初始化这个主类。

5.当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,则需要先触发这个方法句柄所对应的类的初始化。

而在类初始化之前,它自然经过了加载和连接阶段了。

3.类加载器
根据所用到的类的全限定名获得它的二进制字节流就是类加载器做的事情。相对于其他阶段,加载阶段可以说是可控性最强的阶段了,可以使用系统提供的类加载器来加载,也可以自定义类加载器来加载。
(1)Bootstrap Loader启动类加载器
(2)Extension Loader扩展类加载器
(3)App Loader系统类加载器
在程序运行时,比如通过java命令运行一个java程序时,虚拟机会这样做:


 1. 创建一个启动类加载器。(它是由C++语言实现的。是虚拟机自身的一部分)
 2. 启动类加载器加载扩展类加载器。
 3. 扩展类加载器加载系统类加载器。
 4. 最后,由系统类加载器来加载你要运行的class文件。

需要注意的是:加载时,默认都是通过APP ClassLoader来加载的。当然,你也直接可以指定用哪个类加载器。那么这时候会有人要问,为什么我用Object.class.getClassLoader();得到的结果却是null,也就是说Object类是用的启动类加载器加载呢?不是说都是从APP ClassLoader加载的吗?这就扯到了类加载的双亲委派模型了。

4.双亲委派模型
简单解释一下双亲委派模型:使用一个类加载器加载指定的类时,该加载器会先从自己所管理的空间中找之前有没有加载过它,加载过的话直接返回,如果没有加载过,就委托父类加载(递归的递),这样一层层递过去,直到Bootstrap类加载器,如果还是找不到(之前从来没有加载过),那么从Bootstrap类加载器开始一层层往下尝试加载,直到最初的类加载器。
下面看一下loadClass的源码,结合注释就能理解上边的双亲委派模型了:

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);
            // c为null 没有被加载过 由父加载器尝试加载
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 看看父加载器是否Bootstrap
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {// 父加载器是Bootstrap
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                // c==null 父加载器没有加载 就自己加载。c = findClass(name);
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

结合这个图片应该可以加速理解
在这里插入图片描述
这样以来你应该就可以理解上一节最后的问题了。
双亲委派模型的好处:考虑一种情况,如果我现在要加载java.lang.Object类,无论我自定义多少个类加载器来加载,总是会委派给启动类加载器,这样系统中的Object类只会有一个。如果不使用双亲委派,用户自定义了一个java.lang.Object,使用自定义类加载器加载,这样系统里就会存在很多不同的Object类(使用不同的类加载器加载出来的两个类,即便来源于同一Class文件,它俩也不相等),系统变得一片混乱。
5.自定义类加载器
自定义类加载器时,保持双亲委派模型
自定义类加载器

public class MyLoader extends ClassLoader {
	//类加载器的加载路径
	private String path;
	
	public MyLoader(String path) {
		this.path = path;
	}

	//要重写findClass而不重写loadClass -> 保持双亲委派模式
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		File f = new File(new File(this.path),name.replace('.', File.separatorChar).concat(".class"));
		Class c = null;
		try {
			//获得class文件的二进制字节流
			InputStream in = new FileInputStream(f);
			ByteArrayOutputStream out = new ByteArrayOutputStream();
			byte[] b = new byte[1024];
			//读入内存
			int count = in.read(b);
			while(count>0) {
				out.write(b, 0, count);
				count = in.read(b);
			}
			//class的二进制信息保存在byte数组里
			byte[] classByte = out.toByteArray();
			//根据二进制信息生成class对象
			in.close();
			c = defineClass(name, classByte, 0, classByte.length);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return c;
	}
}

测试

public class TestMyLoader {
	public static void main(String[] args) throws Exception {
		MyLoader myLoader = new MyLoader("E:\\Program Files (x86)\\Java");
		Class singleton = myLoader.loadClass("com.test.BeiBianyi2");
		Object o = singleton.newInstance();
		System.out.println(o.getClass().getClassLoader());
	}
}

输出
在这里插入图片描述
在我的当前工程的路径下,并没有一个com.test.BeiBianyi2的类,委托给父加载器加载不到,就由自己来加载。重写findClass方法来进行加载。 通过自定义类加载器,加载出了不属于本工程的外部class信息。
6.热加载
在运行过程中,如果class文件发生了变化,不用重新运行就可以得到最新的class文件,这就是热加载。tomcat服务器启动时,每当修改工程中的jsp或者java文件,不用重启服务器就可以获得最新的class信息,这就用到了热加载。
首先,先看一下我要热加载的类
在这里插入图片描述
编写测试类,对上边一小节的测试类做了一些修改。

package com.wch.test;

import java.io.File;

import com.wch.loader.MyLoader;

public class TestMyLoader {
	public static void main(String[] args) throws Exception {
		while(true) {
			MyLoader myLoader = new MyLoader("E:\\Program Files (x86)\\Java");
			Class c = myLoader.loadClass("com.test.BeiBianyi2");
			Object o = c.newInstance();
			System.out.println(c.getClassLoader());
			c.getMethod("testHot").invoke(o);
			Thread.sleep(2000);
		}
	}
}

结果。从下边图片可以看出main线程在不停地运行
在这里插入图片描述
此时,我对热加载的类进行了修改,并重新编译。
下图是修改后的代码
在这里插入图片描述
编译
在这里插入图片描述
可以看到加载到了最新的class文件
在这里插入图片描述
在测试类中,因为我在while条件里new出了ClassLoader对象,每次的类加载器都是不同的,所以在loadClass方法中查找加载过的类找不到,就重新加载。如果将new的代码放在循环外或者在循环中使用单例模式得到ClassLoader,显然是不能实现热加载的。这也是不能用系统提供的加载器的原因。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值