类加载器

Java虚拟机和类

当我们调用Java命令运行某个Java程序时,该命令将会启动一条Java虚拟机进程,同一个Java虚拟机的所有线程,所有变量都处于同一个进程里,他们都使用该Java虚拟机的内存区。

当系统处于如下几种情况,会被终止:

(1)程序运行到最后正常结束。

(2)程序运行到使用System.exit()或Runtime.getRuntime().exit()代码结束程序。

(3)程序执行过程中遇到未捕获的异常或者错误而结束。

(4)程序所在平台强制结束了虚拟机进程。

通过这四点可以看出,当Java程序运行结束时,Java虚拟机进程结束,该进程在内存中状态将会丢失。


类的加载

当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化,如果没有意外,JVM将会连续完成这三个步骤,所以把这三个步骤统称为类加载或类的初始化。

类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说当程序中使用任何类时,系统都会为之创建一个java.lang.Class对象。

类的加载由类加载器来完成,类加载器通常是由Java虚拟机提供,Java虚拟机提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:

(1)从本地文件系统来加载class文件。这是绝大部分程序的类加载方式。

(2)从JAR包中加载class文件,这种方式也很常见,比如JDBC编程时使用的驱动就是放在JAR文件中,Java虚拟机可以从JAR文件中直接加载class文件。

(3)通过网络加载class文件。

(4)把一个Java源文件动态编译,并执行加载。

类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。


类的连接

当类被加载之后,系统为之生成了一个对应的Class对象,接着将会进入连接阶段,连接阶段将会负责把类的二进制数据合并到JRE中。

类连接又可以分为如下三个阶段:

(1)验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。

(2)准备:类准备阶段则负责为类的静态属性分配内存,并设置默认初始值。

(3)解析:将类的二进制数据中的符号引用替换成直接引用。


类的初始化

在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对静态属性进行初始化。在Java类中对静态属性指定初始值有两种方式:声明静态属性时指定初始值;使用静态初始化块为静态属性指定初始值。

Java虚拟机初始化一个类包含如下机构步骤:

(1)假如这个类还没有被加载和连接,程序先加载并连接该类。

(2)假如该类的直接父类还没被初始化,则先初始化其直接父类。(递归初始化)

(3)假如类中有初始化语句,则系统依次执行这些初始化语句。


类初始化的时机

当Java程序首次通过如下六种方式来使用某个类或接口时,系统就会初始化该类或接口:

(1)创建类的实例。为某个类创建实例的方式包括使用new操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。

(2)调用某个类的静态方法。

(3)访问某个类或接口的静态属性,或为该静态属性赋值。

(4)使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。比如:Class.forName("Person");如果系统还未初始化Person,这这行代码将导致该Person类被初始化,并返回Person类对应的java.lang.Class对象。

(5)初始化某个类的子类,当初始化某个类的子类时,该子类的所有父类都会被初始化。

(6)直接使用java.exe命令来运行某个主类,当运行某个主类时,程序会先初始化该主类。

下面是几种特殊情况:

(1)对于一个final型的静态属性,如果该属性可以在编译时就得到属性值,则可以认为该属性可被当成编译时常量。当程序使用编译时常量时,系统会认为这是对该类的被动使用,所以不会导致该类的初始化。反之,如果final类型的静态属性的值不能在编译时得到,必须等到运行时才可以确定该属性的值,如果通过该类来访问该静态属性,将会导致该类被初始化。

(2)当使用ClassLoader类的loadClass()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。当使用Class的forName()静态方法会导致强制初始化该类。


类加载器

类加载器主要负责将.class文件加载到内存中,并为值生成对应的java.lang.Class对象。尽管在Java开发红程序员无须过分关心类加载机制,但应该了解其工作机制。

一旦一个类被载入JVM中,同一个类就不会被再次载入了,载入JVM的类有一个唯一的标识,在Java中,一个类用其全限定类名(包括包名和类名)作为标识。

但在JVM中,一个类用其全限定类名和其类的加载器作为唯一的标识。比如:类名是Person,包名是:类加载器ClassLoader的实例为k1负责加载。这Person类的Class对象在JVM中表示为(Person、pg、k1)。

当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构:

(1)Bootstrap  ClassLoader:根类加载器。

(2)Extension  ClassLoader:扩展类加载器。

(3)System  ClassLoader:系统类加载器。

Bootstrap  ClassLoader被称为引导(也称为原始或根)类加载器。它负责加载Java的核心类。根类加载器非常特殊,它并不是java.lang.ClassLoader的子类,而是有Java虚拟机自身实现的。

