Java GC
Java GC
火力全開 发表于1年前
Java GC
  • 发表于 1年前
  • 阅读 3
  • 收藏 0
  • 点赞 0
  • 评论 0

腾讯云 技术升级10大核心产品年终让利>>>   

原文:Java GC工作原理以及Minor GC、Major GC、Full GC简单总结

名词解释:

GC:垃圾收集器

Minor GC:新生代GC,指发生在新生代的垃圾收集动作,所有的Minor GC都会触发全世界的暂停(stop-the-world),停止应用程序的线程,不过这个过程非常短暂。

Major GC/Full GC:老年代GC,指发生在老年代的GC。

JVM:Java Virtual Machine(Java虚拟机)的缩写。

正文:

>堆

众所周知,所有通过new创建的对象的内存都在堆中分配,堆被划分为新生代和老年代,新生代又被进一步划分为Eden和Survivor区,而Survivor由FromSpace和ToSpace组成。

新生代:新创建的对象都是用新生代分配内存,Eden空间不足时,触发Minor GC,这时会把存活的对象转移进Survivor区。

老年代:老年代用于存放经过多次Minor GC之后依然存活的对象。

结构图如下:

JVM内存结构之堆  

>栈

每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果。

>本地方法栈

用于支持native方法的执行,存储了每个native方法调用的状态。

>方法区

存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用持久代(PermanetGeneration)来存放方法区。

以上是JVM内存组成结构。

 

进入正题:JVM垃圾回收机制

JVM分别对新生代和老年代采用不同的垃圾回收机制。

GC 触发条件:Eden区满了触发Minor GC,这时会把Eden区存活的对象复制到Survivor区,当对象在Survivor区熬过一定次数的Minor GC之后,就会晋升到老年代(当然并不是所有的对象都是这样晋升的到老年代的),当老年代满了,就会报OutofMemory异常。

新生代的GC(Minor GC):

新生代通常存活时间较短基于Copying算法进行回收,所谓Copying算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和FromSpace或ToSpace 之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。

在执行机制上JVM提供了串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew):

串行GC

在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。

并行回收GC

在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。

并行GC

与老年代的并发GC配合使用。

老年代的GC(Major GC/Full GC):

老 年代与新生代不同,老年代对象存活的时间比较长、比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被 标记的对象,回收后对用空出的空间要么进行合并、要么标记出来便于下次进行分配,总之目的就是要减少内存碎片带来的效率损耗。

在执行机制上JVM提供了串行 GC(Serial MSC)、并行GC(Parallel MSC)和并发GC(CMS)。

串行GC(Serial MSC)

client模式下的默认GC方式,可通过-XX:+UseSerialGC强制指定。每次进行全部回收,进行Compact,非常耗费时间。

并行GC(Parallel MSC)(吞吐量大,但是GC的时候响应很慢)

server模式下的默认GC方式,也可用-XX:+UseParallelGC=强制指定。可以在选项后加等号来制定并行的线程数。

并发GC(CMS)(响应比并行gc快很多,但是牺牲了一定的吞吐量)

使 用CMS是为了减少GC执行时的停顿时间,垃圾回收线程和应用线程同时执行,可以使用-XX:+UseConcMarkSweepGC=指定使用,后边接 等号指定并发线程数。CMS每次回收只停顿很短的时间,分别在开始的时候(Initial Marking),和中间(Final Marking)的时候,第二次时间略长。CMS一个比较大的问题是碎片和浮动垃圾问题(Floating Gabage)。碎片是由于CMS默认不对内存进行Compact所致,可以通过 -XX:+UseCMSCompactAtFullCollection。

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor区,并将对象年龄设为 1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1,当它的年龄增加到一定程度(默认为15)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

GC判断对象是否"存活"或"死去"(GC回收的对象)

1.引用计数器算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值加1;当引用失效时,计数器的值减;当该对象的计数器的值为0时,标志该对象失效。

2.跟搜索算法

基本思路:通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明对象是不可用的。

附JVM GC组合方式:

 

原文:Java虚拟机精讲之内存分配与垃圾回收

 

1 线程共享内存区

Java堆区

  • 用于存储Java对象实例,但是不一定是Java对象内存分配的唯一选择(为了降低GC频率).在JVM启动的时候大小就已经设定好了.(-Xmx最大 -Xms起始) 超过最大内存的时候,抛出OOM异常.
  • 实际的内存空间可以不连续,是GC的重点区域.
  • YoungGen新生代(Eden, From Survivor, To Survivor) ; OldGen老年代(OldGen).根据不同的代选择不同的GC算法.

方法区

  • 存储了每一个Java类的结构信息,比如 运行时常量池 , 字段和方法数据 , 构造函数 , 普通方法的字节码内容 以及 类,实例,接口初始化时要用到的特殊方法等数据.
  • jvm规范没有对方法区的实现有明确要求,在HotSpot中,它属于Java堆区的一部分
  • 在JVM启动的时候创建,也可以不连续,有时候被称为永久代,它不会频繁的GC,但是不代表它里面数据永远不会被回收.如果没有显示要求不对方法区进行内存回收的情况下,GC的回收目标仅针对方法区的常量池类型卸载
  • 超过-XX:Max-PermSize也会OOM

运行时常量池

  • 属于方法区的一部分.运行时常量池就是字节码文件中常量池表的运行时表示形式.
  • 当类加载器成功的将一个类或者接口装载进JVM后,就会创建与之对应的运行时常量池.

2 线程私有内存区

PC寄存器

  • 无OOM
  • 为什么每个线程都要?答:CPU做任务切换,每个线程都要记录正在执行的当前字节码指令地址,从而每个线程都可以独立计算.
  • JVM的解释器需要通过改变PC寄存器的值来明确下一条有关执行什么样的字节码指令

    Java栈(Java虚拟机栈)

  • Java栈用于存储栈帧,生命周期和线程的生命周期一致
  • 对应Java堆中存储对象,Java栈中局部变量表用于存储各类原始数据类型,对象引用,以及returnAdress类型.

3 自动内存管理

内存分配原理

  • jvm包含三种引用了类型:类类型,数组类型,接口类型,分别对应创建的值是 类实例, 数组实例, 以及实现了某个接口的派生类实例.
  • 下图是jvm中具体的创建对象实例 

  • 分配内存有指针碰撞(已用的未用的分两边,通过修改中间指针的偏移量) 和 空闲列表(Free List)

堆区和方法区

  • 堆区和方法区是线程共享,在并发环境下从堆区中划分内存空间是非线程安全的.所以一个类如果在分配内存之前已经成功完成类装载步骤之后,JVM会优先选择在TLAB(本地线程分配缓冲区,是堆区中一块线程私有的区域,包含在Eden空间内,缺省很小,占Eden的1%,可以通过参数调整),可以避免一系列的非线程安全问题,还能提升内存分配的吞吐量.这种内存分配方式叫做快速分配策略.(-XX:UseTLAB)
  • 一旦对象在TLAB空间分配内存失败,JVM会尝试通过加锁来确保原子性,从而直接在Eden分配内存.
  • 分配好内存空间后,接下来就是初始化对象实例,首先对分配的内存空间进行零值初始化,再接下来是初始化对象头和实例数据.到这里,一个Java对象实例才是真正的创建成功.

逃逸分析与栈上分配

  • 一个对象被定义在方法体内部之后,一旦其应用被外部成员引用,这个对象就发生了逃逸,反之,JVM就会为其在栈帧中分配内存空间(这是优化技术)
public class StackAllocation {
  public StackAllocation obj;

  //逃逸
  public StackAllocation getStackAllocation() {
    return null == obj ? new StackAllocation() : obj;
  }

  //为成员变量赋值,逃逸
  public void setStackAllocation() {
    obj = new StackAllocation();
  }

