java类加载机制概述1

从事java研发必然少不了对java类加载机制的涉及,本文结合例子讲述java classloader工作机制。

一 jvm 类加载机制

1)jvm位置:java是运行在java虚拟机上的程式,java虚拟机物理层面上来讲,就是我们安装在电脑上的jre目录/lib/jvm.dll(版本不同,可能存在于jre目录/lib/client/jvm.dll,jre目录/lib/server/jvm.dll),这是java字节码运行的基础,它不是由java语言编写,所以我们阅读jdk源码时遇到native函数,基本上就是调用jvm相关的代码。

2)jdk和jre关系:从oracle官网上下载java环境,可以选择jdk或者jre进行安装,他们的关系可以理解为子集的概念,jdk是jre运行环境再加上一些java开发的工具集,查看jdk目录结构如下(例子为jdk1.6.37版本)

D:.
├─bin
│  └─server
├─include
│  └─win32
├─jre
│  ├─bin
│  │  ├─dtplugin
│  │  ├─plugin2
│  │  └─server
│  └─lib
│      ├─amd64
│      ├─applet
│      ├─audio
│      ├─cmm
│      ├─deploy
│      ├─ext
│      ├─fonts
│      ├─im
│      ├─images
│      │  └─cursors
│      ├─management
│      ├─security
│      ├─servicetag
│      └─zi
│          ├─Africa
│          ├─America
│          │  ├─Argentina
│          │  ├─Indiana
│          │  ├─Kentucky
│          │  └─North_Dakota
│          ├─Antarctica
│          ├─Asia
│          ├─Atlantic
│          ├─Australia
│          ├─Etc
│          ├─Europe
│          ├─Indian
│          ├─Pacific
│          └─SystemV
└─lib
    └─visualvm
        ├─etc
        ├─platform
        │  ├─config
        │  │  ├─ModuleAutoDeps
        │  │  └─Modules
        │  ├─core
        │  │  └─locale
        │  ├─docs
        │  ├─lib
        │  │  └─locale
        │  ├─modules
        │  │  ├─ext
        │  │  │  └─locale
        │  │  └─locale
        │  └─update_tracking
        ├─profiler
        │  ├─config
        │  │  └─Modules
        │  ├─lib
        │  │  ├─deployed
        │  │  │  ├─jdk15
        │  │  │  │  └─windows-amd64
        │  │  │  └─jdk16
        │  │  │      └─windows-amd64
        │  │  └─locale
        │  ├─modules
        │  │  └─locale
        │  └─update_tracking
        └─visualvm
            ├─config
            │  └─Modules
            ├─core
            │  └─locale
            ├─modules
            │  └─locale
            └─update_tracking

 

 

java官方文档描述jre和jdk关系如图:(链接http://docs.oracle.com/javase/7/docs/)


java类加载机制概述1_第1张图片
 

 

在安装jdk时可以选择是否同时安装jre,如果选择安装,那么系统中就存在两份jre,具体程序运行时会执行哪个jre,windows系统默认搜索规则是:

1. 当前目录下有沒有 JRE子目录

2. 父目录下 JRE 子目录

3.查 詢 Window Registry(HKEY_LOCAL_MACHINE\Software\JavaSoft\Java Runtime Environment\)

注意:安装环境会建议建立JAVA_HOME环境变量并将其加入path中,这样可以避免因为默认搜索规则出的结果造成混淆。如果没有加,可以搜索下自己系统中有几个java.exe,本人系统中有三个,分别在c:/windows/system32/java.ext;

d:/program files/java/jdk_1_6_37/bin/java.exe;

d:/program files/java/jre/bin/java.exe

 

因为没有将JAVA_HOME路径加入到path,path路径是c:/windows/system32;......所以在命令行下执行java Main系统默认执行的是c:/windows/system32/java.exe(除非命令行在其他两个java.exe所在目录),这一点可以通过分别修改三个路径下java.exe文件到新名字java1.exe来验证到底执行的是哪个目录

3)java类加载机制:jdk带有三个系统类加载器:bootstrap加载器;扩展加载器;系统加载器,他们的关系如下表

类加载器

被加载加载器 parent 父类 类型 默认加载目录/文件 备注

bootstrap加载器

       

 sun.boot.class.path系统属性所指路径,指向jre下/lib,如rt.jar

 

