前言
Java是一门面向对象的编程语言,Java程序运行无时无刻都要依赖着对象,我们java虚拟机要创建她、养育她、管理她最后还要销毁她,HotSpot可以说对对象无微不至的照顾,想成为HotSpot一样吗?管理着成千上万的‘女朋友’,胖虎带着你们一探究竟。
正文
无中生有不可能,对象是活活存在的,所以第一步我们要创建她。
对象的创建
首先我们虚拟机在碰到new的指令时会检查是否能在常量池中找到这个类的符号引用,并检查这个符号引用的类是否经过了加载、解析、初始化(这是类的加载过程,后面文章会有详细介绍),如果没经过这些过程就需要走一遍类的加载过程。
如果类已经加载我们进行下一步,就是分配内存,这阶段分配多大的内存已经在类加载过程确定了,接下来我们把这块内存分配在堆里。
对象分配
分配内存有两种方式,一种是指针碰撞,另一种是空闲列表。具体用那个根据堆是否完整,就是没有内存碎片,是否完整又取决于垃圾收集器是否带压缩整理功能。一般来说复制算法不会产生内存碎片,用于新生代。像CMS这种基于标记清除算法,通常采用空闲列表。
简单说下两种分配方式
指针碰撞:这时候我们内存是完整的,使用的内存放一边,未使用的放一边,在这两个区域的临界点有一个指针,这时候来了一个对象指针向空闲内存移动对象大小的位置,这种方式叫指针碰撞。
空闲列表:这时候我们已使用和未使用的内存相互交错,虚拟机维护了一个表,记录着哪些内存可用,在分配对象的时候找一个够大的区域给他,然后更新这个表,这种叫空闲列表。
分配对象是并发操作,可能造成线程安全问题,解决这个问题有两个方式,这两个方式是共存的。第一个虚拟机采用CAS配上失败重试保证原子性,第二也是我上文提到过的缓冲区(TLAB),每个缓冲区都是线程私有的,TLAB用完了并分配新的时,才需要同步锁。虚拟机是否使用缓冲区通过参数-XX:+/-UseTLAB 默认开启。
对象终于分配完内存了,这时候的对象就像受精卵,一切属性都为初始值,接下来就要对对象进行必要的设置,就好比受精卵发育的过程,需要四肢、头、五官等,对象也是一样的,对象在内存中的组成有对象头、实例数据、对齐填充。
对象组成
对象头分为两部分,一部分叫MarkWord。另一部分叫类型指针。
MarkWord:用于存储自身运行时的数据,如hash码、GC分代年龄、锁标志、线程持有的锁、偏向线程ID、偏向时间戳、对象分代年龄等信息。
Mark Word结构并不是固定的,它会随着锁状态标志的变化而变化,而且里面的数据也会随着锁状态标志的变化而变化,这样做的目的是为了节省空间。
类型指针:他用于指向类元信息,其实就是对象是哪个类的实例。
实例数据:这部分区域才是正真存储有效信息的,也就是我们在程序中定义的各种类型的字段内容。。
对齐填充:他可有可无,没什么意义,就是一个占位符的作用,对象头是8字节的倍数,如果达不到8的整数倍就用他补齐。
经过了多年的努力,这个女孩子已经长大了,需要走向社会,结交朋友,这时候单身狗们就要找她要联系方式访问他。
对象的访问
女孩子长大了,上了户口得到了教育最后被一个叫姑爷的人连盆带根搬走了。
对象创建了,分配了内存,设置好了各种参数后要被别人使用了。
我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象,由于在Java虚拟机规范里面只规定了 reference类型(是一个指向对象的引用),并没有定义这个引用应该通过什么种方式去定位、访问到堆中的对象的具体位置,对象访问方式也是取决于虚拟机实现而定的。
对象引用分为两种,一种是使用句柄池,另一种是直接引用。
就虚拟机HotSpot而言,它是使用直接引用方式进行对象访问,但在整个软件开发的范围来看,各种 语言、框架中使用句柄池来访问的情况也十分常见。
HotSpot主张年轻人自由恋爱,不需要媒婆(句柄池)。
小伙伴们了解句柄池就好,重点知道直接引用。
句柄方式:堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体各自的地址信息。
直接引用:reference中存储的直接就是对象地址。
直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问的在Java中非常频繁,因此这类开销积小成多也是一项非常可 观的执行成本。
经历过上面阶段的对象也算是见过世面的。人有生老病死,有的人生病了也能抢救回来,有的人英年早逝,有的人长命百岁,对象也是一样,有的对象存活时间很短,有的对象存活时间很长,有的对象通过两次标记又活了下来。所以下面也是对象的最后阶段了,要么生,要么死。
对象的生与死
Java堆中存在的对象实例,垃圾回收器再进行回收前,首先要判断对象是生存还是死亡。
我能给你一切,我就有能力收回来。
判断对象生死算法
引用计数法:给对象添加一个引用计数器,当对象被引用时,计数值加1,引用失效时,计数值减1。任何时刻计数器的值为0时就是对象没有被引用。
客观的说引用计数法实现简单,执行高效,但是主流的java虚拟机没有采用这种算法,最主要的原因是很难解决对象之间相互循环引用的问题。如果A引用B,B引用A,两个对象的引用计数器都不是0,但实际上这两个对象都已经没用了。
可达性分析算法:可达性分析的基本思想是是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索走过的路径成为引用链,当一个对象到起始点没有任何引用链相连时,也就是起始点到这个对象不可达,则可以证明这个对象是不可用的,就可以判定为可以被回收的对象,等待GC进行回收。这个算法是目前主流虚拟机使用的。
在java语言中,可作为GC Roots的对象包括下面几种:
-
方法区中类静态属性引用的对象
-
方法区中常量引用的对象
-
虚拟机栈(栈帧中的本地变量表)中引用的对象
-
本地方法栈中JNI(即一般说的native方法)引用的对象
对象引用
我们希望描述一类对象:当内存空间足够时保留,如果内存经过垃圾回收后仍然紧张就抛弃,所以在JDK1.2对引用进行了扩充。
强引用(Strong Reference)指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用(Soft Reference)用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收之后还没有足够的内存,才会抛出内存溢出异常。SoftReference类实现了软引用。
弱引用(Weak Reference)用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。WeakReference类来实现弱引用。
虚引用(Phantom Reference)也称为幽灵引用或幻影引用,最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过弱引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。PhantomReference类来实现虚引用。
对象:我还可以再抢救一下
在可达性分析中不可达的对象,例如上述图中Object5、Object6、Object7,真正要进行回收,还要经过两次标记过程。
第一次标记筛选的条件是此对象是否有必要执行finalize()方法,对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,这两种情况下finalize()方法都没必要执行,那么直接死去吧;
如果有必要执行,这个对象会放在一个叫F-Queue的对列中,稍后由一个虚拟机自动建立优先级较低的Finalizer线程去执行,稍后GC会对F-Queue中的对象进行第二次标记,如果在这里调用finalize()方法里,对象重新建立了与引用链上的引用,对象就会移出被即将回收的集合。但是一个对象的finalize()方法只能被执行一次,在第二次回收的的时候,finalize()方法不会被再次执行。
finalize()方法是对象逃脱死亡的最后机会,如果想在这次机会中抢救回来,只需要与引用链上的对象建立关系,例如把自己赋给某个类的变量或者对象成员变量上。
虽然finalize()方法可以拯救对象,但是不建议大家在代码中使用。系统对任何一个对象对finalize()只会调用一次,即使使用了一次finalize()方法,在对象面临下次回收时就不会执行,一样会被回收,这就叫跑的了和尚跑不了庙,你的命运就是到这了,再挣扎一会也没用,这种做法完全是一种自我安慰,所以最好就是大家忘了Java中的finalize()方法。