类加载过程

1. 类加载机制和过程

1.1 类加载时机:JVM规范并没有明确规定加载过程在什么时候执行,但是严格规定了如下几种情况,如果类未初始化会对类进行初始化:

(1)创建类的实例

(2)访问类的静态变量(不包括常量)

(3)访问类的静态方法

(4)反射(如Class.forName("my.xyz.Test"))

(5)当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化

(6)虚拟机启动时,定义了main()方法的那个类先初始化

1.2 类加载过程

类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。

image.png

(1)加载过程

a. 加载指的是将类的class文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。

b.类加载的最终产物就是位于堆中的Class对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口

(2)连接过程

有了类对应的Class对象后,连接阶段负责把类的二进制数据合并到JRE中(意思就是将java类的二进制代码合并到JVM的运行状态之中)

a. 验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理。

b. 准备:为静态变量(类变量)分配内存并设置静态变量初始值的阶段,这些内存都将在方法区中进行分配,此时给变量赋的值全部是对应类型的默认值(比如int赋为0),初始化阶段才对变量赋予指定的值

public static final int a = 1;    //此阶段a的值为0

c. 解析:在虚拟机常量池中寻找类、接口、字段和方法的符号引用并将其替换为直接引用

//符号引用
String str = "abc";
System.out.println("str = " + str);
//直接引用
System.out.println("str = " + "abc");

(3)初始化过程

a. 初始化阶段是执行类构造器<clinit>() 方法的过程。代码从上往下执行静态变量赋值和静态代码块代码。

b.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化

c. 虚拟机会保证一个类的<clinit>() 方法在多线程环境中被正确加锁和同步

public static final int a = 1;    //此阶段a的值变为1

(4)举个例子:

class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;
 
    private SingleTon() {
        count1++;
        count2++;
    }
 
    public static SingleTon getInstance() {
        return singleTon;
    }
}
 
public class Test {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        System.out.println("count1=" + singleTon.count1);
        System.out.println("count2=" + singleTon.count2);
    }
}

输出:

count1=1
count2=0

分析:

SingleTon singleTon = SingleTon.getInstance();调用了类的SingleTon调用了类的静态方法,触发类的初始化

类加载的时候在准备过程中为类的静态变量分配内存并初始化默认值 singleton=null count1=0,count2=0

类初始化化,为类的静态变量赋值和执行静态代码快。singleton赋值为new SingleTon()调用类的构造方法

调用类的构造方法后count=1;count2=1

继续为count1与count2赋值,此时count1没有赋值操作,所有count1为1,但是count2执行赋值操作就变为0

2.类加载器

类加载器主要用于类的加载阶段,它可以加载不同来源的类的二进制数据,比如:本地Class文件、Jar包Class文件、网络Class文件等。Java类加载器由JVM提供,是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

(1)启动类加载器/引导类加载器(bootstrap class loader)

负责加载加载 Java 的核心类(java.*类),即$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,并不继承自 java.lang.ClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

(2)扩展类加载器(extensions class loader)

sun.misc.Launcher$ExtClassLoader类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

继承关系:

java.lang.Object
       --- java.lang.ClassLoader
              --- java.security.SecureClassLoader
                      ---  java.net.URLClassLoader
                            --- sun.misc.Launcher$ExtClassLoader

(3)系统类加载器/应用类加载器(system class loader)

sun.misc.Launcher$AppClassLoader,也被称为应用类加载器。它负责在JVM启动时加载来自--classpath选项、java.class.path系统属性,或者CLASSPATH变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器

继承关系:

java.lang.Object
       --- java.lang.ClassLoader
              --- java.security.SecureClassLoader
                      ---  java.net.URLClassLoader
                            --- sun.misc.Launcher$AppClassLoader

AppClassLoader和ExtClassLoader不存在继承关系,都继承自URLClassLoader,最外层的java.lang.ClassLoader是一个抽象类

(4)自定义类加载器

需要继承AppClassLoader

3.双亲委派机制

image.png

3.1 工作原理

一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父 类加载器去执行,如果父 类加载器还存在其父 类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。然后自顶向下,如果父 类加载器可以完成类加载任务,就成功加载并返回,倘若父 类加载器无法完成此加载任务,往下一层层尝试可以加载该类的子 类加载器。

3.2 优势

(1)Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次

(2)其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设自定义了一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新自定义的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

3.3 破坏这种机制

(1)首先要双亲委派机制是如何实现的,其实是在ClassLoader的loadClass()方法中,逻辑为:1、先检查类是否已经被加载过 2、若没有加载则调用父加载器的loadClass()方法进行加载 3、若父加载器为空则默认使用启动类加载器作为父 类加载器。 4、如果父类加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

(2)如何破坏双亲委派机制:自定义类加载器,重写loadClass()方法,使其不进行双亲委派即可。

如果不想破坏这种机制,又想自定义类加载器,则集成ClassLoader后只重写findClass()方法,写明自定义类加载器会加载哪些类即可。

(3)Tomcat破坏了双亲委派机制:

Tomcat是web容器,那么一个web容器可能需要部署多个应用程序。

不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。

如多个应用都要依赖hollis.jar,但是A应用需要依赖1.0.0版本,但是B应用需要依赖1.0.1版本。这两个版本中都有一个类是com.hollis.Test.class。

如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。

所以,Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。

Tomcat的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值