Extension  ClassLoader被称为扩展类加载器,它负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的目录)中JAR的类包。通过这种方式,我们就可以为Java扩展核心类以外的新功能,只要我们把自己开发的类打包成JAR文件,然后放入JAVA_HOME/jre/lib/ext路径即可。

System  ClassLoader被称为系统(也称为应用)类加载器,它负责在JVM启动时,加载来自命令java中的-classpath选项或java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。

程序可以通过ClassLoader的静态方法getSystemClassLoader()获取该类加载器。如果没有特别指定,则用户自定义的类加载器都以该加载器作为它的父加载器。


类加载机制:

Java虚拟机的类加载机制主要有如下三种机制:

(1)全盘负责:所谓全盘负责,就是说当一个类加载器负责加载某个Class的时候,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示的使用其他类加载器。

(2)父类委托所谓父类委托则是先让parent类加载器试图加载该Class,只有在父类加载器无法加载个该类时才尝试使用自己的类路径加载该类。

(3)缓存机制缓存机制将会保证所有被加载过的Class都会被缓存,当程序中需要使用某个Calss时,类加载器先从缓存中搜索该Class,只有当缓存中不存在该Class对象时,系统才会从新读取该类对应的二进制数据,并将其转换为Class对象,并存入缓存中。这就是为什么我们修改了Class后,程序必须重新启动JVM,程序所作的修改才会生效的原因。


类加载器加载Class大致分为如下八个步骤:

(1)检测此Class是否载入过(即在缓存中是否有此Class),如果有则直接进入第8步。否则进入第2步。

(2)如果父加载器不存在(如果没有父加载器,则要么parent一定是根加载器,要么本事就是根加载器),则跳到第4步。如果父加载器存在,则进入第3步。

(3)请求父加载器载入目标类,如果成功载入则跳到第8步,不成功接着执行第5步。

(4)请求使用根加载器载入目标类,如果成功跳到第8步,如果不成功跳到第7步。

(5)需找Class文件(从与此ClassLoader相关的类路径中寻找)。如果找到则执行第6步,如果找不到跳到第7步。

(6)从文件中载入Class,成功载入后跳到第8步。

(7)抛出ClassNotFoundException异常。

(8)返回Class。

其中第5步和第6步允许重写ClassLoader的findClass方法来实现自己的载入策略,甚至重写loadClass方法来实现自己的载入过程。


创建并使用自定义的类加载器

Java虚拟机中除根加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。

ClassLoader类如下两个关键方法:

(1)loadClass(String name,boolean resolve):该方法为ClassLoader的入口点,根据指定的二进制名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。

(2)findClass(String name):根据二进制名称来查找类。

自定义ClassLoader,可以通过重新上面两个方法来实现,推荐从新findClass方法,因为loadClass方法执行步骤如下:

(1)用findLoadedClass(String)来检查释放已经加载类,如果已经加载则直接返回。

(2)在父类加载器上调用loadClass方法。如果父类加载器为null,则使用根类加载器来加载。

(3)调用findClass(String)方法查找类。

在ClassLoader里还有一个核心方法:Class  defineClass(String  name,byte[ ] b, int off, int len),该方法负责将指定类的字节码文件,即class文件读入自己数组b内,并把它转化为Class对象,该字节码文件可以来源文件,网络等。

ClassLoader中还包含如下一些普通方法:

(1)findSystemClass(String name):从本地文件系统装入文件,它在本地文件系统中寻找类文件,如果存在,就使用definClass将原始字节转换为Class对象,以将该文件转换成类。

(2)static  getSystemClassLoader():这是一个静态方法,用于返回系统类加载器。

(3)getParent():获取该类加载器的父类加载器。

(4)resolveClass(Class<?>  c):链接指定的类。类加载器可以使用此方法来链接类c。

(5)findLoadedClass(String name):如果此Java虚拟机已装载了名name的类,则直接返回该类对应的Class实例;否则返回null,该方法是Java类加载里缓存机制的体现。

经典实例:

public class CompileClassLoader extends ClassLoader {

	private byte[] getBytes(String filename) throws IOException {
		File file = new File(filename);
		long len = file.length();
		byte[] raw = new byte[(int) len];
		FileInputStream fin = new FileInputStream(file);
		// 一次性读取class文件的全部二进制数据
		int r = fin.read(raw);
		if (r != len) {
			throw new IOException("无法读取全部文件:" + r + "!=" + len);
		}
		fin.close();
		return raw;
	}

