Tianyi's Blog Tianyi's Blog
首页
  • 计算机网络
  • 操作系统
  • 计算机科学
  • Nginx
  • Vue框架
  • 环境配置
  • Java
  • JVM
  • Spring框架
  • Redis
  • MySQL
  • RabbitMQ
  • Kafka
  • Mirror Sites
  • Dev Tools
  • Docker
  • Jenkins
  • Scripts
  • Windows
  • 科学上网
  • 旅行
  • 网站日记
  • 软件
  • 电子产品
  • 杂野
  • 分类
  • 友情链接
GitHub (opens new window)

Tianyi

一直向前,永不停止
首页
  • 计算机网络
  • 操作系统
  • 计算机科学
  • Nginx
  • Vue框架
  • 环境配置
  • Java
  • JVM
  • Spring框架
  • Redis
  • MySQL
  • RabbitMQ
  • Kafka
  • Mirror Sites
  • Dev Tools
  • Docker
  • Jenkins
  • Scripts
  • Windows
  • 科学上网
  • 旅行
  • 网站日记
  • 软件
  • 电子产品
  • 杂野
  • 分类
  • 友情链接
GitHub (opens new window)
  • Java

  • Golang

  • JVM的奇妙世界

    • 虚拟机1-宏观视角
    • 虚拟机2-字节码
    • 虚拟机3-输入层
      • 虚拟机4-运行时数据区
      • 虚拟机5-垃圾收集器
      • 虚拟机6-调优
      • JVM调优
    • Spring

    • Spring增强封装

    • Redis

    • MySQL

    • RabbitMQ

    • Kafka

    • 分享

    • 后端
    • JVM的奇妙世界
    tianyi
    2024-04-14
    目录

    虚拟机3-输入层

    类加载子系统是JVM的入口,负责把字节码文件加载到内存中。它不只限于硬盘上的class文件,网络或其他来源的字节流,只要符合JVM规范,都能被加载。什么是JVM规范?简单说,就是字节码得包含魔数、版本号、常量池、访问标识、字段表、方法表等这些标准结构。

    # 类加载过程

    类加载的过程分三个阶段:加载(Loading)、链接(Linking)和初始化(Initialization)。我来拆开讲讲。

    第一步,加载。JVM启动时,程序需要某个类,类加载器先检查它是否已加载过。如果没加载过,就读取字节码的静态数据,把它放进内存。这一步就是单纯的“搬运工”,数据进来了,但还没生效。

    第二步,链接,分三个小步骤:验证、准备、解析。验证是检查字节码符不符合规范,比如开头是不是“CAFEBABE”魔数,结构是不是完整。准备是为类的成员变量设默认值,比如int默认给0。解析稍微复杂点,就是把字节码里的符号引用(比如字符串表示的类名)转成内存里的直接引用(实际地址)。这步就像把地图上的地名变成具体坐标。

    第三步,初始化。主要是执行类的构造器方法(<clinit>),处理静态变量的赋值和静态块的逻辑。三个阶段走完,类就正式可用了,程序可以开始实例化对象。

    总结一下,类加载子系统是JVM和外界交互的关键,把字节码变成内存里的可用数据,分加载、链接、初始化三个阶段,每步各司其职。

    # 加载

    加载阶段,顾名思义,就是把字节码文件读进JVM。具体干了三件事:第一,类加载器读取字节码的二进制流,简单说就是“读文件”。第二,把文件里的静态数据(比如字段、方法、常量池)解析出来,存到JVM内存的方法区。方法区是运行时数据区的核心,专门存类的描述信息。第三,在堆区生成一个java.lang.Class对象,作为方法区的访问入口。

    举个例子:磁盘上有个A.class文件,程序里写了“new A()”,JVM一看内存里没这个类,就启动加载。类加载器读A的字节码,发现它有个父类BaseA,那就先加载BaseA。加载完后,BaseA和A的信息都放进方法区,各自在堆里生成一个Class对象。程序通过“A.class”拿到这个Class实例,再用“getField(‘age’)”从方法区提取字段“age”的描述。这就是加载的全过程。

    综上所述,加载阶段是类加载的第一步,读字节码、存方法区、建Class对象,还得管父类加载。有个细节要注意:加载子类时,父类必须先加载。比如A依赖BaseA,BaseA没加载就没法继续。而且,所有类都隐式继承Object,所以Object是最先被加载的。

    # 链接

    我们聊聊类加载的第二阶段——链接(Linking)

    链接阶段包括三个子步骤:验证、准备、解析,缺一不可。咱们一个个来说。

    先说验证。验证就是检查字节码符不符合JVM规范,分四块:1. 文件格式校验,比如开头是不是“CAFEBABE”魔数,版本号、常量池、字段表这些格式对不对;2. 语义分析,看类、方法、字段的定义符不符合Java规则,比如有没有继承final类;3. 字节码验证,分析数据流和控制流,比如类型转换能不能成功,或者方法分支有没有漏掉return;4. 符号引用校验,检查字节码里提到的类名、方法名、字段名存不存在,不存在就报错,比如“ClassNotFoundException”。这步只看字节码本身,跟运行时无关。

    再说准备。准备很简单,就是给类的静态变量(static)赋默认初始值。比如“public static int a = 100”,在准备阶段,a先被赋值为0,至于100,得等到后面的初始化阶段。不同类型有不同默认值:int是0,引用类型是null,就这么简单。

    最后讲解析。解析是核心,把字节码里的符号引用变成直接引用。啥意思?符号引用是静态的文本,比如类A继承BaseA,字节码里用字符串记录“com.test.A”和“com.test.BaseA”的关系。加载后,内存里生成Class A和Class BaseA两个对象,通过指针动态关联起来,这就是直接引用。解析阶段处理四种东西:类、字段、方法、接口,都是把文本关系转成内存里的实际指针。

    链接阶段是验证字节码合法性、准备默认值、解析引用关系,三步走完,字节码就从静态文件变成内存里能用的东西。

    # 初始化

    那我们继续聊聊类加载的最后一个阶段——初始化(Initialization)

    初始化阶段的核心是执行类的构造器方法<clinit>,全称“class initialization”。这个方法不是我们写的,而是编译器自动生成,专门用来完成类的初始化。啥叫初始化?就是给静态变量赋值和执行静态代码块。

    有五个重点要掌握:

    1. <clinit>干啥? 它负责执行静态变量赋值和静态代码块。比如有个类A,里面有“static int start = 10”和一个static块,<clinit>会把这两部分合起来变成字节码,按顺序执行。前面准备阶段赋的是默认值0,到这儿才赋10。
    2. 子类和父类的顺序 子类初始化前,父类的<clinit>先跑。比如类A继承Base,运行时先执行Base的<clinit>,再到A的。这是自然的,因为加载子类时父类得先加载。
    3. 没静态内容就不生成<clinit> 如果类没静态变量也没静态块,<clinit>压根不会生成。没东西要初始化,要它干嘛?
    4. 怎么看加载过程? JVM有个选项“-XX:+TraceClassLoading”,启动时加上,能看到类加载的详细顺序。比如先加载Base,再加载A,初始化也是这个顺序,调试时很实用。
    5. 只初始化一次咋保证? <clinit>自带同步锁。多线程同时加载类,只有第一个线程能执行,其他的等着。第一个跑完,类就初始化好了,后面的线程直接用现成的Class对象,不重复执行。

    举个例子:我写了个类A,继承Base,A有静态变量和块,main里用100个线程new A。加个10秒睡眠模拟阻塞,结果是父类先初始化,子类静态块只跑一次,后99个线程瞬间完成。因为<clinit>加锁,初始化只发生一次。

    # 类加载器的分类

    类加载器是JVM里负责把字节码加载进内存的工具。根据加载的内容和位置,它分成四种:启动类加载器、扩展类加载器、应用程序类加载器和自定义类加载器。

    1. 启动类加载器
      最底层,用C语言写,加载JVM核心类库,比如rt.jar里的Object、String、Thread。这些都在jre/lib目录下,只有包名以java、javax、sun开头的才加载,这是沙箱机制,防黑客乱加破坏性代码。注意,它不加载全部lib里的包,只认固定的。

    2. 扩展类加载器
      用Java写,类名叫ExtClassLoader,加载jre/lib/ext目录下的扩展包,比如j3ec.jar。平时开发用得少,但你可以把自己的包扔进ext目录,它也会加载。上级是启动类加载器,但不是继承关系。

    3. 应用程序类加载器
      也叫系统类加载器,用Java写,类名叫AppClassLoader,加载classpath下的类和第三方包,比如Spring、Hibernate这些。跟我们开发最相关,默认就是它干活,上级是扩展类加载器。

    4. 自定义类加载器
      自己写,可以加载非classpath或非jre的类,甚至网络上的字节流。比如类不在文件里,是数据流,自定义加载器就能搞定。

    怎么看类加载器长啥样?拿代码举例:

    1. 在ClassLoaderSample类里,用ClassLoaderSample.class.getClassLoader()能拿到加载它的加载器。运行输出AppClassLoader,因为它在classpath下。换成j3ec.jar里的类,输出ExtClassLoader,因为在ext目录。
    2. String.class.getClassLoader()输出null,不是没加载器,而是启动类加载器用C写,JVM管不着,返回空。

    总结:类加载器分四种,启动管核心,扩展管ext,应用管classpath,自定义管特殊场景。最常用的是应用程序类加载器。

    # 自定义类加载器

    先说为啥要自定义类加载器。平时开发基本用不到,但特殊场景得靠它:1. 字节码从网络来,不是文件,比如远程服务器传来的二进制流;2. 字节码不在jre/lib、ext或classpath里,系统加载器找不到;3. 安全需求,字节码加密传输,得解密后再加载。这些情况少见,99%时候启动、扩展、应用程序类加载器就够了。

    那自定义类加载器咋写?核心是继承ClassLoader,重写findClass方法。两步:1. 自己指定路径读字节码二进制流,比如从C盘根目录读;2. 用defineClass把二进制流转成Class对象。比如我写了个MyClassLoader1,用FileInputStream读C盘的ClassSample字节码,存到字节数组,再丢给defineClass,就加载完了。MyClassLoader2代码一样,就名字不同。

    重点来了,面试常问:“Class实例在JVM里全局唯一吗?”很多人直接说“是”,因为类加载会先查JVM里有没有,重复就不加载。但有个前提——得是同一个类加载器。如果不同加载器加载同一个字节码,结果就不一样。咋验证?我在测试类Application里用MyClassLoader1和MyClassLoader2各加载一次ClassSample,输出它们的Class对象和哈希码。结果呢?加载器不同,哈希码不同,说明是两个对象。所以,结论是:同一个类,不同加载器加载,在JVM里生成不同的Class实例;只有同一加载器下,Class实例才唯一。

    # 双亲委派模型

    双亲委派模型听着高大上,其实很简单。JVM加载类时,加载器把任务一级级往上推到顶层,再一级级往下试着加载,直到成功。举个例子:我定义个类A,加载时咋走?第一步,系统类加载器接到任务,不直接干活,而是往上推给扩展类加载器,扩展再推给引导类加载器,这叫“委派”。第二步,从顶往下试,引导类加载器一看,A不是rt.jar里的核心类,pass;扩展类加载器一看,也不是ext里的,pass;最后系统类加载器认领,因为A在classpath下归它管,就加载了。

    如果是Object这种核心类呢?一样从系统类加载器往上推到引导类加载器,引导一看属于rt.jar,直接加载,下面两层就不用动了。

    为啥这么麻烦?两个好处:

    1. 避免重复加载。同一个加载器加载的类在JVM里全局唯一,像Object永远是引导类加载器搞定,不会多份乱跑。
    2. 沙箱安全。核心包以java、javax、sun开头,用户不能随便污染。比如你自定个java.lang.Customer,双亲委派往上一推,沙箱机制发现包名不对,直接禁掉,保护系统。

    面试题来了:“定义个java.lang.Customer会咋样?”我在自己的工程试了下,包名设成java.lang,跑程序报错:“不能用java开头的包名”。为啥?系统类加载器管自定义类,但java.lang是核心包,双亲委派推到顶,沙箱检查不通过,加载就被拦了。这题考你加载器分工、双亲委派流程和沙箱机制。

    总结:双亲委派是先往上推任务,再往下试加载,保证类唯一和安全。面试问核心包污染,直接甩沙箱机制就行。

    完善页面 (opens new window)
    虚拟机2-字节码
    虚拟机4-运行时数据区

    ← 虚拟机2-字节码 虚拟机4-运行时数据区→

    最近更新
    01
    JDK
    02-23
    02
    BadTasteCode && 优化
    09-11
    03
    Gradle 实践操作指南及最佳实践
    09-11
    更多文章>
    Theme by Vdoing | Copyright © 2021-2025 Tandy | 粤ICP备2023113440号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式