虚拟机出于安全等因素考虑,不会加载< Java_Runtime_Home >/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的

扩展加载器

 

bootstrap加载器

 

bootstrap加载器(因为此加载器由非java语言编写,在jvm中标识为null,所以一个加载器的parent为null表示它是由bootstrap加载器加载)

 java.lang.ClassLoader->java.security.SecurClassLoader->java.net.URLClassLoader sun.misc.Launcher$ExtClassLoader  java.ext.dirs属性所指路径,指向java.exe所在jre下/lib/ext子目录,可以将自己的class文件放入这个目录,交由扩展加载器加载,可以通过–Djava.ext.dirs=xxx 改变  jvm中只存在一份,一旦建立,再通过System.setProperty()修改系统属性不会起作用

系统加载器

 

bootstrap加载器

 

扩展加载器

 java.lang.ClassLoader->java.security.SecurClassLoader->java.net.URLClassLoader sun.misc.Launcher$AppClassLoader  默认为.目录

再取java.class.path属性所指路径,可以通过java -cp xxx 来改变

最后取环境变量CLASSPATH下的class文件和jar文件

  jvm中只存在一份,一旦建立,再通过System.setProperty()修改系统属性不会起作用

 

在 Java 之中,每个类都是由某个类型加载器(ClassLoader 的实体)来载入,因此,Class 类型的实体中,都会有记录载入它的ClassLoader 的实体(注意:如果值是null,不代表它不是由类加载器载入,而是代表这个类別是由(bootstrap loader,也有人称root loader)所载入,只不过这个类型加载器不由java书写,所以逻辑上没有实体 )

 

二 自定义类加载器

加载类到内存中分两种方式:1)预加载 ;2)显示加载。预加载是虚拟机在启动的时候将rt.jar中的类一次加载到内存,因为这些类都是基础类,会被频繁使用到,预加载可以减少运行时IO开销,显示加载可以:1)使用new()操作符 2)java.lang.Class 裡的forName() 3)java.lang.ClassLoader 裡的loadClass()

要查看类加载详情,可以使用java -verbose:class xxx来输出。看下面一段代码:

 

public class Main
{
public static void main(String args[])
{
A a1 = new A() ;
a1.print() ;
B b1 = new B() ;
b1.print() ;
}
}

public class A //与Main在同一个路径下
{
public void print()
{
System.out.println("Using Class A") ;
}
}

public class B //与Main在同一个路径下
{
public void print()
{
System.out.println("Using Class B") ;
}
}
 到Main所在目录执行javac *.java,查看生成了三个calss文件,再执行java -verbose:class Main > load.log ,查看load.log内容如下:

 

 

[Opened D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]
[Loaded java.lang.Object from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]
[Loaded java.io.Serializable from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]
[Loaded java.lang.Comparable from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]
...
...
[Loaded java.security.Principal from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]
[Loaded Main from file:/D:/deep_java/]
[Loaded A from file:/D:/deep_java/]
Using Class A
[Loaded B from file:/D:/deep_java/]
Using Class B
[Loaded java.lang.Shutdown from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]
 由此可见class类加载顺序。

 

也可以使用以下方式加载类:

 

import java.net.* ;
public class Test
{
public static void main(String args[]) throws Exception
{
		Class c = Class.forName(args[0]) ; //第一种载入class对象方法
		Object o = c.newInstance() ;
		//Class c = Class.forName(args[0],true,off.getClass().getClassLoader()) ;//true参数表示载入同时进行初始化,这个参数在SPI接口和实现类加载中非常有用
		Test off = new Test() ;
		System.out.println("类型准备载入") ;
		//ClassLoader loader = off.getClass().getClassLoader() ;//第二种载入class对象方法,使用了对象引用Class的classloader
		//Class c = loader.loadClass(args[0]) ;
		System.out.println("类型准备实例化") ;
		Object o = c.newInstance() ;
		Object o2 = c.newInstance() ;
		}
}
 

 

了解了默认类加载机制后,可以手工打造一个加载器,ExtClassLoader和AppClassLoader都是继承URLClassLoader,自己的加载器也可以继承自这个类:

 

