文章目录ClassLoader(类加载器)
类的加载、连接与初始化
主动使用(六种)
被动使用
类的加载
类的验证
类的准备
类的解析
类的初始化
类的初始化的步骤
类的初始化时机
类加载器
类加载的父委托机制
ClassLoader
运行时包
创建用户自定义的类加载器
findClass
不同类加载器的命名空间关系
类的卸载
ClassLoader(类加载器)
Java虚拟机与程序的生命周期在如下几种情况下,Java虚拟机将结束生命周期:执行了System.exit()方法
程序正常执行结束
程序在执行过程中遇到了异常或错误而异常终止
由于操作系统出现错误而导致Java虚拟机进程
类的加载、连接与初始化加载:查找并加载类的二进制数据
连接验证:确保被加载的类的正确性
准备:为类的静态变量分配内存,并将其初始化为默认值
解析:把类中的符号引用转换为直接引用
3.初始化:为类的静态变量赋予正确的初始值
4.Java程序对类的使用方式可分为两种(1)主动使用(2)被动使用
5.所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。
主动使用(六种)创建类的实例 eg: new Test();
访问某个类或接口的静态变量,或者对该静态变量赋值 eg: int b =Test.a; || Test.a = b;
调用类的静态方法 eg: Test.doSomething();
反射(如Class.forName(“com.shengsiyuan.Test”))
初始化一个类的子类eg:
class Child extends Parent{ public static int a =3; } Child.a = 4;
6.Java虚拟机启动时被标明为启动类的类(Java Test)
被动使用除了以上六种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的化
类的加载1.类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
2.加载.class文件的方式从本地系统中直接加载
通过网络下载.class文件(URLClassLoader)
从zip,jar等归档文件中加载.class文件
从专有数据库中提取.class文件
将Java源文件动态编译为.class
3.类的加载的最终产品是位于堆区中的Class对象
4.Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的。
5.有两种类型的类加载器
(1)Java虚拟机自带的加载器根类加载器(Bootstrap)(使用 C++编写,程序员无法在 Java 代码中获得该类)
扩展类加载器(Extension),使用 Java 代码实现
系统类加载器(应用加载器)(System),使用 Java 代码实现
(2)用户自定义的类加载器java.lang.ClassLoader的子类(用户自定义的类加载器都是java.lang.ClassLoader 的子类)
用户可以定制类的加载
package JVM.classloader;
public class Test1 {
public static void main(String[] args) {
Class clazz = null;
Class clazz2 = null;
try {
clazz = Class.forName("java.lang.String");
//获取类加载器,如果是根类加载器返回null
System.out.println(clazz.getClassLoader());
clazz2 = Class.forName("JVM.classloader.C");
System.out.println(clazz2.getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class C{
}
6.类加载器并不需要等到某个类被“首次主动使用”时再加载它。
7.JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告
类的验证
1.类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。
2.类的验证的内容类文件的结构检查:确保类文件遵从Java类文件的固定格式。
语义检查:确保类本身符合Java语言的语法规定,比如验证final类型的类没有子类,以及final类型的方法没有被覆盖
字节码验证:确保字节码流可以被Java虚拟机安全地执行。字节码流代表Java方法(包括静态方法和实例方法),它是由被称作操作码的单字节指令组成的序列,每一个操作码的单字节指令组成的序列,每一个操作码后都跟着一个或多个操作数。字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数。
二进制兼容性的验证:确保相互引用的类之间协调一致。例如在Worker类的gotoWork()方法中会调用Car类的run()方法。Java虚拟机在验证Worker类时,会检查在方法区内是否存在Car类的run()方法,假如不存在(当Worker类和Car类的版本不兼容,就会出现这种问题),就会抛出NoSuchMethodError错误。
类的准备
1.在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如,对一下Sample类,在准备阶段,将为int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0,为long类型的静态变量b分配8个字节的内存空间,并且赋予默认值0.
public class Sample{
private static int a = 1;
public static long b;
static{
b = 2;
}
...
}
类的解析
1.在解析阶段,Java虚拟机会把类的二进制数据中的符号引用替换为直接引用。例如在Worker类的gotoWork()方法中会应用Car类的run()方法。
public void gotoWork(){
car.run(); //这段代码在Worker类的二进制数据中表示为符号引用
}
2.在Worker类的二进制数据中,包含了一个对Car类的run()方法的符号引用,它由run()方法的全名和相关描述符组成。在解析阶段,Java虚拟机会把这个符号引用替换为一个指针,该指针指向Car类的run()方法在方法区内的内存位置,这个指针就是直接引用。
类的初始化
在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:(1)在静态变量的声明处进行初始化;(2)在静态代码块中进行初始化。例如在以下代码中,静态变量a和b都被显式初始化,而静态变量c没有被显式初始化,它将保持默认值0.
public class Sample{
private static int a = 1; //在静态变量的声明处进行初始化
public static long b;
public static long c;
static{
b = 2; //在静态代码块中进行初始化
}
...
}
package JVM.classloader;
public class Singleton {
/**
* 主动使用(6)
* 调用静态方法会加载这个类
* 给所有静态变量赋默认值
* singleton = null; counter1 = 0; counter2 = 0;
* 注意这里counter2的0不是等号后面的0,而是int类型的默认值0
* 然后顺序执行,将singleton = new Singleton();
* 调用构造方法,counter1 == 1; counter2 == 1;
* 然后再执行counter2 = 0;
* 所以结果时counter1 = 1;counter2 = 0;
*/
private static Singleton singleton = new Singleton();
public static int counter1;
public static int counter2 = 0;
private Singleton(){
counter1++;
counter2++;
}
public static Singleton getInstance(){
return singleton;
}
}
class MyTest{
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("counter1 = " + singleton.counter1);
System.out.println("counter2 = " + singleton.counter2);
}
}
结果是:
counter1 = 1
counter2 = 0
如果将
private static Singleton singleton = new Singleton();
public static int counter1;
public static int counter2 = 0;
改为
public static int counter1;
public static int counter2 = 0;
private static Singleton singleton = new Singleton();
结果是:
counter1 = 1
counter2 = 1
2.静态变量的声明语句,以及静态代码块都被看作类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行它们。例如当以下Sample类被初始化后,它的静态变量a的取值为4.
public class Sample{
static int a = 1;
static { a = 2; }
static { a = 4; }
public static void main(String args[]){
System.out.println("a = " + a); // 打印a=4
}
}
类的初始化的步骤
1.假如这个类还没有被加载和连接,那就先进行加载和连接。
2.加入类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类。
3.假如类中存在初始化语句,那就依次执行这些初始化语句。
package JVM.classloader;
class FinalTest{
//x的为常量,在编译时就确定了,调用常量不会初始化FinalTest类
public static final int x = 6/3;
static {
System.out.println("FinalTest static block");
}
}
public class Test2 {
public static void main(String[] args) {
System.out.println(FinalTest.x);
}
}
结果是:
2
package JVM.classloader;
import java.util.Random;
class FinalTest2{
//x的为变量,在运行时才确定它的值,所以会初始化FinalTest2
public static final int x = new Random().nextInt(100);
static {
System.out.println("FinalTest static block");
}
}
public class Test3 {
public static void main(String[] args) {
System.out.println(FinalTest2.x);
}
}
结果是:
FinalTest static block
32
类的初始化时机
1.当Java虚拟机初始化一个类时,要求它的所有父类都系应被初始化,但是这条规则并不适用于接口。
2.在初始化一个类时,并不会先初始化它所实现的接口。
3.初始化一个接口时,并不会先初始化它的父接口。
4.因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
package JVM.classloader;
class Parent{
static int a = 3;
static {
System.out.println("Parent static block");
}
}
class Child extends Parent{
static int b = 4;
static {
System.out.println("Child static block");
}
}
public class Test4 {
static {
System.out.println("Test4 static block");
}
public static void main(String[] args) {
System.out.println(Child.b);
}
}
结果是:
Test4 static block
Parent static block
Child static block
4
package JVM.classloader;
class Parent2{
static int a = 3;
static {
System.out.println("Parent2 static block");
}
}
class Child2 extends Parent2{
static int b = 4;
static {
System.out.println("Child2 static block");
}
}
public class Test5 {
static {
System.out.println("Test5 static block");
}
public static void main(String[] args) {
Parent2 parent;
System.out.println("-----------");
parent = new Parent2();
System.out.println(Parent2.a);
System.out.println(Child2.b);
}
}
结果是:
Test5 static block
Parent2 static block
3
Child2 static block
4
5.程序中对子类的“主动使用”会导致父类被初始化;但对父类的“主动”使用并不会导致子类初始化(不可能说生成一个 Object 类的对象就导致系统中所有的子类都会被初始化)
6.只有当程序访问的静态变量或静态方法确实在 当前类或当前接口中定义 时,才可以认为是对类或接口的主动使用
package JVM.classloader;
class Parent3 {
static int a = 3;
static{
System.out.println("Parent3 static block");
}
static void doSomething(){
System.out.println("do something");
}
}
class Child3 extends Parent3{
static{
System.out.println("Child3 static block");
}
}
public class Test6 {
public static void main(String[] args) {
System.out.println(Child3.a);
Child3.doSomething();
}
}
结果是:
Parent3 static block
3
do something
7.调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
package JVM.classloader;
class C1{
static{
System.out.println("Class C1");
}
}
public class Test7 {
public static void main(String[] args) throws Exception{
//获得系统类加载器
ClassLoader loader = ClassLoader.getSystemClassLoader();
Class> clazz = loader.loadClass("JVM.classloader.C1");
System.out.println("-----------");
clazz = Class.forName("JVM.classloader.C1");
}
}
结果是:
Class C1
类加载器
1.类加载器用来把类加载到Java虚拟机中。从JDK1.2版本开始,类的加载过程采用父亲委托机制,这种机制能更好地保证Java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当Java程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。
2.类加载器的父亲委托机制(Parent Delegation)
3.Java虚拟机自带了以下几种加载器。根(Bootstrap)类加载器:该加载器没有父加载器。它负责加载虚拟机的核心类库,如java.lang.*等。例如java.lang.Object就是由根类加载器加载的。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承java.lang.ClassLoader类。
扩展(Extension)类加载器:他的父加载器为根类加载器。它从java.ext.dirs系统属性所制定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载。扩展类加载器是纯Java类,是java.lang.ClassLoader类的子类。
系统(System)类加载器:也称为应用类加载器,它的父加载器为扩展类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。系统类加载器是纯Java类,是java.lang.ClassLoader类的子类。
4.父子加载器并非继承关系,也就是说子加载器不一定是继承了父加载器。
5.除了以上虚拟机自带的加载器以外,用户还可以定制自己的类加载器(User-defined Class Loader)。Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器应该继承ClassLoader类。
6.JVM 自带的类加载器之间的关系:
类加载的父委托机制
1.在父委托机制中,各个加载器按照父子关系形成了树形结构,除了根类加载器以外,其余的类加载器都有且只有一个父加载器
2.
Class sampleClass = loader2.loadClass("Sample");
loader2首先从自己的命名空间中查找Sample类是否已经被加载,如果已经加载,就直接返回代表Sample类的Class对象的引用。
如果Sample类还没有被加载,loader2首先请求loader1代为加载,loader1再请求系统类加载器代为加载,系统类加载器再请求扩展类加载器代为加载,扩展类加载器再请求根类加载器代为加载。若根类加载器和扩展类加载器都不能加载,则系统类加载器尝试加载,若能加载成功,则将Sample类所对应的Class对象的引用返回给loader1,loader1再将引用返回给loader2,从而成功将Sample类加载进虚拟机。若系统类加载器不能加载Sample类,则loader1尝试加载Sample类,若loader1也不能成功加载,则loader2尝试加载。若所有的父加载器及loader2本身都不能加载,则抛出ClassNotFoundException异常。
若有一个类加载器能成功加载Sample类,那么这个类加载器被称为定义类加载器,所有能成功返回Class对象的引用的类加载器(包括定义加载器)都被称为初始类加载器。
3.定义类加载器:如果某个类加载器能够加载一个类,那么该类加载器就称作:定义类加载器;定义类加载器及其所有子加载器都称作:初始类加载器。假设loader1实际加载了Sample类,则loader1为Sample类的定义类加载器,loader2和loader1为Sample类的初始类加载器。
4.需要指出的是,加载器之间的父子关系实际上指的是加载器对象之间的包装关系,而不是类之间的继承关系。一对父子加载器可能是同一个加载器类的两个实例,也可能不是。在子加载器对象中包装了一个父加载器对象。例如loader1和loader2都是MyClassLoader类的实例,并且loader2包装了loader1,loader1是loader2的父加载器。
ClassLoader loader1 = new MyClassLoader();
//参数loader1将作为loader2的父加载器
ClassLoader loader2 = new MyClassLoader(loader1);
5.当生成一个自定义的类加载器实例时,如果没有指定它的父加载器,那么系统类加载器就将成为该类加载器的父加载器。
ClassLoader
protected ClassLoader()
Creates a new class loader using the ClassLoader returned by the method getSystemClassLoader() as the parent class loader.
6.父委托机制的有点是能够提高软件系统的安全性,因此在此机制下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父加载器加载的可靠代码。例如,java.lang.Object类总是由根类加载器加载,其他任何用户自定义的类加载器都不可能加载含有恶意代码的java.lang.Object类。
7.每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。
运行时包
1.由同一类加载器加载的属于相同包的类组成了运行时包。决定两个类是不是属于同一运行时包,不仅要看他们的包名是否相同,还要看定义类加载器是都相同。只有属于同一运行时包的类才能互相访问包可见(即默认访问级别)的类和类成员。这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的可见成员。假设用户自己定义了一个类java.lang.Spy,并由用户自定义的类加载器加载,由于java.lang.Spy和核心类库java.lang.*由不同的加载器加载,它们属于不同的运行时包,所以java.lang.Spy不能访问核心类库java.lang包中的包可见成员。
创建用户自定义的类加载器
要创建用户自己的类加载器,只需扩展java.lang.ClassLoader类,然后覆盖他的findClass(String name)方法即可,该方法根据参数指定的类的名字,返回对应的Class对象的引用。
findClass
protected Class> findClass(String name) throws ClassNotFoundException
Finds the class with the specified binary name. This method should be overridden by class loader implementations that follow the delegation model for loading classes, and will be invoked by the loadClass method after checking the parent class loader for the requested class. The default implementation throws a ClassNotFoundException.
package JVM.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader{
private String name;//类加载器的名字
private String path = "d:/";//加载类的路径
private final String fileType = ".class";//class文件的扩展名
public MyClassLoader(String name) {
super();//让系统类加载器成为该类加载器的父加载器
this.name = name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public MyClassLoader(ClassLoader parent, String name) {
super(parent);//显式指定该类加载器的父加载器
this.name = name;
}
@Override
public String toString() {
return this.name;
}
//这里不显式调用这个方法,而是通过loadClass调用
@Override
public Class> findClass(String name) throws ClassNotFoundException {
byte[] data = this.loadClassData(name);
return this.defineClass(name, data, 0, data.length);
}
private byte[] loadClassData(String name) {
InputStream inputStream = null;
byte[] data = null;
ByteArrayOutputStream byteArrayOutputStream = null;
try {
name = name.replace(".", "\\");
inputStream = new FileInputStream(new File(path + name + fileType));
byteArrayOutputStream = new ByteArrayOutputStream();
int ch = 0;
while (-1 != (ch = inputStream.read())) {
byteArrayOutputStream.write(ch);
}
data = byteArrayOutputStream.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
finally {
try {
inputStream.close();
byteArrayOutputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return data;
}
public static void main(String[] args) throws Exception{
//loader1的父加载器是系统加载器
MyClassLoader loader1 = new MyClassLoader("loader1");
loader1.setPath("E:\\IdeaProjects\\Java\\out\\production\\Java\\");//D:/myapp/serverlib
//loader2的父加载器是loader1
MyClassLoader loader2 = new MyClassLoader(loader1, "loader2");
loader2.setPath("E:\\IdeaProjects\\Java\\out\\production\\Java\\");//D:/myapp/clientlib
//loader3的父加载器是根加载器
MyClassLoader loader3 = new MyClassLoader(null, "loader3");
loader3.setPath("E:\\IdeaProjects\\Java\\out\\production\\Java\\");//D://myapp/otherlib
/**
*因为loader2的父加载器是loader1,loader1的父加载器是系统加载器
* 而Sample.class和Dog.class和MyClassLoader.class是在一个目录的
* 所以系统加载器就能加载,当系统加载器加载不了时,就交给loader1加载
* 如果loader1加载不了就交给loader2加载,如果loader2加载不了就会抛出异常
*/
test(loader2);
test(loader3);
}
public static void test(ClassLoader loader) throws Exception{
Class clazz = loader.loadClass("JVM.classloader.Sample");
Object object = clazz.newInstance();
}
}
package JVM.classloader;
public class Sample {
public int v1 = 1;
public Sample(){
System.out.println("Sample is loaded by:" + this.getClass().getClassLoader());
new Dog();
}
}
package JVM.classloader;
public class Dog {
public Dog(){
System.out.println("Dog is loaded by:" + this.getClass().getClassLoader());
}
}
结果是:
Sample is loaded by:sun.misc.LauncherAppClassLoader@18b4aac2Dogisloadedby:sun.misc.Launcher AppClassLoader@18b4aac2Dog is loaded by:sun.misc.LauncherAppClassLoader@18b4aac2Dogisloadedby:sun.misc.LauncherAppClassLoader@18b4aac2
Sample is loaded by:loader3
Dog is loaded by:loader3
package JVM.classloader;
public class Test8 {
public static void main(String[] args) {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
System.out.println(classLoader);
while (null != classLoader) {
classLoader = classLoader.getParent();
System.out.println(classLoader);
}
}
}
结果是:
sun.misc.LauncherAppClassLoader@18b4aac2系统类加载器sun.misc.Launcher AppClassLoader@18b4aac2 系统类加载器sun.misc.LauncherAppClassLoader@18b4aac2系统类加载器sun.misc.LauncherExtClassLoader@330bedb4 扩展类加载器
null 根类加载器
不同类加载器的命名空间关系
同一个命名空间内的类是相互可见的。
子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。
由父加载器加载的类不能看见子加载器加载的类。
如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
用反射可以访问
类的卸载
1.当Sample类被加载、连接和初始化后,它的生命周期就开始了。当代表Sample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
2.由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。前面已经介绍过,Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。
3.由用户自定义的类加载器所加载的类是可以被卸载的。
Class clazz = loader3.loadClass("JVM.classloader.Sample");
System.out.println(clazz.hashCode());
Object object = clazz.newInstance();
loader3 = null;
clazz = null;
object = null;
loader3 = new MyClassLoader("loader1");
loader3.setPath("E:\\IdeaProjects\\Java\\out\\production\\Java\\");
clazz = loader3.loadClass("JVM.classloader.Sample");
System.out.println(clazz.hashCode());
结果是:
1265094477
Sample is loaded by:loader3
Dog is loaded by:loader3
2125039532
运行以上程序时,Sample类由loader3加载。在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。另一方面,一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。由此可见,代表Sample类的Class实例与loader3之间为双向关联关系。
一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用,此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。