  //引用成员变量的值,逃逸
  public void useStackAllocation1() {
    StackAllocation obj = getStackAllocation();
  }

  //对象的作用于仅在方法体内,未逃逸
  public void useStackAllocation2() {
    StackAllocation obj = new StackAllocation;
  }
}

对象内存布局与OOP-Klass模型

GC算法和垃圾收集器

  • GC(Garbage Collector 垃圾收集器),其实内存划分(新生代,老年代)是完全依赖于GC的设计.当内存空间中的内存消耗达到一定的阈值之后,GC会进行垃圾回收.
  • 可以根据以下6点评估一款GC的性能
  1. 吞吐量:程序的运行时间/(程序的运行时间 + 内存回收的时间)
  2. 垃圾收集开销
  3. 暂停时间
  4. 收集频率
  5. 堆空间:java堆区锁占用的内存大小
  6. 快速:一个对象从诞生到被垃圾回收所经历的时间.

垃圾标记: 根搜索算法

  • 一般不用引用计数法.无法解决死亡对象的互相引用导致无法垃圾回收,所以HotSpot使用根搜索算法,只有能够被根对象集合直接或者间接连接的对象才是存活对象.当目标对象不可达的时候,便可以在instanceOopDesc的MarkWord中将其标记为垃圾对象.
  • 根对象集合:
  1. Java栈中的对象引用
  2. 本地方法栈中的对象引用
  3. 运行时常量池中的对象引用
  4. 方法区中类静态属性的对象引用(所以这个要注意 静态变量的定义!)
  5. 与一个类对应的唯一数据类型的Class对象

垃圾回收:分代收集算法

  • 上一步成功的区分出了存活和死亡对象.接下来就是垃圾回收算法.

    标记-清除算法(Mark-Sweep)

  • 把垃圾回收任务分为两个阶段,分别是垃圾回收和内存释放.简单,但是效率低下,而且会产生内存碎片

    复制算法(Copying)

  • 就是因为这个算法,所以内存是分代的!
  • java的大多数对象都是瞬时对象,生命周期非常短.复制算法广泛用于新生代中.新生代分为Eden,From Suvivor和To Suvivor,Eden和另外两个Survior空间缺省所占比例为8:1,可以通过-XX:SurvivorRatio调整.
  • 当执行一次Minor GC,Eden中存活的对象会被复制到To Suvivor空间内,并且之前经历过一次Minor GC并在From Suvivor存活下来的对象如果还年轻的话会被复制到To Suvivor.

  • 满足两种特殊情况,Eden和From空间的存活对象不会被复制到To空间.

  1. 存活的对象的分代年龄超过-XX:MaxTenuringThreshold所指的的阈值时,会直接晋升到老年代中.
  2. 当To空间的容量达到阈值的时候,存活的对象也会直接晋升到老年代中
  • 当执行完Minor GC之后,Eden空间和From空间将会被清空,存活下来的对象会全部存在To空间内,接下来From空间和To空间将会互换位置(无非就是使用To空间作为一个临时的空间交换角色,所以务必保证一个survivor空间是空的)
  • 不适用与老年代中的内存回收,因为老年代中的对象的生命周期都比较长,所以会有很多的复制,效率和效果都不太好

标记-压缩算法(Mark-Compact)

  • 适用于老年代
  • 当标记出垃圾对象之后,会将所有的存活对象移动到一个规整且连续的空间,然后执行Full GC(Major GC),垃圾回收之后,已用和未用的内存都各自一边.

总结

  • 新生代的GC算法一速度优先,新生代的对象生命周期都非常短暂,内存空间也比较小.所以这块的垃圾回收会非常频繁.
  • 老年代的GC算法使用更节省内存的算法,老年代的对象生命周期都比较长,并且老年代占了绝大部分的堆空间.

