3.2 对象判活算法

判断对象死亡的两种做法

  1. 引用计数算法,在对象中添加引用计数器,多一个引用,计数器+1;引用失效,计数器-1;计数器为0 ,表示对象不再使用,可以被回收。缺点(无法解决两个对象互相引用的问题,事实证明jvm虚拟机不是通过引用计数算法来判断对象存活的)
  2. 可达性分析算法, 通过一系列被称为“gc root”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为“引用链”,当一个对象到gc root 没有任何引用链项链,称为“不可达”,证明此对象是不可用的。

在java 语言中,可以作为gc roots的对象包括以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
  2. 方法区中的静态属性引用的对象。
  3. 方法区中常量引用的对象
  4. 本地方法栈jni(native方法)引用的对象。

GC管理的主要区域是Java堆,一般情况下只针对堆进行垃圾回收。方法区、栈和本地方法区不被GC所管理,因而选择这些区域内的对象作为GC roots,被GC roots引用的对象不被GC回收。

对象引用的4中引用
  1. 强引用,就是普遍存在的 Object a = new Object() 显示的引用,只要强引用存在,垃圾收集器永远不会回收被引用的对象
  2. 软引用,有用但非必须的对象。系统将会在发生内存溢出前,将这些被软引用关联的对象进行第二次回收。如果没有回收后还没有足够内存,就会抛出内存溢出异常
  3. 弱引用,非必须对象,被弱引用关联的对象只能生存到下一次垃圾收集之前。无论内存是否足够,都会被回收
  4. 虚引用(幽灵引用,幻影引用)不能对对象的生存构成影响,他的使用目的是为了在这个对象被收集器回收时收到一个系统通知。
两次标记

0

回收方法区(永久代)
  1. 方法区回收效率低
  2. 回收的主要内容: 废弃的常量 与 无用的类。 判断废弃的常量与java堆中的对象回收类似,判断是否有对象引用常量池里的常量。 判断废弃的类要满足以下三个条件:
    1. 该类的所有类实例都已经被回收,也就是java堆中不存在改类的任何实例
    2. 加载该类的classloader已经被回收
    3. 该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

​ 满足以上方法,可以对类进行回收,但不是必然会回收。

1
2
3
4
5
是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc
参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading、-XX
+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX
+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要
FastDebug版的虚拟机支持。
  1. 在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

3.3 垃圾收集算法

jvm内存分配

1

  1. 年轻代分为eden和Survivor两个去,Eden区占大容量,Survivor两个区占小容量,Hotspot默认比例是8:1:1

  2. jdk1.8之后,取消永久代,取而代之的是被称为元空间Metaspace的区域,元空间存放在本地内存中,从而接解决了永久代的OOM异常,因为元空间的大小没有逻辑限制,与实际物理内存大小相同。

运行时常量池也相应的转移到元空间中。

  1. jdk1.7后字符串常量池从运行时常量池剥离,放入到堆内存中。

