今天咱们来说讲jvm
里类加载
的过程,咱们写了那么多类,殊不知道类的加载过程,岂不是很尴尬
。java
jvm
的启动是经过引导类加载器(bootstrap class loader)建立一个初始类(initial class)来完成的,这个类是由jvm
的具体实现指定的。[来自官方规范]程序员
jvm组成结构之一就是类装载器子系统
,咱们今天就来仔细讲讲这个组件。bootstrap
你们经过这个流程图,了解一下咱们写好的Java代码是如何执行的,其中要经历类加载器
这个流程,咱们就来仔细讲讲这里面的知识点。安全
暂时看不懂这两张图不要紧,跟着老哥
往下看 网络
类的生命周期包括:加载、连接、初始化、使用和卸载,其中加载
、连接
、初始化
,属于类加载的过程
,咱们下面仔细讲解。使用是指咱们new对象进行使用,卸载指对象被垃圾回收掉了。数据结构
经过类的全限定名(包名 + 类名),获取到该类的.class
文件的二进制字节流将二进制字节流所表明的静态存储结构,转化为方法区运行时的数据结构架构
在
内存
中生成一个表明该类的java.lang.Class
对象,做为方法区这个类的各类数据的访问入口jvm
总结:加载二进制数据到内存
—> 映射成jvm能识别的结构
—> 在内存中生成class文件
。ide
连接是指将上面建立好的class类合并至Java虚拟机中,使之可以执行的过程,可分为验证
、准备
、解析
三个阶段。加密
① 验证(Verify)
确保class文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全。
② 准备(Prepare)
为类中的
静态字段
分配内存,并设置默认的初始值,好比int类型初始值是0。被final修饰的static字段不会设置,由于final在编译的时候就分配了
③ 解析(Resolve)
解析阶段的目的,是将常量池内的符号引用转换为直接引用的过程(将常量池内的符号引用解析成为实际引用)。若是符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的连接以及初始化。)事实上,解析器操做每每会伴随着 JVM 在执行完初始化以后再执行。 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明肯定义在《Java 虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动做主要针对类、接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
初始化就是执行类的构造器方法init
()的过程。这个方法不须要定义,是javac编译器自动收集类中全部类变量的赋值动做和静态代码块中的语句合并来的。
若该类具备父类,
jvm
会保证父类的init
先执行,而后在执行子类的init
。
这个类加载器使用C/C++语言实现的,嵌套在JVM内部,java程序没法直接操做这个类。它用来加载Java核心类库,如:
JAVA_HOME/jre/lib/rt.jar
、resources.jar
、sun.boot.class.path
路径下的包,用于提供jvm运行所需的包。并非继承自java.lang.ClassLoader,它没有父类加载器
它加载
扩展类加载器
和应用程序类加载器
,并成为他们的父类加载器出于安全考虑,启动类只加载包名为:java、javax、sun开头的类
Java语言编写,由sun.misc.Launcher$ExtClassLoader
实现,咱们能够用Java程序操做这个加载器派生继承自java.lang.ClassLoader,父类加载器为
启动类加载器
从系统属性:
java.ext.dirs
目录中加载类库,或者从JDK安装目录:jre/lib/ext
目录下加载类库。咱们就能够将咱们本身的包放在以上目录下,就会自动加载进来了。
Java语言编写,由sun.misc.Launcher$AppClassLoader
实现。派生继承自java.lang.ClassLoader,父类加载器为
启动类加载器
它负责加载
环境变量classpath
或者系统属性java.class.path
指定路径下的类库它是程序中默认的类加载器,咱们Java程序中的类,都是由它加载完成的。
咱们能够经过
ClassLoader#getSystemClassLoader()
获取并操做这个加载器
通常状况下,以上3种加载器能知足咱们平常的开发工做,不知足时,咱们还能够自定义加载器
好比用网络加载Java类,为了保证传输中的安全性,采用了加密操做,那么以上3种加载器就没法加载这个类,这时候就须要
自定义加载器
自定义加载器实现步骤
继承java.lang.ClassLoader
类,重写findClass()方法若是没有太复杂的需求,能够直接继承
URLClassLoader
类,重写loadClass
方法,具体可参考AppClassLoader
和ExtClassLoader
。
获取ClassLoader几种方式
它是一个抽象类,其后全部的类加载器继承自 ClassLoader(不包括启动类加载器)
// 方式一:获取当前类的 ClassLoader clazz.getClassLoader() // 方式二:获取当前线程上下文的 ClassLoader Thread.currentThread().getContextClassLoader() // 方式三:获取系统的 ClassLoader ClassLoader.getSystemClassLoader() // 方式四:获取调用者的 ClassLoader DriverManager.getCallerClassLoader()
jvm对class文件采用的是按需加载的方式,当须要使用该类时,jvm才会将它的class文件加载到内存中产生class对象。
在加载类的时候,是采用的双亲委派机制
,即把请求交给父类处理的一种任务委派模式。
(1)若是一个类加载器
接收到了类加载
的请求,它本身不会先去加载,会把这个请求委托给父类加载器
去执行。
(2)若是父类还存在父类加载器,则继续向上委托,一直委托到启动类加载器:Bootstrap ClassLoader
(3)若是父类加载器能够完成加载任务,就返回成功结果,若是父类加载失败,就由子类本身去尝试加载,若是子类加载失败就会抛出ClassNotFoundException
异常,这就是双亲委派模式
在Java应用中存在着不少服务提供者接口(Service Provider Interface,SPI),这些接口容许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,通常存在rt.jar包中,由Bootstrap类加载器加载。而Bootstrap类加载器没法直接加载SPI的实现类,同时因为双亲委派模式的存在,Bootstrap类加载器也没法反向委托AppClassLoader加载器SPI的实现类。在这种状况下,咱们就须要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器(双亲委派模型的破坏者)就是很好的选择。
从图可知rt.jar核心包是有Bootstrap类加载器加载的,其内包含SPI核心接口类,因为SPI中的类常常须要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)没法经过Bootstrap类加载器加载,所以只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程当中抛弃双亲委派加载链模式,使程序能够逆向使用类加载器,固然这也使得Java类加载器变得更加灵活。
自定义 String 类,可是在加载自定义 String 类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程当中会先加载 JDK 自带的文件(rt.jar 包中的 javalangString.class),报错信息说没有 main 方法就是由于加载的 rt.jar 包中的 String 类。这样能够保证对 Java 核心源代码的保护,这就是沙箱安全机制。