-
@Description:通过子类引用父类的静态字段,不会导致子类初始化
-
@author: 牧小农
-
@create: 2021-02-27 11:42
-
@Version 1.0
**/
public class Test1 {
static {
System.out.println(“Init Superclass!!!”);
}
public static void main(String[] args) {
int x = Son.count;
}
}
class Father extends Test1{
static int count = 1;
static {
System.out.println(“Init father!!!”);
}
}
class Son extends Father{
static {
System.out.println(“Init son!!!”);
}
}
输出:
Init Superclass!!!
Init father!!!
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。至于是否要触发子类的加载和验证,在虚拟机中并未明确规定,这点取决于虚拟机的具体实现。对于Sun HotSpot虚拟机来说,可通过-XX:+TraceClassLoading参数观察到此操作会导致子类的加载。
上面的案例中,由于count字段是在Father类中定义的,因此该类会被初始化,此外,在初始化类Father的时候,虚拟机发现其父类Test1 还没被初始化,因此虚拟机将先初始化其父类Test1 ,然后初始化子类Father,而Son始终不会被初始化;
- 通过数组定义来引用类,不会触发此类的初始化
/**
-
@program: jvm
-
@ClassName Test2
-
@description:
-
@author: muxiaonong
-
@create: 2021-02-27 12:03
-
@Version 1.0
**/
public class Test2 {
public static void main(String[] args) {
M[] m = new M[8];
}
}
class M{
static {
System.out.println(“Init M!!!”);
}
}
运行之后我们会发现没有输出 “Init M!!!”,说明没有触发类的初始化阶段
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
/**
-
@program: jvm
-
@ClassName Test3
-
@description:
-
@author: muxiaonong
-
@create: 2021-02-27 12:05
-
@Version 1.0
**/
public class Test3 {
public static void main(String[] args) {
System.out.println(ConstClass.COUNT);
}
}
class ConstClass{
static final int COUNT = 1;
static{
System.out.println(“Init ConstClass!!!”);
}
}
上面代码运行后也没有输出 Init ConstClass!!!
,这是因为虽然在Java源码中引用了ConstClass 类中的常量COUNT ,但其实在编译阶段通过常量传播优化,已经将常量的值 "1"
存储到Test3 常量池中了,对常量ConstClass.COUNT的引用实际都被转化为Test3 类对自身常量池的引用了,也就是说,实际上Test3 的Class文件之中并没有ConstClass类的符号引用入口,这两个类在编译为Class文件之后就不存在关系
有一个名叫Class文件,它静静的躺在了硬盘上,吃香的喝辣的,他究竟需要一个怎么样的过程经历了什么,才能够从舒服的硬盘中到内存中呢?class进入内存总共有三大步。
-
加载(Loading)
-
连接(Linking)
-
初始化(Initlalizing)
1、加载
加载 是 类加载(Class Loading) 过程的一个阶段,加载 是 类加载(Class Loading) 过程的一个阶段,加载是指将当前类的class文件读入内存中,并且创建一个 java.lang.Class
的对象,也就是说,当程序中使用任何类的时候,系统都会创建一个叫 java.lang.Class
对象
在加载阶段,虚拟机需要完成以下三个事情:
-
通过一个类的全限定名类获取定义此类的二进制字节流(没有指明只能从一个Class文件中获取,可以从其他渠道,如:网络、动态生成、数据库等)
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
-
在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口
类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。加载阶段与连接阶段的部分内容是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在夹在阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。
2、连接
当类被加载之后,系统会生成一个对应的Class对象,就会进入 连接阶段,连接阶段负责把类的二进制数据合并到JRE中,连接阶段又分为三个小阶段
1.1 验证
验证是连接阶段的第一步,这一阶段的主要目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。Java语言相对于 C/C++ 来说本身是相对安全的语言,验证阶段是非常重要的,这个阶段是否严谨,决定了Java虚拟机能不能承受恶意代码的攻击,当验证输入的字节流不符合Class文件格式的约束时,虚拟机会抛出一个 java.lang.VerifyError
异常或者子类异常,从大体来说验证主要分为四个校验动作:文件格式验证、元数据验证、字节码验证、符号引用验证
文件格式验证: 主要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。主要包含以下几个方面:
-
文件格式是否以
CAFEBABE
开头 -
主次版本是否在虚拟机处理的范围内
-
常量池的常量是否有不被支持的常量类型
-
指向常量的各种索引值是否有指向不存在的常量或者不符合类型的常量
-
CONSTANT_Utf8_info 型的常量是否有不符合UTF8编码的数据
-
Class文件中各个部分及文件本身是否有被删除的活附件的信息
元数据验证: 主要是对字节码描述的信息进行语义分析,主要目的是对类的元数据进行语义校验,分析是否符合Java的 语言语法的规范,保证不存在不符合Java语言的规范的元数据的信息,该阶段主要验证的方面包含以下几个方面:
-
这个类是否有父类(除java.lang.Object)
-
这个类的父类是否继承了不允许被继承的类(被final 修饰的类)
-
如果这个类不是抽象类,是否实现了父类或接口之中要求的所有方法
-
类中的字段、方法是否和父类产生矛盾
字节码验证: 最重要也是最复杂的校验环节,通过数据流和控制流分析程序语义是否合法、符合逻辑的。主要针对类的方法体进行校验分析,保证被校验的类在运行时不会危害虚拟机安全的事情
-
保证任何时候操作数栈的数据类型和指令代码序列都能配合工作(例如在操作栈上有一个int类型的数据,保证不会在使用的时候按照long类型来加载到本地变量表中)
-
跳转指令不会条状到方法体以外的字节码指令上
-
保证方法体中的数据转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是不能把父类赋值给子类数据类型
符号引用验证: 针对符号引用转换直接引用的时候,这个装换工作会在第三阶段(字节码验证)解析阶段中发生。主要是保证引用一定会被访问到,不会出现类无法访问的问题。
1.2 准备
为类变量 分配内存并设置类变量初始值的阶段,这些变量所使用的内存都会在方法区进行分配,在准备阶段是把class文件静态变量赋默认值,注意:不是赋初始值,比如我们 public static int i = 8
,在这个步骤 并不是把 i 赋值成8 ,而是先赋值为0
基本类型的默认值:
| 数据类型 | 默认值 |
| — | — |
| int | 0 |
| long | 0L |
| short | (short)0 |
| char | ‘\u0000’ |
| byte | (byte)0 |
| boolean | false |
| float | 0.0f |
| double | 0.0d |
| reference | null |
在通常情况下初始值是0,但是如果我们把上面的常量加一个final 类修饰的话,那么这个时候初始值就会编程我们指定的值 public static final int i = 8
编译的时候Javac会把i的初始值变为8,
1.3 解析
把class文件常量池里面用到的符号引用转换为直接内存地址,直接可以访问到的内容
符号引用:以一组符号来描述所引用的目标,符号可以是任何字面形式的字面量,只要不会出现冲突能够定位到就可以
直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,如果有了直接引用,那引用的目标必定已经在内存中存在了
3、初始化
初始化是给类的静态变量赋正确的初始值,刚才我们有讲到准备阶段是复制默认值,而初始化是给静态变量赋值初始值,看下面的语句:
public static int i = 8
首先字节码文件被加载到内存后,先进行连接验证,通过准备阶段,给i分配内存,因为是static,所以这个时候i 等于int类型的默认初始值是0,所以i 现在是 0,到了初始化的时候,才会真正把i 赋值为8
类加载器负责加载所有的类,并且为载入内存中的类生成一个 java.lang.Class实例对象,如果一个类被加载到JVM中后,同一个类不会再次被载入,就像对象有一个唯一的标识,同样载入的JVM的类也有一个唯一的标识。JVM本身有一个类加载器的层次,这个类加载器本身就是一个普通的Class,所有的Class都是被类加载器加载到内存中,我们可以称之为ClassLoader,一个顶级的父类,也是一个abstract抽象类。
Bootstrap: 类加载器的加载过程,分成不同的层次来进行加载,不同的类加载器加载不同的Class,作为最顶层的Bootstrap,它加载lib里JDK最核心的内容,比如说rt.jar charset.jar等核心类,当我们调用getClassLoader()拿到这个加载器结果是一个Null的时候,代表我们已经达到了最顶层的加载器
Extension: Extension加载器扩展类,加载扩展包里的各种各样的文件,这些扩展包在JDK安装目录 jre/lib/ext下的jar
App: 就是我们平时用到的application ,用来加载classpath指定的内容
Custom ClassLoader: 自定义ClassLoader,加载自己自定义的加载器 Custom ClassLoader 的父类加载器是 application 的父类加载器是 Extension的父类加载器是Bootstrap
注意:他们不是继承关系,而是委托关系
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://img-blog.csdnimg.cn/img_convert/717894192efb50d2f21c390ea6dce61d.jpeg)
最后
按照上面的过程,4个月的时间刚刚好。当然Java的体系是很庞大的,还有很多更高级的技能需要掌握,但不要着急,这些完全可以放到以后工作中边用别学。
学习编程就是一个由混沌到有序的过程,所以你在学习过程中,如果一时碰到理解不了的知识点,大可不必沮丧,更不要气馁,这都是正常的不能再正常的事情了,不过是“人同此心,心同此理”的暂时而已。
“道路是曲折的,前途是光明的!”
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
img" style=“zoom: 33%;” />
最后
按照上面的过程,4个月的时间刚刚好。当然Java的体系是很庞大的,还有很多更高级的技能需要掌握,但不要着急,这些完全可以放到以后工作中边用别学。
学习编程就是一个由混沌到有序的过程,所以你在学习过程中,如果一时碰到理解不了的知识点,大可不必沮丧,更不要气馁,这都是正常的不能再正常的事情了,不过是“人同此心,心同此理”的暂时而已。
“道路是曲折的,前途是光明的!”
[外链图片转存中…(img-1wMA4r5r-1713550728317)]
[外链图片转存中…(img-T4DIBC0G-1713550728318)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!