文章目录
1. 对象
一个对象包含的信息有:对象头,对象实例数据、对齐填充
(图片来源于网络)
2. 怎么创建对象
创建对象,要完成的是在内存给对象分配到一块空间,并设置相应的信息。创建对象需要以下5步:
Step1:类加载检查
检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
Step2:分配内存
类加载之后,虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从Java
堆中划分出来。
Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java
代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC
分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
Step5:执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java
程序的视角来看,对象创建才刚开始,<init>
方法还没有执行,所有的字段都还为零。所以一般来说,执行 new
指令之后会接着执行 <init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
3. 内存分配方式 - 怎么给对象分配内存
分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
3.1 指针碰撞
用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界值指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
(图片来源于网络)
3.2 空闲列表
虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的。在分配的时候,找块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
(图片来源于网络)
4. 对象访问定位 - 怎么找到对象
主流的访问方式有使用句柄和直接指针两种。
4.1 使用句柄
Java
堆中将会划分出一块内存来作为句柄池,reference
中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
(图片来源于网络)
4.2 直接指针
Java
堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference
中存储的直接就是对象的地址。
(图片来源于网络)
5. 如何判断对象不可用
5.1 引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
对象无法回收的情况:
如果有两个对象相互引用,那么它们的引用计数器都不为0,垃圾回收器无法回收它们。
5.2 可达性分析
从 GC Root
作为起点,向下搜索,遍历可以到达的节点。节点走过的路径是引用链,如果一个对象没有引用链相连,那么就说明这个对象是不可用的。
GC Root
的特点是 当前存活的对象。它需要保证引用所指向的对象都是活着的,当前线程栈帧中的对象在当前时刻肯定是活着的。
可以作为 GC Root
的有(栈、方法区、同步锁持有的对象):
-
虚拟机栈(栈帧中的本地变量表)中引用的对象。
-
本地方法栈(
Native
方法)中引用的对象 -
方法区中类静态属性引用的对象
static
-
方法区中常量引用的对象。
String s = "abc"
-
所有被同步锁持有的对象。对象的方法中有加锁(用
synchronized
修饰)的方法