面试必问的 JVM 类加载机制,你懂了吗?

面试必问的 JVM 类加载机制,你懂了吗?JVM类加载机制高频面试题

大家好,又见面了,我是你们的朋友全栈君。

前言

本次带来 JVM 的另一块重要内容,类加载机制,不废话,直接开怼。

正文

1、类加载的过程。

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

面试必问的 JVM 类加载机制,你懂了吗?1)加载

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

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

2)验证

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

3)准备

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

面试必问的 JVM 类加载机制,你懂了吗?

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种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载器之间的关系一般如图所示。

面试必问的 JVM 类加载机制,你懂了吗?

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。

面试必问的 JVM 类加载机制,你懂了吗?

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

8、Tomcat 的类加载器?

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

面试必问的 JVM 类加载机制,你懂了吗?

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。

面试必问的 JVM 类加载机制,你懂了吗?

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

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

面试必问的 JVM 类加载机制,你懂了吗?

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

最后

我是囧辉,一个坚持分享原创技术干货的程序员

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/142248.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 武汉java公司排名_武汉十大it培训机构

    武汉java公司排名_武汉十大it培训机构说起Java大家一定不陌生,毕竟Java这几年通过互联网+理念慢慢的渗透到了各大行业中,现在的Java软件开发岗位尤为火爆。同时也吸引着不少年轻人选择通过Java培训加入到行业中,在武汉,Java培训机构也是不少,想要在其中选择一家适合自己的是不太简单的,在这里,排名榜小编作为一名IT行业的观察者,从课程设计、教师资质、就业等多方面对武汉Java培训机构进行了一系列的考察和筛选,得到了如下武汉Java培训机构排名榜单,排名结果仅供大家参考:1.武汉动力节点上榜理由:我相信大家对于动力节点的.

    2022年10月3日
    0
  • Golang 新手要注意的陷阱和常见错误(一)[通俗易懂]

    Golang 新手要注意的陷阱和常见错误(一)

    2022年2月15日
    39
  • Stack overflow at line 解决办法(重复引入JS导致)

    Stack overflow at line 解决办法(重复引入JS导致)这几天碰到了个莫名其妙的问题,我在一个TR的onDblClick事件里写了一个window.open(XXX);可是每当我双击这一行的时候总是给我报Stackoverflowatline7这个错误,在网上找了很多方法都不行,后来查看源文件发现我把一个外部JS引入了2次,我本身的jsp引入的一次,我每个jsp都会去引入一个公共的jsp,在公共jsp里面我又引入了一次。后来我把本身的jsp引入

    2022年7月15日
    12
  • tof相机很好的总结~

    tof相机很好的总结~1.1TOF初探TOF是Timeofflight的简写,直译为飞行时间的意思。所谓飞行时间法3D成像,是通过给目标连续发送光脉冲,然后用传感器接收从物体返回的光,通过探测光脉冲的飞行(往返)时间来得到目标物距离。这种技术跟3D激光传感器原理基本类似,只不过3D激光传感器是逐点扫描,而TOF相机则是同时得到整幅图像的深度信息。TOF相机与普通机器视觉成像过程也有类似之处,都是由光源、光

    2022年5月9日
    47
  • 数仓建设 | ODS、DWD、DWM等理论实战(好文收藏)

    数仓建设 | ODS、DWD、DWM等理论实战(好文收藏)本文目录:一、数据流向二、应用示例三、何为数仓DW四、为何要分层五、数据分层六、数据集市七、问题总结导读数仓在建设过程中,对数据的组织管理上,不仅要根据业务进行纵向的主题域划分,还需要横向的数仓分层规范。本文作者围绕企业数仓分层展开分析,希望对你有帮助。因文章太长,本文不是完结版,文末可获取完整PDF版从事数仓相关工作的人员都知道数仓模型设计的首要工作之一就是进行模型分层,可见模型分层在模型设计过程中的重要性,确实优秀的分层设计是一个数仓项目能否建设成功的核心要素,让数

    2022年6月26日
    71
  • 软件著作权说明书模板_软件设计方案怎么写

    软件著作权说明书模板_软件设计方案怎么写1.引言1.1编写目的1.2项目背景1.2项目概要总体要求2.1系统功能概述2.2系统功能要求软件开发3.1软件需求分析3.2软件的概要设计3.2.1软件概要设计说明3.2.3基本设计概念和处理流程3.3软件的详细设计3.3.1系统结构3.3.2模块设计说明3.3.3爬虫模块3.3.4日志模块3.3.5数…

    2022年9月22日
    0

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号