类的加载和加载器

JVM对象从new到回收的过程

类的装载

虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。

作用:加载Class文件

加载

通过类的全限定名获取定义此类的二进制字节流
将字节流所代表的静态存储结构转换为方法区的运行时数据结构
在内存中生成一个代表该类的 Class 对象,作为方法区类信息的访问入口

何时类加载
  1. 定义了main的类,启动main方法时该类会被加载
  2. 创建类的实例,即new对象的时候
  3. 访问类的静态方法
  4. 访问类的静态变量
  5. 反射 Class.forName()
拓展:字节码的好处,编译型与解释型

采用字节码的好处?
java程序通过编译器编译成字节码文件,也就是计算机可以识别的二进制。
java虚拟机就是将字节码文件解释成二进制段。
采用字节码的最大好处是:可以实现一次编译到处运行,也就是java的与平台无关性

编译型:编译型语言open in new window 会通过编译器open in new window将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。

解释型:解释型语言open in new window会通过解释器open in new window一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。

Java 语言“编译与解释并存”?
这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件),这种字节码必须由 Java 解释器来解释执行

验证

确保Class文件的字节流中包含的信息符合JVM规范,保证在运行后不会危害虚拟机自身的安全。即安全性检查,主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。

准备

为 static 变量分配内存:static变量在分配空间和赋值是在两个阶段完成的。分配空间在准备阶段完成,赋值在初始化阶段完成

  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

解析

将常量池内的符号引用替换为直接引用的过程。符号引用用于描述目标,直接引用直接指向目标的地址。

  • 未解析时,常量池中的看到的对象仅是符号,未真正的存在于内存中
  • 解析以后,会将常量池中的符号引用解析为直接引用

初始化

初始化阶段就是执行类构造器<cinit>()V方法的过程,为**类变量(static)**初始化值(自定义值)。

初始化发生的时机: 概括得说,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发父类初始化
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName new 会导致初始化

类的实例化顺序:

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName 的参数 2 为 false 时
cinit方法如果执行失败了怎么办,这个类还能用吗

在Java类加载的过程中,cinit 方法实际上指的是类的静态初始化方法,也就是类的静态代码块或者静态变量的初始化代码。如果类的静态初始化方法执行失败,通常会导致类的初始化失败,这意味着这个类不能被正常使用。会抛出异常,如 ExceptionInInitializerError

cinit 方法实际上是类的静态初始化方法,也就是类的静态代码块或者静态变量的初始化代码。在Java中,类的静态初始化方法只会执行一次,无论类被加载多少次,静态初始化方法只会在首次加载类的时候执行。因此,cinit 方法不会多次执行。一旦类的静态初始化方法执行过,后续对同一个类的加载都不会再次触发静态初始化方法的执行。这种机制确保了类的静态初始化只会在需要的时候执行一次,避免了不必要的开销和重复操作。

分配内存

在类装载后,接下来虚拟机将为新⽣对象分配内存。

分配在哪?主要就是根据JVM的分配机制:对象优先分配Eden

  1. 先TLAB分配
  2. 再通过CAS在Eden区分配
  3. 大对象直接分配到老年代

TLAB:线程本地分配缓冲区,为每⼀个线程预先在 Eden 区分配⼀块⼉私有的缓存区域,JVM 在给线程中的对象分配内存时,⾸先在 TLAB 分配,当对象⼤于 TLAB 中的剩余内存或 TLAB 的内存已⽤尽时(或者未开启TLAB),再采⽤上述的 CAS 进⾏内存分配。默认情况TLAB仅占每个Eden区域的1%。它的主要目的是在多线程并发环境下需要进行内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。

为什么要CAS分配内存?
多个并发执行的线程需要创建对象、申请分配内存的时候,有可能在 Java 堆的同一个位置申请,这时就需要对拟分配的内存区域进行加锁或者采用 CAS 等操作,保证这个区域只能分配给一个线程。

初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。

设置对象头

初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。

执⾏init⽅法

执⾏ new 指令之后会接着执⾏ ⽅法,把对象按照程序员的意愿进⾏初始化,这样⼀个真正可⽤的对象才算完全产⽣出来。

后面就可以开始使用了。

对象晋升(年轻代)

  1. 长期存活的对象进入老年代: 通过参数 -XX:MaxTenuringThreshold 可以设置对象进入老年代的年龄阈值。对象在 Survivor 区每经过一次 Minor GC ,年龄就增加 1 岁,当它的年龄增加到一定程度,就会被晋升到老年代中。
  2. 动态对象年龄判定: 并非对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需达到 MaxTenuringThreshold 年龄阈值。

垃圾回收

JVM使用算法来判断对象是否可被回收,使用两种方法,引用计数(过时)和可达性分析。垃圾回收机制

类与类加载器

实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等

启动类加载器

引导类加载器属于JVM的一部分,由C++代码实现。

引导类加载器负责加载<JAVA_HOME>\jre\lib路径下的核心类库,由于安全考虑只加载 包名 java、javax、sun开头的类。

