从事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启动类加载器的加载路径 java 类加载路径_Java启动类加载器的加载路径

在安装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 执行结果如下: