类的加载过程
类的加载过程
JVM运行期间完成类的加载、连接和初始化:
装载
查找并获取被加载类的字节码的二进制数据。如通过网络获取、本地文件系统、数据库中提取、动态生成(如动态代理)。
连接/链接:将二进制数据合并到JVM的运行时环境中去
a) 验证:确保字节码文件的内容是正确的,内容是符合JVM规范的(结构检查、语义检查)
b) 准备:为静态变量分配内存,并初始化为对应数据类型的默认值
c) 解析:将类中的符号引用(类、接口、字段和方法)转换为直接引用
初始化
初始化过程只会有一次。会依次执行静态成员的赋值语句和静态代码块。所以可以通过为类添加静态代码块来判断类是否被初始化。
静态变量的赋值以及静态代码块都是从上往下执行的,所以静态代码块中访问到的静态变量,必须要在静态代码块之上,否则编译就会报错。
通过添加参数 -XX:+TraceClassLoading ,运行程序时会有日志输出,能观察到哪个类被装载和连接(前两步),并且来源于哪个文件。
以上这几个阶段都是运行时期完成的,编译时期不会执行以上操作。这几个步骤的最终结果就是JVM中的一个字节码对象(java.lang.Class)。字节码对象可能会出现只装载了,却还没初始化的情况。HotSpot虚拟机将字节码对象存放在方法区。
关于静态代码块和构造代码块:
静态代码块:可以存在多个,类初始化时依次执行。
构造代码块:可以存在多个,类实例被创建时依次执行,执行完后才执行构造函数。构造函数与构造代码块的次序可随意。匿名内部类不能显式指定构造函数,所以可以通过构造代码块来完成类似构造函数的工作。
上面提到初始化过程中会依次执行静态成员的赋值语句和静态代码块,如果次序不正确的话,会导致数据问题:
static class Singleton { private Singleton() { data++; } static Singleton singleton = new Singleton(); public static int data = 0; } public static void main(String[] args) { System.out.println(Singleton.singleton.data); // 输出 0 }
解决办法就是把两个赋值语句换一下次序,结果就正常输出1了。
类的主动使用和被动使用
主动使用会触发字节码对象的初始化,被动使用不会触发初始化。
主动使用的七种情况:
- 创建类的实例
- 访问类或接口的静态变量,或对该静态变量赋值
- 反射,如Class.forName
- 初始化一个类的子类,即子类被触发初始化时,父类字节码对象如果没被初始化,则进行初始化。如果一个接口被触发初始化,则父接口不会被初始化
- 启动类,如程序启动时,会对被标记为启动类的类进行初始化
- JDK1.7提供的动态语言支持。
class.forName这个方法最终调用了3个参数的重载方法,里面有一个参数来确定是否对类进行初始化,默认值为true,可以设置为false,这样就不会初始化类了。
对类的使用,除了以上7中情况外,其余都属于被动使用。
被动使用不会导致类的初始化(如classLoader.loadClass加载的类就不会被初始化),但可能会触发这个类的装载过程,这称为“预先加载”。
当JVM认为某个类可能要被使用时,才会触发这个预先加载过程,期间如果被预先加载的类的class文件缺失、或校验有问题,则等到这个类被主动使用时,才会抛出LinkageError错误,否则不会报告错误。
被动使用的示例演示
一个通用的观察类或接口的初始化过程是否有被触发:
静态代码块有被执行,说明类被初始化。但是接口没有静态代码块,所以可以通过观察静态成员是否有被初始化:
interface Api {
static Thread handler = new Thread(){
{
System.out.println("interface init");
}
};
}
通过这个技巧来观察类在不同情况下的初始化行为,后续列举的示例都是通过这个技巧来确定类或接口是否有被初始化。
1. 通过子类来访问父类的静态字段,如Child.str 。父类的静态代码块会被触发,而子类不会,此时子类属于被动使用。因为实际访问的是父类中的字段,而不是子类的字段,实际被访问的类才会被初始化,这对应主动使用的第2条规律。
2. 访问类的静态常量时并且这是个编译时期能确定下来的常量,则通过访问这个字段时,不会触发类的初始化,也属于类的被动使用。
// 运行时不会发生变化,即编译时期能确定具体的值 public static final String str = "123"; // 运行时,具体的值才能确定下来,编译时期无法确定 public static final int r = new Random().nextInt();
这属于编译器的优化:反编译代码可以发现,这个常量被存放到了调用类的常量池中,获取数据时是直接从当前类的常量池中获取,从而使这次静态成员的访问与被调用类无关。
这个规律也同样可以应用在接口上。
3. 仅仅创建引用,如 A a; 不会导致类A的初始化。同理,A[] arr = new A[1] ,也不会导致A的初始化。
关于数组类
数组类都是由JVM运行时动态创建出来。对应的字节码的命名规则:
一维数组:[ + 元素的全限定名
二维数组:[[ + 元素的全限定名
对于基本类型的数组,基本类型没有全限定名,但用一个字母来代替,如int 对应I,char对应C等。
原文地址:https://www.cnblogs.com/hellohello/p/12002335.html
- Redis实现信息已读未读状态提示
- HashMap源码理解
- JSP Layout详细介绍
- J2Cache——Java两级缓存框架
- SSM三大框架整合详细总结(Spring+SpringMVC+MyBatis)
- 不到百行代码实现,类似iPhone的滑块开关
- 《深入理解Java虚拟机》(一)Java虚拟机发展史
- Servlet是如何实现MVC的?
- 基础篇章:关于 React Native 的props,state,style的讲解
- 《深入理解Java虚拟机》(三)垃圾收集器与内存分配策略
- 一个类似于进度和打卡进度的自定义view
- 《深入理解Java虚拟机》(四)虚拟机性能监控与故障处理工具
- 第七章:Shiro的Session管理——深入浅出学Shiro细粒度权限开发框架
- 《深入理解Java虚拟机》(六)堆内存使用分析,垃圾收集器 GC 日志解读
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 一天一大 leet(每日温度)难度:中等 DAY-11
- Go语言入门(五)结构体&练习
- 利用hexo和github或coding 搭免费个人博客
- window 指令之 tree
- Go语言入门(六)结构体后续&指针
- 一天一大 leet(二叉树的序列化与反序列化)难度:困难 DAY-16
- 一天一大 leet(三数之和)难度:中等 DAY-12
- MongoDB Docker版本:基础入门和复制集
- Django连接MySql使用models处理数据
- 一天一大 leet(爬楼梯)难度:简单 DAY-13
- 一天一大 leet(最长公共前缀)难度:简单 DAY-15
- Go语言入门(七)goroutine和channel
- 一天一大 leet(从先序遍历还原二叉树)难度:困难 DAY-18
- Go语言入门(八)线程安全&锁
- 一天一大 leet(最佳观光组合)难度:中等 DAY-17