您的位置:首页 > 编程语言 > Java开发

【JVM基础知识】java类加载机制

2017-10-13 00:00 513 查看

一、什么是Classloader

一个Java程序要想运行起来,首先需要经过编译生成 .class文件,然后创建一个运行环境(jvm)来加载字节码文件到内存运行,而.class 文件是怎样被加载中jvm中的就是Java Classloader所做的事情。

那么.class文件什么时候会被类加载器加载到jvm中运行呢?比如执行new操作时候,当我们使用Class.forName(“包路径+类名”)、Class.forName(“包路径+类名”,initialize,classloader)、ClassLoader.loadclass(“包路径+类名”);时候就触发了类加载器去类加载对应的路径去查找*.class,并创建Class对象。

Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释并执行类中的static块;

ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

:Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

二、Java自带的Classloader

如何寻找类加载器先来看个例子:

public class ClassLoaderTest {

public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);// 应用类加载器
System.out.println(loader.getParent());// 扩展类加载器
System.out.println(loader.getParent().getParent());
}

}

运行后,输出结果:

sun.misc.Launcher$AppClassLoader@56a96eba
sun.misc.Launcher$ExtClassLoader@da4a1c9
null

从上面的结果可以看出,并没有获取到ExtClassLoader的父Loader,原因是Bootstrap ClassLoader(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。

这几种类加载器的层次关系如下图所示:



注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。一般我们都认为ExtClassloader的父类加载器是BootStarpClassloader,但是其实他们之间根本是没有父子关系的,只是在ExtClassloader找不到要加载类时候会去委托BootStrap加载器去加载。

站在Java虚拟机的角度来讲,只存在两种不同的类加载器:

启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;

所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。

站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

2.1 BootstrapClassloader

引导类加载器,又称启动类加载器,是最顶层的类加载器,主要用来加载Java核心类,如rt.jar、resources.jar、charsets.jar等,Sun的JVM中,执行java的命令中使用-Xbootclasspath选项或使用- D选项指定sun.boot.class.path系统属性值可以指定附加的类,它不是 java.lang.ClassLoader的子类,而是由JVM自身实现的该类c 语言实现,Java程序访问不到该加载器。

通过下面代码可以查看该加载器加载了哪些jar包:

package test;

import java.net.URL;

public class ClassLoaderTest {

public static void main(String[] args) {
getJars();
}

public static void getJars() {
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
}

}

执行结果:

file:/Z:/JDK/jre/lib/resources.jar
file:/Z:/JDK/jre/lib/rt.jar
file:/Z:/JDK/jre/lib/sunrsasign.jar
file:/Z:/JDK/jre/lib/jsse.jar
file:/Z:/JDK/jre/lib/jce.jar
file:/Z:/JDK/jre/lib/charsets.jar
file:/Z:/JDK/jre/lib/jfr.jar
file:/Z:/JDK/jre/classes

写到这里大家应该都知道,我们并没有在classpath里面指定这些类的路径,为啥还是能被加载到jvm并使用起来了吧,因为这些是bootstarp来加载的。

2.2 ExtClassloader

扩展类加载器,主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar包或者由java.ext.dirs系统属性指定的jar包。放入这个目录下的jar包对所有AppClassloader都是可见的(后面会知道ExtClassloader是AppClassloader的父加载器)。那么ext都是在哪些地方加载类呢?

System.out.println(System.getProperty("java.ext.dirs"));


2.3 AppClassloader

系统类加载器,又称应用加载器,本文说的SystemClassloader和APPClassloader是一个东西,它负责在JVM启动时,加载来自在命令java中的-classpath或者java.class.path系统属性或者 CLASSPATH操作系统属性所指定的JAR类包和类路径。调用ClassLoader.getSystemClassLoader()可以获取该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器,这点通过ClassLoader的无参构造函数可以知道如下:

protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}

执行以下代码即可获得classpath加载路径:

System.out.println(System.getProperty("java.class.path"));


2.4 类加载器原理

Java类加载器使用的是双亲委派机制,如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

那么问题来了,为啥使用这种方式呢?因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

双亲委派模型意义

1、系统类防止内存中出现多份同样的字节码;

2、保证Java程序安全稳定运行。

下面我们从源码看如何实现双亲委派机制:

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
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.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

分析代码知道:

首先从jvm缓存查找该类,如果该类之前被加载过,则直接从jvm缓存返回该类,否者看当前类加载器是否有父加载器;

如果有父加载器的话则委托为父类加载器进行加载,否者委托BootStrapClassloader进行加载;

如果还是没有找到,则调用当前Classloader的findclass方法进行查找。

从上面源码知道要想修改类加载委托机制,实现自己的载入策略可以通过覆盖ClassLoader的findClass方法或者覆盖loadClass方法来实现。

Java应用启动过程是首先Bootstarp Classloader加载rt.jar包里面的sun.misc.Launcher类,而该类内部使用BootstarpClassloader加载器构建和初始化Java中三种类加载和线程上下文类加载器,然后在根据不同场景去使用这些类加载器去自己的类查找路径去加载类。

参考
http://ifeve.com/classloader%e8%a7%a3%e6%83%91/ http://www.cnblogs.com/ityouknow/p/5603287.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Classloader