从ClassLoad开始说起
ClassLoader顾名思义就是我们所常见的类加载器,其作用就是将编译后的class文件加载内存当中.在应用启动时,JVM通过ClassLoader加载相关的类到JVM当中.在具体了解ClassLoader之前我们先来了解下JVM的类加载机制.
1. 类加载机制
- 使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。
- 初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。
- 使用java.lang.reflect包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化。
- 虚拟机启动时,用户会先初始化要执行的主类(含有main)
- jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化。
2. 类加载过程
2.1 加载
加载过程主要完成三件事情:
- 通过类的全限定名来获取定义此类的二进制字节流
- 将这个类字节流代表的静态存储结构转为方法区的运行时数据结构
- 在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。
这个过程主要就是类加载器完成。(对于HotSpot虚拟而言,Class对象较为特殊,其被放置在方法区而不是堆中)
2.2 验证
此阶段主要确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。主要包括以下四个阶段:
- 文件格式验证:基于字节流验证,验证字节流符合当前的Class文件格式的规范,能被当前虚拟机处理。验证通过后,字节流才会进入内存的方法区进行存储。
- 元数据验证:基于方法区的存储结构验证,对字节码进行语义验证,确保不存在不符合java语言规范的元数据信息。
- 字节码验证:基于方法区的存储结构验证,通过对数据流和控制流的分析,保证被检验类的方法在运行时不会做出危害虚拟机的动作。
- 符号引用验证:基于方法区的存储结构验证,发生在解析阶段,确保能够将符号引用成功的解析为直接引用,其目的是确保解析动作正常执行。换句话说就是对类自身以外的信息进行匹配性校验。
- 5.
2.3 准备
仅仅为类变量(static修饰的变量)分配内存空间并且设置该类变量的初始值(这里的初始值指的是数据类型默认的零值),这里不包含用final修饰的static,因为用final修饰的类变量在javac执行编译期间就会分配,同时要注意,这里不会为实例变量分配初始化。类变量会分配在方法区中,而实例变量会在对象实例化是随着对象一起被分配在java堆中。举例:
public static int value=33;
这据代码的赋值过程分两次,一是上面我们提到的阶段,此时的value将会被赋值为0;而value=33这个过程发生在类构造器的方法中。
()
2.4 解析
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
2.5 初始化
类加载过程的最后一步,到该阶段才真正开始执行类中定义的java代码,同样该阶段也是初始化类变量和其他资源(执行static字段和静态代码块),换句话说该阶段是执行类构造器方法的过程。
()
()方法是由编译器自动收集类中所有的类变量的赋值动作和静态语句块(static{})中的语句合并而成。
方法和实例构造方法不同
()
不同,它不需要显示的调用父类的
()
,虚拟机会保证父类
()
的)方法在子类的
(
方法之前执行完成,也就是说,父类的静态语句块和静态变量优先于子类中变量赋值操作。
()
方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量赋值的操作,那么编译器就不会为这个类生
()
成)方法。接口中不能使用静态语句块,单仍然有变量赋值的操作,所以仍然可以生成
(
)方法,但与类不同的执行接口
(
方法不需要先执行父接口的
()
方法,另外,接口的实现类在初始化时也一样不会执行接口的
()
方法。
()
3. 类加载器
把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作交给虚拟机之外的类加载器来完成。这样的好处在于,我们可以自行实现类加载器来加载其他格式的类,只要是二进制字节流就行,这就大大增强了加载器灵活性。
通常系统自带的类加载器分为三种:
启动类加载器(Bootstrap ClassLoader):由C/C++实现,负责加载
目录下或者是-Xbootclasspath所指定路径下目录以及系统属性sun.boot.class.path制定的目录中特定名称的jar包到虚拟机内存中。在JVM启动时,通过Bootstrap ClassLoader加载rt.jar,并初始化sun.misc.Launcher从而创建Extension ClassLoader和Application ClassLoader的实例.
\jre\lib
需要注意的是,Bootstrap ClassLoader智慧加载特定名称的类库,比如rt.jar.这意味我们自定义的jar扔到也不会被加载.
\jre\lib
我们可以通过以下代码,查看Bootstrap ClassLoader到底初始化了那些类库:URL[] urLs = Launcher.getBootstrapClassPath().getURLs(); for (URL urL : urLs) { System.out.println(urL.toExternalForm()); }扩展类加载器(Extension Classloader):只有一个实例,由sun.misc.Launcher$ExtClassLoader实现,负责加载
目录下或是被系统属性java.ext.dirs所指定路径目录下的所有类库。
\lib\ext
应用程序类加载器(Application ClassLoader):只有一个实例,由sun.misc.Launcher$AppClassLoader实现,负责加载系统环境变量ClassPath或者系统属性java.class.path制定目录下的所有类库,如果应用程序中没有定义自己的加载器,则该加载器也就是默认的类加载器.该加载器可以通过java.lang.ClassLoader.getSystemClassLoader获取.
以上三种是我们经常认识最多,除此之外还包括线程上下文类加载器(Thread Context ClassLoader)和自定义类加载器.
至于自定义加载器就更简单了,JVM运行我们通过自定义的ClassLoader加载相关的类库.
3.1 类加载器的双亲委派模型
不难发现,该种加载流程的好处在于:
- 可以避免重复加载,父类已经加载了,子类就不需要再次加载
- 更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。
接下来,我们看看双亲委派模型是如何实现的:
protected Class
loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先先检查该类已经被加载过了 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) { //父类加载器抛出异常,无法完成类加载请求 } if (c == null) {
// long t1 = System.nanoTime(); //父类加载器无法完成类加载请求时,调用自身的findClass方法来完成类加载 c = findClass(name); 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怎么知道一个某个类加载器的父加载器呢?如果你有此疑问,请重新再看一遍.
3.2 类加载器的特点
- 运行任何一个程序时,总是由Application Loader开始加载指定的类。
- 一个类在收到加载类请求时,总是先交给其父类尝试加载。
- Bootstrap Loader是最顶级的类加载器,其父加载器为null。
3.3 类加载的三种方式
- 通过命令行启动应用时由JVM初始化加载含有main()方法的主类。
- 通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。
- 通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。
3.4 自定义类加载器的两种方式
利用现成的类加载器进行加载:
1. 利用当前类加载器 Class.forName(); 2. 通过系统类加载器 Classloader.getSystemClassLoader().loadClass(); 3. 通过上下文类加载器 Thread.currentThread().getContextClassLoader().loadClass();URLClassLoader loader=new URLClassLoader(); loader.loadClass();
类加载实例演示:
命令行下执行HelloWorld.java
public class HelloWorld{
public static void main(String[] args){ System.out.println("Hello world"); } }
该段代码大体经过了一下步骤:
- 寻找jre目录,寻找jvm.dll,并初始化JVM.
- 产生一个Bootstrap ClassLoader;
- Bootstrap ClassLoader加载器会加载他指定路径下的java核心api,并且生成Extended ClassLoader加载器的实例,然后Extended ClassLoader会加载指定路径下的扩展java api,并将其父设置为Bootstrap ClassLoader。
- Bootstrap ClassLoader生成Application ClassLoader,并将其父Loader设置为Extended ClassLoader。
- 最后由AppClass ClassLoader加载classpath目录下定义的类——HelloWorld类。
我们上面谈到 Extended ClassLoader和Application ClassLoader是通过Launcher来创建,现在我们再看看源代码:
public Launcher() { Launcher.ExtClassLoader var1; try { //实例化ExtClassLoader var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { //实例化AppClassLoader this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } //主线程设置默认的Context ClassLoader为AppClassLoader. //因此在主线程中创建的子线程的Context ClassLoader 也是AppClassLoader Thread.currentThread().setContextClassLoader(this.loader); String var2 = System.getProperty("java.security.manager"); if(var2 != null) { SecurityManager var3 = null; if(!"".equals(var2) && !"default".equals(var2)) { try { var3 = (SecurityManager)this.loader.loadClass(var2).newInstance(); } catch (IllegalAccessException var5) { ; } catch (InstantiationException var6) { ; } catch (ClassNotFoundException var7) { ; } catch (ClassCastException var8) { ; } } else { var3 = new SecurityManager(); } if(var3 == null) { throw new InternalError("Could not create SecurityManager: " + var2); } System.setSecurityManager(var3); } }
3.5 非常重要
那么如果ClassA中引用了ClassB呢?当类加载器在加载ClassA的时候,发现引用了ClassB,此时类加载如果检测到ClassB还没有被加载,则先回去加载.当ClassB加载完成后,继续回来加载ClassA.换句话说,类会通过自身对应的来加载其加载其他引用的类.
JVM规定,对于任何一个类,都需要由加载它的类加载器和这个类本身一同确立在java虚拟机中的唯一性,通俗点就是说,在jvm中判断两个类是否是同一个类取决于类加载和类本身,也就是同一个类加载器加载的同一份Class文件生成的Class对象才是相同的,类加载器不同,那么这两个类一定不相同.
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/217451.html原文链接:https://javaforall.net
