美文网首页
一个对象的前世今生

一个对象的前世今生

作者: 掩流年 | 来源:发表于2019-12-29 00:48 被阅读0次

对象的诞生

本文讨论的对象,限于普通的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。不同的对象,有各自的精彩,这既是对象的前世与今生。

相关文章

  • 一个对象的前世今生

    对象的诞生 本文讨论的对象,限于普通的Java对象,不包括数组,class对象等。在Java中,当程序执行发现ne...

  • 将军在上之男昭女惜重生三世千年孽缘

    前世!今生!来世再续! 前世欠谁!今生还!来世再续前缘! 前世因!今生续!来世果!

  • 三世因果经

    欲知前世因,今生受者是。 欲知后世果,今生作者是。 前世你欠我五分,今生却还我十分。 前世我欠你一个承诺,今生你毁...

  • 人死,并非如灯灭……

    “今生,是前世的“来生”,是来生的“前世”。在今生中,我们能见到自己的前世与来生。回溯前世,是为了改善今生;回到今...

  • 前世今生来世缘

    谈何前世情 今生还 今生情 来世还 前世孽债 前世还 未了 今生还 今生欠 今生还 谈何来世还 来世欠 来世还 能...

  • iOS Device ID 的前世今生

    iOS Device ID 的前世今生 iOS Device ID 的前世今生

  • 何世许今生

    前世的怨,今生的恨;前世的悲,今生的苦;前世的善,今世的乐。

  • 前生不欠 今生不见

    若無前世緣,何來今生見 前世不回眸,今生怎擦肩 前世若不欠,今生亦不見 今生且不欠,來生即自願

  • 今生的钥匙,前世的锁!

    今生的钥匙,前世的锁! 2019-05-24 午夜 今生的钥匙,前世的锁, 今生的...

  • 苹果新贵Swift之前世今生

    苹果新贵Swift之前世今生 苹果新贵Swift之前世今生

网友评论

      本文标题:一个对象的前世今生

      本文链接:https://www.haomeiwen.com/subject/relsoctx.html