0
点赞
收藏
分享

微信扫一扫

【JVM系列】Java对象的创建、分配、GC这些知识懂没懂?反正姐是懂了

文章目录

五一还要值班儿

一、JVM中的对象


四种引用类型

强引用
  一般Object obj = new Object()这种创建的对象都是强引用,只有对象有强引用关联就不会被GC。

软引用
  SoftReference包装的对象的引用就是软引用。在报内存溢出之前,先去回收这些软引用对象,回收完了有空间了就不报溢出了。但是,被包装对象还存在强引用,则被包装对象不会被回收。
  JVM在反射过程中动态生成的类的Class对象,他们都是软引用的

弱引用
  WeakReference包装的对象的引用 就是弱引用。
  弱引用的对象只能存活到下次GC之前,但是如果被包装的对象还是存在强引用,则被包装对象不会被回收。

虚引用
  PhantomReference包装的对象的引用就是虚引用。一般用来监控垃圾回收器是否能正常工作(放入了对应的队列则表示回收了)。


对象new的过程

1、检查对应类是否已经加载
  如果还没有加载,则先执行对应类的加载过程,包手执行clinit方法。

2、分配内存空间
  对象内存的分配有两种方式,“指针碰撞”与“空闲列表”。

  什么是指针碰撞?
  将所有用过的内存放一边,没用的放一边,中间用一个指针作为分界点的指示器,堆中内存是绝对规整的。分配内存时,只是把指针向空闲那边移动一段与对象大小相等的距离,这种分配方式叫“指针碰撞”

  什么叫空闲列表?
  堆中内存不规整时,虚拟机就维护了一个列表,记录了哪些内存块是可用的。分配内存时,从列表中找到一块足够大的空间给对象,并更新列表上的记录。这种方式叫“空闲列表”

  两种分配方式到底会用哪一种呢?
  分配方式由Java堆是否规整决定,而堆是否规整又由垃圾回收器决定(是否带压缩功能),所以会用哪一种方式分配内存主要是看你选的什么垃圾回收器。CMS有内存碎片(不规则即用空闲列表),Serial、ParNew带压缩整理,(复制算法也整理的)

3、赋零值
  内存分配完后(还没到构造),虚拟机会将分配到的内存空间都初始化零值(int初始为0,boolean初始化为false等等)

4、设置对象头
  此时会给对象头加上:类型指针、hash码(hashCode)、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等,对象头里的内容后面会专门说明。

5、初始化
  调用构造方法,先给普通成员变量赋值,然后才真正执行构造里的代码。对象初始化里面的内容会特别细,这里不举那些复杂的例子了,有性趣的可以参考这篇文章:[Java对象初始化详细过程]


对象的组成

  每一个对象都由三部分组成,分别是:对象头、实例数据、对齐填充

  对象头
  对象头又包括“markword”和“类型指”针。
  “markword”是包括哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳这些运行时数据。
  “类型指针”是指向该对象的类的元数据的指针,可判断该对象是哪个类的实例

  实例数据
  实例数据可简单理解为我们类里的那些全局变量

  对齐填充
  HotSpot VM要求对象大小必须是8字节的整数倍,如果没有对齐时,则需填充到8的整数倍来补全。因为有可能对象大小已经是8字节的整数倍,所以对齐填充不是必须存在的。


如何访问对象

  要找到一个对象有两种方式,分别是“句柄”和“直接指针”

  直接指针
  如果“采用直接指针”的方式,则对象在方法栈中(即局部变量)的reference存的地址直接就是对象的真实地址。
  这种方式访问很快,缺点就是如果对象的地址发生了变化,那么栈中的reference也得跟着修改。
  HostSpot用的直接指针,因为访问对象比移动对象更频繁,所以更好

  句柄
  如果采用句柄的方式,则对象在方法栈的局变量表中reference存的是句柄的地址。
  句柄存的是对象对应的方法区的类型数据和对象在堆中的实例数据的地址。句柄池在堆中,可以理解就是一个全是地址的池子。

  为什么要搞个句柄池呢?
  这样做的好处是:如果对象被移动(垃圾回收),则只需要改动句柄中的实际地址,而不需要改动refercence里的值


判断对象存活

  都听过GC(没听过下面会说),在回收一个对象前一定需要判断该对象是否存活,那是怎么判断的呢?

  引用计数法
  就是有一个地方引用,引用计数就+1,如果引用不为0则说明存活。这种方式有个很大缺点就是,如果两个对象相互引用,就永远认为是存活的,从而不能对该对象进行GC。

  根可达分析
  通过“GC Root”作为起点开始向下搜索,搜索走过的路叫引用链。当一个对象没有任何引用链则说明此对象死掉了。
  GC Root主要就是常量、静态变量、运行时方法中的变量直接引用的对象。


