Java虚拟机类加载机制

##一、类加载时机
在java语言中类的加载、连接和初始化过程都是在程序运行期间完成的,因此在类加载时的效率相对编译型语言较低。除此之外,任何一个类只有在运行期间使用到该类的时候才会将该类加到内存中。总之,java依赖于运行期间动态加载和动态链接来实现类的动态使用。其整个流程如下:
这里写图片描述
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后,这是为了支持Java语言的动态绑定。
什么情况下开始类加载,Java虚拟机规范中没有强制约束,交给虚拟机具体实现自由把握。但是初始化阶段,虚拟机规范则严格规定了有且仅有5种情况对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
(1) 遇到new(实例化对象)、getstatic(读取静态字段)、putstatic(设置静态字段)、invokestatic(调用静态方法)这4个字节码指令时,如果类没有进行过初始化,则先触发类的初始化。(注:类初始化只会触发一次,而对象初始化可以多次,例如:new了多个String对象,整个虚拟机进程只会触发一次String类的初始化,但是会触发多次String对象初始化)
(2) 对类进行反射调用的时候,如果类没有初始化,则需要先触发其初始化。
(3) 初始化一个类的时候,如果发现其父类还没有初始化,则先触发父类初始化,再触发自己类的初始化。
(4) 虚拟机启动时,虚拟机会先初始化主类(包含main()方法的类)
(5) 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果包含REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。

以上5种情形会触发类进行初始化,这5种场景称为对一个类进行主动引导;除此之外,多有引用类的方式都不会触发初始化,称为被动引导。以下是3个被动引导的例子:

public class ParentClass {
	static{
		System.out.println("ParentClass init");
	}
	
	public static int value = 123;
	
	public ParentClass() {
		System.out.println("new ParentClass instance");
	}
}

public class SubClass extends ParentClass{
	static{
		System.out.println("SubClass init");
	}
	
	public SubClass() {
		System.out.println("new SubClass instance");
	}
}

public class ConstClass {
	static{
		System.out.println("ConstClass init");
	}
	public static final String HELLO_WORLD = "hello world";
}

//为防止前面运行的代码影响后面代码的执行结果,模块1、2、3分别运行
public static void main(String[] args) {
	/* 模块1
	 * 输出:ParentClass init
             123
	 *  对于静态字段,只有直接定义这个字段的类才会初始化,因此子类引用父类定义的静态字段,只会触发父类的初始化,而子类不会初始化
	 */
	System.out.println(SubClass.value);
	System.out.println("-------------------------------------");
		
	/*  模块2
	 *  输出:-------------------------------------
              ParentClass init
	 *  通过数组定义来引用类,不会触发此类的初始化.(数组类本身不是通过类加载器创建,它是由Java虚拟机直接创建)
	*/
	ParentClass[] parentClasses = new ParentClass[10];
	System.out.println("-------------------------------------");
	ParentClass objClass = parentClasses[0];
	objClass.value = 555;//调用数组类的静态字段,触发类的初始化
		
/*  模块3
	*  输出:hello world
	*  
 *  常量在编译时就放入常量池,本质上没有直接引用定义常量的类,不会触发定义常量的类的初始化
	*/
		System.out.println(ConstClass.HELLO_WORLD);
	}

##二、类加载的过程
###2.1 加载
在加载阶段,虚拟机需要完成以下3件事:
(1)通过类的全局限定名来获取定义此类的二进制字节流;
(2)将字节流转为运行时数据结构;
(3)在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在就 Java 虚拟机中的唯一性,也就是说,即使两个类来源于同一个 Class 文件,只要加载它们的类加载器不同,那这两个类就必定不相等。

###2.2 验证
因为class文件来源不一定是Java源码编译,甚至可能是文本编辑器直接编写来产生Class文件。虚拟机需要检查输入字节流是否合法,是对自身的保护机制。验证阶段的4个校验动作:
(1)文件格式验证,class文件是否符合格式规范,如以0XCAFEBABE开头等等;
(2)元数据验证(数据类型校验),对字节码描述进行语义分析,如:是否有父类,是否继承或覆盖final等等;
(3)字节码验证(方法体校验),例如操作数栈是int类型,却按long加载到本地变量表;不会跳转到方法体以外的字节码指令上;不会将对象付给不相干类数据类型等等。
(4)符号引用验证,如能否通过全限定名找到对应的类,private等访问性是否可被当前类访问。
对于虚拟机的类加载机制来说,验证阶段非常重要,但不是一定是必要的阶段(因为对运行期没有影响),如果所运行的全部代码都已经反复使用验证过,可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类的加载时间。

