java虚拟机JVM类加载机制原理(面试必问)

目录
  • 1、类加载的过程。
    • 1)加载
    • 2)验证
    • 3)准备
    • 4)解析
    • 5)初始化
  • 2、Java 虚拟机中有哪些类加载器?
    • 1)启动类加载器(Bootstrap ClassLoader):
    • 2)扩展类加载器(Extension ClassLoader):
    • 3)应用程序类加载器(Application ClassLoader):
  • 3、什么是双亲委派模型?
  • 4、为什么使用双亲委派模式?
  • 5、有哪些场景破坏了双亲委派模型?
    • 1)线程上下文类加载器
    • 2)Tomcat 的多 Web 应用程序
    • 3)OSGI 实现模块化热部署
  • 6、为什么要破坏双亲委派模型?
  • 7、如何破坏双亲委派模型?
    • 1)继承 ClassLoader
    • 2)重写 loadClass 方法
  • 8、Tomcat 的类加载器?
    • 1)Bootstrap ClassLoader:
    • 2)System ClassLoader
    • 3)Common ClassLoader:
    • 4)WebappX ClassLoader:
  • 9、Tomcat 的类加载过程?
    • 1)首先本地缓存 resourceEntries
    • 2)检查 JVM 是否已经加载过该类
    • 3) 检查要加载的类是否是 Java SE 的类
    • 4)针对委托属性 delegate
    • 5)尝试从本地加载类
    • 7)走到这,代表步骤6加载失败
    • 8)走到这边代表所有的尝试都加载失败
  • 10、JDBC 使用线程上下文类加载器的原理

1、类加载的过程。

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

1)加载

“类加载”过程的一个阶段,在加载阶段,虚拟机需要完成以下3件事情:

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

2)验证

连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

3)准备

该阶段是正式为类变量(static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里所说的初始值“通常情况”下是数据类型的零值,下表列出了Java中所有基本数据类型的零值。

4)解析

该阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行。

5)初始化

到了初始化阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序制定的主观计划去初始化类变量和其他资源。

我们也可以从另外一种更直接的形式来表达:初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>() 不是程序员在 Java 代码中直接编写的方法,而是由 Javac 编译器自动生成的。

<clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

2、Java 虚拟机中有哪些类加载器?

从 Java 虚拟机的角度来讲,只存在两种不同的类加载器:

一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;

另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

从Java开发人员的角度来看,绝大部分Java程序都会使用到以下3种系统提供的类加载器。

1)启动类加载器(Bootstrap ClassLoader):

这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。

2)扩展类加载器(Extension ClassLoader):

这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

3)应用程序类加载器(Application ClassLoader):

这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载器之间的关系一般如图所示。

3、什么是双亲委派模型?

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

类加载的源码如下:

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 {
                    // 2、将类加载请求先委托给父类加载器
                    if (parent != null) {
                        // 父类加载器不为空时,委托给父类加载进行加载
                        c = parent.loadClass(name, false);
                    } else {
                        // 父类加载器为空,则代表当前是Bootstrap,从Bootstrap中加载类
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果父类加载器抛出ClassNotFoundException
                    // 说明父类加载器无法完成加载请求
                }

                if (c == null) {
                    // 3、在父类加载器无法加载的时候,再调用本身的findClass方法来进行类加载
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

4、为什么使用双亲委派模式?

1)使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。

2)如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

5、有哪些场景破坏了双亲委派模型?

目前比较常见的场景主要有:

1)线程上下文类加载器

典型的:JDBC 使用线程上下文类加载器加载 Driver 实现类

2)Tomcat 的多 Web 应用程序

3)OSGI 实现模块化热部署

6、为什么要破坏双亲委派模型?

原因其实很简单,就是使用双亲委派模型无法满足需求了,因此只能破坏它,这边以面试常问的 Tomcat 为例。