对象的分配策略

1.栈上分配
  如果对象没有逃逸,则通过标量替换,将对象分解成标量后存在栈帧的局部变量表上。一般只会放些小对象。
  “标量”指不可进一步分解的量,如int、long这此基本数据量
  “聚合量”指可以被进一步分解的量,如Java中的对象

2.优先分配到Eden区
  Eden区如果不够则会进一次YGC

3.大对象直接进入老年代
  大对象指需要大量连续内存空间的对象,如很长的字符串、容量很大的数组。大对象要放在年轻代,容易触发GC,而且复制的开销也大。短命的大对象,放在老年代不好,浪费空间。
  XX:PretenureSizeThreshold =2M 可设置大对象阈值

4.对象年龄动态判定
  如果Survivor区相同年龄的所有对象大小的总和大于S0空间的一半,则>=该年龄的所有对象就直接进入老年代。
  所以如果刚往S0区放的对象就很大,则Survivor区的对象全都会进入老年代

5.长期存活的将进入老年代
  在Survivor区,每经历一次MinorGC,年龄+1,一般是加到15(CMS是6)就进入老年代
  XX:MaxTenuringThreshold 设置值

6.线程本地分配缓冲(TLAB)
  线程初始化时,给当前线程在Eden区申请一块单独使用的内存缓冲。用于存放很快就死掉的对象。默认开启。
  新对象,栈中放不下就先放TALB中,如果还是放不下,还是会放在Eden区或老年代中。
  TLAB只是让每个线程都有私有的 分配指针,存对象的内存空间还是可以被其它线程访问的,只是其它线程无法在这个区域分配而已

7.空间分配担保
  在MinorGC前,会先看老年代最大可用空间,是否大于新生成代所有对象占用空间总和,如果大于,则此次MinorGC安全
  如果小于,则要看XX:HandlePromotionFailuer是否允许担保失败,如果不担保,则会进行一次Full GC
  如果担保,则判断老年代最大可用空间是否大于历次晋升到老年代对象的平均大小,大于进行MinorGC,小于则进行一次Full GC。
  担保失败,则也会进行一次FullGC


二、对象的回收(GC)


垃圾回收算法

复制算法
  复制算法将内存划分成大小相等的两块,每次只用一块,当其中一块用完了,就将还活着的对象复制到另一块,然后把剩下的垃圾一次性全干掉。
  缺点就是内存利用率低,由于对象存放的位置变了,前面提到的直接指针也得更新。
  新生代的对象大部分死得快,所以复制的对象也会少,所以该垃圾回收算法适合新生代

Appel式回收
  将新生代分配一块较大的Eden区,和两块较小的survivor区(eden占80%,两个survivor区各占10%)。
  大多数对象很快就死,就不用去survivor区了,把内存利用率也提升到了90%。这比纯复制算法要优秀得多。

标记-清除
  扫描第一遍时,标记出要回收的对象;扫描第二遍时,清除所有标记的对象
  相对于标记-整理算法,stw时间更短,因为整理时涉及对象的移动。缺点是产生大量内存碎片。适用于老年代【目前只有CMS用】。

标记-整理
  标记完成后,让所有存活的对象移到一端,然后直接清理垃圾。
  优点是没有内存碎片,缺点是对象移动时,增加系统负担,同时还要暂停用户线程,去更新前面提到的直接指针。也适用于老年代。


垃圾回收器

各种垃圾回收器

垃圾回收器算法,并发回收对象备注
Serial复制算法,单线程年轻代适用于几十到一两百兆的堆空间
Serial Old标记-整理,单线程老年代适用于几十到一两百兆的堆空间
ParNew复制算法,并行年轻代是Serial的多线程版本,经常配合CMS。JDK9直接合并在CMS了,但后续版本准备淘汰了
Parallel Scavenge复制算法,并行年轻代类似ParNew,更加关注吞吐量
Parallel old标记-整理,并行老轻代Parallel Scavenge的老年代版本,配合Parallel Scavenge一起用。JDK8默认它俩的组合
CMS标记-清除,并行老轻代以获取最短回收停顿时间为目标,因为没有整理所以更快。适用于几个G 到 20G 左右的堆空间
G1标记-整理,并行年轻代、老年代可实现 STW 的时间可预测, G1 将堆内存“化整为零”

上面的垃圾回收器着重选择CMS和G1进行说明。


CMS垃圾回收器

