当程序主动使用某个类时, 如果该类还未被加载到内存中, 则 JVM 会通过加载, 连接, 初始化 3 个步骤来对该类进行初始化. 如果没有意外, JVM 将会连续完成 3 个步骤, 所以有时也把这个 3 个步骤统称为类加载或类初始化.
一, 类加载过程
1. 加载
加载指的是将类的 class 文件读入到内存, 并为之创建一个 java.lang.Class 对象, 也就是说, 当程序中使用任何类时, 系统都会为之建立一个 java.lang.Class 对象.
类的加载由类加载器完成, 类加载器通常由 JVM 提供, 这些类加载器也是前面所有程序运行的基础, JVM 提供的这些类加载器通常被称为系统类加载器. 除此之外, 开发者可以通过继承 ClassLoader 基类来创建自己的类加载器.
通过使用不同的类加载器, 可以从不同来源加载类的二进制数据, 通常有如下几种来源.
从本地文件系统加载 class 文件, 这是前面绝大部分示例程序的类加载方式.
从 JAR 包加载 class 文件, 这种方式也是很常见的, 前面介绍 JDBC 编程时用到的数据库驱动类就放在 JAR 文件中, JVM 可以从 JAR 文件中直接加载该 class 文件.
通过网络加载 class 文件.
把一个 Java 源文件动态编译, 并执行加载.
类加载器通常无须等到 "首次使用" 该类时才加载该类, Java 虚拟机规范允许系统预先加载某些类.
2. 链接
当类被加载之后, 系统为之生成一个对应的 Class 对象, 接着将会进入连接阶段, 连接阶段负责把类的二进制数据合并到 JRE 中. 类连接又可分为如下 3 个阶段.
1)验证: 验证阶段用于检验被加载的类是否有正确的内部结构, 并和其他类协调一致. Java 是相对 C++ 语言是安全的语言, 例如它有 C++ 不具有的数组越界的检查. 这本身就是对自身安全的一种保护. 验证阶段是 Java 非常重要的一个阶段, 它会直接的保证应用是否会被恶意入侵的一道重要的防线, 越是严谨的验证机制越安全. 验证的目的在于确保 Class 文件的字节流中包含信息符合当前虚拟机要求, 不会危害虚拟机自身安全. 其主要包括四种验证, 文件格式验证, 元数据验证, 字节码验证, 符号引用验证.
四种验证做进一步说明:
文件格式验证: 主要验证字节流是否符合 Class 文件格式规范, 并且能被当前的虚拟机加载处理. 例如: 主, 次版本号是否在当前虚拟机处理的范围之内. 常量池中是否有不被支持的常量类型. 指向常量的中的索引值是否存在不存在的常量或不符合类型的常量.
元数据验证: 对字节码描述的信息进行语义的分析, 分析是否符合 java 的语言语法的规范.
字节码验证: 最重要的验证环节, 分析数据流和控制, 确定语义是合法的, 符合逻辑的. 主要的针对元数据验证后对方法体的验证. 保证类方法在运行时不会有危害出现.
符号引用验证: 主要是针对符号引用转换为直接引用的时候, 是会延伸到第三解析阶段, 主要去确定访问类型等涉及到引用的情况, 主要是要保证引用一定会被访问到, 不会出现类等无法访问的问题.
2)准备: 类准备阶段负责为类的静态变量分配内存, 并设置默认初始值.
3)解析: 将类的二进制数据中的符号引用替换成直接引用. 说明一下: 符号引用: 符号引用是以一组符号来描述所引用的目标, 符号可以是任何的字面形式的字面量, 只要不会出现冲突能够定位到就行. 布局和内存无关. 直接引用: 是指向目标的指针, 偏移量或者能够直接定位的句柄. 该引用是和内存中的布局有关的, 并且一定加载进来的.
3. 初始化
初始化是为类的静态变量赋予正确的初始值, 准备阶段和初始化阶段看似有点矛盾, 其实是不矛盾的, 如果类中有语句: private static int a = 10, 它的执行过程是这样的, 首先字节码文件被加载到内存后, 先进行链接的验证这一步骤, 验证通过后准备阶段, 给 a 分配内存, 因为变量 a 是 static 的, 所以此时 a 等于 int 类型的默认初始值 0, 即 a=0, 然后到解析(后面在说), 到初始化这一步骤时, 才把 a 的真正的值 10 赋给 a, 此时 a=10.
二, 类加载时机
创建类的实例, 也就是 new 一个对象
访问某个类或接口的静态变量, 或者对该静态变量赋值
调用类的静态方法
反射(Class.forName("com.lyj.load"))
初始化一个类的子类(会首先初始化子类的父类)
JVM 启动时标明的启动类, 即文件名和类名相同的那个类
除此之外, 下面几种情形需要特别指出:
对于一个 final 类型的静态变量, 如果该变量的值在编译时就可以确定下来, 那么这个变量相当于 "宏变量".Java 编译器会在编译时直接把这个变量出现的地方替换成它的值, 因此即使程序使用该静态变量, 也不会导致该类的初始化. 反之, 如果 final 类型的静态 Field 的值不能在编译时确定下来, 则必须等到运行时才可以确定该变量的值, 如果通过该类来访问它的静态变量, 则会导致该类被初始化.
三, 类加载器
类加载器负责加载所有的类, 其为所有被载入内存中的类生成一个 java.lang.Class 实例对象. 一旦一个类被加载如 JVM 中, 同一个类就不会被再次载入了. 正如一个对象有一个唯一的标识一样, 一个载入 JVM 的类也有一个唯一的标识. 在 Java 中, 一个类用其全限定类名 (包括包名和类名) 作为标识; 但在 JVM 中, 一个类用其全限定类名和其类加载器作为其唯一标识. 例如, 如果在 pg 的包中有一个名为 Person 的类, 被类加载器 ClassLoader 的实例 kl 负责加载, 则该 Person 类对应的 Class 对象在 JVM 中表示为 (Person.pg.kl). 这意味着两个类加载器加载的同名类:(Person.pg.kl) 和(Person.pg.kl2)是不同的, 它们所加载的类也是完全不同, 互不兼容的.
JVM 预定义有三种类加载器, 当一个 JVM 启动的时候, Java 开始使用如下三种类加载器:
1)根类加载器(Bootstrap class loader): 它用来加载 Java 的核心类, 是用原生代码来实现的, 并不继承自 java.lang.ClassLoader(负责加载 $JAVA_HOME 中 jre/lib/rt.jar 里所有的 class, 由 C++ 实现, 不是 ClassLoader 子类). 由于引导类加载器涉及到虚拟机本地实现细节, 开发者无法直接获取到启动类加载器的引用, 所以不允许直接通过引用进行操作.
下面程序可以获得根类加载器所加载的核心类库, 并会看到本机安装的 Java 环境变量指定的 jdk 中提供的核心 jar 包路径:
- public class ClassLoaderTest {
- public static void main(String[] args) {
- URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
- for(URL url : urls){
- System.out.println(url.toExternalForm());
- }
- }
- }
运行结果:
2)扩展类加载器(extensions class loader): 它负责加载 JRE 的扩展目录, lib/ext 或者由 java.ext.dirs 系统属性指定的目录中的 JAR 包的类. 由 Java 语言实现, 父类加载器为 null.
3)系统类加载器 (system class loader): 被称为系统(也称为应用) 类加载器, 它负责在 JVM 启动时加载来自 Java 命令的 - classpath 选项, java.class.path 系统属性, 或者 CLASSPATH 换将变量所指定的 JAR 包和类路径. 程序可以通过 ClassLoader 的静态方法 getSystemClassLoader()来获取系统类加载器. 如果没有特别指定, 则用户自定义的类加载器都以此类加载器作为父加载器. 由 Java 语言实现, 父类加载器为 ExtClassLoader.
类加载器加载 Class 大致要经过如下 8 个步骤:
检测此 Class 是否载入过, 即在缓冲区中是否有此 Class, 如果有直接进入第 8 步, 否则进入第 2 步.
如果没有父类加载器, 则要么 Parent 是根类加载器, 要么本身就是根类加载器, 则跳到第 4 步, 如果父类加载器存在, 则进入第 3 步.
请求使用父类加载器去载入目标类, 如果载入成功则跳至第 8 步, 否则接着执行第 5 步.
请求使用根类加载器去载入目标类, 如果载入成功则跳至第 8 步, 否则跳至第 7 步.
当前类加载器尝试寻找 Class 文件, 如果找到则执行第 6 步, 如果找不到则执行第 7 步.
从文件中载入 Class, 成功后跳至第 8 步.
抛出 ClassNotFountException 异常.
返回对应的 java.lang.Class 对象.
四, 类加载机制:
1.JVM 的类加载机制主要有如下 3 种.
全盘负责: 所谓全盘负责, 就是当一个类加载器负责加载某个 Class 时, 该 Class 所依赖和引用其他 Class 也将由该类加载器负责载入, 除非显示使用另外一个类加载器来载入.
双亲委派: 所谓的双亲委派, 则是先让父类加载器试图加载该 Class, 只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类. 通俗的讲, 就是某个特定的类加载器在接到加载类的请求时, 首先将加载任务委托给父加载器, 依次递归, 如果父加载器可以完成类加载任务, 就成功返回; 只有父加载器无法完成此加载任务时, 才自己去加载.
缓存机制. 缓存机制将会保证所有加载过的 Class 都会被缓存, 当程序中需要使用某个 Class 时, 类加载器先从缓存区中搜寻该 Class, 只有当缓存区中不存在该 Class 对象时, 系统才会读取该类对应的二进制数据, 并将其转换成 Class 对象, 存入缓冲区中. 这就是为很么修改了 Class 后, 必须重新启动 JVM, 程序所做的修改才会生效的原因.
2. 这里说明一下双亲委派机制:
双亲委派机制, 其工作原理的是, 如果一个类加载器收到了类加载请求, 它并不会自己先去加载, 而是把这个请求委托给父类的加载器去执行, 如果父类加载器还存在其父类加载器, 则进一步向上委托, 依次递归, 请求最终将到达顶层的启动类加载器, 如果父类加载器可以完成类加载任务, 就成功返回, 倘若父类加载器无法完成此加载任务, 子加载器才会尝试自己去加载, 这就是双亲委派模式, 即每个儿子都很懒, 每次有活就丢给父亲去干, 直到父亲说这件事我也干不了时, 儿子自己才想办法去完成.
双亲委派机制的优势: 采用双亲委派模式的是好处是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系, 通过这种层级关可以避免类的重复加载, 当父亲已经加载了该类时, 就没有必要子 ClassLoader 再加载一次. 其次是考虑到安全因素, java 核心 API 中定义类型不会被随意替换, 假设通过网络传递一个名为 java.lang.Integer 的类, 通过双亲委托模式传递到启动类加载器, 而启动类加载器在核心 Java API 发现这个名字的类, 发现该类已被加载, 并不会重新加载网络传递的过来的 java.lang.Integer, 而直接返回已加载过的 Integer.class, 这样便可以防止核心 API 库被随意篡改.
----------------
来源: http://www.bubuko.com/infodetail-3349624.html