我们知道 Tomcat 容器可以同时部署多个 Web 应用程序,多个 Web 应用程序很容易存在依赖同一个 jar 包,但是版本不一样的情况。例如应用1和应用2都依赖了 spring ,应用1使用的 3.2.* 版本,而应用2使用的是 4.3.* 版本。

如果遵循双亲委派模型,这个时候使用哪个版本了?

其实使用哪个版本都不行,很容易出现兼容性问题。因此,Tomcat 只能选择破坏双亲委派模型。

7、如何破坏双亲委派模型?

破坏双亲委派模型的思路都比较类似,这边以面试中常问到的 Tomcat 为例。

其实原理非常简单,我们可以看到上面的类加载方法源码(loadClass)的方法修饰符是 protected,因此我们只需以下几步就能破坏双亲委派模型。

1)继承 ClassLoader

Tomcat 中的 WebappClassLoader 继承 ClassLoader 的子类 URLClassLoader。

2)重写 loadClass 方法

实现自己的逻辑,不要每次都先委托给父类加载,例如可以先在本地加载,这样就破坏了双亲委派模型了。

8、Tomcat 的类加载器?

Tomcat 的类加载器如下图所示:

1)Bootstrap ClassLoader:

可以看到上图中缺少了 Extension ClassLoader,在 Tomcat 中 Extension ClassLoader 被集成到了 Bootstrap ClassLoader 里面。

2)System ClassLoader

就是 Application ClassLoader:Tomcat 中的系统类加载器不会加载 CLASSPATH 环境变量的内容,而是从以下资源库构建 System 类加载器。

  • $CATALINA_HOME/bin/bootstrap.jar,包含用于初始化Tomcat服务器的 main() 方法,以及它所依赖的类加载器实现类。
  • $CATALINA_BASE/bin/tomcat-juli.jar 或 $CATALINA_HOME/bin/tomcat-juli.jar,日志实现类。
  • 如果 $CATALINA_BASE/bin 中存在 tomcat-juli.jar,则使用它来代替 $CATALINA_HOME/bin中的那个。
  • $CATALINA_HOME/bin/commons-daemon.jar

3)Common ClassLoader:

从名字也看出来来了,主要包含一些通用的类,这些类对 Tomcat 内部类和所有 Web 应用程序都可见。

该类加载器搜索的位置由 $CATALINA_BASE/conf/catalina.properties 中的 common.loader 属性定义,默认设置将按照顺序搜索以下位置。

$CATALINA_BASE/lib 中未打包的类和资源

$CATALINA_BASE/lib 目录下的JAR 文件

$CATALINA_HOME/lib 中未打包的类和资源

$CATALINA_HOME/lib 目录下的JAR文件

4)WebappX ClassLoader:

Tomcat 为每个部署的 Web 应用程序创建一个单独的类加载器,这样保证了不同应用之间是隔离的,类和资源对其他 Web 应用是不可见的。加载的路径如下:

Web应用的 /WEB-INF/classes 目录下的所有未打包的类和资源Web应用的 /WEB-INF/lib 目录下的 JAR 文件中的类和资源

9、Tomcat 的类加载过程?

Tomcat 的类加载过程,也就是 WebappClassLoaderBase#loadClass 的逻辑如下。

1)首先本地缓存 resourceEntries

如果已经被加载过则直接返回缓存中的数据。

2)检查 JVM 是否已经加载过该类

如果是则直接返回。

3) 检查要加载的类是否是 Java SE 的类

如果是则使用 BootStrap 类加载器加载该类,以防止 webapp 的类覆盖了 Java SE 的类。

例如你写了一个 java.lang.String 类,放在当前应用的 /WEB-INF/classes 中,如果没有此步骤的保证,那么之后项目中使用的 String 类都是你自己定义的,而不是 rt.jar 下面的,可能会导致很多隐患。

4)针对委托属性 delegate

显示设置为 true、或者一些特殊的类(javax、org 包下的部分类),使用双亲委派模式加载,只有很少部分使用双亲委派模型来加载。

5)尝试从本地加载类

如果步骤5中加载失败也会走到本步骤,这边打破了双亲委派模型,优先从本地进行加载。

