-
前言
-
编译
-
Class文件
-
- Class文件结构
-
Class文件示例
-
- 魔数
-
主次版本号
-
其他
-
类加载机制
-
- 加载(Loading)
-
- 双亲委派模式
-
破坏双亲委派模式
-
常见异常
-
ClassNotFoundException和NoClassDefFoundError
-
连接(Linking)
-
- 验证(Verification)
-
准备(Preparation)
-
解析(Resolution)
-
- 常见异常
-
符号引用
-
直接引用
-
初始化(Initialization)
-
- 初始化顺序
-
初始化实战举例
-
使用(Using)
-
卸载(Unloading)
-
总结
===============================================================
上一篇我们粗略的介绍了一下Java虚拟机的运行时数据区,并对运行时数据区内的划分进行了解释,今天我们就会从类加载开始分析并会深入去看看数据是具体以什么格式存储到运行时数据区的。
===============================================================
一个.java文件经过编译之后,变成了了.class文件,主要经过留下步骤:
.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> .class文件 。
具体的过程不做分析,涉及到编译原理比较复杂,我们需要分析的是.class文件到底是一个什么样的文件?
====================================================================
在Java中,每个类文件包含单个类或接口,每个类文件由一个8位字节流组成。所有16位、32位和64位的量都是通过分别读取2个、4个和8个连续的8位字节来构建的。
Java虚拟机规范中规定,Class文件格式使用一种类似于C语言的伪结构来存储数据,class文件中只有两种数据类型,无符号数和表。注意**,class文件中没有任何对齐和填充的说法,所有数据都按照特定的顺序紧凑的排列在Class文件中**。
- 无符号数
属于数据的基本类型,以u1,u2,u4,u8来表示1个字节,2个儿字节,4个字节,8个字节(在Java SE平台中,这些类型可以通过readUnsignedByte、readUnsignedShort和接口java.io.DataInput中的的readInt方法进行读取)。
- 表
由0个或多个大小可变的项组成,用于多个类文件结构中,也就是说一个类其实就相当于是一个表。
一个Class文件大致由如下结构组成:
ClassFile {
u4 magic;//魔数
u2 minor_version;//次版本号
u2 major_version;//主版本号
u2 constant_pool_count;//常量池数量
cp_info constant_pool[constant_pool_count-1];//常量池信息
u2 access_flags;//访问标志
u2 this_class;//类索引
u2 super_class;//父类索引
u2 interfaces_count;//接口数(2位,所以一个类最多65535个接口)
u2 interfaces[interfaces_count];//接口索引
u2 fields_count;//字段数
field_info fields[fields_count];//字段表集合
u2 methods_count;//方法数
method_info methods[methods_count];//方法集合
u2 attributes_count;//属性数
attribute_info attributes[attributes_count];//属性表集合
}
这个结构在本篇文章里不会一一去解释,如果一一去解释的话一来显得很枯燥,二来可能会占据大量篇幅,这些东西脑子里面有个整体的概念,需要的时候再查下资料就好了,后面的内容中,如果遇到一些非常常用的类结构含义会进行说明,如魔数等还是有必要了解一下的。
我们先任意写一个示例TestClassFormat.java文件:
package com.zwx.jvm;
public class TestClassFormat {
public static void main(String[] args) {
System.out.println(“Hello JVM”);
}
}
然后进行编译,得到TestClassFormat.class,利用16进制打开:
因为Java虚拟机只认Class文件,所以必然会对Class文件的格式有严格的安全性校验。
魔数
每个Class文件中都会以一个4字节的魔数(magic)开头(u4),即上图中的CA FE BA BE(咖啡宝贝)用来标记一个文件是不是一个Class文件。
主次版本号
魔数之后的2个字节(u2)就是minor_version(次版本号),再往后2个字节(u2)记录的是major_version(次版本号),这个还是非常有必要了解的,下面这个异常我想可能很多人都曾经遇到过:
java.lang.UnsupportedClassVersionError: com/zwx/demo : Unsupported major.minor version 52.0
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631)
这个异常就是提示主版本号不对。
Java中的版本号是从45开始的,也就是JDK1.0对应到Class文件的主版本号就是45,而JDK8对应到的主版本就是52。
上图中类文件的主版本号(第7和第8位)00 34 ,转成10进制就是52,也就是这个类就用JDK1.8来编译的,然后因为我用的是JDK1.6来运行,就会报上面的错了,因为高版本的JDK能向下兼容低版本的Class文件,但是不能向上兼容更高版本的Class文件,所以就会出现上面的异常。
其他
其他还有很多校验,比如说常量池的一些信息和计数,访问权限(public等)及其他一些规定,都是按照Class文件规定好的顺序往后紧凑的排在一起。
==================================================================
.java文件经过编译之后,就需要将class文件加载到内存了了,并将数据按照分类存储在运行时数据区的不同区域。
一个类从被加载到内存,再到使用完毕之后卸载,总共会经过5大步骤(7个阶段):
加载(Loading),连接(Linking),初始化(Initialization),使用(Using),卸载(Unloading) ,其中连接(Linking)又分为:验证(Verification),准备(Preparation),解析(Resolution)。
加载指的是通过一个完整的类或接口名称来获得其二进制流的形式并将其按照Java虚拟机规范将数据存储到运行时数据区。
类的加载主要是要做以下三件事:
-
1、通过一个类的全限定名获取定义此类的二进制字节流。
-
2、将这个二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构。
-
3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
上面的第1步在虚拟机规范中并没有说明Class来源于哪里,也没有说明怎么获取,所以就会产生了非常多的不同实现方式,下面就是一些常用的实现方式:
-
1、最正常的方式,读取本地经过编译后的.class文件。
-
2、从压缩包,如:zip,jar,war等文件中读取。
-
3、从网络中获取。
-
4、通过动态代理动态生成.class文件。
-
5、从数据库中读取。
执行Class(类或者接口)的加载操作需要一个类加载器,而一个良好的,合格的类加载器需要具有以下两个属性:
-
1、对于同一个Class名称,任何时候都应该返回相同的类对象
-
2、如果类加载器L1委派另一个类加载器L2来加载一个Class对象C,那么以下场景出现的任何类型T,两个类加载器L1和L2应返回相同的Class对象:
(1) C的直接父类或者父接口类型;
(2) C中的字段类型;
(3) C中方法或构造函数的中的参数类型;
(4) C中方法的返回类型
在Java中的类加载器不止一种,而对于同一个类,用不同的类加载器加载出来的对象是不相等的,那么Java是如何保证上面的两点的呢?
这就是双亲委派模式,Java中通过双亲委派模式来防止恶意加载,双亲委派模式也确保了Java的安全性。
双亲委派模式
双亲委派模式的工作流程很简单,当一个类加载器收到加载请求时,自己不去加载,而是交给它的父加载器去加载,以此类推,直到传递到最顶层类加载器,而只有当父加载器反馈说自己无法加载这个类,子加载器才会尝试去加载这个类。
上图中就是双亲委派模型,细心的人可能注意到,顶层加载器我使用了虚线来表示,因为顶层加载器是一个特殊的存在,没有父加载器,而且从实现上来说,也没有子加载器,是一个独立的加载器,因为扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)两个加载器从继承关系来看,是有父子关系的,均继承了URLClassLoader。但是虽然从类的继承关系来说启动类加载器(Bootstrap ClassLoader)没有子加载器,但是逻辑上扩展类加载器(Extension ClassLoader)还是会将收到的请求优先交给启动类加载器(Bootstrap ClassLoader)来进行优先加载。
-
启动类加载器(Bootstrap ClassLoader),负责加载$JAVA_HOME\lib下的类或者被参数-Xbootclasspath指定的能被虚拟机识别的类(通过jar名字识别,如:rt.jar),启动类加载器由Java虚拟机直接控制,开发者不能直接使用启动类加载器。
-
扩展类加载器(Extension ClassLoader),负责加载$JAVA_HOME\lib\ext下的类或者被java.ext.dirs系统变量指定的路径中所有类库(System.getProperty(“java.ext.dirs”)),开发者可以直接使用这个类加载器。
-
应用程序类加载器(Application ClassLoader),负责加载$CLASS_PATH中指定的类库。开发者能直接使用这个类加载器,正常情况下如果在我们的应用程序中没有自定义类加载器,一般用的就是这个类加载器。
-
自定义类加载器。如果需要,可以通过java.lang.ClassLoader的子类来定义自己的类加载器,一般我们都选择继承URLClassLoader来进行适当的改写就可以了。
破坏双亲委派模式
双亲委派模式并不是一个强制性的约束模型,只是一种推荐的加载模型,虽然大家大都遵守了这个规则,但是也有不遵守双亲委派模型的,比如:JNDI,JDBC等相关的SPI动作并没有完全遵守双亲委派模式
破坏双亲委派模式的一个最简单的方式就是:继承ClassLoader类,然后重写其中的loadClass方法(因为双亲委派的逻辑就写在了loadClass()方法中)。
常见异常
如果加载过程中发生异常,那么可能抛出以下异常(均为LinkageError的子类):
-
ClassCircularityError:extends或者implements了自己的类或接口
-
ClassFormatError:类或者接口的二进制格式不正确
-
NoClassDefFoundError:根据提供的全限定类名找不到对应的类或者接口
ClassNotFoundException和NoClassDefFoundError
还有一个异常ClassNotFoundException可能也会经常遇到,这个看起来和NoClassDefFoundError很相似,但其实看名字就知道ClassNotFoundException是继承自Exception,而NoClassDefFoundError是继承自Error。
- ClassNotFoundException
当JVM要加载指定文件的字节码到内存时,发现这个文件并不存在,就会抛出这个异常。这个异常一般出现在显式加载中,主要有以下三种场景:
(1)调用Class.forName() 方法
(2)调用ClassLoader中的findSystemClass() 方法
(3)调用ClassLoader中的loadClass() 方法
解决方法:一般需要检查classpath目录下是否存在指定文件。
- NoClassDefFoundError
这个异常一般出现在隐式加载中,出现的情况是可能使用了new关键字,或者是属性引用了某个类,或者是继承了某个类或者接口,或者是方法中的某个参数中引用了某个类,这时候就会触发JVM隐式加载,而在加载时发现类并不存在,则会抛出这个异常。
解决方法:确保每个引用的类都在当前classpath下
链接是获取类或接口类型的二进制形式并将其结合到Java虚拟机的运行时状态以便执行的过程。链包含三个步骤:验证,准备和解析。
注意:因为链接涉及到新数据结构的分配,所以它可能会抛出异常OutOfMemoryError。
验证(Verification)
这个步骤很好理解,类加载进来了肯定是需要对格式做一个校验,要不然什么东西都直接放到内存里面,Java的安全性就完全无法得到保障。
主要验证以下几个方面:
-
1、文件格式的验证:比如说是不是以魔数开头,jdk版本号的正确性等等。
-
2、元数据验证:比如说类中的字段是否合法,是否有父类,父类是否合法等等
-
3、字节码验证:主要是确定程序的语义和控制流是否符合逻辑
如果验证失败,会抛出一个异常VerifyError(继承自LinkageError)。
准备(Preparation)
准备工作是正式开始分配内存地址的一个阶段,主要为类或接口创建静态字段(类变量和常量),并将这些字段初始化为默认值。
以下是一些常用的初始值:
| 数据类型 | 默认值 |
| — | — |
| int | 0 |
| long | 0L |
| short | (short)0 |
| float | 0.0f |
| double | 0.0d |
| char | ‘\u0000’ |
| byte | (byte)0 |
| boolean | false |
| 引用类型 | null |
需要注意的是,假设某些字段的在常量池中已经存在了,则会直接在春被阶段就会将其赋值。
如:
static final int i = 100;
这种被final修饰的会直接被赋初始值,而不会赋默认值。
解析(Resolution)
解析阶段就是将常量池中符号引用替换为直接引用的过程。在使用符号引用之前,它必须经过解析,解析过程中符号引用会对符号引用的正确性进行检查。
注意:因为Java是支持动态绑定的,有些引用需要等到具体使用的时候才会知道具体需要指向的对象,所以解析这个步骤是可以在初始化之后才进行的。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
知其然不知其所以然,大厂常问面试技术如何复习?
1、热门面试题及答案大全
面试前做足功夫,让你面试成功率提升一截,这里一份热门350道一线互联网常问面试题及答案助你拿offer
2、多线程、高并发、缓存入门到实战项目pdf书籍
3、文中提到面试题答案整理
4、Java核心知识面试宝典
覆盖了JVM 、JAVA集合、JAVA多线程并发、JAVA基础、Spring原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB 、Cassandra、设计模式、负载均衡、数据库、一致性算法 、JAVA算法、数据结构、算法、分布式缓存、Hadoop、Spark、Storm的大量技术点且讲解的非常深入
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
mg-zHccbyYu-1713529214347)]
3、文中提到面试题答案整理
[外链图片转存中…(img-7lzJTwgq-1713529214350)]
4、Java核心知识面试宝典
覆盖了JVM 、JAVA集合、JAVA多线程并发、JAVA基础、Spring原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB 、Cassandra、设计模式、负载均衡、数据库、一致性算法 、JAVA算法、数据结构、算法、分布式缓存、Hadoop、Spark、Storm的大量技术点且讲解的非常深入
[外链图片转存中…(img-xAdcU5g1-1713529214352)]
[外链图片转存中…(img-9LDtxQDt-1713529214354)]
[外链图片转存中…(img-fXPD5u9a-1713529214355)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!