​ 字符串池里的内容是在类加载完成后完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用存到String pool中,(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符“身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。

垃圾收集算法
  1. 标记-清除 mark-sweep

    1. 标记需要清除的对象
    2. 删除被标记对象

    缺点:

  • 使的内存碎片化,产生大量不连续内存,当需要分配较大对象但是无法找到足够的连续 内存时,会触发另一次垃圾回收动作

  • 效率不高,标记与清除两个过程效率都不高

  1. 复制算法 copy
    1. 将内存分为大小相等的两块,每次只使用一块
    2. 当一块内存用完了的时候,将所有存活的对象全部依次按顺序拷贝到另一块内存上
    3. 清空掉原先已使用的内存空间

​ 优点:每次对办个区域回收,不用考虑内存碎片,按顺序分配内存,实现简单,运行高效

​ 缺点:由于要将内存缩小为原有内存的一半,代价高。

​ 现代商用虚拟机基本将这种算法用在年轻代(youngGen)上。survivor区被分为两块内存,每次使用eden和其中一块survivor区,当发生回收时,将eden和survivor上存活的对象拷贝到另一块survivor矿建上。然后清理掉eden和刚才使用过的survivor空间。Hotspot虚拟机默认eden与survivor的大小比例是8:1。

​ 当survivor空间不够用时,需要依赖老年代进行分配担保。

  1. 标记-整理 mark-compact

    1. 标记需要清理的对象
    2. 将存活的对象顺序向一端移动
    3. 清除掉边界以外的内存
  2. 分代收集算法

    就是根据不同的分代规则和其内存对象的特性,选用不同的收集算法。比如年轻代用复制算法,老年代用标记清除或者标记整理算法。

HotSpot算法实现
  1. stop in World,当执行GC时jvm会停顿所有真正执行的线程。

  2. 枚举根节点

    Hotsprot虚拟机OopMap用来存放对象的引用的指针,实现快速得知对象是否被引用,而不用检查所有执行上下文娱引用位置。

    OopMap 的设置是在safepoint,或 safeRange

  3. safePoint 安全点

    程序执行时只有到达安全点,才能暂停,执行GC。产生safePoint的地方,是需要长时间运行的特征的地方。例如,循环的末尾,方法调用返回前,调用方法的call之后,抛出异常的位置,这些位置的目的是为了防止大循环或者程序调用的时候,一直不进入safePoint,导致其他线程等待它进入safePoint

    中断采用主动式中断(Voluntary Suspension)

  4. safeRange 安全区域

    在一段代码片段中,引用关系不会发生变化,在区域中的任意地方开始GC都是安全的,当线程进入到safeRange中时,首先表示自己进入safeRange,然后直到出safeRange时都不用管是否有gc,jvm发起GC也不会管他是不是在safeRange,当线程要离开safeRange时,会检查系统是否已经完成证GC过程,如果完成了就继续执行,否则就等待,直到接收可以安全离开safeRange的信号为止。

垃圾收集器

2

  1. Serial GC是单线程GC,client模式下的新生代收集器,有明显的停止时间

  2. ParNew GC 是Serial GC的并行版本,Server模式下的的新生代收集器。有明显的停止时间,

  3. Parallel Scavenge GC, 关注吞吐量的新生代GC 。

    吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

  4. Serial old 是单线程收集器,用于老年代。

  5. Parallel Old GC 是并行收集器, Parallel Scavenge GC 的 老年代版本,使用 标记—整理算法

  6. CMS(Concurrent Mark Sweep) 以最短回收停顿时间为目标的收集器。 使用标记—清除算法

    四个步骤:

    1. 初始标记(CMS initial mark) 需要 “stop the world” 标记GCroot能直接关联的对象
    2. 并发标记(CMS concurrent mark) GC Roots Tracing
    3. 重新标记(CMS remark) 需要 “stop the world” 标记并发期间发生变动的对象
    4. 并发清除(CMS concurrent sweep)
  7. G1收集器,将java堆划分成不同的独立区域(region),然后以Region单位进行回收,整体上来看是基于标记—整理算法,单在从局部(两个region之间)是基于“复制算法实现的”。两种结合使得G1在运作时不会产生内存空间碎片。region 中的 Remenbered Set 帮助记录不同region中的对象引用关系,避免全堆扫描。

    不计算Remenbered Set 的操作,G1 GC 的运行大致分为4个步骤:

  8. 初始标记(Initial Marking)

  9. 并发标记(Concurrent Marking)

  10. 最终标记(Final Marking)

  11. 筛选回收(Live Data Counting and Evacuation)

GC 名词解释
  • Minor GC 年轻代GC Minor GC频繁,一般回收速度快
  • Major GC(Full GC) 发生在老年代的GC。 一次 Full GC 本身不会触发 Minor GC, 但是可以通过配置使在执行Full GC之前 先执行Minor GC。Major GC速度比Minor GC 慢10倍以上
  • Full GC的次数 = 老年代GC时 stop the world的次数
  • Full GC的时间 = 老年代GC时 stop the world的总时间
  1. Major GC 与 Full GC 之间的关系

    可以认为Major GC == Full GC,他们是一个概念,就是针对老年代/永久代进行GC。

    Full GC 的本意是针对老年代,Metaspace的回收也是有Full GC 来完成的。 Metaspace 用量到达配置的 MetaspaceSize 就会触发一次 FGC 然后扩容 Metaspace,如果你使用的是 CMS,那就委托给 CMS GC 进行

3.4 内存分配与回收策略

  1. TLAB Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域

  2. 对象优先分配在Eden

  3. -XX:PretenureSizeThreshold 参数可以配置大对象直接在老年代分配,这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存复制。

  4. 长期存活的对象进入老年代。-XX:MaxTenuringThreshold 可配置对象进入老年代的阈值。当出现对象是大小达到survivor空间的一半,满足这个规则的时候,即使对象不满足MaxTenuringThreshold所设定的年龄阈值,对象也会进入到老年代

  5. HandlePromotionFailure 参数开关 用于是否要启用新生代的分配担保,1.5以前是不启用,6以后是默认启用,以减少full gc的次数

    分配担保就是,当发生minor GC时,使用复制算法,而survivor空间 不够用时,会使用老年代的空间,来保证有足够的空间拷贝。如果老年代的最大可用连续空间大于新生代所有对象的总空间,则可用确保minor gc的安全。如果不满足,虚拟机就要HandlePromotionFailure 是否开启,如果过开启,则会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure为false,那这时也要改为进行一次Full GC