	// 定义编译指定Java文件方法
	private boolean compile(String javaFile) throws IOException {
		System.out.println("CompileClassLoader:正在编译" + javaFile + "......");
		// 调用系统的javac命令
		Process p = Runtime.getRuntime().exec("javac" + javaFile);
		try {
			// 其他线程都等待这个线程完成。
			p.waitFor();
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		// 获取javac线程的退出值
		int ret = p.exitValue();
		return ret == 0;
	}

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		// TODO Auto-generated method stub

		Class clazz = null;
		// 将包路径中的点替换成斜线
		String fileStub = name.replace(".", "/");
		String javaFileName = fileStub + ".java";
		String classFilename = fileStub + ".class";
		File javaFile = new File(javaFileName);
		File classFile = new File(classFilename);
		// 当指定Java源文件存在,其class文件不存在,或者Java源文件的修改时间比class文件修改时间晚,则重写编译。
		if (javaFile.exists()
				&& (!classFile.exists() || javaFile.lastModified() > classFile
						.lastModified())) {
			try {
				// 如果编译是吧,或该class文件步存在
				if (!compile(javaFileName) || !classFile.exists()) {
					throw new ClassNotFoundException("ClassNotFoundException:"
							+ javaFileName);
				}

			} catch (Exception e) {
				// TODO: handle exception
				e.printStackTrace();
			}
		}
		// 如果class文件存在,系统负责将该文件转换成Class对象
		if (classFile.exists()) {
			try {
				// 将class文件的二进制数据读入数组
				byte[] raw = getBytes(classFilename);
				// 嗲用ClassLoader的defineClass方法将二进制数据转换成Class对象
				clazz = defineClass(name, raw, 0, raw.length);
			} catch (Exception e) {
				// TODO: handle exception
				e.printStackTrace();
			}
		}
		// 如果clazz定位null,表明加载失败,则抛出异常
		if (clazz == null) {
			throw new ClassNotFoundException(name);
		}
		return clazz;
	}

	public static void main(String[] args) throws Exception {
		// 如果运行该程序时没有参数,即没有目标类
		if (args.length < 1) {
			System.out.println("缺少运行的目标类,请按如下格式运行Java源文件:");
			System.out.println("java  compileClassLoader  ClassName");
		}
		// 第一个参数是需要运行的类
		String progClass = args[0];
		// 剩下的参数将作为运行目标类时的参数,所以将这些参数赋值到一个新数组中
		String progArgs[] = new String[args.length - 1];
		System.arraycopy(args, 1, progArgs, 0, progArgs.length);
		CompileClassLoader ccl = new CompileClassLoader();
		// 加载需要运行的类
		Class<?> clazz = ccl.loadClass(progClass);
		// 获取运行的主方法
		Method main = clazz.getMethod("main", (new String[0]).getClass());
		main.invoke(null, progArgs);

	}

}

URLClassLoader类

Java为ClassLoader提供了一个URLClassLoader实现类,该类也是系统类加载器和扩展类加载器的父类,URLClassLoader功能比较强大,它即可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。

URLClassLoader提供了如下两个构造器:

(1)URLClassLoader(URL[ ] urls):使用默认的父类加载器来创建一个ClassLoader对象,该对象将从urls所指定的系列路径来查询并加载类。

(2)URLClassLoader(URL[ ] urls,ClassLoader  parent):使用指定的父类加载器创建一个ClassLoader对象,其功能和上一个方法一样。

经典实例:

public class URLClassLoaderTest {

	private static Connection conn;

	// 定义一个获取数据库连接的方法
	public static Connection getConn(String url, String user, String password)
			throws Exception {
		if (conn == null) {
			// 创建一个URL数组
			URL[] urls = { new URL("file:mysal-connector-java-3.1.10-bin.jar") };
			// 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader
			URLClassLoader myClassLoader = new URLClassLoader(urls);
			// 加载MySQL的JDBC驱动,并创建默认实例
			Driver driver = (Driver) myClassLoader.loadClass(
					"com.mysql.jdbc.Driver").newInstance();
			// 创建一个设置JDBC连接属性的Properties对象
			Properties props = new Properties();
			// 设置用户名和密码
			props.setProperty("user", user);
			props.setProperty("password", password);
			// 调用Driver对象的connect方法来获得数据库连接
			conn = driver.connect("jdbc:mysql://localhost:3306/mysql", props);
		}
		return conn;
	}

	public static void main(String[] args) {
		try {
			System.out.println(getConn("jdbc:mysql///j2ee", "root", "32147"));
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

当我们创建URLClassLoader时传入一个URL数组参数,则该ClassLoader就可以从这系列URL指定的资源中加载指定类,这里的URL可以以file:为前缀,表明从本地文件系统加载,可以以http:为前缀,表明从互联网通过HTTP访问来加载,也可以以ftp:为前缀,表明从互联网通过FTP访问来加载。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值