垃圾收集器

  • 上面说的是JVM的内存回收算法,接下来说的是GC的具体实现.有许多的GC版本,比如:Serial/Serial Old,Parallel/Parallel Old,CMS,G1
  • 这些新生代和老年代的GC算法可以自由组合 

  • 两个非常重要的概念:串行还是并行回收,并发还是"Stop-the-World"机制

  • 串行回收简单来说旧书多个CPU可以用时,也只有一个CPU用于垃圾回收操作,并且在执行垃圾回收的时候,程序中的工作线程会被暂停(注意这个的区别),串行回收缺省在Client模式下的JVM.并行收集可以运用多个CPU同时进行垃圾收集,提升了吞吐量,但是还是要"STW",这个一定要注意.
  • 第二个概念是说的是回收的时候,要不要stw的,最新的G1也做不到完全不需要STW.

串行回收:Serial收集器

  • 用于新生代,采用复制算法,串行回收和STW.是Client下的缺省新生代GC
  • Serial Old 采用标记-压缩算法,其他一样.
  • -XX:+UseSerialGC

并行回收:ParNew收集器

  • 用并行方式执行内存回收,其他和Seial几乎没有区别.在单个cpu下,ParNew不见得会比Serial高效
  • 在某些注重低延迟的应用场景中,ParNew+CMS(Concurrent-Mark-Sweep)收集器组合执行Server模式下的内存回收几乎是最佳的选择.
  • -XX:+UseParNewGC

程序吞吐量优先:Parallel收集器

  • 复制算法,并行回收和STW,
  • 和ParNew最大的不通是Parallel可以控制程序的吞吐量大小.
  • -XX:GCTimeRatio 设置内存回收的时间所占JVM运行总时间的比例(也就是控制GC的执行频率,公式为1/(1+N),默认值是99,也就是说将只有1%的时间用于执行垃圾回收)
  • -XX:MaxGCPauseMillis设置STW的暂停时间阈值,如果指定了该选项,Parallel收集器将会尽可能地在指定时间范围完成内存回收.
  • 注意!吞吐量和低延迟这两个目标其实是存在相互竞争的矛盾,如果选择吞吐量优先,就会降低内存回收的执行频率,这会导致GC需要更长的暂停时间来执行垃圾回收.如果选择以低延迟优先,那么为了降低每次GC的时间,只能更加频繁的进行GC,这会导致吞吐量下降.
  • -XX:UseAdaptiveSizePolicy 用于甚至GC的自动分代大小调整策略,这个JVM就是自动调整相关参数
  • Parallel Old采用标记-压缩算法,Parallel+parallel Old是个不错的选择

低延迟:CMS(Concurrent-Mark-Sweep)

  • 是优秀的老年代收集器,采用标记-清除算法,也会stw
  • 有以下阶段:
  1. 初始标记阶段
  2. 并发标记阶段
  3. 再次标记阶段,因为工作线程和垃圾回收线程共同工作,所以需要再次标记.所以不可避免的还是会有漏网之鱼
  4. 并发清除阶段
  • 由于采用标记-清除(一般老年代采用标记压缩),所以不可避免的会有内存碎片,所以使用CMS为新对象分配内存空间的时候,将无法使用指针碰撞技术,只能选择空闲列表进行内存分配
  • -XX:+UseCMSCompactAtFullCollection,用于在执行完指定的FullGC之后是否对内存空间进行压缩整理.
  • -XX:CMSFullGCsBeforeCompaction 用于设置执行多少次Full GC之后对内存空间进行压缩整理
  • 一个要纠正的点是 Full-GC是遍布整个堆空间的,不只是在老年代中.而CMS可以提供-XX:CMSInitiatingOccupancyFraction用于设置当老年代的内存使用率达到多少的时候执行内存回收.(缺省92%) 注意 这里的内存回收仅限于老年代,所以可以有效的降低FullGC的次数

区域分代式:G1(Garbage-First)收集器

 

  • 点赞
  • 收藏
  • 分享
粉丝 15
博文 164
码字总数 15911