垃圾回收器与内存分配策略

来源:互联网 发布:stm32 压缩算法 编辑:程序博客网 时间:2024/05/21 07:51

垃圾回收器与内存分配策略

@(jvm)[java, jvm, 自动内存管理机制]

  • 垃圾回收器与内存分配策略
  • 对象存活判定算法
    • 1 引用计数算法
    • 2 可达性分析算法
    • 3 引用描述
    • 4 生存还是死亡
    • 5 回收方法区
  • 垃圾回收算法
    • 1 标记-清除算法
    • 2 复制算法
    • 3 标记-整理算法
    • 4 分代收集算法
  • HotSpot的算法实现
    • 1 枚举根节点
    • 2 安全点
    • 3 安全区域
  • 垃圾收集器
    • 1 新生代收集器
      • 11 Serial收集器
      • 12 ParNew收集器
      • 13 Parallel Scavenge收集器
    • 2 老年代收集器
      • 21 Serial Old收集器
      • 22 Parallel Old收集器
      • 23 CMS收集器
    • 3 G1收集器
  • 内存分配与回收策略
    • 1 对象优先在Eden分配
    • 2 大对象直接进入老年区
    • 3 长期存活的对象将进入老年代
    • 4 动态对象年龄判定
    • 5 空间分配担保

自动内存管理:给对象分配内存及回收分配给对象的内存

垃圾回收主要在Java堆和方法区

1. 对象存活判定算法

1.1. 引用计数算法

描述:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。计数器值为0时立刻回收

评价:实现简单,判定效率也很高,但是很难解决对象之间相互循环引用的问题,所以不用

objA.instance = objB;objB.instance = objA;

1.2. 可达性分析算法

描述:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连接时,即不可达,则证明此对象不可达
在Java中,可作为GC Roots的对象包括下面几种:
1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
2. 方法区中类静态属性引用的对象
3. 方法区中常量引用的变量
4. 本地方法栈中JNI(即Native方法)引用的对象

1.3. 引用描述

JDK1.2后,将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度逐渐减弱。

强引用:指在程序代码之中普遍存在的,类似Object object = new Object()这类的引用。只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象。
软引用:指一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。
弱引用:用来描述非必需对象,被引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
虚引用;为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

1.4. 生存还是死亡

要真正宣告一个对象死亡,至少要经历两个标记过程:
1. 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行finallize()方法

当对象没有覆盖finalize()方法,或者finalize()已经被虚拟机调用过,则没有必要执行
如果有必要执行,这个对象会被放到F-Queue队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(可以触发这个方法,但是不能保证会运行结束)

  1. finalize()方法是对象逃脱死亡命运的最后一次机会。GC将会F-Queue中的对象进行第二次小规模的标记,如果对象在finalize()中重新与引用链上的任何一个对象建立关联,比如把this赋值给某个类对象或者对象的成员变量,那就可以在第二次标记时移出即将回收的集合。
    但是要注意:每个对象的finalize()方法只会被系统自动调用一次,同时,不建议使用finalize()方法,因为运行代价高昂,不确定性大。

1.5. 回收方法区

jvm规范中不要求虚拟机在方法区实现垃圾回收,而且在方法区中进行垃圾回收性价比比较低。

HotSpot永久代的垃圾回收包括两部分:废弃常量无用的类
- 常量池中字面量、类、接口、方法、字段的符号引用都类似,当系统中没有任何一个对象引用这些量,就会进行内存回收。
- 无用的类需要同时满足以下3个条件,此时可以对类回收,但不是必然。
1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
2. 加载该类的ClassLoader已经被回收
3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

2. 垃圾回收算法

2.1. 标记-清除算法

算法分为标记清除两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

缺点:1.效率问题。标记和清除两个过程的效率都不高。2. 空间问题。标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致之后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2.2. 复制算法

算法将可用内存按容量划分为大小相等的两块,每次只是用其中一块。当这一块用完时,就将还存活的对象复制到另一块上,然后把已使用的内存空间一次性清理掉。

优点:不用考虑内存碎片,实现简单,运行高效。
缺点:将内存缩小为原来的一半

新生代中98%对象存活率低,所以可以利用这一特点采用复制算法。

现代商业虚拟机采用这种算法来回收新生代,将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor空间。当回收时,复制Eden和Survivor中还存活的对象到另一块Survivor中,最后清理掉Eden和那块Survivor。HotSpot默认Eden和Survivor比例为8:1当Survivor空间不够用时,需要依赖老年代进行分配担保

2.3. 标记-整理算法

老年代对象存活率较高,不适合用复制算法,可以用标记-整理算法。

算法中标记过程同上,然后让所有存活的对象都相一端移动,然后直接清理掉端边界以外的内存。

2.4. 分代收集算法

根据对象存活周期的不同将内存划分为几块,一般分为新生代和老年代,新生代对象存活率较低,可以采用复制算法,老年代对象存活率高,没有额外空间,必需采用标记-清除算法或标记-整理算法。

新生代:所有新生成的对象首先都是放在新生代的。新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。
年老代(Old Generation):在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

3. HotSpot的算法实现

3.1. 枚举根节点

可达性分析算法中两个重要的方面:
1. 可作为GC Roots的节点很多,如果要逐个检查,会消耗很多时间
2. GC停顿,这项工作必须在一致性条件下进行

一致性指在整个分析期间执行系统看起来像冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况。

HopSpot的实现中,使用一组称为OopMap的数据结构来记录哪些地方存放着对象引用。在类加载完成后,HotSpot就把对象内什么偏移量时什么类型的数据计算出来,在JIT编译过程中,在特定的位置记录下栈和寄存器中哪些位置是引用。

