HotSpot中的对象

点击上方“晏霖”,选择“置顶或者星标”

曾经有人关注了我

后来他有了女朋友

1.3 HotSpot中的对象

Java是一门面向对象的编程语言,Java程序运行无时无刻都要依赖着对象,我们可以把Java中的对象比喻人的一生,我们就 要创建它、养育它、管理它最后无论是生老病死我们还要销毁它,在我们虚拟中,这一切都交给HotSpot,HotSpot可以说对对象无微不至的照顾。

 

1.3.1对象的创建

首先我们虚拟机在碰到new的指令时会检查是否能在常量池中找到这个类的符号引用,并检查这个符号引用的类是否经过了加载、解析、初始化,如果没经过这些过程就需要走一遍类的加载过程。

如果类已经加载我们进行下一步,就是分配内存,这阶段分配多大的内存已经在类加载过程确定了,接下来我们把这块内存分配在堆里。

对象分配

分配内存有两种方式,一种是指针碰撞,另一种是空闲列表。具体用那个根据堆是否完整,就是没有内存碎片,是否完整又取决于垃圾收集器是否带压缩整理功能。一般来说复制算法不会产生内存碎片,用于新生代。像CMS这种基于标记清除算法,通常采用空闲列表。

简单说下两种分配方式:

指针碰撞:这时候我们内存是完整的,使用的内存放一边,未使用的放一边,在这两个区域的临界点有一个指针,这时候来了一个对象指针向空闲内存移动对象大小的位置,这种方式叫指针碰撞。

空闲列表:这时候我们已使用和未使用的内存相互交错,虚拟机维护了一个表,记录着哪些内存可用,在分配对象的时候找一个够大的区域给他,然后更新这个表,这种叫空闲列表。

分配对象是并发操作,可能造成线程安全问题,解决这个问题有两个方式,这两个方式是共存的。第一个虚拟机采用CAS配上失败重试保证原子性,第二也是我上文提到过的缓冲区(TLAB),每个缓冲区都是线程私有的,TLAB用完了并分配新的时,才需要同步锁。虚拟机是否使用缓冲区通过参数-XX:+/-UseTLAB 默认开启。

对象终于分配完内存了,这时候的对象就像受精卵,一切属性都为初始值,接下来就要对对象进行必要的设置,就好比受精卵发育的过程,需要四肢、头、五官等,对象也是一样的,对象在内存中的组成有对象头、实例数据、对齐填充。

 

1.3.2对象的组成

在HostSpot虚拟机中,对象的组成有三部分,分别是:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)如图1-5所示,其中对象头中的MarkWord存储的对象数据较为重要。

对象头分为两部分,一部分叫MarkWord。另一部分叫类型指针。

MarkWord:用于存储自身运行时的数据,如hash码、GC分代年龄、锁标志、线程持有的锁、偏向线程ID、偏向时间戳、对象分代年龄等信息。

Mark Word结构并不是固定的,它会随着锁状态标志的变化而变化,而且里面的数据也会随着锁状态标志的变化而变化,这样做的目的是为了节省空间。

类型指针:他用于指向类元信息,其实就是对象是哪个类的实例。

实例数据:这部分区域才是正真存储有效信息的,也就是我们在程序中定义的各种类型的字段内容。

对齐填充:他可有可无,没什么意义,就是一个占位符的作用,对象头是8字节的倍数,如果达不到8的整数倍就用他补齐。

HotSpot中的对象

 

图1-5 对象在堆中的布局

 

1.3.3对象的访问

我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象,由于在Java虚拟机规范里面只规定了 reference类型(是一个指向对象的引用),并没有定义这个引用应该通过什么种方式去定位、访问到堆中的对象的具体位置,对象访问方式也是取决于虚拟机实现而定的。

对象访问分为两种,一种是使用句柄池,另一种是直接引用。

对于虚拟机HotSpot而言,它是使用直接引用方式进行对象访问,但在整个软件开发的范围来看,各种 语言、框架中使用句柄池来访问的情况也十分常见。

我们了解句柄池就好,重点知道直接引用。

句柄方式:如图1-6所示。堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体各自的地址信息。

HotSpot中的对象

 

图1-6 句柄访问

直接引用:如图1-7所示。reference中存储的直接就是对象地址。

直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问的在Java中非常频繁,因此这类开销积小成多也是一项非常可 观的执行成本。

HotSpot中的对象

 

图1-7 直接引用

 

1.3.4对象的引用

当reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称其为引用。

我们希望描述一类对象:当内存空间足够时则保留,如果内存经过垃圾回收后仍然紧张就抛弃,所以在JDK1.2对引用进行了扩充,按照对象引用的强度分为4种引用。

强引用(Strong Reference)指在程序代码之中普遍存在的,类似“Object  obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用(Soft Reference)用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收之后还没有足够的内存,才会抛出内存溢出异常。