###2.3 准备
正式为类变量(没有final的static修饰的变量)分配内存并设置类变量初始值(数据类型的0值)的阶段。例如:public static int value = 123;准备阶段后的初始值是0,而不是123。因为此时尚未执行任何Java方法,赋值为123的指令存放在类构造器()方法中,需要到初始化阶段才会执行。但如果上面的static变量被final修饰,在编译时就会为value生成ConstantValue属性,在准备阶段就会根据ConstantValue设置为123。

###2.4 解析
与C之类的纯编译型语言不同,Java类文件在编译过程中只会生成class文件,并不会进行连接操作,这意味在编译阶段Java类并不知道引用类的实际地址,因此只能用“符号引用”来代表引用类。在解析阶段,JVM可以通过解析该符号引用,来确定类的真实内存地址。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。包括以下四个部分:
(1)类或接口解析;(2)字段解析;(3)类方法解析;(4)接口方法解析。

###2.5 初始化
类初始化阶段是类加载过程的最后一步,前面由虚拟机主导和控制,初始化阶段才真正执行类中定义的Java程序代码。在准备阶段,变量已经赋值过一次,初始化阶段会根据主观计划初始化变量和其他资源。
初始化阶段是执行类构造器()方法的过程,() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句产生。如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器不会为这个类生成()方法。虚拟机会保证一个类的() 方法在多线程环境中被正确地加锁、同步。

/*
 *  演示 类的<clinit>()方法在多线程环境被自动加锁、同步
 *  输出:    Thread[main,5,main] init class            5秒之后才会有下面的打印
			Thread[thread1,5,main]---start
			Thread[thread2,5,main]---start
			Thread[thread2,5,main]  实例构造方法
			Thread[thread2,5,main]---run over!
			Thread[thread1,5,main]  实例构造方法
			Thread[thread1,5,main]---run over!
 */
public class ClassLoad {
    static{
    	if (true) {
			System.out.println(Thread.currentThread() +" init class");

			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
    }
    
    public ClassLoad() {
    	System.out.println(Thread.currentThread() +"  实例构造方法");
	}
    
    public static void main(String[] args) {
    	
		Runnable runnable = new Runnable() {
			
			public void run() {
				System.out.println(Thread.currentThread() + "---start");
				ClassLoad classLoad = new ClassLoad();
				System.out.println(Thread.currentThread() + "---run over!");
			}
		};
		
		Thread thread1 = new Thread(runnable, "thread1");
		Thread thread2 = new Thread(runnable, "thread2");
		thread1.start();
		thread2.start();
	}
}

##三、类加载器
###3.1 双亲委派模型
从Java虚拟机的角度来看,只存在两种不同的类加载器:
(1) 启动类加载器,由C++实现,是虚拟机的一部分;
(2) 其他类加载器,Java语言实现,独立于虚拟机外部,并且自身继承与抽象类java.lang.ClassLoader
从Java开发人员角度,分为3种系统提供的类加载器:
(1) 启动类加载器,负责将存放在$JAVA_HOME/jre/lib目录,并且被虚拟机识别的类库加载到虚拟机内存中。
(2) 扩展类加载器,负责加载$JAVA_HOME/jre /lib/ext中的所有类库,开发者可以直接使用扩展类加载器。
(3) 应用程序类加载器,由于这个类加载器是ClassLoader中getSystemClassLoader()方法的返回值,所有一般也称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果没有自定义类加载器,一般程序中默认使用的就是这个类加载器。
Java应用程序都是由这3种类加载器相互配合加载,如有必要,还可以加入自定义类加载器。这些类加载器的关系如图所示:
这里写图片描述
除了顶层的启动类加载器外,其余的类加载器都有自己的父类加载器。
双亲委派模型的工作过程如下:
(1)当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
(2)如果没有找到,就去委托父类加载器去加载。父类加载器也会采用同样的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就委托父类的父类去加载,一直到启动类加载器。如果父加载器为空了,就代表使用启动类加载器(C++实现)作为父加载器去加载。
(3)如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用拓展类加载器来尝试加载,继续失败则会使用AppClassLoader来加载,继续失败则会抛出一个异常ClassNotFoundException,然后再调用当前加载器的findClass()方法进行加载。
双亲委派模型的实现代码,摘自android-15\java\lang\ClassLoader.java抽象类

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
//首先检查请求的类是否已经被加载过,已加载直接返回
    Class<?> clazz = findLoadedClass(className); 
    if (clazz == null) {//未加载过
        try {
            clazz = parent.loadClass(className, false);//委派父类加载器加载
        } catch (ClassNotFoundException e) {
            // Don't want to see this.
        }

        if (clazz == null) {//父类加载器无法加载,自身findClass方法进行类加载
            clazz = findClass(className);
        }
    }

