SE高阶(10):类加载机制—类加载器、类初始化和URLClassLoader

关于类加载机制的知识,先简要了解一下虚拟机(JVM)。当我们使用eclipse或者命令行调用命令javac.exe运行Java程序时,系统就会启动一个虚拟机把类加载进内存中,类加载的过程就是需要了解的类加载机制。

虚拟机特点:

  • 每启动一次Java程序,都会单独启动一个JVM进程来运行;
  • JVM是一个进程,JVM的数据不是共享的;
  • Java程序结束后,JVM进程也会结束,同时JVM内存区的所有状态全部丢失,类存储在内存区中的数据会回归原始状态。

        内存区数据表示的是数据存储在内存中,而非对象持久化或者进行数据库操作那种,那两种的数据都不是存储在内存区。例如使用office编写文档,不保存的话,当电脑死机或者软件崩溃时会导致文档内容消失。

         在两个主类中分别写两个死循环,依次运行,在任务管理器中能看到两个JVM进程(javaw.exe)在运行。

测试JVM结束后内存区状态是否丢失案例:

public class Test01 {
	public static int num = 10;
	public static void main(String[] args) {
		num = 20;
		System.out.println("Test01的num值:" + num);
	}
}
public class Test02 {
	public static void main(String[] args) {
		Test01 t1 = new Test01();
		System.out.println("" + Test01.num);
	}
}
  • 案例说明:创建两个主类,先运行Test01类,对num再次赋值20,打印输出20,执行结束,JVM进程结束。运行Test02类,调用Test01的num值,重新初始化Test01类,打印输出10。该案例简单验证了JVM结束后内存区状态会丢失。
以上仅仅是简单了解虚拟机,要想深入了解请看《深入理解java虚拟机》。