import java.net.* ;
public class Test
{
public static void main(String args[]) throws Exception
{
		URL u = new URL("file:/D:/deep_java/test/lib/") ;
		URLClassLoader ucl = new URLClassLoader(new URL[]{ u }) ;
		Class c = ucl.loadClass(args[0]) ;
		Assembly asm = (Assembly) c.newInstance() ;
		asm.start() ;
		URL u1 = new URL("file:/D:/deep_java/test/lib/") ;
		URLClassLoader ucl1 = new URLClassLoader(new URL[]{ u1 }) ;
		Class c1 = ucl1.loadClass(args[0]) ;
		Assembly asm1 = (Assembly) c1.newInstance() ;
		asm1.start() ;
		System.out.println(Test.class.getClassLoader()) ;
		System.out.println(u.getClass().getClassLoader()) ;
		System.out.println(ucl.getClass().getClassLoader()) ;
		System.out.println(c.getClassLoader()) ;
		System.out.println(asm.getClass().getClassLoader()) ;
		System.out.println(u1.getClass().getClassLoader()) ;
		System.out.println(ucl1.getClass().getClassLoader()) ;
		System.out.println(c1.getClassLoader()) ;
		System.out.println(asm1.getClass().getClassLoader()) ;
		System.out.println(Assembly.class.getClassLoader()) ;
		
}
 deep_java/test/ 目录结构如下:

 

 

├─Test.class
├─Assembly.class
├─lib
│  ├─ClassA.class
│  ├─ClassB.class
│  ├─ClassC.class
Assembly 是一个接口,ClassA ClassB ClassC都实现了这个接口,Test主程序在运行时将参数名作为Class名动态加载。命令行输入java -verbose:class Test ClassA 执行结果如下:
sun.misc.Launcher$AppClassLoader@37b90b39
null
null
java.net.URLClassLoader@55f33675
java.net.URLClassLoader@55f33675
null
null
java.net.URLClassLoader@525483cd
java.net.URLClassLoader@525483cd
sun.misc.Launcher$AppClassLoader@37b90b39
注意:两个对象的类加载器是不同的,基础类的加载器是bootstrap加载器,所以打印出来是null
 现在如果把lib目录下的class都移到test目录下,即目录为:

 

 

├─Test.class
├─Assembly.class
├─ClassA.class
├─ClassB.class
├─ClassC.class
 代码中url的路径URL u1 = new URL("file:/D:/deep_java/test/lib/") ;修改为

 

URL u1 = new URL("file:/D:/deep_java/test/") ;看看执行结果是啥:

 

sun.misc.Launcher$AppClassLoader@53004901
null
null
sun.misc.Launcher$AppClassLoader@53004901
sun.misc.Launcher$AppClassLoader@53004901
null
null
sun.misc.Launcher$AppClassLoader@53004901
sun.misc.Launcher$AppClassLoader@53004901
sun.misc.Launcher$AppClassLoader@53004901
 为啥ClassA类的加载器都变成了系统类加载器AppClassLoader呢,这是因为jvm的双亲委托机制在起作用。注意下加载Test主程序的加载器是sun.misc.Launcher$AppClassLoader@53004901,自己创建的类加载器URLClassLoader默认将sun.misc.Launcher$AppClassLoader@53004901作为自己的parent,当loadClass方法被调用时,默认机制会首先请求parent去findClass(),如果找不到再自己加载,因为sun.misc.Launcher$AppClassLoader@53004901默认加载目录是当前目录,刚好能够加载到ClassA,所以每次ClassA都能被这个加载器加载。

 

 

大家可能会注意到ClassLoader的两个方法,loadClass 和 findClass,看下它的源码:

 

