Java——GC(垃圾回收)

36 篇文章 0 订阅
21 篇文章 4 订阅

垃圾回收机制的意义

C++程序员非常头疼的一个问题就是内存管理,而垃圾回收机制使得Java程序员不用关心内存动态分配和垃圾回收的问题,交由JVM去处理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有时也将其称为“对象游离”。

存在内存泄漏的可能原因:

  1. 静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象 Object 也不能被释放,因为他们也将一直被Vector等应用着。
  1. 各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。
  1. 监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

内存溢出和内存泄漏:

内存溢出(out of memory): 是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;

内存泄露 (memory leak): 是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。memory leak会最终会导致out of memory!

哪些对象会被GC

可达性分析法

一个对象在没有任何强引用指向他或该对象通过根节点不可达时需要被垃圾回收器回收。不过要注意的是被判定为不可达的对象不一定就会成为可回收对象,被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

当一个对象通过一系列根对象(比如静态属性引用的常量)都不可达时就会被回收。简而言之,当一个对象的所有引用都为null。循环依赖不算做引用,如果对象A有一个指向对象B的引用,对象B也有一个指向对象A的引用,除此之外,它们没有其他引用,那么对象A和对象B都、需要被回收。

java中可作为 GC Root 的对象有:

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

堆内存的划分

Java 中对象都在堆上创建,为了GC,堆内存分为三个部分,也可以说三代,分别称为新生代,老年代和永久代。

新生代(Young generation)

其中新生代又进一步分为Eden区,Survivor 1区和Survivor 2区(比例一般8:1:1)。新创建的对象会分配在Eden区,当该区满了后,第一次Minor GC将存活的对象复制到一个Survivor区,在经历下一次Minor GC后,将Eden和已使用的Survivor区中存活的对象复制到另一个Survivor 区,并且将这些对象的年龄加1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。需要注意的是,一些大对象(大对象是指需要大量连续存储空间的对象,比如长字符串或数组)可能会直接存放到老年代。

老年代(Tenured / Old Generation)

老年代内存比新生代也大很多(大概比例是2:1),当老年代内存满时触发 Major GC 即 Full GC,Full GC 发生频率比较低,老年代对象存活时间比较长,存活率标记高。

永久代(Perm Area)

永久代一般用来存储类的元信息、静态文件,如Java类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

Metaspace 元空间

这里特别注意java8 中已经没有持久代,其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
  -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
  除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
  -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

JDK 8 中永久代向元空间的转换,为什么要做这个转换?
  1、字符串存在永久代中,容易出现性能问题和内存溢出。
 2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
 3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
参考:Java8内存模型—永久代(PermGen)和元空间(Metaspace)

为什么这样划分

  1. 为什么要分新生代和老年代

    Java的垃圾回收器采用的算法是分代回收算法。分代回收算法=复制算法+标记整理算法。不同类型的对象生命周期(新生代和老年代)决定了更适合采用哪种算法。

  2. 新生代为什么分一个Eden区和两个Survivor区
    假设新生代只有一个Eden区,当GC操作后,需要将Eden区存活对象复制到另外一块区,所以新生代需要额外划分一块Survivor区,用于存放GC后存活的对象。
    为什么要有两个Survivor区?
    因为第二次GC操作Eden区和Survivor区也需要被清理,这时就需要另一块空间,所以Survivor区需要一分为二。

  3. 一个Eden区和两个Survivor区的比例为什么是8:1:1
    新创建的对象都是放在Eden空间,这是很频繁的,尤其是大量的局部变量产生的临时对象,这些对象绝大部分都应该马上被回收,能存活下来被转移到survivor空间的往往不多。所以,设置较大的Eden空间和较小的Survivor空间是合理的,大大提高了内存的使用率。
    8:1:1这个比例是可以调整的,包括上面的新生代和老年代的1:2的比例也是可以调整的。
    参考:JVM之垃圾回收机制

何时GC

  • 当年轻代内存满时,会引发一次普通minor GC,该GC仅回收年轻代。需要强调的时,年轻代满是指Eden代满,Survivor满不会引发GC
  • 当年老代满时会引发Full(major) GC,Full GC将会同时回收年轻代、年老代
  • 当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载

GC算法

复制算法

把内存空间划为两个区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。优点:实现简单,运行高效,克服句柄的开销和解决堆碎片。缺点:会浪费一定的内存。一般新生代采用这种算法。

标记清除算法

分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。该算法的缺点是效率不高并且会产生不连续的内存碎片,一般用于老年代的垃圾回收。

标记整理算法

标记阶段与标记清除算法一样,但后续并不是直接对可回收的对象进行清理,而是让所有存活对象都向一端移动,然后清理。该算法不会造成内存碎片,一般用于老年代的垃圾回收。在基于该算法的收集器的实现中,一般会增加句柄和句柄表。

垃圾收集器