弱引用(Weak Reference)用来描述非必需对象的,但程度比软引用更弱一些,被弱引用关联的对象只能生存生存到下一次垃圾收集发生之前。也就是只要当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

虚引用(Phantom Reference)也称为幽灵引用或幻影引用,最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过弱引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

 

1.3.5对象生死算法

我们知道在Java虚拟机中几乎所有的对象都存在堆中,垃圾收集器也是重点对这个区域进行垃圾回收,而且回收的效果也是可观的,所以我们虚拟机就要时刻清楚哪些对象是“存活”的,哪些对象是“死亡”的。

引用计数法:给对象添加一个引用计数器,当对象被引用时,计数值加1,引用失效时,计数值减1。任何时刻计数器的值为0时就是对象没有被引用。

客观的说引用计数法实现简单,执行高效,但是主流的Java虚拟机没有采用这种算法,最主要的原因是很难解决对象之间相互循环引用的问题。如果A引用B,B引用A,两个对象的引用计数器都不是0,但实际上这两个对象都已经没用了。

可达性分析算法:如图1-8所示。可达性分析的基本思想是是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索走过的路径成为引用链,当一个对象到起始点没有任何引用链相连时,也就是起始点到这个对象不可达,则可以证明这个对象是不可用的,就可以判定为可以被回收的对象,等待GC进行回收。这个算法是目前主流虚拟机使用的。

HotSpot中的对象

 

图1-8 可达性算法判断对象是否可回收

 

在Java语言中,可作为GC Roots的对象包括下面几种:

l 方法区中类静态属性引用的对象。

l 方法区中常量引用的对象。

l 虚拟机栈(栈帧中的本地变量表)中引用的对象。

l 本地方法栈中JNI(即一般说的native方法)引用的对象。

l 所有被同步(synchronized)持有的对象。

 

1.3.6 finalize()

在可达性分析中不可达的对象,例如上述图中Object5、Object6、Object7,真正要进行回收,还要经过两次标记过程。

第一次标记筛选的条件是此对象是否有必要执行finalize()方法,对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,这两种情况下finalize()方法都没必要执行,那么直接死去;

如果有必要执行,这个对象会放在一个叫F-Queue的对列中,稍后由一个虚拟机自动建立优先级较低的Finalizer线程去执行,稍后GC会对F-Queue中的对象进行第二次标记,如果在这里调用finalize()方法里,对象重新建立了与引用链上的引用,对象就会移出被即将回收的集合。但是一个对象的finalize()方法只能被执行一次,在第二次回收的的时候,finalize()方法不会被再次执行。

finalize()方法是对象逃脱死亡的最后机会,如果想在这次机会中抢救回来,只需要与引用链上的对象建立关系,例如把自己赋给某个类的变量或者对象成员变量上。

虽然finalize()方法可以拯救对象,但是不建议大家在代码中使用。系统对任何一个对象对finalize()只会调用一次,即使使用了一次finalize()方法,在对象面临下次回收时就不会执行,一样会被回收,这就叫跑的了和尚跑不了庙,你的命运就是到这了,再挣扎一会也没用,这种做法完全是一种自我安慰,所以最好就是大家忘了Java中的finalize()方法。

 

1.3.7坚强的方法区

本小结的最后来特别说一次下方法区,也是为下文做引用。方法区这部分的垃圾回收一直不被人重视,虚拟机规范也里也没说这部分用什么实现垃圾收集,好比JDK11的ZGC收集器就对不支持该区域的类型卸载。在堆区域新生代中每次都会有过半的对象被回收,回收的价值相当可观的,然而方法区的垃圾收集的条件就显得格外苛刻,也是由于这方面HotStop也没有在方法区的垃圾回收做过多的工作。

我们进一步分析原因。前面我们已经知道方法区存储了什么数据,最多的就是常量池中的常量还有类的信息,所以Java虚拟机就要对这两部分进行回收。首先我们来看一下常量这部分,举个例子String str=“hello”这个hello曾经被放进来常量池,假设现在没有任何一个引用使用“hello”这个字符串,发生垃圾回收时就会把这个常量清除掉。判断常量就很简单来,下面来看一下类是否满足回收的条件,我们知道类有他的加载器,还有类中所有的内容信息,数据量之大,还有就是这个类是否和别的类有什么引用关系。所在在虚拟机中就有三个方面判断这个类是否废弃。

l 这个类及其子类的实例已经全部被回收。

l 该类的加载器已经被回收了。

l 该类对应的 Class对象没有被任何地方引用了,不能再通过反射获取了。

 

胖虎

热 爱 生 活 的 人

终 将 被 生 活 热 爱

我在这里等你哟!

HotSpot中的对象

HotSpot中的对象