3.2. 安全点

OopMap没有为每条指令都生成,而是在安全点,即程序执行到达安全点才开始GC。

1.选择安全点标准:是否具有让程序长时间执行的特征。最明显的就是指令序列复用,例如方法调用,循环跳转,异常跳转等。


  1. 如何在GC发生时让所有线程都在最近的安全点停顿。

抢占式中断:GC发生时,所有线程全部中断,如果有线程中断的地方不在安全点上,就恢复线程让它跑到安全点上。 ————该方法已废弃
主动式中断:当GC需要中断线程时,设置一个标志位。各个线程执行时主动去轮询这个标志位,发现中断标志为真时就自己中断挂起

3.3. 安全区域

当程序没有分配CPU时间,例如sleep或block时,线程无法走到标志位去,此时需要通过安全区域解决。

安全区域:在这段代码内,引用关系不会发生变化

当JVM要发起GC时,就不用管标识自己为安全区域的线程了。当线程执行完安全区域的代码时,如果GC没有完成,则挂起。

4. 垃圾收集器

这里写图片描述
垃圾收集器发展的需求就是用户线程的停顿时间在不断缩短,
新生代与老年代之间的对象引用通过Remembered Set来避免全盘扫描

4.1. 新生代收集器

4.1.1. Serial收集器

单线程:只会使用一个CPU或一条收集线程去完成垃圾回收,同时他在进行垃圾回收时,必须暂停其他所有的工作线程,直到它收集结束。

优点:简单高效,,专心做垃圾收集可以获得最高的单线程收集效率,是虚拟机运行在Client模式下的默认新生代收集器

4.1.2. ParNew收集器

多线程版本:使用多条线程进行垃圾收集,但是仍需要中断用核线程

是许多Server模式下首选的新生代GC,因为只有Serial和ParNew可以与CMS配合工作。同时在多线程环境下相比Serial更能有效利用CPU资源。

4.1.3. Parallel Scavenge收集器

目标:达到一个可控制的吞吐量

=/(+)

两个参数:控制最大垃圾收集停顿时间的参数,直接设置吞吐量大小的参数
还有一个参数可以设置GC自适应的调节策略

4.2. 老年代收集器

4.2.1. Serial Old收集器

是Serial的老年代版本,单线程,使用标记-整理算法。Client模式下使用。

4.2.2. Parallel Old收集器

是Parallel Scavenge收集器的老年代版本。多线程,标记-整理算法。
在server端与Parallel Scavenge组合成为”吞吐量优先“的版本。所以适用场合:注重吞吐量以及CPU资源敏感

4.2.3 CMS收集器

目标:获取最短回收停顿时间。
基于标记-清除算法
分为以下4个步骤:

初始标记:只标记GC Roots能直接关联到的对象,速度快,需要中断用户线程
并发标记:GC Roots tracing过程,
重新标记:修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。需要中断用户线程,中断时间略长于初始标记阶段
并发清除

3个缺点:

  1. CMS收集器对CPU资源非常敏感。因为会占用总线程数的一部分。
  2. 无法处理浮动垃圾。并发清除阶段产生的垃圾只能在下次GC时清除,所以需要预留一部分空间提供并发收集时的程序使用。当CMS运行期间预留的内存无法满足程序需要,就会出现”Concurrent Mode Failure”,此时需要临时启用Serial Old收集器来重新进行老年代的垃圾收集
  3. 基于标记-清除会产生空间碎片,这将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,你是无法找到足够大的连续空间来分配,触发Full GC。

4.3. G1收集器

面向服务端
特点:

并行与并发
分代收集
空间整合:将整个Java堆划分为多个大小相等的独立区域,新生代和老年代是一部分Region的集合。整体基于标记-整理算法,两个region之间基于复制算法实现
可预测的停顿;G1跟踪各个region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

步骤;

初始标记
并发标记
最终标记
筛选回收:堆优先级高的region回收

5. 内存分配与回收策略

大方向上,就是在堆上分配。对象主要分配在新生代上的Eden区,如果启动了本地线程分配缓冲,将按线程优先级在TLAB上分配。少数情况下也可能直接分配在老年代上。

5.1. 对象优先在Eden分配

大多数情况下,对象在Eden区分配,当Eden区没有足够的内存时,虚拟机将发起一次Minor GC。
具体分配过程见《为甚么要设置两个Survivor区》

5.2. 大对象直接进入老年区

大对象:需要大量来内需内存空间的对象,例如很长的字符串以及数组。写程序应该尽量避免。
虚拟机设置了一个大对象门限值,另大于这个设置值的对象直接在老年代中分配,目的是避免在Eden和Survivor区之间发生大量的内存复制。

5.3. 长期存活的对象将进入老年代

虚拟机为每个对象定义一个对象年龄计数器。对象没熬过一次Minor GC,年龄将增加1岁,默认增加到15岁则进入老年代

5.4. 动态对象年龄判定

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到门限要求的年龄。

5.5. 空间分配担保

在发生Minor GC之前,

if(老年代最大可用的连续空间是否 > 新生代所有对象总空间)    Minor确保安全else    if(虚拟机查看HandlePromotionFailure允许担保失败)        if(老年代最大可用连续空间大于历次晋升到老年代对象的平均大小)            尝试Minor GC(有风险)        else             进行Full GC    else        进行Full GC

一般还是会把HandlePromotionFailure打开,避免Full GC过于频繁

原创粉丝点击