对象的生命周期
实例化对象的方式
- 明确使用
new
操作符 - 调用
Class
或是Constructor
对象的newInstance
方法 - 调用现有对象的
clone
方法 - 通过
ObjectInputStream
类的getObject
方法反序列化
实例化对象的过程
- 在堆中为对象分配内存
- 将实例变量初始化为默认值
- 如果使用了
TLAB
,这一项也可提前至TLAB
分配时进行。
- 如果使用了
- 设置对象头
- 注意对象的
hash
码是到真正使用时Object::hashCode()
才会调用
- 注意对象的
- 为实例变量赋予正确的初始值,执行
<init>
方法 - 对象终结,被垃圾收集器回收
- 如果类声明了一个finalize终结方法,垃圾收集器会在释放这个实例占据的内存前执行这个方法。
为对象分配内存
对象所需的内存大小在类加载完毕后便可以确定,
为对象分配空间实际上等同于把一块确定大小的内存从Java
堆中划分出来。
- 指针碰撞(Bump The Poiner)
假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的放在另一边,
中间使用一个指针作为分界点的指示器,那分配内存就只是将指针往空闲空间方向移动
和对象大小相等的距离即可。
- 空闲列表(Free List)
若已使用和未使用的内存交错在一起,虚拟机就必须维护一个列表,记录哪些内存块是
可用的。分配内存时先查找列表,找到一块空间分给对象,然后更新列表。
分配方式的选择
实际上取决于堆是否规则,堆是否规则取决于使用的垃圾收集器是否具有空间压缩整理(Compact
)能力。
如 Serial
、ParNew
。使用了这些虚拟机,分配的方式将是指针碰撞,否则理论上只能是空闲列表。
分配空间时的并发处理
对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也是不安全的。
两种解决方案:
对分配内存空间的动作进行同步处理(实际上虚拟机是采用
CAS
配上失败重试的方式保证更新操作的原子性)把内存分配的动作按照线程划分在不同的空间进行,即每个线程在
Java
堆中预先分配一小块内存,称为本地线程分配缓冲(
Thread Local Allocation Buffer
,TLAB
),哪个线程需要分配内存,就在哪个线程的本地缓存区中分配,只有本地缓存区用完了,分配完才需要同步锁定。
虚拟机是否使用
TLAB
,可以通过-XX:+/-UseTLAB
参数来设定。
为对象赋值
- 如果通过
clone
方法,则将被克隆的实例的值拷贝到新的实例 - 如果通过
ObjectInputStream
的readObject
方法,则通过流中读入的值来初始化 - 否则,调用对象的实例初始化方法
<init>
Java编译器为它编译的每一个类至少生成一个实例初始化方法,在Java的class文件中,这个实例初始化方法称为
<init>
。针对一个类的构造方法,Java编译器都产生一个<init>
方法。若不声明任何构造方法,则会默认产生一个无参数的构造方法。
在进行垃圾回收时,如果被回收的对象覆盖了finalize()
方法,那么该对象会被放置在一个名为F-Queue
的队列中,
并在稍后由一条虚拟机自动创建、低调度优先级的Finalizer
线程去执行它们的finalize()
方法。
在finalize()
中对象可以拯救自己,只要与引用链上的任何一个对象建立关联即可(可达性分析算法的情况),
不过finalize()
方法只会触发一次。
用
jmap -finalizerinfo vmid
可以显示F-Queue
中等待Finalizer
线程执行finalize()
方法的对象。