    return clazz;
}

双亲委派模型的好处:
(1)主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
(2)同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。

###3.2 类加载示例说明

public class HelloWorld{
	    public static void main(String[] args){
	        System.out.println("Hello world");
	    }
}

这段代码的大致经过:
1.首先寻找jre目录的jvm.dll,并初始化JVM,之后会生成一个启动类加载器(Bootstrap ClassLoader),
2.启动类加载器会加载指定目录下的java核心API(如:jdk1.7.0_17\jre\lib\rt.jar),并生成扩展类加载器(Extended ClassLoader)实例;
3.扩展类加载器加载指定路径下的扩展java API(如:jdk1.7.0_17\jre\lib\ext\*.jar),并将父加载器设为启动类加载器(注:是父加载器,而不是父类;启动类加载器的父加载器为null);
4. 启动类加载器生成应用类加载器(AppClass ClassLoader)实例,并将其父加载器设为扩展类加载器;
5. 最后应用类加载器加载ClassPath目录定义的类HelloWorld(工程目录/bin/HelloWorld.class)。

###3.3 自定义类加载器
(1)从ClassLoader.java的源码看出,调用loadClass时会先根据委派模型在父加载器中加载,如果加载失败,则会调用当前加载器的findClass来完成加载。
(2)因此我们自定义的类加载器只需要继承ClassLoader,并覆盖findClass方法,下面是一个实际例子,在该例中我们用自定义的类加载器去加载我们事先准备好的class文件。

####3.3.1 定义一个MyClass类
生成字节码文件MyClass.class,放到F盘根目录(任意目录都行,调用自定义ClassLoader时传入正确路径即可),以备自定义类加载器加载。

public class MyClass {
	private String nameString;
	
	public String toString() {
		return "我叫 " + "MyClass" + ", 我是由 " + getClass().getClassLoader().getClass()+" 加载进来";
	} 
}

####3.3.2 自定义类加载器
自定义一个类加载器,需要继承ClassLoader类,并实现findClass方法。

public class MyClassLoader extends ClassLoader{
	private String classPathString;
	private ClassLoader parentClassLoader;
	
	public MyClassLoader(String path) {
		this(path, getSystemClassLoader());
	}
	
	public MyClassLoader(String path, ClassLoader parClassLoader) {
		super(parClassLoader);
		this.classPathString = path;
		this.parentClassLoader = parClassLoader;
	}
	
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		String classNameString = name.substring(name.lastIndexOf(".")+1);//不包含最后一个“.”所有+1
		String filePathString = classPathString + classNameString + ".class";
		File file = new File(filePathString);
		
		System.out.println("MyClassLoader.findClass()   package=" + name + "  filePathString=" + filePathString);
		try {
			byte[] bytes = getClassBytes(file);
			//把二进制字节流组成的文件转换为一个java.lang.Class
			Class<?> clazz = this.defineClass(name, bytes, 0, bytes.length);
			return clazz;
		} catch (Exception e) {
			// TODO: handle exception
		}
		
		return super.findClass(name);
	}
	
	/*
	 * 重载loadClass方法可能会破坏双亲委派机制,不建议重载该方法,除非特殊需求
	 *
	 */
	@Override
	public Class<?> loadClass(String name) throws ClassNotFoundException {
	   Class<?> clazz = findLoadedClass(name);//首先检查是否被加载过
	   if (clazz == null) {//未加载过,
		   try {
			   if (parentClassLoader != null) {
				   clazz = parentClassLoader.loadClass(name);//委派父类加载加载
			   }
		   } catch (ClassNotFoundException e) {
				// TODO: handle exception
				//e.printStackTrace();
		   }
		   
		   if (clazz == null) {//父加载器无法加载,自身加载
			   clazz = findClass(name);
		   }
	   }
	   
	   /*
	    * 如果直接用自己加载类,会提示无法"java.lang.NoClassDefFoundError: java/lang/Object",这个类很早就应该加载过了,为何会抛异常找不到该类呢
	    * 由此说明:1、加载子类会先检查父类是否加载;
	    *           2、类的唯一性是由类加载器和类本身共同决定的,同一个类只要加载器不同,就是不同的两个类
	    */
//	   if (clazz == null) {
//		   clazz = findClass(name);
//	   }
	   return clazz;
	}
	
	//将.class文件读取为字节流
	private byte[] getClassBytes(File file) throws Exception {
		FileInputStream fileInputStream = new FileInputStream(file);
		FileChannel fileChannel = fileInputStream.getChannel();
		ByteArrayOutputStream baosArrayOutputStream = new ByteArrayOutputStream();
		WritableByteChannel writableByteChannel = Channels.newChannel(baosArrayOutputStream);
		ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
		
		int i;
		while (true) {
			i = fileChannel.read(byteBuffer);
			if (i==0 || i==-1) {
				break;
			}
			byteBuffer.flip();
			writableByteChannel.write(byteBuffer);
			byteBuffer.clear();
		}
		fileInputStream.close();
		return baosArrayOutputStream.toByteArray();
	}
}

