目录
-
-
- 类加载机制
-
- 加载
- 连接
- 初始化
- 类加载|初始化的时机(jvm什么时候加载|初始化一个类)
- 类加载方式
- 对象的创建过程
- 对象的内存布局
- 对象的访问方式
类加载机制
类加载也叫类初始化,包括加载、连接、初始化三个步骤。
加载
jvm将类的.class文件读到内存中,并创建对应的java.lang.Class对象。
加载由类加载器来完成,jvm提供了3种类加载器
- Bootstrap ClassLoader :根类加载器,也叫做引导类加载器,负责加载jdk中的核心类。根类加载器是用C++写的,不继承java.lang.ClassLoader。
- Extension ClassLoader :扩展类加载器,负责加载jdk中的扩展类(非核心类),扩展类加载器是用java写的,继承自ClassLoader。
- System ClassLoader:系统类加载器,负责加载第三方jar包、我们自己写的类。系统类加载器是用java写的,继承自ClassLoader。
可以继承ClassLoader类来实现自定义的类加载器。
加载顺序:根类加载器、扩展类加载器、系统类加载器、自定义的类加载器
jvm的类加载机制有3种,这3种机制共同作用,一起完成类的加载
- 全盘负责:使用类加载器加载一个类时,这个类所依赖(引用)的类也由该类加载器加载
- 父类委托:先使用父类的类加载器来加载,如果父类的类加载器加载不了,再使用类本身的类加载器来加载
- 缓存机制:jvm会缓存加载过的类的class对象,要使用某个类时,先在缓存中搜索是否有对应的class对象,有就直接使用、不再加载,没有才加载。运行程序后,如果修改了某个类,需要重启jvm才会生效,不然使用的是之前缓存的class对象。
类加载的大致过程
类加载是按需加载,使用该类时才加载,类加载生成的class对象用全类名唯一标识。
因为存在缓存机制,一个类只加载一次,且一个类在内存中最多只有一个class对象。
为什么要使用双亲委派机制去加载类?
避免重复加载同一个.class文件
连接
加载获得的class对象是二进制数据,连接是把class对象放到jre中(把class对象连接到jre),jre即java运行时环境。
连接分为3个阶段
- 校验:检验被加载的类的内部结构是否正确、和其他类是否协调一致
- 准备:为类的成员变量分配内存,设置默认的初始值,比如int赋为0,引用型赋为null
- 解析:将class对象中的符号引用替换为直接引用
class对象是加载时生成的,加载时class对象中的成员变量还没有分配内存,不知道该成员变量在内存中的地址,只能用符号引用暂时表示该成员变量在内存中的地址。连接的准备阶段给成员变量分配内存,之后就可以用直接引用(内存地址)替换掉符号引用。
初始化
初始化是对类进行初始化,不是对类的对象进行初始化。类初始化会执行static代码块、初始化static静态成员。
类加载|初始化的时机(jvm什么时候加载|初始化一个类)
- 创建类的实例。包括通过new来创建、通过反射来创建、通过反序列化来创建。
- 通过类名调用类的静态成员
- 初始化这个类的子类之前
- 通过java.exe运行主类时,JVM会先初始化这个主类,再执行这个主类
类加载方式
主要有两种
- 隐式加载:使用new创建对象,隐式调用类加载器,加载对应的类到 JVM 中,这是最常见的类加载方式
- 显式加载:使用 loadClass()、forName() 等方法显式加载需要的类,获取到 Class 对象后,调用 Class 对象的 newInstance() 方法来创建类的实例
两种类加载方式的区别
- 隐式加载能直接获取类的实例,显式加载需要调用 Class 对象的 newInstance() 方法来生成类的实例
- 隐式加载能使用带参的构造函数,而Class对象的 newInstance() 不能传入参数,如果要使用带参的构造函数,可以通过反射获取到该类带参的构造方法,通过反射调用带参的构造方法来创建实例
loadClass() 、 forName() 的区别
loadClass() 只执行类加载的第一步:加载,后续操作均未进行;Class.forName() 执行了类加载的整个过程(3步)。
对象的创建过程
1、先在常量池中定位该类的符号引用,判断是否已有该类的class对象,如果没有则先加载该类。加载时会执行static代码块、初始化静态成员
2、在堆中分配内存空间。有2种分配方式
- 指针碰撞:适用于连续的内存空间,包括开辟一块内存、移动指针两个步骤
- 空闲列表:适用于琐碎的内存空间,包括开辟一块内存、修改空闲列表两个步骤
都是2个步骤,不具有原子性,可能出现并发问题,jvm采用CAS算法实现乐观锁,搭配失败重试来保证内存分配的成功率。
3、初始化分配的内存空间。分配内存空间时是按成员变量的类型进行分配的,此时初始化成员变量为默认值,比如int型初始化为0,引用型初始化为null
4、设置对象头的相关数据,比如GC分代年龄、对象的hashCode、锁状态标识、元数据信息
5、执行普通初始块、构造函数
对象的内存布局
1、对象头用于存储对象的元数据信息
- Mark Word 部分存储对象自身的运行时数据,比如哈希值、gc分代年龄、锁状态标识
- 类型指针指向对象所属的类的元数据,标识对象所属的类
2、实例数据存储的是对象本身的数据,即各成员变量的值
3、对齐填充部分只是让实例数据占用的内存空间是8的倍数,无实际意义
对象的访问方式
对象创建之后,在java虚拟机栈中进行访问,有2种访问方式
- 直接指针访问:虚拟机栈的局部变量表中存储对象的引用(reference类型),通过引用直接访问对象
- 句柄访问:用句柄存储对象的引用,句柄放在句柄池中,虚拟机栈的局部变量表中存储对象的句柄,相当于二级指针。句柄池是堆中的一块内存。
直接指针访问效率高,但gc回收对象时效率低;句柄访问效率低,但gc回收对象时效率高。HotSpot虚拟机采用的是直接指针访问。