类加载的5个过程
类加载的本质
将描述类的数据 从Class文件加载到内存并且对数据进行校验 转换解析和初始化 最终新城虚拟机直接使用java使用类型
类加载过程
加载
作用
将外部的Class文件加载到虚拟机并且存储到方法区内
具体流程
通过类名的全限定名来获取定义此类的二进制数据
将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.class对象 作为方法区该类的各种数据的访问入口
注意
数组类是通过java虚拟机直接创建 不通过类的加载机制
验证
作用 确
保加载进来的class文件包含的信息符合jvm的要求
具体流程
文件格式的校验
元数据校验
字节码校验
符号引用校验
准备
作用
为类变量分配内存 并且设置类变量的初始值
具体流程
为类的static变量在方法区中分配内存
将上述变量的初始值设置为0
注意
实例变量不在该阶段分配内存
若该类为常量(final修饰) 直接复制开发者定义的值
解析
作用
将常量池内的符号引用转为直接引用
具体流程
解析对象(类/接口) 方法 (类方法 接口方法 方法类型 方法句柄) 字段
注意
实例变量不在该阶段分配内存
因为类方法和私有方法符合 "编译器可知 , 运行期不可变" 的要求 即不会被继承或者重写 所以适合类加载过程进行解析
若类变量为常量 (final 修饰 ) 则直接赋值开发者定义的值
初始化
作用
初始化类变量 静态语句块
具体流程
生成类构造器 clinit() 即合并所有类变量和静态语句块
执行clinit()方法
注意
类构造器clinit区别于类构造器 init
不需要调用父类构造器
子类clinit执行前 父类的clinit一定会被执行
虚拟机第一个执行的clinit是 java.lang.object
静态语句块只可被赋值不能被访问
接口与类不同 执行子接口的clinit并不需要执行负借口的clinit
对象的创建 内存分配 访问定位
对象的创建
A a =new A(); //当遇到关键字new指令时,Java对象创建过程便开始
类加载过程.png
加载过程
类加载检查
检查该new指令的参数 是否在常量池中定位到了一个类的符号引用 没有即创建对象失败
检查该类符号引用代表的类是否已经被加载,解析和初始化过
如果没有 需要先执行类的加载过程
为对象分配内存
对象所需要的内存大小在类加载完成后便可以完全确定
内存分配 根据java堆内存是否绝对规整分为
指针碰撞 Compat 收集器
假设java堆内存绝对规整 内存分配采用指针碰撞
分配形式: 已使用内存在一边 未使用的在另一边 中间放一个座位分界点的指示器
那么 分配对象内存 = 指针针向未使用内存一定一段与对象大小相等的距离
空闲列表 CMS 收集器
假设java堆内存不规整 内存分配将采用空闲列表
分配形式 :虚拟机维护着一个记录可用内存块的列表 在分配时从列表中找到一块足够大的空间划分给对象实例 ,并更新列表上的记录
内存创建在虚拟机中非常常见 存在并发情况下也会引起线程不安全
解决办法
同步处理分配内存空间 虚拟机采用CAS + 失败重试 保证更新操作的原子性
把内存分配行为按照线程划分在不同的内存空间进行
即每个线程在 Java堆中预先分配一小块内存(本地线程分配缓冲(Thread Local Allocation Buffer ,TLAB)),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时才需要同步锁。
虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
将内存空间初始化为0
内存分配完成后 虚拟机需要将分配的内存空间初始化为0
保证对象的实例字段在使用时不可复制就能直接适应 (默认为0)
如果使用本线程分配缓冲(TLAB) 这一工作过程可以提前至TLAB分配时进行
对对象进行必要的设置
设置对象是哪个类的实例 如何找到类的源数据 对象的哈希吗 对象的GC分代信息等
这个信息存放在对象的对象头中
对象的内存分配
在java对象创建后 打底是如何被存储在java内存中的呢
在java虚拟机中 对象内存中 存储布局可以分为三块
1. 对象头
1.1 对象自身的运行时数据 ( Mark Word )
如Hash 码 GC分代 锁状态 线程持有的锁 偏向线程 id 偏向时间戳
该部分数据设计成1个非固定的数据结构 一边在绩效的空间存储更多信息
1.2 对象类型指针
对象指向它的类元数据的指针
虚拟机通过这个指针确定这个对象是哪个类的实例
1.3 数组的对象头
如果是对象是数组 name在对象头中还有一块记录数组长度的数据
因为虚拟机可以通过普通java 对象的元数据确定对象的大小 但是从数组的元数据中无法确定数组的大小
2. 实例数据
存储的信息 对象真正有效的信息
代码中定义的字段内容
这部分数据的存储顺序会受到虚拟机分配参数(FieldAllocationStyle)和字段在Java源码中定义顺序的影响。
3. 对齐填充 (非必须)
存储 占位符
占位作用
因为对象的大小必须是8字节的整数倍(即对象的大小不是8字节的整数倍),就需要通过对齐填充来补全。
对象的访问定位
对象建立后 如何去访问对象?
实际上 需要访问的是 对象类型数据 和 对象实例数据
java 程序 通过 栈上的引用类型(reference) 来访问java栈上的对象
句柄访问
直接指针访问
JVM 分派
分派: 确定执行那个方法的过程
静态分派
1.1 定义
根据变量的静态类型进行方法分派的行为 根据变量的静态类型 确定执行那个方法 发生在编译器 不由JVM来执行
public class Test {
// 类定义
static abstract class Human {
}
// 继承自抽象类Human
static class Man extends Human {
}
static class Woman extends Human {
}
// 可供重载的方法
public void sayHello(Human guy) {
System.out.println("hello,Human!");
}
public void sayHello(Man guy) {
System.out.println("hello Man!");
}
public void sayHello(Woman guy) {
System.out.println("hello Woman!");
}
// 测试代码
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
Test test = new Test();
test.sayHello(man);
test.sayHello(woman);
}
}
// 运行结果
hello,Human!
hello,Human!
1.2 方法重载
重载=静态分派 =根据变量的静态类型确定执行那个重载方法
1.3 变量的静态类型发生变化
强制装换类型 改变变量的静态类型
Human man = new Man();
test.sayHello((Man)man);
1.4 静态分配的优先级匹配
静态分派优先选择参数类型一致的重载方法
没有最合适的方式 进行重载时 会选择(第二优先级)的方法重载 基本数据类型优先级分配
第二优先级顺序 : char > int > long > float > double > character > serializable> object .. args..
最后的args 为可变长参数 可以理解为数组
因为char转为byte 或者short过程不安全 所以不会选择参数类型为byte或者short进行重载
引用类型分配 根据继承关系进行优先级匹配
动态分派
2.1 定义
根据变量的动态类型确定执行那个方法
// 定义类
class Human {
public void sayHello(){
System.out.println("Human say hello");
}
}
// 继承自 抽象类Human 并 重写sayHello()
class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
// 测试代码
public static void main(String[] args) {
// 情况1
Human man = new man();
man.sayHello();
// 情况2
man = new Woman();
man.sayHello();
}
}
// 运行结果
man say hello
woman say hello
// 原因解析
// 1. 方法重写(Override) = 动态分派 = 根据 变量的动态类型 确定执行(重写)哪个方法
// 2. 对于情况1:根据变量(Man)的动态类型(man)确定调用man中的重写方法sayHello()
// 3. 对于情况2:根据变量(Man)的动态类型(woman)确定调用woman中的重写方法sayHello()