public class Demo1 {
    public static void main(String[] args) {
        //Bootstrap 引导类加载器
        //打印为null,是因为Bootstrap是C++实现的。
        ClassLoader classLoader = Object.class.getClassLoader();
        System.out.println(classLoader);

        //查看引导类加载器会加载那些jar包
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (URL urL : urLs) {
            System.out.println(urL);
        }
    }

}

输出

null
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/resources.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/rt.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/sunrsasign.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/jsse.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/jce.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/charsets.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/jfr.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/classes

拓展类加载器

全类名:sum.misc.Launch$ExtClassLoader,Java语言实现。

扩展类加载器的父加载器是Bootstrap启动类加载器 (注:不是继承关系)

扩展类加载器负责加载<JAVA_HOME>\jre\lib\ext目录下的类库。

import com.sun.javafx.webkit.WebPageClientImpl;

public class Demo1 {
    public static void main(String[] args) {
		//ext目录下的类,获取加载器
        ClassLoader classLoader = WebPageClientImpl.class.getClassLoader();
        System.out.println(classLoader);
    }
}

输出

sun.misc.Launcher$ExtClassLoader@330bedb4

应用程序类加载器

全类名: sun.misc.Launcher$AppClassLoader

系统类加载器的父加载器是ExtClassLoader扩展类加载器(注: 不是继承关系)。

系统类加载器负责加载 classpath环境变量所指定的类库,是用户自定义类的默认类加载器。

public class Demo1 {
    public static void main(String[] args) {
        ClassLoader classLoader = Demo1.class.getClassLoader();
        System.out.println(classLoader);
    }

}

输出:

sun.misc.Launcher$AppClassLoader@18b4aac2

双亲委派模型

一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求委派给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

双亲委派模型的具体实现代码在 java.lang.ClassLoader 中,此类的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出ClassNotFoundException ,此时尝试自己去加载。

源码

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 1.首先查找该类是否已经被该类加载器加载过了
        Class<?> c = findLoadedClass(name);
        //如果没有被加载过
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 2. 有上级的话,委派上级 loadClass
                    c = parent.loadClass(name, false);
                } else {
                    // 3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
                //捕获异常,但不做任何处理
            }

            if (c == null) {
                // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
                long t1 = System.nanoTime();
                c = findClass(name);

                // 记录时间
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
				}
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

例如:

public class Demo1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Demo1.class.getClassLoader().loadClass("com.seven.jvm.F");
        System.out.println(aClass.getClassLoader());
    }
}

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
  2. sun.misc.Launcher$AppClassLoader // 2 处,委派上级
  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader 查找
  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 F 这个类,显然没有,就会捕获异常,但是不处理
  6. sun.misc.Launcher$$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 F 这个类,显然没有,回到 sun.misc.Launcher $AppClassLoader 的 // 2 处
  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在 classpath 下查找,找到了

双亲委派模型目的:

可以防止内存中出现多份同样的字节码。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个 java.lang.Object 的同名类并放在 ClassPath 中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的 Object 类,那么类之间的比较结果及类的唯一性将无法保证。

依赖传递原则

假设类C是由加载器L1定义加载的,那么类C中所依赖的其他类将会通过L1进行加载。

如下:假设Demo2是由L1定义加载的,那么类Demo2中所依赖的类F和接口IDemo将会通过L1进行加载。甚至还用到了String类,String也会由L1加载,但由于双亲委派模型,String类最终会由Bootstrap ClassLoader进行加载。(前提是这些依赖的类尚未加载)

import com.seven.jvm2.F;
import com.seven.jvm2.IDemo;

public class Demo2 implements IDemo {

    @Override
    public void run() {
        F f = new F();
       String s = f.toString().toLowerCase();
        System.out.println(s);
    }
}

打破双亲委派模型

什么时候需要打破双亲委派模型?比如类A已经有一个classA,恰好类B也有一个clasA 但是两者内容不一致,如果不打破双亲委派模型,那么类A只会加载一次

只要在加载类的时候,不按照UserCLASSlOADER->Application ClassLoader->Extension ClassLoader->Bootstrap ClassLoader的顺序来加载就算打破打破双亲委派模型了。比如自定义个ClassLoader,重写loadClass方法(不依照往上开始寻找类加载器),那就算是打破双亲委派机制了。

打破双亲委派模型有两种方式:

  1. 自定义一个类加载器的类,并覆盖抽象类java.lang.ClassL oader中loadClass…)方法,不再优先委派“父”加载器进行类加载。(比如Tomcat)
  2. 主动违背类加载器的依赖传递原则
    • 例如在一个BootstrapClassLoader加载的类中,又通过APPClassLoader来加载所依赖的其它类,这就打破了“双亲委派模型”中的层次结构,逆转了类之间的可见性。
    • 典型的是Java SPI机制,它在类ServiceLoader中,会使用线程上下文类加载器来逆向加载classpath中的第三方厂商提供的Service Provider类。(比如JDBC)

Tomcat