CMS的四个工作阶段
  1.初始标记
  仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。会暂停所有用户线程。

  2.并发标记
  标记从 GCRoots 开始关联的所有对象开始遍历整个可达分析路径的对象。和用户线程同时工作,耗时较长。

  3.重新标记
  修正因 并发标记 期间用户线程继续运行而导致标记变动的记录。暂停所有用户线程,时间较短。
  此时会产生漏标(三色标记里会说明),通过 增量更新 解决

  4.并发清除
  和用户线程并行的去清除垃圾。所以耗时的清除就不会有STW


G1垃圾回收器

G1原理
  为了实现 STW 的时间可预测, G1 将堆内存“化整为零” , 将堆内存划分成多个大小相等独立区域( Region)
  每一个 Region都可以根据需要, 扮演新生代的 Eden 空间、 Survivor 空间, 或者老年代空间
  优先回收垃圾多的区域(存活的对象少)

  一般只不断调优暂停时间,不修改堆的大小,不然就放弃了G1的自动调优
  暂停时间如果太小,就会减少Eden区和survivor区的大小


Region
  Region 可能是 Eden,也有可能是 Survivor,也有可能是 Old
  每个 Region 的大小可以通过参数-XX:G1HeapRegionSize 设定, 取值范围为 1MB~32MB,且应为 2 的 N 次幂
  另外 Region 中还有一类特殊的 Humongous 区域, 专门用来存储大对象。G1 认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象
  超过了整个 Region 容量的超级大对象, 将会被存放在 N 个连续的 Humongous Region 之中, G1 的进行回收大多数情况下都把 HumongousRegion 当作老年代的一部分来进行看待


G1的四个工作阶段
  1.初始标记
  仅仅只是标记一下 GC Roots 能直接关联到的对象, 并且修改 TAMS 指针的值

  2.并发标记
  从 GC Root 开始对堆中对象进行可达性分析, 递归扫描整个堆里的对象图, 找出要回收的对象。耗时较长。
  当对象图扫描完成以后,并发时有引用变动的对象 ,这些对象会漏标
  漏标的对象会被一个叫做SATB(snapshot-at-the-beginning)算法来解决

  3.最终标记
  处理并发阶段结后仍遗留下来的最后那少量的 SATB 记录。会有短暂的停顿

  4.筛选回收
  更新 Region 的统计数据, 对各个 Region 的回收价值和成本进行排序, 根据用户所期望的停顿时间来制定回收计划


三色标记

原理
  用三种颜色将不同的对象用不同的颜色进行标记:

黑色:标记根对象,或者子对象都被扫描过的对象
灰色:标记本身扫描了,但子对象还未扫描完的对象
白色:标记没有被扫描到的对象【垃圾】

浮动垃圾
  若一个已经被标记成黑色或者灰色的对象,突然变成了垃圾,由于不会再对黑色标记过的对象重新扫描,所以不会被发现,那么这个对象不是白色的但是不会被清除,重新标记也不能从GC Root中去找到,所以成为了浮动垃圾
  浮动垃圾对系统的影响不大,留给下一次GC进行处理即可

漏标
  标记到一半时,白色对象可能是灰色对象引用的对象,结果在并发标记的时候,变成了黑色对象的引用对象。由于黑色对象的子对象认为是扫描完的,所以该白色对象就会被当成垃圾回收

漏标解决方案
  CMS通过增量更新解决:当一个白色对象被一个黑色对象引用时,将黑色对象标记为灰色,让垃圾回收器重新扫描
  G1则是在并发标记前做一个快照,完成后如果对比发现白色对象与灰色对象的引用关系消失,则将该引用推到GC的栈中,在最终标记时,只需要短暂停顿扫描该引用就行了,这样白色对象还会被扫描到


跨代引用

跨代引用定义
  老年代与年轻代对象相互引用,比如在回收新生代对象时,就需要扫描整个老年代来找到对新生代的所有引用,这是十分浪费性能的
  为了避免跨代引用扫描整个堆,所有了“记忆集”,这样只是通过引用直接找到哪些是跨代引用的

记忆集(Rset)
  它是记录非收集区域指向收集区域指针的集合。
  在垃圾回收时可用于判断是否有引用。但并不是所有跨代引用都记录,所以提供了几种精度:
  1.字长精度:每个记录精确到一个字长,该字包含了跨代指针
  2.对象精度:每个记录精确到一个对象,对象里有字段含有跨代指针
  3.卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。它的实现就是卡表

卡表(card table)
  卡表与记忆集的关系,相当于hashMap与Map的关系
  卡表是一个简单的字节数组,下标对应一块内存块(即卡页),如果卡页内有对象包含跨代指针,则该数组下标对应的值为1(即dirty),没有则为0
  在 G1 中是每 一个 Region 都需要一个 RSet 的内存区域,导致有 G1 的 RSet 可能会占据整个堆容量的 20%乃至更多

举报

相关推荐

0 条评论