本文是根据是在《北京圣思园深入JAVA虚拟机系列视频》的基础上自己整理而来,内容范围没有超过其系列所述,在此给予说明。
在进入ClassLoader的分析之前我们先看一个JAVA程序例子。
class Singleton {
/* case 1 */
private static Singleton singleton = new Singleton();
public static int counter1;
public static int counter2 = 0;
/**
* case 2
* public static int counter1 = 0;
* public static int counter2 = 0;
* private static Singleton singleton = new Singleton();
*/
private Singleton() {
counter1++;
counter2++;
}
public static Singleton getInstance() {
return singleton;
}
}
public class MyTest {
public static void main(String[] args) {
Singleton
singleton = Singleton.getInstance();
System.out.println("counter1 = " + singleton.counter1);
System.out.println("counter2 = " + singleton.counter2);
}
}
/**
* result in case 1:
* counter1 = 1
* counter2 = 0
* result in case 2:
* counter1 = 1
* counter2 = 1
*/
上面的代码在case1 与case2 条件下运行结果却不一样,仅仅由于private static Singleton singleton = new Singleton();位置不同,要想了解其中的原因需要从类的使用时JVM完成的动作说起,在一个类被JVM使用时大致经历如下三步(加载
— 链接(验证--准备--解析) — 初始化)
那么一个类被JVM使用时,必须预先经历如上图所述的加载过程。那么什么样的条件才会触发上述过程的执行呢?JAVA程序使用类分为主动使用和被动使用,在JVM的实现规范中要求,所有类的“主动使用“虚拟机才执行上述过程初始化相应的类,那么问题就归结为“主动使用”的意义。
1. 创建类的实例。Object A = new ClassA();
2. 访问某个类或接口的静态变量或对静态变量赋值。如Class A{static a} 访问A.a时。需要 指出的是访问类的static final int x = 0(编译时常量)并不被认为是类的主动使用,同样 的假如有条件 Class A extends B;B{static a}如果使用A.a时只会初始化类B,这种情况被认 为是对父类的主动使用。
3. 调用类的静态方法
4.使用反射机制(Class.ForName(xxx)),而ClassLoader.load(并不会初始化类)
5. 初始化一个类的子类时,父类也被主动使用
6. 启动类(java TestMain)
下面文章将针对上述过程给出比较详细的说明。
加载过程
总的来说类的加载是JVM使用类加载器(如系统类加载器、扩展加载器、根加载器)在特定的加载路径里寻找class文件,并将class文件中的二进制数据读入到内存中,其中class的数据结构被放置在运行时数据区的方法区类,并且在堆区里创建该类的Class对象,用来封装类的数据结构信息。其中类加载类的方式有:文件系统加载、网络加载、zip
jar 归档文件加载、数据库中提取、动态编译的源文件加载。
类加载的最终产品是位于堆区中的Class对象,其封装了类在方法区内数据结构,并且向Java程序员提供了访问方法区内数据结构的接口,需要指出的是,类的加载并不都是主动使用时才加载,加载器可以实现为有预加载功能,如使用一定的算法预测类的使用。在上面的叙述中我们提到过JVM使用类加载器对class文件进行加载(本文后面部分将着重描述类的加载机制)。
连接过程
类加载后,就是连接阶段了,连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行环境中去。连接的第一个阶段是类的验证,验证的内容如下:
1.类文件结构的检查,确保类的文件遵循java类文件的固定格式。
2. 语义检查:确保类本身符合java语言的语法规定,比如验证final类型没有子 类,final方法没有被从写,private没有被重写。
3. 字节码验证:确定字节码流可以被java虚拟机安全的执行。
4.二进制兼容验证:确保相互应用的类之间协调一致。
做完验证之后就是类的准备阶段,完成的工作为类的静态变量分配内存并设置为初始值。如有类
Sample{
Static int a = 1;
Static long b;
}
系统会为 a 分配4个字节,并设置初始值为 0,为b分配8个字节并设置初始值为 0.
做完类的准备工作之后就是类的解析,主要工作就是把类中的二进制中的符号引用替换为直接引用。我们举个例子 void show{
objectA.print()};objectA.print() 就是对ClassA的一个符号引用,经过解析之后该处的代码会被一个指向ClassA中方法区print方法的指针。
初始化过程
下面是类的初始化过程,初始化的主要步骤为:检查该类是否已经被加载和连接;如果该类有父类,且没有初始化,对父类进行加载
连接 初始化;假如类中存在初始化语句,依次执行初始化语句。而静态变量的声明以及静态代码块都被看作是类的初始化语句,java虚拟机会按照初始化语句在类文件中的顺序依次来执行他们。如 static int a =1,与static{ a = 3}这样的语句都会被JVM顺序执行。
前面提到,当JVM初始化一个类时要求他的父类已经被初始化,这样的机制并不适用于接口,初始化一个类时,它实现的接口并不需要被初始化,初始化一个接口时,其父接口也不需要被初始化,只有当程序首次使用接口中的静态变量时,才会导致接口的初始化。
通过上面的论述,我们大致对类的使用有了一个初步的了解,接下来我们将分析本文开始时提出的那个程序的运行结果
在case 1 中,在MyTest那种使用 Singleton
singleton = Singleton.getInstance();这样的语句为类的主动使用这会触发Singleton类的加载 连接 初始化。
private static Singleton singleton = new Singleton();(A)
public static int counter1;(B)
public static int counter2 = 0;(C)
在加载完后的连接阶段的准备期,会为singleton分配内存,设定默认值为null,counter1默认值为 0,counter2 默认值为 0. 进入初始化阶段,第一步为singleton赋值 会调用Singleton的构造方法,此时执行counter1++,counter2++ counter1 = 1,counter2 = 1;第二步为counter1赋值,由于没有赋值语句counter1
仍为1;第三步为
counter2赋值,counter2 被赋值为 0,所以结果是counter1 =1
Counter2 =0
在case2 中 使用如下语句
public static int counter1;(A)
public static int counter2 = 0;(B)
private static Singleton singleton = new Singleton();(C)
连接准备阶段结束后counter1 = 0,counter2 = 0,singleton = null;初始化时 第一步A语句不用初始化 counter1 =0;第二部B语句初始化为0,counter2 =0;第三步 调用构造函数 counter1++,counter2++
counter1 = 1,counter2 = 1;所以case2的结果为 counter1 =1 counter2 = 1
在下面的部分,我将给大家深入的介绍一下JVM的类加载器。
类加载器分析
在上面的例子中我们看到一个类的主动使用会经历加载、链接、初始化的过程,类的加载需要使用JVM的类加载器,JVM的类加载器使用父亲委托机制。我们首先看看JVM的类加载器树形结构:
BootStrap(根类加载器)
||
Extend(扩展类加载器)
||
System(系统类或应用类加载器)
||
用户自定义类加载器
从上面的结构可以看出,JVM的类加载器一共有四大类,其中根类加载器、扩展类加载器、系统类加载器(应用类加载器)为JVM自带的类加载器。
1.根类加载器。负责加载虚拟机的核心类库,如java.lang.*等。根类加载器从系统 属性sun.boot.class.path 所指定的目录中加载类库。根类加载器的实现依赖于底层 操作系统,属于jvm实现的一部分,使用c++语言编写。它并没有ClassLoader类, 也没有父加载器。使用如stringObject.getClass().getClassLoader()将返null。
2. 扩展类加载器(Ext)。从图中可以看出它的父加载器是根加载器。它 从java.ext.dirs系统属性所指定的目录中加载类库,或者从jdk的安装目录 的jre\lib\ext子目录下加载类库,如果你把用户创建的jar放在这个目录下会被扩展类 加载器加载。扩展类加载器使用纯java实现,继承了ClassLoader。
3. 系统类加载器,也叫应用类加载器(APP),它的父加载器是Ext加载器。它从 环境 变量里(安装JDK时设立的classpath)或从系统属性java.class.path加载 类,它是 用户自定义的类加载器的默认父加载器,采用纯java实现,继承 自ClassLoader。
4. 用户自定义加载器。系统类加载器的子类,必须要继承自ClassLoader之类,并 且重 写findClass方法。
假设我们自定义ClassLoader 为loadA,并且使用loadA.load(Class),所谓的父亲委托机制就是loadA
委托父加载器(App)加载Class,App委托Ext,Ext委托BootStrap,如果BootStrap不能加载,则让Ext加载,逐级下发,如果直到loadA还不能加载Class这抛出ClassNotFindException。委托机制是SUN公司基于安全性考虑的,这样可以保证Object这样的重要类只能有JVM加载。我们定义若一个类加载器能够成功加载类Class,我们则称这个加载器为该类的定义加载器,其下的子加载器为初始化加载器。如在上述的类之中,假设App类加载器加载了类Class 这App为定义加载器,APP与LoadA为初始化加载器。
需要指出的是加载器之间的父子关系并不是指类之间的继承关系,而是指加载器之间的包装关系。一对父子可能是同一个类加载器的实例,也可能不是。例如我们自定义类加载器MyClassLoader。LoadA = new MyClassLoader(); LoadB = new MyClassLoader(loadA), 我们称loadB包装了loadA,LoadA是loadB的父加载器。
运行时包决定了protecetd类和protected成员是否能够访问。我们知道所有的protected的成员需要同一个包下的类才能访问。如果我们定义java.lang.Spy类,我们是否就能访问java.lang.*下的核心protected资源呢?运行时包包括,包名相同,类加载器相同,所有java.lang.Spy 与java.lang.* 不在相同的运行时包,答案是否定的。
下面我们以视频中一个详细的例子来讲述用户自定义加载器的实现。首先给出例子中类加载器的树形关:
BootStrap(根类加载器)
|| \\
|| LoaderC -----> d:app/otherlib
Extend(扩展类加载器)
||
System(系统类或应用类加载器)
||
LoaderB ------> d:app/serverlib
||
LoaderA ------> d:app/clientlib
在例子中我们定义三个类加载器,在重写的findClass方法中设定好加载路径。JDK
API给出一个自定义ClassLoad的方法:
class MyClassLoader extends ClassLoader {
public Class findClass(String
name) {
byte[] b =
loadClassData(name);
return
defineClass(name, b, 0, b.length);
}
private byte[]
loadClassData(String name) {
// load the class data
from the connection
. . .
}
}
从上面的代码可以看出通过重写findClass方法并在loadClassData中设定好加载.class 文件的路径可以实现自定义的加载机制。
假设我们已经定义好自己的加载器MyClassLoader,我们使用如下的代码便能够构造出例子中的类加载器树形结构。
MyClassLoader loadB = new MyClassLoader(“loadB”);
loadB.setPath(“D://app//serverlib”);
MyClassLoader loadA = new MyClassLoader(loadB,“loadA”);
loadB.setPath(“D://app//cientlib”);
MyClassLoader loadC = new MyClassLoader(null,”loadC”)//null代表父加载器为Bootstrap
loadC.setPath(“D://app//otherlib”);
我们一个测试类Sample 和一个测试类Dog进行加载测试。
Class Sample{
Static{
New Dog();
}
}
Class文件的存放路径如下:
d:app/syslib/MyClassLoader.class
d:app/serverlib/Sample.class
d:app/clientlib/Dog.class
d:app/otherlib/Sample.class,Dog.class
TestCase1 :
Class clazz =
loadA.loadClass(“Sample”);
Clazz.newInstance();
TestCase2 :
Class clazz = loadB.loadClass(“Sample”);
Clazz.newInstance();
TestCase3 :
Class clazz =
loadC.loadClass(“Sample”);
Clazz.newInstance();
在case1 中
由加载器的树形结构可以看出:loadA加载Sample时委托父亲LoadB加载Sample,由于再向上委托并不能加载Sample,所以 Sample 由LoadB在 app/serverlib 下加载,
对于Dog类,LoadA的所有父加载器都不能加载,所以有loadA在 app/clientlib下加载
在case2 中 有loadB在
app/serverlib中加载 Sample,由于loadB 与其父加载器都不能加载Dog,所以会抛出ClassNotfoundException。此时如果把Dog拷贝到 syslib下,Dog类就会被appCloader加载,而不会出现ClassNotFound错误。
在Case3 中LoadC直接委托Bootstrap加载Sample,由于无法加载只能有自己加载,所以Sample 与Dog都会从 app/otherlib/下加载.
假设 我们在MyClassLoader中写意给main方法测试
TestCase4 :
Class clazz =
loadA.loadClass(“Sample”);
Sample sample = Clazz.newInstance();
此时会导致一个NoClassDefError,这主要是JVM的类加载器命名空间规则导致的,在jvm中子加载器的命令空间包含了父加载器加载的所有类,反过来则不成立,因为MyClassLoader类是有appLoader加载的,所以其看不见有LoadB与loadA加载的类。
在这里顺便提一下,在一个类主动使用时,该类就开始起生命周期 加载,链接,初始化,使用,卸载。Jvm自带的加载器加载的Class是不能够被卸载的,只有用户自定义加载器加载的类才能被卸载,卸载机制是根据对类的引用计数情况而定,这与GC根据引用情况回收垃圾差不多。
appendix : 视频下载链接文件(电驴)
分享到:
相关推荐
深入理解`ClassLoader`的源码对于Java开发者来说是提升技能的重要环节,尤其是在进行模块化、插件化开发或者进行系统优化时。自定义`ClassLoader`能够帮助我们实现特定的加载策略,比如隔离不同版本的库或动态加载类...
《深入理解ClassLoader工作机制》 Java虚拟机(JVM)中的ClassLoader是负责加载类到内存中的核心组件。它不仅承担着将字节码转换为可执行对象的重任,还参与了类生命周期的各个阶段,包括加载、验证、准备、解析、...
首先,让我们深入了解ClassLoader的基本概念。在传统编程语言中,如C或C++,程序通常是一个单一的可执行文件,而Java则不同,它的编译结果是以平台无关的类文件格式存在。每个类文件对应于一个单独的Java类,且这些...
Java ClassLoader机制是Java运行时环境中的核心组件之一,它负责加载类到JVM(Java虚拟机)中,使得...深入理解ClassLoader的工作原理,不仅有助于提升编程能力,还能帮助开发者在面对复杂系统设计时做出更明智的决策。
类的生命周期包括加载、验证、准备、解析、初始化五个阶段。其中,加载阶段是最初始的阶段,主要由`ClassLoader`完成。在这个阶段,`ClassLoader`负责完成以下三项基本工作: 1. **通过一个类的全限定名来获取该类...
找到后,ClassLoader会解析字节码并创建Class对象,然后进行验证,确保代码的安全性。 在Java中,用户可以通过自定义ClassLoader来实现特定的加载逻辑,比如从数据库或网络中加载类。这使得Java可以支持许多高级...
本示例"ClassLoader小例子"将深入探讨这个概念,并通过一个具体的程序来演示其工作原理。下面我们将详细讨论ClassLoader的基本概念、工作流程以及如何自定义ClassLoader。 1. **ClassLoader的基本概念** - 类加载...
本文将深入浅出地探讨JVM ClassLoader的工作原理和相关知识点。 首先,ClassLoader可以分为三种基本类型:Bootstrap ClassLoader、Extension ClassLoader和Application ClassLoader。Bootstrap ClassLoader是JVM...
本文将深入解析Java中的Classloader运行机制,特别是从JVM的角度探讨类加载器的委托机制。 首先,Java中的Classloader分为两类:用户自定义的和JVM内置的。用户自定义的Classloader通常是`java.lang.ClassLoader`的...
本案例将深入探讨如何创建一个自定义的ClassLoader,利用Java反射和注解技术实现运行时功能更新。 首先,了解ClassLoader的工作原理。Java中的类加载过程分为三个阶段:加载、验证、准备、解析和初始化。默认情况下...
《深入Java虚拟机_ClassLoader》是一本专注于Java虚拟机(JVM)中ClassLoader部分的专著,旨在帮助读者深入理解Java程序如何加载、链接和初始化类。ClassLoader是Java平台核心特性的一部分,它负责查找和加载类到JVM...
本文将深入探讨ClassLoader的工作原理和类加载机制,帮助开发者理解这个至关重要的概念。 1. 类加载机制概述 Java的类加载机制遵循“双亲委派模型”(Delegation Model)。当一个类被加载时,它首先会尝试由当前...
ClassLoader原理的理解对于深入学习Java和进行系统优化至关重要。这篇博文将带你深入了解ClassLoader的工作机制。 首先,我们来理解ClassLoader的基本概念。ClassLoader是一个抽象的概念,它是Java中的一个接口,...
本文将深入探讨“使用classloader动态加载Class”的相关知识点,同时参考了博客链接:https://ldbjakyo.iteye.com/blog/1046984。 首先,理解ClassLoader的工作原理至关重要。Java中的类加载过程分为三个主要阶段:...