    public Class<?> loadClass(String name) throws ClassNotFoundException {
	return loadClass(name, false);
    }

protected synchronized Class<?> loadClass(String name, boolean resolve)
	throws ClassNotFoundException
    {
	// First, check if the class has already been loaded
	Class c = findLoadedClass(name);
	if (c == null) {
	    try {
		if (parent != null) {
		    c = parent.loadClass(name, false);
		} else {
		    c = findBootstrapClassOrNull(name);
		}
	    } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            if (c == null) {
	        // If still not found, then invoke findClass in order
	        // to find the class.
	        c = findClass(name);
	    }
	}
	if (resolve) {
	    resolveClass(c);
	}
	return c;
    }
loadClass(String name)的逻辑是:先检查已经加载的类,如果未加载,先请求parent加载器加载,如果未找到再调用自己的findClass(name)方法。双亲委托机制在loadClass中体现出来。因此自己写类加载器,最好是覆盖findClass(name)方法,而不是loadClass方法,保留默认的双亲委托机制,以免程序在应用程式下可以用,迁移到web下出现问题。至于为啥java要使用双亲委托机制,主要是考虑安全问题,如果一个java核心类被用户自己的class覆盖了,程序运行时可能会出现不可预知的错误,系统将会变得脆弱。双亲委托机制可以保证只要不篡改jre目录下的jar文件,虚拟机加载的基础类就不会被应用程序私有类影响。

 

看看自己如何重写findClass方法

 

import mylib.Target;

public class Test{
	public static void main(String args[]) throws Exception{
		MyClassLoader mcl = new MyClassLoader("myClassLoaderA");
		System.out.println("myClassLoaderA->parent="+mcl.getParent()) ;
		Class target = mcl.loadClass("Target");
		System.out.println("Target classloader ===============" + target.getClassLoader());
		Object o = (Object) target.newInstance();
		MyClassLoader mclB = new MyClassLoader("myClassLoaderB");
		System.out.println("myClassLoaderB->parent="+mclB.getParent()) ;
		Class targetb = mcl.loadClass("Target");//注释1
		Target o1 = (Target) target.newInstance();//注释2
		System.out.println("Target classloader ===============" + o1.getClass().getClassLoader());
	}
}

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MyClassLoader extends ClassLoader {
	private String name;
	public MyClassLoader(String name) {
		super(); // 通过这个构造方法生成的类加载器,它的父加载器是系统类加载器
		this.name = name;
	}

	public MyClassLoader(String name, ClassLoader loader) {
		super(loader); // 通过这个这个构造方法生成的类加载器,该加载器的父加载器是loader,如果为空,则父加载器为根加载器
		// 子类继承父类,如果不显式写出调用父类的哪个构造方法,那么就默认调用父类的无参构造函数
		this.name = name;
	}
	public String toString() {
		return this.name;
	}
	// 要重写findclass这个方法,loadclass会调用它
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		// TODO Auto-generated method stub
		byte[] data = null;
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(
					"D:\\deep_java\\classLoader\\mylib\\" + name
							+ ".class");
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		ByteArrayOutputStream abos = new ByteArrayOutputStream();
		int ch = 0;
		try {
			while (-1 != (ch = fis.read())) {
				abos.write(ch); // 把字节一个一个写到输出流中
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		data = abos.toByteArray(); // 把输出流中的字节弄成一个字节数组
		return this.defineClass("mylib." + name, data, 0,
				data.length, null);
	}
}	
目录结构为:
 
├─Test.class
├─MyClassLoader.class
├─mylib
├-├─Target.class
 注释1:不同类加载器加载的类不能相互赋值,即使两个类字节码完全相同,如果两个classloader类从同一个class文件加载,执行时会报链接错误Exception in thread "main" java.lang.LinkageError: loader (instance of MyClassL//oader): attempted duplicate class definition for name: "mylib/Target"
注释2:不同类加载器加载的类之间不能赋值或者隐式转换,会报运行时类型转换错误 java.lang.ClassCastException: mylib.Target cannot be cast to mylib.Target at Test.main(Test.java:19)

二 特殊类加载器

jvm默认的类加载机制和双亲委托机制能够解决类加载的安全问题,但是有些场景下无法满足

场景一:SPI规范定义的接口在java基础包中,预加载时是被bootstrap加载器载入,但是实现类是各个厂商的jar包,会被其他加载器加载 

场景二:某些框架必须查看用户创建的而非本身创建的类和资源,所以两种特殊类加载器应运而生:

 

1 线程上下文加载

默认使用系统类加载器,如果不做任何修改,任何线程的默认加载器是系统类加载器,但是线程加载器是可以自己制定的,制定的就可以不遵循双亲委派。因为很多情况下双亲委派解决不了,所以需要定制的,这也算sun给自己开的一个后门

2 伙伴加载器

Eclipse 新闻组中用来解释伙伴类加载的流行示例是 HibernateHibernate 框架必须查看用户创建而非 Hibernate 本身一部分的类和资源
 

 

 

 

你可能感兴趣的:(ClassLoader)