一、对象创建过程
我们在程序中new一个对象时,在JVM内部,是一个什么样的流程?我们可以先看下图图示:
由上图我们不难看出,new一个对象时,是一个什么样的过程,那在这些过程中,我们需要重点关注分配内存和设置对象头,在给对象分配内存时,有两种机制,分别是指针碰撞、空闲列表:
- 指针碰撞(默认是选择这种分配方法):前提是java堆中的内存是规整的,所有用过的内存和空闲的都是各自分开的,两者的中间会放入一个指针,作为分界点的指示器,那在分配内存时,只需要挪动这个指针一段距离即可完成内存分配;
- 空闲列表:这种情况是针对内存是不规整的场景,就是说=>使用过的内存和空闲的内存是在堆中是交错存在的,那就不能使用指针碰撞了,这时,虚拟机就维护了一个列表,记录堆上那些内存是可用的,在内存分配时,就从这个列表中找到一块足够大的空间划分给当前的对象实例,然后更新列表上的记录即可完成内存分配;
但是不管是使用哪种方案进行内存分配,在并发场景下,都需要保证线程是安全的,那如何去保证的呢,JVM中采用了两种方案,分别是:CAS、本地线程分配缓冲:
- CAS:也就是Compare and swap,比较交换算法,,通过失败重试的方式更新操作原子性来保证对分配内存空间的操作是同步处理的;
- 本地线程分配缓冲(TLAB):就是每个线程在堆中预先分配一小块内存,通过-XX:+/-UseTLAB参数来设定虚拟机是否使用,同同时可以通过参数-XX:TLABSize置顶TLAB的大小;
要设置对象头,我们需要先了解一个对象中包含什么模块,每个模块都是做什么的,我们可以先看图示:
在这个图中,对齐填充如果收到指针压缩后,对齐的位数会在一定程度上减少,那什么是压缩指针呢?为什么要进行指针压缩呢?
在64位的虚拟机中,对象头的标记字段占64位,而类型指针又占64位。也就是说一个对象额外占用的字节就是16个字节。以Integer对象为例,它仅有一个int类型的私有字段,占4个字节。因此,每个Integer的额外开销至少400%,这也就是Java为什么要引入基本数据类型的原因之一。为了减少内存开销,64位Java虚拟机引入了压缩指针概念(对应虚拟机选项 -XX:+UseCompressedOops,默认开启),将堆中原本64位的Java对象指针压缩成32位的。
这样一来,对象头的类型指针也会被压缩成32位,使得对象头大小从16字节降低为12字节。压缩指针不仅可以作用对象头的类型指针,还可以作用引用类型的字段,引用类型的数组。
二、对象的内存底层分配、回收机制
2.1、对象的内存底层分配
由上图可知:给对象分配内存时,不是直接上来就将对象分配到堆内存中,首先会对当前的对象进行逃逸分析,具体解释在图中也有体现,如果不是逃逸对象,就可以将当前对象放到栈内存中,放到栈的方式我们可以采用标量替换方式,紧接着如果对象没放栈上,我们就需要下判断当前的对象是否为大对象,大对象是直接放入堆内存的老年代中的,如果不是大对象,那就需要给当前对象分配内存,分配内存的方法主要有本地线程分配缓存、CAS方式,之后对象就完成了内存分配
对象从年轻代到老年代场景分析:
- 大对象直接进入老年代:创建对象时,如果是大对象,是直接放入老年代中,原因是为了避免为大对象分配内存时的复制操作而降低效率;
- 长期存活的对象会进入老年代:在MinorGC时,如果对象没被回收,那对象就会从Eden区转到Survivor区,与此同时,也会对当前对象的代年龄+1,当这个年龄到达一定程度时,就会晋升到老年代去,也可以通过参数 -XX:MaxTenuringThreshold 来设置这个阈值;
- 动态年龄判断机制:如果一批对象放入Survivor的任何一个区时,内存大小占当前Survivor区的50%,这时,就会将当前对象年龄N(含N)以上的对象,会直接放入老年代中去,这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的。
- 老年代空间分配担保机制:当前对象在做MinorGC之前,JVM都会计算老年代的剩余空间,如果计算出的这个剩余空间是小于年轻代中所有对象总和的(包括垃圾对象),如果还配置了参数-XX:-HandlePromotionFailure(jdk1.8默认就设置了),就会用当前计算的值和年轻代每次对象进入老年代的对象总和取平均值比较,如果还是小于平均值的话,就会fullGC,否则就只进行MinorGC,如果没有配置-XX:-HandlePromotionFailure参数,就会直接进行FullGC,再进行MinorGC,具体如图示:
2.2、对象的内存底层回收
2.2.1、引用计数法
给对象添加一个引用计数器,每当有一个地方引用时,计数器就会就+1,当引用失效,计数器-1,当计数器为0时,就代表没有被引用,那就被定为垃圾,这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题
2.2.2、可达性分析算法
将GC Roots对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象,GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量,具体如下图:
三、小结
这部分内容,我们也是循序渐进的引入学习,首先,我们是先了解对象的创建过程,再有创建过程中内存分配、回收展开学习,这部分涉及到JVM内存的分配、回收等,那我们就需要着重的往这个分配的过程进行深入学习;