####3.3.3 在main方法里使用

public class Main {
    public static void main(String args[]) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("F:/");
        Class<?> clazz = classLoader.loadClass("ClassLoader.MyClass");//带上包名
        		
        Object obj = clazz.newInstance();
        System.out.println(obj);  
    }
}

执行结果:

这里写图片描述
由于loadClass方法的双亲委托机制,需要删除工程中的MyClass.class文件,才会查找指定目录(F:/ MyClass.class)的字节码文件。

哪些场景会用到自定义类加载器呢?
例如:字节码(*.class文件)不在本地工程,而在数据库或云端,进行动态部署;防反编译,java代码很容易反编译,把自己的字节码进行加密(最简单的对字节码进行与0xff异或运算后存储),类加密后就不能再用系统自带的ClassLoader去加载类了(因为加密后,.class不符合标注java字节码格式规范),需要自定义ClassLoader在加载类时先解密类(同样进行一次与0xff异或运算就还原了),再加载。

##总结
Java系统提供了3种类加载器,分别是:
启动类加载器,负责加载$JAVA_HOME/jre/lib目录中能被虚拟机识别的类库,开发者不能直接使用(启动类加载器是C++实现);
扩展类加载器,负责加载$JAVA_HOME/jre /lib/ext中的所有类库,开发者可以直接使用扩展类加载器(Java实现)。
应用程序类加载器(也称系统类加载器),负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器(Java实现)。
从ClassLoader.java的源码看出,调用loadClass时会先根据委派模型在父加载器中加载,如果加载失败,则会调用当前加载器的findClass来完成加载。因此我们自定义的类加载器只需要继承ClassLoader,并覆写findClass方法。
Java应用程序都是由这3种类加载器相互配合加载,如有必要,还可以加入自定义类加载器。类加载器之间满足双亲委派的关系,即加载类时都是先委派给父加载器加载,如果父加载器不能加载,才由自身加载,如果自己也不能加载,会抛出ClassNotFoundException的异常。双亲委派模型的好处是避免用户自己编写的类动态替换 Java的一些核心类,而且能避免类的重复加载。

参考博客链接 http://blog.csdn.net/seu_calvin/article/details/52315125

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java类加载机制是指将类的字节码文件加载到内存中,并在运行时将其转换为可执行的代码的过程。Java类加载机制遵循了一定的规则和顺序,可以分为以下几个步骤: 1. 加载:类加载的第一步是加载,即将类的字节码文件加载到内存中。Java类加载器负责从文件系统、网络或其他来源加载类的字节码文件。加载过程中会进行词法和语法的验证,确保字节码文件的正确性。 2. 链接:类加载的第二步是链接,即将已经加载的类与其他类或者符号进行关联。链接分为三个阶段: - 验证:验证阶段确保类的字节码文件符合Java虚拟机规范,包括检查文件格式、语义验证等。 - 准备:准备阶段为静态变量分配内存空间,并设置默认初始值。 - 解析:解析阶段将符号引用转换为直接引用,例如将类或者方法的符号引用解析为对应的内存地址。 3. 初始化:初始化是类加载的最后一步,在此步骤中会执行类的初始化代码,对静态变量进行赋值和执行静态代码块。类的初始化是在首次使用该类时触发的,或者通过反射方式调用`Class.forName()`方法来强制初始化。 Java类加载机制是动态的,可以根据需要加载和卸载类,它还支持类的继承、接口实现、内部类等特性。类加载机制Java语言的重要特性之一,它为Java提供了强大的动态性和灵活性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值