类的加载机制

 
        系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类。当主动使用某个类时,该类还未被加载到内存中,系统会通过加载、连接、初始化三个 阶段对该类进行初始化。 类的生命周期从被加载到虚拟机内存中开始,到卸载出内存结束。其中的过程分为七个阶段,加载阶段到初始化阶段属于类加载。生命周期如下:
  • 加载---->验证---->准备---->解析----->初始化---->使用----->卸载(验证、准备、解析属于连接阶段

加载阶段

  • 通过一个类的全限定名来获取定义此类的二进制字节流;
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  • 在java堆中生成一个代表这个类的Class对象,作为访问方法区中这些数据的入口(反射机制就需要使用Class实例)
  • 因为没有指明从哪里获取以及怎样获取类的二进制字节流,所以可以自定义类加载器来控制加载阶段

连接阶段

  • 类加载之后,系统会生成一个对应的Class对象,接着进入连接阶段,该阶段为类变量分配内存和设置默认初始值,这个内存分配发生在方法区中。
  • 该阶段还没有对实例变量进行内存分配,实例变量会在对象实例化时随着对象一起分配在JAVA堆中。

初始化阶段

  • 初始化阶段是对类变量进行初始化,如果声明类变量时没有指定初始值,那使用系统分配的默认值;
  • 类变量初始化有两种方式:1、声明类变量时指定值,2、在静态代码块中为类变量指定值;
  • 初始化一个类时,会先对该类的父类初始化,然后依次类推,直到所有父类资源都被初始化完成。所以Object类总是第一个初始化。

注意:静态初始化块和声明类变量都是初始化语句,执行顺序是谁在前面谁先执行,但两者都是属于类本身,所以比类实例化优先执行。

初始化语句执行顺序案例:

public class Test01 extends Father{
	public static void main(String[] args) {
		new Test01();
	}
}
class Father{
	protected static int a;//未指定值
	public Father() {System.out.println("执行了父类构造器...");}
	//普通代码块
	{
		System.out.println("执行了父类普通代码块...");
	}
	//静态初始化块
	static {
		System.out.println("a的值:" + a);
		a = 10;
		System.out.println("静态初始化块之后:" + a);
	}
}
//a的值:0
//静态初始化块之后:10
//执行了父类普通代码块...
//执行了父类构造器...
  • 案例说明:如果不创建Test01()实例,那么只有父类静态代码块和类变量被初始化。Test01实例化则会调用父类构造器,这会导致Father类的初始化。输出结果是先执行普通代码块,然后执行父类构造器(普通代码块在实例化中执行顺序最优先,调用父类构造器时不会创建父类对象)

类初始化的时机

  • 创建类的实例:1.使用new来创建实例,2.使用反射方式来创建实例,3.通过反序列化生成实例;
  • 调用类方法(静态方法);
  • 访问类变量或者接口的类变量,或者为其赋值;
  • 初始化某个类的子类,也会导致该类被初始化;
  • 调用java.exe命令运行某个类,该类会被优先初始化。

初始化注意点:1.访问常量不会导致初始化,因为常量在编译时就已经确定值了。但如果常量的值是运行时才被确定,就会导致类初始化;2.使用反射获取某个类或者接口的Class实例会导致类初始化。例如Class.forName()方法对数据库驱动类初始化。

个人理解:执行的操作需要以类存在作为前提就会导致类初始化。例如执行对象实例化、调用类变量、反序列化等操作,如果类不存在就会出错。


类加载器

       类生命周期中的加载阶段就是由类加载器来完成,是类加载机制的核心。类加载器负责从文件系统、网络或任何其它资源中加载class文件到内存中,并生成对应的Class实例,只要一个类被加载到JVM中,就不会重复载入。 注意:同一个类使用不同的类加载器加载,这两个类是不相同、互不兼容的。
Java虚拟机运行时,默认有三个类加载器:
  1. Bootstrap   ClassLoader(原生类加载器)-----------加载路径:JRE/lib/rt.jar
  2. Extension   ClassLoader(扩展类加载器)-----------加载路径:JRE/lib/ext/*.jar或者任何指向java.ext.dirs的路径
  3. Application ClassLoader(应用类加载器)-----------加载路径:classpath环境变量指定的jar或目录。未指定classpath则使用当前类路径。
       除了默认的三种类加载器,我们还可以通过继承ClassLoader类创建类加载器,自定义类加载器的父加载器是Application ClassLoader,应用类加载器的父加载器是扩展类加载器,依次向上类推。
      ExtClassLoader和AppClassLoader都是Java实现的,而Bootstrap类加载器是C/C++语言编写的,内嵌到虚拟机中,它是Java的顶级类加载器,所以获取Bootstrap加载器是返回null。

获取默认类加载器案例:

	public static void main(String[] args) {
		//获取当前类
		System.out.println("Test02的类加载器:" + Test02.class.getClassLoader());
		ClassLoader cl = ClassLoader.getSystemClassLoader();
		//递归获取所有类加载器
		while(cl != null) {
			System.out.println(cl);
			cl = cl.getParent();
		}
	}
  • 案例说明:打印输出ExtClassLoader和AppClassLoader,但没有Bootstrap加载器,因为Bootstrap不是Java实现的,返回null值。

加载器遵循原则:

  1. 父类委托:类加载器加载一个类时,会委托它的上层类加载器来对其加载,一直往上委托到Bootstrap类加载器,当所有上层加载器都不能加载该类时,才会从自身的类路径中去加载类文件
  2. 全盘负责:某个类被类加载器加载成功后,该类中引用的其他类也会被同一个类加载器所加载。保证加载的类属于同一个。
  3. 单一性:父加载器加载过的类不能被子加载器加载第二次。

注意:自定义类加载器可以重写loadClass(),但违反了父类委托和单一性原则,不要这样做。一般重写的是findClass(),通过ClassLoader的源码也可以看出该方法就是用来重写的。

URLClassLoader类

除了三个默认类加载器和自定义加载器,Java还提供一个URLClassLoader类,该类能从远程主机获取Class文件加载类,也能通过本地来加载类。URLClassLoader加载Class文件无需包名,而使用Class.forName()加载类时需要加入包名。

使用URLClassLoader加载JDBC案例:
public class Test {
	private static Connection con = null;
	public static Connection getJDBC(String user, String pwd, String database) throws Exception {
		//定位本地驱动jar包
		URL[] urls = {new URL("file:///E://sqljdbc4.jar")};
		//属性类设置数据库的账号密码
		Properties pro = new Properties();
		pro.setProperty("user", user);
		pro.setProperty("password", pwd);
		//创建URLClassLoader对象
		URLClassLoader ucl = new URLClassLoader(urls);
		//加载驱动类(初始化),之后返回驱动类Class实例
		Class cla = ucl.loadClass("com.microsoft.sqlserver.jdbc.SQLServerDriver");
		//把该类对象转成Driver
		Driver driver = (Driver)cla.newInstance();
		//连接驱动,返回Connection对象
		con = driver.connect(database, pro);
		System.out.println("连接成功!");
		return con; 
	}
	public static void main(String[] args) throws Exception {
		String url = "jdbc:sqlserver://localhost:1433;DatabaseName=TestDB"; //连接的数据库
		Connection con = Test.getJDBC("sa", "system", url);
	}
}

关于Class.forName()的使用和了解     

Class.forName()加载的类要包含包名,不然找不到加载的类。
Class.forName()常用于加载JDBC的驱动类,除了之外还可以用于获取一个类的Class实例。 
Class.forName()和使用加载器的loadClass()有所区别:类加载器的loadClass()方法只加载类,不会导致初始化,除非执行了引发初始化的操作。而Class.forName()会对类加载并初始化。





你可能感兴趣的:(Java基础笔记)