7)走到这,代表步骤6加载失败

如果之前不是使用双亲委派模式,则在这边会委托给父类加载器来尝试加载。

8)走到这边代表所有的尝试都加载失败

抛出 ClassNotFoundException。

10、JDBC 使用线程上下文类加载器的原理

JDBC 功能相关的基础类是由 Java 统一定义的,在 rt.jar 里面,例如 DriverManager,也就是由 Bootstrap ClassLoader 来加载,而 JDBC 的实现类是在各厂商的实现 jar 包里,例如 MySQL 是在 mysql-connector-java 里,oracle、sqlserver 也会有各自的实现 jar。

此时需要 JDBC 的基础类调用其他厂商实现并部署在应用程序的 ClassPath 下的 JDBC 服务提供接口(SPI,Service Provider Interface)的代码。当类A调用类B时,此时类B是由类A的类加载器来负责加载,而 JDBC 的基础类都是由 Bootstrap ClassLoader 来加载,但是 Bootstrap ClassLoader 是不认识也不会去加载这些厂商实现的代码的。

因此,Java 提供了线程上下文类加载器,允许通过 Thread#setContextClassLoader/Thread#getContextClassLoader() 来设置和获取当前线程的上下文类加载器。如果创建线程时没有设置,则会继承父线程的,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器(Application ClassLoader)。

综上,JDBC 可以通过线程上下文类加载器,来实现父类加载器“委托”子类加载器完成类加载的行为,这个就明显不遵守双亲委派模型了,不过这也是双亲委派模型自身的缺陷导致的,希望大家以后多多支持我们!

(0)