在Tomcat部署项目时,,是把war包放到tomcat的webapp下,这就意味着一个tomcat可以运行多个Web应用程序。

假设现在有两个Web应用程序,它们都有一个类,叫做User,并且它们的类全限定名都一样,比如都是com.yyy.User,但是他们的具体实现是不一样的。那么Tomcat如何保证它们不会冲突呢?

Tomcat给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找,这样就做到了Web应用层级的隔离。

但是并不是Web应用程序的所有依赖都需要隔离的,比如要用到Redis的话,Redis就可以再Web应用程序之间贡献,没必要每个Web应用程序每个都独自加载一份。因此Tomcat就在WebAppClassLoader上加个父加载器ShareClassLoader,如果WebAppClassLoader没有加载到这个类,就委托给ShareClassLoader去加载。(意思就类似于将需要共享的类放到一个共享目录下)

Web应用程序有类,但是Tomcat本身也有自己的类,为了隔绝这两个类,就用CatalinaClassLoader类加载器进行隔离,CatalinaClassLoader加载Tomcat本身的类

Tomcat与Web应用程序还有类需要共享,那就再用CommonClassLoader作为CatalinaClassLoader和ShareClassLoader的父类加载器,来加载他们之间的共享类

Tomcat加载结构图如下:
在这里插入图片描述

JDBC

实际上JDBC定义了接口,具体的实现类是由各个厂商进行实现的(比如MySQL)

类加载有个规则:如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。

而在用JDBC的时候,是使用DriverManager获取Connection的,DriverManager是在java.sql包下的,显然是由BootStrap类加载器进行装载的。当使用DriverManager.getConnection ()时,需要得到的一定是对应厂商(如Mysql)实现的类。这里在去获取Connection的时候,是使用「线程上下文加载器」去加载Connection的,线程上下文加载器会直接指定对应的加载器去加载。也就是说,在BootStrap类加载器利用「线程上下文加载器」指定了对应的类的加载器去加载

线程上下文加载器

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC 。

这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器来加载的;SPI的实现类是由系统类加载器来加载的。启动类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能委派给系统类加载器,因为它是系统类加载器的祖先类加载器。

线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

线程上下文加载器的一般使用模式(获取 - 使用 - 还原)

ClassLoader calssLoader = Thread.currentThread().getContextClassLoader();
 
try {
    //设置线程上下文类加载器为自定义的加载器
    Thread.currentThread.setContextClassLoader(targetTccl);
    myMethod(); //执行自定义的方法
} finally {
    //还原线程上下文类加载器
    Thread.currentThread().setContextClassLoader(classLoader);
}

总结

  1. 类的加载过程:
    • 加载:将java文件加载为字节码class文件
    • 验证:确保Class文件的字节流中包含的信息符合JVM规范
    • 准备:为 static 变量分配空间,设置默认值
    • 解析:将常量池内的符号引用替换为直接引用
    • 初始化:执行类构造器
  2. JDK中有三个默认类加载器:AppClassLoader、ExtClassLoader、BootStrapClassLoader。AppClassLoader的父加载器为Ext ClassLoader、Ext ClassLoader的父加载器为BootStrap ClassLoader。
  3. 什么是双亲委派机制:加载器在加载过程中,先把类交由父加载器进行加载,父加载器没找到才由自身加载。
  4. 双亲委派机制目的:为了防止内存中存在多份同样的字节码(安全)
  5. 依赖传递原则:如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。
  6. 什么时候需要打破双亲委派模型?比如类A已经有一个classA,恰好类B也有一个clasA 但是两者内容不一致,如果不打破双亲委派模型,那么类A只会加载一次
  7. 如何打破双亲委派机制:
    • 自定义ClassLoader,重写loadClass方法(只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制)(Tomcat)
    • 主动违背类加载器的依赖传递原则(JDBC)
  8. 打破双亲委派机制案例:Tomcat
    • 为了Web应用程序类之间隔离,为每个应用程序创建WebAppClassLoader类加载器
    • 为了Web应用程序类之间共享,把ShareClassLoader作为WebAppClassLoader的父类加载器,如果WebAppClassLoader加载器找不到,则尝试用ShareClassLoader进行加载
    • 为了Tomcat本身与Web应用程序类隔离,用CatalinaClassLoader类加载器进行隔离,CatalinaClassLoader加载Tomcat本身的类
    • 为了Tomcat与Web应用程序类共享,用CommonClassLoader作为CatalinaClassLoader和ShareClassLoader的父类加载器
  9. 线程上下文加载器:由于类加载的规则,很可能导致父加载器加载时依赖子加载器的类,导致无法加载成功(BootStrap ClassLoader无法加载第三方库的类),所以存在「线程上下文加载器」来进行加载。
  10. 打破双亲委派机制案例:JDBC
    • JDBC的接口的具体的实现类是由各个厂商进行实现的(比如MySQL),因此在用JDBC的时候,需要得到厂商实现的类。这里就使用「线程上下文加载器」,线程上下文加载器直接指定对应的加载器去加载对应厂商的具体的实现类。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值