对象的诞生
本文讨论的对象,限于普通的Java对象,不包括数组,class对象等。
在Java中,当程序执行发现new
的指令时,便会尝试着去创建一个对象。在创建之前,首先回去运行时常量池中检查,传给new
指令的参数,也就是类名,是否能定位到它的符号引用。如果能定位到,则为新生对象分配内存,如果定位不到,就会执行类加载机制,类加载流程会在之后的文章中讨论。
内存划分方式
类加载检查通过之后,有两种给对象划分内存的方式,分别是:
- 指针碰撞(Bump The Pointer)
- 空闲列表(Free List)
1.指针碰撞
这种方式很好理解,在内存比较规整的情况下,虚拟机会找到当前内存分配的指针,然后在紧接着的一块内存中为这个对象分配相应的区域。
2.空闲列表
由于程序在运行时,会一直进行无引用对象的回收,如果GC在没有空间压缩整理的方式,这样会导致内存空间往往不是连续规整的。这时候,就需要维护一个列表,告知虚拟机哪些空间是可用的。当对象被分配内存的时候,需要查询空闲列表的内存地址以及内存大小,为对象分配相应的空间。
对比来说,指针碰撞的方式比空间列表效率高得多。一般的主流虚拟机使用的收集器都采用这种方式。
对象创建的同步
对象的创建在虚拟机中是非常频繁的,在并发情况下,采用指针碰撞的方式修改一个指针的位置是不安全的。在虚拟机中,常常采用CAS的方式来保持同步,对于CAS的解释,可以参照我的这篇文章。
JVM还提供了另一种方式来保持同步,即本地线程分配缓冲(Thread Local Allocation Buffer),这种方式在线程创建对象的之前,为每个线程在堆中提供了一块内存,哪些线程需要创建对象,就在相对应的内存区域进行创建。设置TLAB的方式可以增加JVM参数-XX:+/-UserTLAB
。
对象头的设置
在给对象分配完内存之后,JVM还需要设置对象头,对象头的数据结构如下图所示:
对象头
在对象头中包含两部分信息:
1.存储自身运行时的数据,如上图所示,hashCode,GC分代年龄,锁状态标志位,线程持有锁等等。这部分数据在32位机器和64位机器中分别是32bit和64bit。这部分数据也被官方称为“Mark Word”。
2.类型指针,通过该指针可以知道此对象属于哪个类。
Java初始化对象
在JVM的工作做完之后,才开始会执行Java中的对象创建工作,即执行构造函数。对应在类文件的<init>()
方法。
对象的死亡
一个对象的死亡可以说是曲折离奇。在JVM的世界中,程序计数器,虚拟机栈,本地方法栈随着线程的生命周期死去。每一个栈帧中分配多少内存在类结构确定下来的时候就是已知的,而堆中的内存分配是在运行时进行的,对于一个对象而言,它的生命周期结束是需要垃圾收集器管理的。
可达性分析算法
现代大多数虚拟机都抛弃了引用计数法去管理内存GC,而是使用可达性分析算法。
GC root
可达性分析算法,管理模型如上图所示,当于GC root断开连接的对象,会被告知回收。
在Java体系中,固定的可作为GC Roots的对象包括:
- 在虚拟机栈中引用的对象,方法栈中的参数,局部变量,临时变量等。
- 在方法区中类静态属性引用的对象,Java类的引用类型静态变量。
- 在方法区中常量引用对象,譬如字符串常量池的引用。
- 在本地方法栈中JNI引用的对象
- Java虚拟机内部的引用,如基本的数据类型对应的Class对象,一些异常对象,还有系统类加载器。
- 所有被同步锁持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调,本地代码缓存等。
垃圾回收算法
1.标记-清除算法
从GCRoot节点开始遍历,对存活的对象进行标记。然后对堆内存从头到尾进行线性遍历,回收不可达对象。但是这样会造成堆内存中存在很多碎片空间,可能无法为较大对象分配内存。
2.复制算法
把堆空间划分为两块,一块存放对象,一块空闲。每次将存活的对象复制到空闲内存块上,然后清空存放对象块的内存。这种算法可以很好的解决碎片化的问题,简单高效,但是这种算法只适合于对象存活率较低的情况,而且它付出了一半堆内存代价。
3.标记-整理算法
和标记-清除算法
相似,先将存活的对象标记,清除的时候按照内存地址一次排列存活的对象,然后将末端内存地址回收。这样的代价比较高,但是很好的解决了空间碎片的问题,适合于对象存活较高的情况。
4.分代收集算法
这种算法是一套组合算法,为Java堆分配了不同的内存空间,执行不同的回收算法。如下图所示:
在Java8之后,取消了永久代,只保留了年轻代和老年代内存划分。
年轻代由于对象存活率较低,所以常常使用复制算法,使用的GC是Mintor GC。老年代对象存活率高,所以使用标记整理算法,使用Full GC。
堆在大多数情况下,对象优先分配在Eden中,当Eden区域没有足够的空间进行分配的时候,虚拟机将发起一次Mintor GC,当虚拟机发现没有内存可以回收的时候,就会放入Survivor区域中,一般而言,Eden区域和两块Survivor中的区域内存占比是8:1:1,当Survivor中的空间还是不足的时候,就会让对象提前进入老年代。
对于普通对象而言,诞生于Eden区,扛过第一次Mintor GC便会进入Survivor区,之后每发生一次Mintor GC,如果对象还没被清楚就会在Survivor区的两块内存中相互轮换,每次轮换年龄都会加一,默认的年龄阶段是15,当超过这个值,对象就会被发配到老年代。对于特别大的对象(使用-XX:+PretenuerSizeThreshold设置阈值),超过阈值大小的内存,不会诞生于Eden区,而会被直接放入老年代。
老年代存放的是生命周期较长的对象。Full GC比Mintor GC慢很多,通常这个数字在10倍左右,但是Full GC的执行频率很低,有以下执行条件:
- 老年代空间不足
- 永久代空间不足(JDK 7及之前)
- Mintor GC晋升到老年代的平均大小大于老年代的剩余空间。
- 调用System.gc()
- 使用RMI来进行RPC管理JDK应用,每小时执行一次full GC
除此之外,当年轻代和老年代空间都满了的时候,会执行stop-the-world,它的作用是暂停出了gc线程之外的所有线程,等gc完成之后,在恢复工作区。设计者中有个笑话是“你妈妈在打扫你屋子的时候,会先让你乖乖坐在椅子上别在扔垃圾了。”,这样看来,这种设计还是很符合人之常情的。。当然在stop-the-world发生之前,工作线程会先停在一个safepoint,然后再去执行指令。
很多时候gc调优,其实就是减少stop-the-world发生的时间。
总结
我们看到一个对象的生命周期是很曲折的。但是本质上,其实和人生的意义是相似的,无非是“我从哪里来,要到哪里去”的哲学,不同的对象寓意了不同的人生百态,虚引用对象天生只能做个标记或者哨兵,软应用的使命可以作缓存,但是撑不过第一次GC,弱引用在该牺牲的时候还是会被牺牲掉。当然强引用也有不同的命运,有些从年轻代就被回收了,有些坚持到了老年代,有些厉害的出生便是老年代,更有一批力量可以stop-the-world。不同的对象,有各自的精彩,这既是对象的前世与今生。
网友评论