新生代常用的垃圾收集器:Serial、PraNew、Parallel Scavenge
老年代常用的垃圾收集器:Serial Old、Parallel Old、CMS

  1. Serial 收集器:新生代单线程收集器,一种古老的收集器,标记和清理都是单线程,优点是简单高效,缺点必须暂停所有用户线程。

  2. Serial Old 收集器:老年代单线程收集器,Serial收集器的老年代版本,采用的是Mark-Compact(标记整理)算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。

  3. ParNew 收集器:新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。

  4. Parallel Scavenge 收集器:新生代并行收集器,追求高吞吐量,高效利用CPU,能够了达到一个可控的吞吐量,它在回收期间不需要暂停其他用户线程。吞吐量一般为99%,吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。

  5. Parallel Old 收集器:Parallel Scavenge 收集器的老年代版本,并行收集器,吞吐量优先,使用多线程和Mark-Compact算法

  6. CMS(Concurrent Mark Sweep)收集器:高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择

  7. G1:G1收集器是当今比较流行的收集器,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

CMS 和 G1

CMS

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。这是因为CMS收集器工作时,GC工作线程与用户线程可以并发执行,以此来达到降低收集停顿时间的目的。
CMS收集器仅作用于老年代的收集,是基于标记-清除算法的,它的运作过程分为4个步骤:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

其中,初始标记、重新标记这两个步骤仍然需要Stop-the-world(把耗时的部分抽出来)。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始阶段稍长一些,但远比并发标记的时间短。

CMS收集器缺点:

CMS收集器对CPU资源非常敏感。
CMS收集器无法处理浮动垃圾(Floating Garbage)。
CMS收集器是基于标记-清除算法,该算法的缺点都有(产生碎片不适合新生代)。

G1

G1重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域(新生代和老年代没有物理隔离)。这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操作在多长时间内完成。即G1提供了接近实时的收集特性

G1收集的运作过程大致如下:

  • 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
  • 并发标记(Concurrent Marking):是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  • 最终标记(Final Marking):是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
  • 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
优点:
  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-the-world停顿的时间,部分其他收集器原来需要停顿Java线程执行的GC操作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
  • 分代收集
  • 空间整合:与CMS的标记-清除算法不同,G1从整体来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对于CMS的一个优势,降低停顿时间是G1和CMS共同的关注点。

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1会通过一个合理的计算模型,计算出每个Region的收集成本并量化,这样一来,收集器在给定了“停顿”时间限制的情况下,总是能选择一组恰当的Regions作为收集目标,让其收集开销满足这个限制条件,以此达到实时收集的目的。

Full GC和并发垃圾回收

并发垃圾回收器的内存回收过程是与用户线程一起并发执行的。通常情况下,并发垃圾回收器可以在用户线程运行的情况下完成大部分的回收工作,所以应用停顿时间很短。但由于并发垃圾回收时用户线程还在运行,所以会有新的垃圾不断产生。作为担保,如果在老年代内存都被占用之前,如果并发垃圾回收器还没结束工作,那么应用会暂停,在所有用户线程停止的情况下完成回收。这种情况称作Full GC,这意味着需要调整有关并发回收的参数了。

由于Full GC很影响应用的性能,要尽量避免或减少。特别是如果对于高容量低延迟的电商系统,要尽量避免在交易时间段发生 Full GC。在对JVM调优的过程中,很大一部分工作就是对于 Full GC 的调节。有如下原因可能导致Full GC:

  1. 年老代(Tenured)被写满

  2. 持久代(Perm)被写满

  3. System.gc()被显示调用

  4. 上一次GC之后Heap的各域分配策略动态变化

与垃圾回收相关的JVM参数

  • -Xms / -Xmx — 堆的初始大小 / 堆的最大大小
  • -Xmn — 堆中年轻代的大小
  • -XX:-DisableExplicitGC — 让System.gc()不产生任何作用
  • -XX:+PrintGCDetails — 打印GC的细节
  • -XX:+PrintGCDateStamps — 打印GC操作的时间戳
  • -XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小
  • -XX:NewRatio — 可以设置老生代和新生代的比例
  • -XX:PrintTenuringDistribution — 设置每次新生代GC后输出幸存者乐园中对象年龄的分布
  • -XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值
  • -XX:TargetSurvivorRatio:设置幸存区的目标使用率

总结

  • 在Java中,对象实例都是在堆上创建;一些类信息,常量,静态变量等存储在方法区。堆和方法区都是线程共享的。

  • 在Java中,GC是由一个被称为垃圾回收器的守护线程执行的。

  • 在从内存回收一个对象之前会调用对象的finalize()方法。

  • 作为一个Java开发者不能强制JVM执行GC;GC的触发由JVM依据堆内存的大小来决定。

  • System.gc()和Runtime.gc()会向JVM发送执行GC的请求,但是JVM不保证一定会执行GC。

  • 发生Major GC时用户线程会暂停,会降低系统性能和吞吐量。

  • JVM的参数-Xmx和-Xms用来设置 Java 堆内存的初始大小和最大值。依据个人经验这个值的比例最好是1:1或者1:1.5。比如,你可以将-Xmx和-Xms都设为1GB,或者-Xmx和-Xms设为1.2GB和1.8GB。

  • Java中不能手动触发GC,但可以用不同的引用类来辅助垃圾回收器工作(比如弱引用或软引用)。

扩展

Java内存模型
JVM调优

参考资料

Java中的垃圾回收机制

深入理解java垃圾回收机制

弄明白CMS和G1,就靠这一篇了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值