面试题解,JVM中的“类加载”剖析

一、JVM类加载机制说一下

其中,从加载到初始化就是我们的类加载阶段,我们逐一来分析

加载

"加载 loading"是整个类加载(class loading)过程的一个阶段,加载阶段JVM需要完成以下 3 件事情:

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

注意:

  • 加载的字节码来源,不一定非得是class文件,可以是符合字节码规范的任意地方,甚至二进制流等
  • 从字节码到内存,是由加载器(ClassLoader)完成的

验证

  • 1、文件格式验证(版本号,是不是CAFEBABYE开头,..........)
  • 2、元数据验证(验证属性、字段、类关系、方法等是否合规)
  • 3、字节码验证
  • 4、符号引用验证
  • 等等...

准备

为class中定义的各种类变量**(静态变量)**分配内存,并赋初始值,注意是对应类型的初始值,赋具体值在后面的初始化阶段。注意!即便是static变量,它在这个阶段初始化进内存的依然是该类型的初始值(零默认值等)!而不是用户代码里的初始值。

举例:

java 复制代码
//类变量:在准备阶段为它开辟内存空间,但是它是int的初始值,也就是 0,而真正123的赋值,是在下面的初始化阶段 
public static int a = 123; 

//类成员变量(实例变量)的赋值是在类对象被构造时才会赋值 
public String address = "北京" 

//final修饰的类变量,编译成字节码后,是一个ConstantValue类型,在准备阶段,直接给定值123,后期也没有二次初始化一说 
public static final int b = 123;

解析

将常量池内的符号引用替换为直接引用的过程。

符号引用是以一组描述符(如类名、字段名和方法签名等)来表示的,而直接引用则是可以直接指向目标实体(如类、字段或方法)的具体内存地址或偏移量。

初始化

类加载的最后一个步骤,经过这个步骤后,类信息完全进入了jvm内存,直到它被垃圾回收器回收

前面几个阶段都是JVM来搞定的。我们也干涉不了,从代码上只能遵从它的语法要求。而这个阶段,是初始化赋值,java虚拟机才真正开始执行类中编写的java程序代码,将主导权移交给应用程序。

所以在这个阶段是执行类中和static相关的所有事,比如静态代码块,静态变量赋值等

那么这些事都会经过javac编译器自动将这些事情编到<clinit>函数

<clinit> 函数是由编译器自动收集类中的所有静态变量的赋值动作和静态语句块( static 代码块)中的语句合并产生的。

<clinit> 函数与类的构造函数(虚拟机视角的 <init> 函数)是不同的, <init> 函数是在运行期创建对象时才执行,而 <clinit> 在类加载的时候就执行了。

虚拟机能保障父类的 <clinit> 函数优先于子类 <clinit> 函数的执行

在 <clinit> 函数中会对类变量赋具体的值,也就是我们说的:

java 复制代码
public static int a = 123; 1

这行代码的123才真正赋值完成。

二、双亲委派机制的过程及作用,三种类加载器,加载过程,双亲委派机制能打破吗?

三种类加载器

先来说一下三种类加载器,类加载器做的事情就是上面 5 个步骤的事(加载、验证、准备、解析、初始化),java提供了3个系统加载器,分别是 Bootstrap ClassLoader、ExtClassLoader 、AppClassLoader

每个类加载器加载不同路径下的类

Bootstrp加载器是用 C++ 语言写的,它在Java虚拟机启动后初始化,它主要负责加载以下路径的文件:

  • %JAVA_HOME%/jre/lib/*.jar
  • %JAVA_HOME%/jre/classes/*
  • -Xbootclasspath 参数指定的路径

ExtClassLoader 是用 Java 写的,具体来说就是sun.misc.Launcher$ExtClassLoader,主要加载:

  • %JAVA_HOME%/jre/lib/ext/*
  • ext 下的所有 classes 目录
  • java.ext.dirs 系统变量指定的路径中类库

AppClassLoader 也是用Java写成的,它的实现类是sun.misc.Launcher$AppClassLoader ,另外我们知道 ClassLoader 中有个getSystemClassLoader 方法,此方法返回的就是它。

  • 负责加载 -classpath 所指定的位置的类或者是jar文档
  • 也是Java程序默认的类加载器

双亲委派模型

双亲委派模型的核心就是,当前类加载器在加载一个类时,先委派给其父加载器,如果父加载器已经加载过了,那么自己就不再加载了。

具体一点来说:

类加载器加载某个类的时候,因为有多个加载器,甚至可以有各种自定义的,他们呈父子关系。这给人一种印象,子类的加载会覆盖父类,其实恰恰相反!

与普通类继承属性不同,类加载器会优先调父类的 loadClass 方法,如果父类能加载,直接用父类的,否则最后一步才是自己尝试加载,从源代码上可以验证。

那为什么这么设计呢?

双亲委派模型作用

避免重复加载、 核心类篡改

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父加载器已经加载了该类时,就没有必要子加载器再加载一次。

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

双亲委派能否打破

比如:Tomcat的 webappClassLoader 加载web应用下的class文件,不会传递给父类加载器,

**问题:**tomcat的类加载器为什么要打破该模型?

首先一个tomcat启动后是会起一个jvm进程的,它支持多个web应用部署到同一个tomcat里,为此

  • 1、对于不同的web应用中的class和外部jar包,需要相互隔离,不能因为不同的web应用引用了相同的jar或者有相同的class导致一个加载成功了另一个加载不了。
  • 2、web容器支持jsp文件修改后不用重启,jsp文件也是要编译成.class文件的,每一个jsp文件对应一个JspClassLoader,它的加载范围仅仅是这个jsp文件所编译出来的那一个.class文件,当Web容器检测到jsp文件被修改时,会替换掉目前JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热部署功能。
相关推荐
独自破碎E16 小时前
Java是怎么实现跨平台的?
java·开发语言
To Be Clean Coder16 小时前
【Spring源码】从源码倒看Spring用法(二)
java·后端·spring
xdpcxq102917 小时前
风控场景下超高并发频次计算服务
java·服务器·网络
想用offer打牌17 小时前
你真的懂Thread.currentThread().interrupt()吗?
java·后端·架构
橘色的狸花猫17 小时前
简历与岗位要求相似度分析系统
java·nlp
独自破碎E17 小时前
Leetcode1438绝对值不超过限制的最长连续子数组
java·开发语言·算法
用户917439653917 小时前
Elasticsearch Percolate Query使用优化案例-从2000到500ms
java·大数据·elasticsearch
yaoxin52112318 小时前
279. Java Stream API - Stream 拼接的两种方式:concat() vs flatMap()
java·开发语言
坚持学习前端日记18 小时前
2025年的个人和学习年度总结以及未来期望
java·学习·程序人生·职场和发展·创业创新
Cosmoshhhyyy18 小时前
《Effective Java》解读第29条:优先考虑泛型
java·开发语言