[TOC]
虚拟机把描述类的数据从Class文件**加载**到内存,并对数据进行**校验**、转换**解析**和**初始化**,最终成为被虚拟机直接使用的Java对象,这就是JVM的类加载机制。
Java天生的可动态扩展的语言特性就是依赖运行期的**动态加载**和**动态连接**实现的。
</br>
## 一:类的生命周期
类的生命周期包括7个部分:加载——验证——准备——解析——初始化——使用——卸载
其中,验证——准备——解析 称为连接阶段。除了解析外,其他阶段是顺序发生的,而解析可以与这些阶段交叉进行,因为Java支持动态绑定(晚期绑定),需要运行时才能确定具体类型。
## 二:类的初始化触发
类的加载机制没有明确的触发条件,但是有5种情况下必须对类进行初始化,那么 加载——验证——准备 就必须在此之前完成了。
1:new、getstatic、putstatic、invokestatic这4个 字节码指令 时对类进行初始化(即:**实例化对象、读写静态对象、调用静态方法时,进行类的初始化**);
2:使用反射机制对类进行调用时,进行类的初始化;
3:初始化一个类,其父类没有初始化时,先初始化其父类;
4:虚拟机启动时,初始化一个执行主类;
5:使用JDK1.7的**动态语言**支持时,如果MethodHandle实例的解析结果为REF\_getstatic、REF\_putstatic、REF\_invokestatic的方法句柄(即:读写静态对象或者调用静态方法),则初始化该句柄对应类;
一般,以上5种情况**最常见的是前三种:实例化对象、读写静态对象、调用静态方法、反射机制调用类、调用子类触发父类初始化**。
## 三:类的加载过程
从用户角度来说,类(对象)的生命周期只需笼统理解为“加载——使用——卸载”即可,无需太过深入。所以,这里的类加载过程就是我们说的 加载——验证——准备——解析(非必须)——初始化 这五个使用前的阶段。
### 1:加载
加载阶段,虚拟机需要完成三件事:**通过类名字获取类的二进制字节流——将字节流的内容转存到方法区——在内存中生成一个Class对象作为该类方法区数据的访问入口**。
其中,第一步:通过类名获取类的二进制字节流是通过类加载器来完成的。其加载过程使用“双亲委派模型”:
类加载器的层次结构为:
![](https://box.kancloud.cn/cdae2f27302b10cc7a1312d90089ab95_324x287.png)
启动类加载器:加载系统环境变量下JAVA\_HOME/lib目录下的类库。
扩展类加载器:加载JAVA\_HOME/lib/ext目录下的类库。
应用程序类加载器(系统类加载器):加载用户类路径Class\_Path指定的类库。(我们可以在使用第三方插件时,把jar包添加到ClassPath后就是使用了这个加载器)
自定义加载器:如果需要自定义加载时的规则(比如:指定类的字节流来源、动态加载时性能优化等),可以自己实现类加载器。
双亲委派模型是指:当一个类加载器收到类加载请求时,不会直接加载这个类,而是把这个加载请求委派给自己父加载器去完成。如果父加载器无法加载时,子加载器才会去尝试加载。
采用双亲委派模型的原因:避免同一个类被多个类加载器重复加载。
### 2:验证
确保class文件的二进制字节流中包含的信息符号虚拟机要求,包括:文件格式验证、元数据验证(数据语义分析)、字节码验证(数据流语义合法性)、符号引用验证(符号引用的匹配性校验,确保解析能正确执行)
### 3:准备
为**类变量(静态变量)**在**方法区**分配内存,并设置**零值**。注意:这里是类变量,不是实例变量,实例变量是对象分配到**堆内存**时根据运行时动态生成的。
### 4:解析
把常量池中的符号引用解析为直接引用:根据符号引用所作的描述,在内存中找到符合描述的目标并把目标指针指针返回。
---
关于符号引用与直接引用,我们还是用一个实例来分析吧。看下面的 Java 代码:
```
package test;
public class Test {
public static void main(String[] args) {
Sub sub = new Sub();
int a = 100;
int d = sub.inc(a);
}
}
class Sub {
public int inc(int a) {
return a + 2;
}
}
```
编译后使用 javap 分析工具,会得到下面的 Class 文件内容:
```
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."":()V
#2 = Class #16 // test/Sub
#3 = Methodref #2.#15 // test/Sub."":()V
#4 = Methodref #2.#17 // test/Sub.inc:(I)I
#5 = Class #18 // test/Test
#6 = Class #19 // java/lang/Object
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 (\[Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Test.java
#15 = NameAndType #7:#8 // "":()V
#16 = Utf8 test/Sub
#17 = NameAndType #20:#21 // inc:(I)I
#18 = Utf8 test/Test
#19 = Utf8 java/lang/Object
#20 = Utf8 inc
#21 = Utf8 (I)I
{
public test.Test();
descriptor: ()V
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
Code:
stack=2, locals=4, args_size=1
0: new #2 // class test/Sub
3: dup
4: invokespecial #3 // Method test/Sub."<init>":()V
7: astore_1
8: bipush 100
10: istore_2
11: aload_1
12: iload_2
13: invokevirtual #4 // Method test/Sub.inc:(I)I
16: istore_3
17: return
}
```
因为篇幅有限,上面的内容只保留了常量池,和 Code 部分。下面我们主要对 inc 方法的调用来进行说明。
**符号引用
**
在 main 方法的字节码中,调用 inc 方法的指令如下:
```
13: invokevirtual #4 // Method test/Sub.inc:(I)I
```
invokevirtual 指令就是调用实例方法的指令,后面的操作数 4 是 Class 文件中常量池的下标,表示用来指定要调用的目标方法。我们再来看常量池在这个位置上的内容:
```
#4 = Methodref #2.#17
```
这是一个 Methodref 类型的数据,我们再来看看虚拟机规范中对该类型的说明:
```
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
```
这实际上就是一种引用类型,tag 表示了常量池数据类型,这里固定是 10。class_index 表示了类的索引,name_and_type_index 表示了名称与类型的索引,这两个也都是常量池的下标。在 javap 的输出中,已经将对应的关系打印了出来,我们可以直接的观察到它都引用了哪些类型:
```
#4 = Methodref #2.#17 // test/Sub.inc:(I)I
|--#2 = Class #16 // test/Sub
| |--#16 = Utf8 test/Sub
|--#17 = NameAndType #20:#21 // inc:(I)I
| |--#20 = Utf8 inc
| |--#21 = Utf8 (I)I
```
这里我们将其表现为树的形式。可以看到,我们可以得到该方法所在的类,以及方法的名称和描述符。于是我们根据 invokevirtual 的操作数,找到了常量池中方法对应的 Methodref,进而找到了方法所在的类以及方法的名称和描述符,当然这些内容最终都是字符串形式。
实际上这就是一个符号引用的例子,符号引用也可以理解为像这样使用文字形式来描述引用关系。
**直接引用
**
符号引用在上面说完了,我们知道符号引用大概就是文字形式表示的引用关系。但是在方法的执行中,只有这样一串字符串,有什么用呢?方法的本体在哪里?下面这就是直接引用的概念了,这里我用自己目前的理解总结一下,直接引用就是通过对符号引用进行解析,来获得真正的函数入口地址,也就是在运行的内存区域找到该方法字节码的起始位置,从而真正的调用方法。
那么将符号引用解析为直接引用的过程是什么样的呢?我这个小渣渣目前也给不出确定的答案,在 JVM里的符号引用如何存储? 里,RednaxelaFX 大大给出了一个 Sun JDK 1.0.2 的实现;在 自己动手写Java虚拟机 中,作者给出了一种用 Go 的简单实现,下面这里就来看一下这个简单一些的实现。在 HotSpot VM 中的实现肯定要复杂得多,这里还是以大致的学习了解为主,以后如果有时间有精力,再去研究一下 OpenJDK 中 HotSpot VM 的实现。
不过不管是哪种实现,肯定要先读取 Class 文件,然后将其以某种格式保存在内存中,类的数据会记录在某个结构体内,方法的数据也会记录在另外的结构体中,然后将结构体之间相互组合、关联起来。比如,我们用下面的形式来表达 Class 的数据在内存中的保存形式:
```
type Class struct {
accessFlags uint16 // 访问控制
name string // 类名
superClassName string // 父类名
interfaceNames []string // 接口名列表
constantPool *ConstantPool // 该类对应的常量池
fields []*Field // 字段列表
methods []*Method // 方法列表
loader *ClassLoader // 加载该类的类加载器
superClass *Class // 父类结构体的引用
interfaces []*Class // 各个接口结构体的引用
instanceSlotCount uint // 类中的实例变量数量
staticSlotCount uint // 类中的静态变量数量
staticVars Slots // 类中的静态变量的引用列表
initStarted bool // 类是否被初始化
}
```
类似的,常量池中的方法引用,也要有类似的结构来表示:
```
type MethodRef struct {
cp *ConstantPool // 常量池
className string // 所在的类名
class *Class // 所在的类的结构体引用
name string // 方法名
descriptor string // 描述符
method *Method // 方法数据的引用
}
```
回到上面符号解析的例子。当遇到 invokevirtual 指令时,根据后面的操作数,可以去常量池中指定位置取到方法引用的结构体。实际上这个结构体中已经包含了上面看到的各种符号引用,最下面的 method 就是真正的方法数据。类加载到内存中时,method 的值为空,当方法第一次调用时,会根据符号引用,找到方法的直接引用,并将值赋予 method。从而后面再次调用该方法时,只需要返回 method 即可。下面我们看方法的解析过程:
```
func (self *MethodRef) resolveMethodRef() {
c := self.ResolvedClass()
method := lookupMethod(c, self.name, self.descriptor)
if method == nil {
panic("java.lang.NoSuchMethodError")
}
self.method = method
}
```
这里面省略了验证的部分,包括检查解析后的方法是否为空、检查当前类是否可以访问该方法,等等。首先我们看到,第一步是找到方法对应的类:
```
func (self *SymRef) ResolvedClass() *Class {
if self.class == nil {
d := self.cp.class
c := d.loader.LoadClass(self.className)
self.class = c
}
return self.class
}
```
在 MethodRef 结构体中包含对应 class 的引用,如果 class 不为空,则可以直接返回;否则会根据类名,使用当前类的类加载器去尝试加载这个类。最后将加载好的类引用赋给 MethodRef.class。找到了方法所在的类,下一步就是从类中找到这个方法,也就是方法数据在内存中的地址,对应上面的 lookupMethod 方法。查找时,会遍历类中的方法列表,这块在类加载的过程中已经完成,下面是方法数据的结构体:
```
type Method struct {
accessFlags uint16
name string
descriptor string
class *Class
maxStack uint
maxLocals uint
code []byte
argSlotCount uint
}
```
这个其实就和 Class 文件中的 Code 属性类似,这里面省略了异常和其他的一些信息。类加载过程中,会将各个方法的 Code 属性按照上面的结构保存在内存中,然后将类中所有方法的地址列表保存在 Class 结构体中。当在 Class 结构体中查找指定方法时,只需要遍历方法列表,然后比较方法名和描述符即可:
```
for c := class; c != nil; c = c.superClass {
for _, method := range c.methods {
if method.name == name && method.descriptor == descriptor {
return method
}
}
}
```
可以看到,查找方法会从当前方法查找,如果找不到,会继续从父类中查找。除此以外,还会从实现的接口列表中查找,代码中省略了这部分,还有一些判断的条件。
最终,如果成功找到了指定方法,就会将方法数据的地址赋给 MethodRef.method,后面对该方法的调用只需要直接返回 MethodRef.method 即可。
### 5:初始化
真正开始执行Java程序代码,该步执行方法根据代码赋值语句,对**类变量和其他资源** 进行初始化赋值。
方法:编译器自动收集类中所有 类变量的赋值语句和静态语句合并而成,收集的顺序是在程序代码出现的顺序。所以,静态语句中只能访问到定义在静态语句块之前的变量,在其之后的变量可以赋值(相当于新建并赋值了)但不可以访问(因为还没出现)。
注:由此步我们就可以得知,我们在分析向上转型的例子时的程序代码的运行顺序了:父类静态内容——子类静态内容——父类构造——子类构造——子类方法 。
在经历了上面5步“加载”阶段后,才真正地可以使用class对象或者使用实例对象。使用过后,不再需要用到该类的class对象或者实例对象时,就会把类卸载掉(发生在方法区的垃圾回收:无用类的卸载)。
## 四:对象的生命周期
对象是由类创建出来的,所以对象的生命周期就是包含在类的生命周期中:
类加载(5步)——创建类的实例对象——使用对象——对象回收——类卸载
- 一.JVM
- 1.1 java代码是怎么运行的
- 1.2 JVM的内存区域
- 1.3 JVM运行时内存
- 1.4 JVM内存分配策略
- 1.5 JVM类加载机制与对象的生命周期
- 1.6 常用的垃圾回收算法
- 1.7 JVM垃圾收集器
- 1.8 CMS垃圾收集器
- 1.9 G1垃圾收集器
- 2.面试相关文章
- 2.1 可能是把Java内存区域讲得最清楚的一篇文章
- 2.0 GC调优参数
- 2.1GC排查系列
- 2.2 内存泄漏和内存溢出
- 2.2.3 深入理解JVM-hotspot虚拟机对象探秘
- 1.10 并发的可达性分析相关问题
- 二.Java集合架构
- 1.ArrayList深入源码分析
- 2.Vector深入源码分析
- 3.LinkedList深入源码分析
- 4.HashMap深入源码分析
- 5.ConcurrentHashMap深入源码分析
- 6.HashSet,LinkedHashSet 和 LinkedHashMap
- 7.容器中的设计模式
- 8.集合架构之面试指南
- 9.TreeSet和TreeMap
- 三.Java基础
- 1.基础概念
- 1.1 Java程序初始化的顺序是怎么样的
- 1.2 Java和C++的区别
- 1.3 反射
- 1.4 注解
- 1.5 泛型
- 1.6 字节与字符的区别以及访问修饰符
- 1.7 深拷贝与浅拷贝
- 1.8 字符串常量池
- 2.面向对象
- 3.关键字
- 4.基本数据类型与运算
- 5.字符串与数组
- 6.异常处理
- 7.Object 通用方法
- 8.Java8
- 8.1 Java 8 Tutorial
- 8.2 Java 8 数据流(Stream)
- 8.3 Java 8 并发教程:线程和执行器
- 8.4 Java 8 并发教程:同步和锁
- 8.5 Java 8 并发教程:原子变量和 ConcurrentMap
- 8.6 Java 8 API 示例:字符串、数值、算术和文件
- 8.7 在 Java 8 中避免 Null 检查
- 8.8 使用 Intellij IDEA 解决 Java 8 的数据流问题
- 四.Java 并发编程
- 1.线程的实现/创建
- 2.线程生命周期/状态转换
- 3.线程池
- 4.线程中的协作、中断
- 5.Java锁
- 5.1 乐观锁、悲观锁和自旋锁
- 5.2 Synchronized
- 5.3 ReentrantLock
- 5.4 公平锁和非公平锁
- 5.3.1 说说ReentrantLock的实现原理,以及ReentrantLock的核心源码是如何实现的?
- 5.5 锁优化和升级
- 6.多线程的上下文切换
- 7.死锁的产生和解决
- 8.J.U.C(java.util.concurrent)
- 0.简化版(快速复习用)
- 9.锁优化
- 10.Java 内存模型(JMM)
- 11.ThreadLocal详解
- 12 CAS
- 13.AQS
- 0.ArrayBlockingQueue和LinkedBlockingQueue的实现原理
- 1.DelayQueue的实现原理
- 14.Thread.join()实现原理
- 15.PriorityQueue 的特性和原理
- 16.CyclicBarrier的实际使用场景
- 五.Java I/O NIO
- 1.I/O模型简述
- 2.Java NIO之缓冲区
- 3.JAVA NIO之文件通道
- 4.Java NIO之套接字通道
- 5.Java NIO之选择器
- 6.基于 Java NIO 实现简单的 HTTP 服务器
- 7.BIO-NIO-AIO
- 8.netty(一)
- 9.NIO面试题
- 六.Java设计模式
- 1.单例模式
- 2.策略模式
- 3.模板方法
- 4.适配器模式
- 5.简单工厂
- 6.门面模式
- 7.代理模式
- 七.数据结构和算法
- 1.什么是红黑树
- 2.二叉树
- 2.1 二叉树的前序、中序、后序遍历
- 3.排序算法汇总
- 4.java实现链表及链表的重用操作
- 4.1算法题-链表反转
- 5.图的概述
- 6.常见的几道字符串算法题
- 7.几道常见的链表算法题
- 8.leetcode常见算法题1
- 9.LRU缓存策略
- 10.二进制及位运算
- 10.1.二进制和十进制转换
- 10.2.位运算
- 11.常见链表算法题
- 12.算法好文推荐
- 13.跳表
- 八.Spring 全家桶
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 事务管理
- 4.SpringMVC 运行流程和手动实现
- 0.Spring 核心技术
- 5.spring如何解决循环依赖问题
- 6.springboot自动装配原理
- 7.Spring中的循环依赖解决机制中,为什么要三级缓存,用二级缓存不够吗
- 8.beanFactory和factoryBean有什么区别
- 九.数据库
- 1.mybatis
- 1.1 MyBatis-# 与 $ 区别以及 sql 预编译
- Mybatis系列1-Configuration
- Mybatis系列2-SQL执行过程
- Mybatis系列3-之SqlSession
- Mybatis系列4-之Executor
- Mybatis系列5-StatementHandler
- Mybatis系列6-MappedStatement
- Mybatis系列7-参数设置揭秘(ParameterHandler)
- Mybatis系列8-缓存机制
- 2.浅谈聚簇索引和非聚簇索引的区别
- 3.mysql 证明为什么用limit时,offset很大会影响性能
- 4.MySQL中的索引
- 5.数据库索引2
- 6.面试题收集
- 7.MySQL行锁、表锁、间隙锁详解
- 8.数据库MVCC详解
- 9.一条SQL查询语句是如何执行的
- 10.MySQL 的 crash-safe 原理解析
- 11.MySQL 性能优化神器 Explain 使用分析
- 12.mysql中,一条update语句执行的过程是怎么样的?期间用到了mysql的哪些log,分别有什么作用
- 十.Redis
- 0.快速复习回顾Redis
- 1.通俗易懂的Redis数据结构基础教程
- 2.分布式锁(一)
- 3.分布式锁(二)
- 4.延时队列
- 5.位图Bitmaps
- 6.Bitmaps(位图)的使用
- 7.Scan
- 8.redis缓存雪崩、缓存击穿、缓存穿透
- 9.Redis为什么是单线程、及高并发快的3大原因详解
- 10.布隆过滤器你值得拥有的开发利器
- 11.Redis哨兵、复制、集群的设计原理与区别
- 12.redis的IO多路复用
- 13.相关redis面试题
- 14.redis集群
- 十一.中间件
- 1.RabbitMQ
- 1.1 RabbitMQ实战,hello world
- 1.2 RabbitMQ 实战,工作队列
- 1.3 RabbitMQ 实战, 发布订阅
- 1.4 RabbitMQ 实战,路由
- 1.5 RabbitMQ 实战,主题
- 1.6 Spring AMQP 的 AMQP 抽象
- 1.7 Spring AMQP 实战 – 整合 RabbitMQ 发送邮件
- 1.8 RabbitMQ 的消息持久化与 Spring AMQP 的实现剖析
- 1.9 RabbitMQ必备核心知识
- 2.RocketMQ 的几个简单问题与答案
- 2.Kafka
- 2.1 kafka 基础概念和术语
- 2.2 Kafka的重平衡(Rebalance)
- 2.3.kafka日志机制
- 2.4 kafka是pull还是push的方式传递消息的?
- 2.5 Kafka的数据处理流程
- 2.6 Kafka的脑裂预防和处理机制
- 2.7 Kafka中partition副本的Leader选举机制
- 2.8 如果Leader挂了的时候,follower没来得及同步,是否会出现数据不一致
- 2.9 kafka的partition副本是否会出现脑裂情况
- 十二.Zookeeper
- 0.什么是Zookeeper(漫画)
- 1.使用docker安装Zookeeper伪集群
- 3.ZooKeeper-Plus
- 4.zk实现分布式锁
- 5.ZooKeeper之Watcher机制
- 6.Zookeeper之选举及数据一致性
- 十三.计算机网络
- 1.进制转换:二进制、八进制、十六进制、十进制之间的转换
- 2.位运算
- 3.计算机网络面试题汇总1
- 十四.Docker
- 100.面试题收集合集
- 1.美团面试常见问题总结
- 2.b站部分面试题
- 3.比心面试题
- 4.腾讯面试题
- 5.哈罗部分面试
- 6.笔记
- 十五.Storm
- 1.Storm和流处理简介
- 2.Storm 核心概念详解
- 3.Storm 单机版本环境搭建
- 4.Storm 集群环境搭建
- 5.Storm 编程模型详解
- 6.Storm 项目三种打包方式对比分析
- 7.Storm 集成 Redis 详解
- 8.Storm 集成 HDFS 和 HBase
- 9.Storm 集成 Kafka
- 十六.Elasticsearch
- 1.初识ElasticSearch
- 2.文档基本CRUD、集群健康检查
- 3.shard&replica
- 4.document核心元数据解析及ES的并发控制
- 5.document的批量操作及数据路由原理
- 6.倒排索引
- 十七.分布式相关
- 1.分布式事务解决方案一网打尽
- 2.关于xxx怎么保证高可用的问题
- 3.一致性hash原理与实现
- 4.微服务注册中心 Nacos 比 Eureka的优势
- 5.Raft 协议算法
- 6.为什么微服务架构中需要网关
- 0.CAP与BASE理论
- 十八.Dubbo
- 1.快速掌握Dubbo常规应用
- 2.Dubbo应用进阶
- 3.Dubbo调用模块详解
- 4.Dubbo调用模块源码分析
- 6.Dubbo协议模块