每个.java类文件都存储着需要执行的程序逻辑,这些文件称为源文件,源文件不能直接被jvm执行,需要经过Java编译器(javac)编译成.class文件,.class文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时jvm将会加载它的class文件,并创建对应的class对象,将class文件加载到虚拟机内存中,这个过程称为类加载。
类加载过程如下图所示:
1、加载:通过类的绝对路径查找此类的字节码文件,并利用字节码文件创建一个Class对象。
2、验证:目的在于确保Class文件的字节流中包含的信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括文件格式验证,元数据验证,字节码验证,符号引用验证。
3、准备:为类的static变量分配内存并且设置该类变量的初始值即0(如static int i=5,准备阶段这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了。注意这里不会为实例变量分配内存和初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆中。
4、解析:有类或接口的解析,字段解析,类方法解析,接口方法解析。
5、初始化:执行静态初始化代码块和静态初始化成员变量(如前面只初始化了默认值的static变量将在这个阶段赋值),成员变量也将被初始化。
虚拟机提供了3种类加载器,启动类加载器(BootstrapClassLoader)、扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader),下面我们分别介绍下。
启动类加载器使用C++语言实现的,是虚拟机自身的一部分,它负责将
扩展类加载器是指Sun公司实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类。它负责加载
//ExtClassLoader类中获取路径的代码
private static File[] getExtDirs() {
//加载/lib/ext目录中的类库
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
应用类加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader,由Java语言实现,是Launcher的静态内部类。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用应用类加载器,应用类加载器一般情况下是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
我们简化下sun.misc.Launcher源码,看看三种类加载器到底负责加载哪些class对象。
public class Launcher {
private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
//设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
Thread.currentThread().setContextClassLoader(loader);
}
/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {}
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {}
String bootClassPath = System.getProperty("sun.boot.class.path")这是 BootstrapClassLoader负负责加载的class文件的路径 ,我们打印下:
System.out.println(System.getProperty("sun.boot.class.path"));
打印结果:
C:\Program Files\Java\jre1.8.0_91\lib\resources.jar;
C:\Program Files\Java\jre1.8.0_91\lib\rt.jar;
C:\Program Files\Java\jre1.8.0_91\lib\sunrsasign.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jsse.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jce.jar;
C:\Program Files\Java\jre1.8.0_91\lib\charsets.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jfr.jar;
C:\Program Files\Java\jre1.8.0_91\classes
上面就是启动类加载器负责加载的class文件,其中就包括了rt.jar。
ExtClassLoader源码中发现:
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];
for(int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}
return var1;
}
我们打印System.getProperty("java.ext.dirs"),打印结果如下:
C:\Program Files\Java\jre1.8.0_91\lib\ext
ExtClassLoader加载了ext目录下的jar包。
AppClassLoader源码发下:
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
AppClassLoader负责加载java.class.path路径下的class文件,我们打印System.getProperty("java.class.path"),打印结果如下:
D:\workspace\ClassLoaderDemo\bin
这个路径其实就是当前Java工程的bin目录,里面存放的是我们自己定义的类的class文件,也就是我们自己的class文件是通过AppClassLoader加载的。
Java虚拟机对class文件采用的是按需加载方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类加载器处理,它一种任务委派模式。注意所谓的“父类”加载器,和继承概念中的“父类”、“超类”不是同一个概念,AppClassLoader的父类加载器是ExtClassLoader,ExtClassLoader的父类加载器是BootstrapClassLoader,自定义类加载器的父类加载器是AppClassLoader。
工作原理:
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成。
双亲委派的优势:
1、避免类的重复加载
Java类随着它的类加载器一起具备了一种带有优先级的层次关系,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
2、更安全,避免java.xxx系统类被随意篡改,保证java.xxx,javax.xxx核心类的安全。
假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过应用类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常:
java.lang.SecurityException: Prohibited package name: java.lang