相关推荐

  • 一文读懂Jvm类加载机制

    前言 一个月没更新了,这个月发生了太多的事情,导致更新的频率大大降低,不管怎样收拾心情,技术的研究不能落下! jvm作为每个java程序猿必须了解的知识,博主推荐一本书<深入理解Java虚拟机>,以前博主在学校的时候看过几遍,每一次看都有新的理解.加上工作了也有一年多的时间了,有必要好好总结一番~ 什么是jvm 平常我们编写代码都是编写的.java文件,怎么部署到机器上运行呢?通过打jar包或者war包,然后部署运行. 如果看过jar包的内容那么就能知道,我们写的.java文件全部被编译成了.

  • JVM类加载机制原理及用法解析

    一.JVM 类加载机制 JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程. 1. 加载: 加载是类加载过程中的第一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口.注意这里不一定非得要从一个 Class 文件获取,这里既 可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的

  • Java虚拟机JVM类加载机制(从类文件到虚拟机)

    一.类加载机制简介 什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构.类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口. 类加载机制:所谓的类加载机制就是虚拟机将class文件加载到内存,并对数据进行验证,转换解析和初始化,形成虚拟机可以直接使用的jav

  • java虚拟机JVM类加载机制原理(面试必问)

    目录 1.类加载的过程. 1)加载 2)验证 3)准备 4)解析 5)初始化 2.Java 虚拟机中有哪些类加载器? 1)启动类加载器(Bootstrap ClassLoader): 2)扩展类加载器(Extension ClassLoader): 3)应用程序类加载器(Application ClassLoader): 3.什么是双亲委派模型? 4.为什么使用双亲委派模式? 5.有哪些场景破坏了双亲委派模型? 1)线程上下文类加载器 2)Tomcat 的多 Web 应用程序 3)OSGI 实现

  • 一篇文章弄懂JVM类加载机制过程以及原理

    目录 一.做一个小测试 二.类的初始化步骤: 三.看看你写对了没? 四.类的加载过程 五.类加载器的分类 1.启动类加载器(引导类加载器) 2.扩展类加载器 3.应用程序类加载器(系统类加载器) 六.类加载器子系统的作用 七.总结 一.做一个小测试 通过注释,标注出下面两个类中每个方法的执行顺序,并写出studentId的最终值. package com.nezha.javase; public class Person1 { private int personId; public Perso

  • 老生常谈Java虚拟机垃圾回收机制(必看篇)

    在Java虚拟机中,对象和数组的内存都是在堆中分配的,垃圾收集器主要回收的内存就是再堆内存中.如果在Java程序运行过程中,动态创建的对象或者数组没有及时得到回收,持续积累,最终堆内存就会被占满,导致OOM. JVM提供了一种垃圾回收机制,简称GC机制.通过GC机制,能够在运行过程中将堆中的垃圾对象不断回收,从而保证程序的正常运行. 垃圾对象的判定 我们都知道,所谓"垃圾"对象,就是指我们在程序的运行过程中不再有用的对象,即不再存活的对象.那么怎么来判断堆中的对象是"垃圾&q

  • Java JVM类加载机制解读

    目录 1.什么是类加载 2.类加载的过程 2.1加载 2.2验证 2.3准备 2.4解析 2.5初始化[重中之重之重中重] 第一段代码: 第二段代码: 第三段代码: 最后一段代码: 总结 1.什么是类加载 首先你要知道一个类的从被加载到虚拟机内存中开始,到被初始化为止,是为类加载的整个过程.下图就是类加载的整个过程: 一个类只有经历了加载.验证.准备.解析.初始化这五个关卡才能被认为是实现了类加载.这,就是类加载. 注意一点:上面五个过程并不是按部就班地"完成",而是按部就班地&quo

  • 深入理解Java虚拟机 JVM 内存结构

    目录 前言 JVM是什么 JVM内存结构概览 运行时数据区 程序计数器 Java虚拟机栈 本地方法栈 方法区 运行时常量池 Java堆 直接内存 前言 JVM是Java中比较难理解和掌握的一部分,也是面试中被问的比较多的,掌握好JVM底层原理有助于我们在开发中写出效率更高的代码,可以让我们面对OutOfMemoryError时不再一脸懵逼,可以用掌握的JVM知识去查找分析问题.去进行JVM的调优.去让我们的应用程序可以支持更高的并发量等......总之一句话,学好JVM很重要! JVM是什么 J

  • Android虚拟机与类加载机制详情

    目录 JVM与Dalvik 基于栈的虚拟机 字节码指令 执行过程 基于寄存器的虚拟机 寄存器 基于寄存器的虚拟机 ART与Dalvik dex2aot dexopt与dexaot Android N的运作方式 ClassLoader 介绍 ClassLoader加载流程与双亲委托机制 类加载 热修复 JVM与Dalvik Android应用程序运行在Dalvik/ART虚拟机,并且每一个应用程序对应有一个单独的Dalvik虚拟机实例.Dalvik虚拟机实则也算是一个Java虚拟机,只不过它执行的

  • Java 虚拟机(JVM)之基本概念详解

    1.类加载子系统:负责从文件系统或者网络中加载Class信息,加载的信息存放在一块称之为方法区的内存空间. 2.方法区:就是存放类信息.常量信息.常量池信息.包括字符串字面量和数字常量等.方法区是辅助堆栈的块永久区,解决堆栈信息的产生,是先决条件. 3.Java堆:再java虚拟机启动的时候建立Java堆,它是java程序最主要的内存工作区域,几乎所有的对象实例都存放到Java堆中,堆空间是所有线程共享的.堆解决的是数据存储问题,即数据怎么放.放在哪儿. 4.直接内存:Java的NIO库允许Ja

  • 一文详解Java中的类加载机制

    目录 一.前言 二.类加载的时机 2.1 类加载过程 2.2 什么时候类初始化 2.3 被动引用不会初始化 三.类加载的过程 3.1 加载 3.2 验证 3.3 准备 3.4 解析 3.5 初始化 四.父类和子类初始化过程中的执行顺序 五.类加载器 5.1 类与类加载器 5.2 双亲委派模型 5.3 破坏双亲委派模型 六.Java模块化系统 一.前言 Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最 终形成可以被虚拟机直接使用的Java类型,这个过程

随机推荐