java类加载机制概述1,java加载机制概述
java类加载机制概述1,java加载机制概述
从事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/)
在安装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 新闻组中用来解释伙伴类加载的流行示例是 Hibernate。Hibernate 框架必须查看用户创建而非 Hibernate 本身一部分的类和资源
相关文章
- 暂无相关文章
用户点评