jvm(2)垃圾回收GC

越努力越幸运!

java的自动垃圾回收机制GC

一.怎么判断垃圾回收还是不回收?

 1.引用计数法

目前主流的虚拟机都没有使用引用计数法,主要原因就是它很难解决对象之间互相循环引用的问题。

  2.可达性分析法

通过一系列称为 GC Roots 的对象作为起始点,从这些点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链连接(用图论的话来说,就是从GC Roots到这个对象不可达),证明此对象不可用。

Java语言中,可作为GC Roots的对象包括:

(1)虚拟机栈(栈帧中的本地变量表)中引用的对象

(2)方法区中类静态属性引用的对象

(3)方法区中常量引用的对象

(4)本地方法栈中JNI ( 即一般说的Native方法)引用的对象

在JDK 1.2之后 ,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference )、软引用(Soft Reference )、弱引用(Weak Reference )、虚引用(Phantom Reference) 4种 , 引用强度依次逐渐减弱。

强引用

指在程序代码之中普遍存在的,类似“Object obj=new Object ( ) ”这类的引用 ,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用

用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会拋出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

弱引用

也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。在JDK1.2之后,提供了PhantomReference类来实现虚引用。

虚引用

也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。

二.对象回收过程

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的 ,这时候它们暂时处于“缓刑” 阶段 ,要真正宣告一个对象死亡 ,至少要经历两次标记过程

如果这个对象被判定为有必要执行finalize() 方法,那么这个对象将会放置在一个叫做 F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。

对于方法区(Hotspot虚拟机的永久代)的回收

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:

(1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例

(2)加载该类的ClassLoader已经被回收

(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

三.垃圾收集算法

1.标记-清除算法

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程如前。

它的主要不足有两个:

(1)效率问题,标记和清除两个过程的效率都不高;

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

 

2. 复制算法 

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

适用于对象存活率低的场景(新生代)

这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针 ,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。

将内存分为一块较大的Eden空间和两块较小的Survivor空间 ,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次地复制到另外一块Survivor空间上,最 后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的90% ( 80%+10% ) ,只有10% 的内存会被 “浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保( Handle Promotion )。

为什么要有两个survivor区?

1.Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

2.设置两个Survivor区最大的好处就是解决了碎片化

为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设现在只有一个survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化
我绘制了一幅图来表明这个过程。其中色块代表对象,白色框分别代表Eden区(大)和Survivor区(小)。Eden区理所当然大一些,否则新建对象很快就导致Eden区满,进而触发Minor GC,有悖于初衷。
一个Survivor区带来碎片化

碎片化带来的风险是极大的,严重影响JAVA程序的性能。堆空间被散布的对象占据不连续的内存,最直接的结果就是,堆中没有足够大的连续内存空间,接下去如果程序需要给一个内存需求很大的对象分配内存。。。画面太美不敢看。。。这就好比我们爬山的时候,背包里所有东西紧挨着放,最后就可能省出一块完整的空间放相机。如果每件行李之间隔一点空隙乱放,很可能最后就要一路把相机挂在脖子上了。

那么,顺理成章的,应该建立两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中。下图中每部分的意义和上一张图一样,就不加注释了。
两块Survivor避免碎片化
上述机制最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片

那么,Survivor为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果Survivor区再细分下去,每一块的空间就会比较小,很容易导致Survivor区满,因此,我认为两块Survivor区是经过权衡之后的最佳方案。

3.标记-整理算法

适用于对象存活率高的场景(老年代)

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是 ,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

标记过程类似“标记-清除”算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,直接清理掉端边界以外的内存,类似于磁盘整理的过程

 

内存申请过程

内存由Perm和Heap组成。其中Heap = {Old + NEW = { Eden , from, to } }。perm用来存放常量等。
heap中分为年轻代(young)和年老代(old)。年轻代又分为Eden,Survivor(幸存区)。Survivor又分为from,to,也可以不只是这两块,切from和to没有先后顺序。其中,old和young区比例可手动分配。

这里写图片描述

当OLD区空间不够时,JVM会在OLD区进行完全的垃圾收集。完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory”Error。

  • Eden

    • 与S0, S1一起组成Young Generation, 称为年轻代。属于堆内存的一部分
    • 可通过-Xmn:设置年轻代大小。SUN官方推荐为整个堆的3/8。
    • 新对象的分配发生该区域内,当新对象(非大对象)无法在该区域分配时便引发Minor GC
    • 大对象是指,需要大量连续内存空间的Java对象,最典型的就是那种很长的字符串及数组。当分配大对象时,如果年轻代中无法放置,则该大对象也会在Old区域中分配。
    • 发生Minor GC时,Eden + S0作为from space, S1 作为to space进行copy,将存活的对象复制到S1中,并清空Eden和S0, 下一次Minor GC时 from space变为Eden + S1, 而to space则变为S0, 并清空Eden和S1。如此反复进行。
    • 在Minor GC时,如果to space的空间无法容纳所有存活的对象,则这部分对象会被直接放到Old 区域
    • 经过数次Minor GC后仍然存活的对象将被移到Old区域。可以通过-XX:MaxTenuringThreshold:设置垃圾最大年龄,如果设置为0则年轻代对象不经过Servivor区,直接进入年老代
    • 可通过-XX:NewRatio:设置年轻代与年老代的大小比值
  • S0, S1

    • 称为Servivor, 与Eden区一起组成Young Generation
    • S0, S1在Minor GC时其中一个肯定会被清空(使用Copy算法)
    • S0, S1的大小是相同的
    • 可以通过-XX:SurvivorRatio:设置年轻代中Eden区与S0或S1大小比值
  • Old

    • 也叫Tenured Generation, 即年老代。属于堆的一部分
    • 当分配大对象时,如果年轻代中无法放置,则该大对象也会在Old区域中分配。
    • 经过数次Minor GC后Young Generation中仍然存活的对象将被移到Old区域。
    • 如果进行Minor GC时to space中放不下全部存活的对象,则这部分对象也会被直接放到该区域
    • 发生在该区域的垃圾回收称为Major GC,一般发生Major GC时伴随着对Perm区域和整个堆的清理,所以又称为Full GC

四.GC的种类

两种类型的GC:

  1. Minor GC

    • 也称为YGC(Young Generation Collection)针对年轻代进行的垃圾回收。
    • 默认情况下Full GC/Major GC会触发Minor GC
    • 使用Copy 算法进行回收,速度较快
    • 对应的回收器有三种:串行GC(Serial Copying), 并行GC(ParNew)和并行回收GC(Parallel Scavenge)
  2. Major GC/Full GC

    • 发生在Old区域的垃圾回收,经常会伴随至少一次的Minor GC
    • 速度一般比Minor慢10倍以上
    • 由于Marjor GC除并发GC外均需要对整个堆及Permanent Generation进行扫描和回收,因此又称为Full GC
    • 对应的回收器有四种:串行GC(Serial MSC), 并行MS GC(Parallel MSC), 并行Compacting GC(Parallel Compacting)和并发GC(CMS)

MajorGC/Full GC的触发

  • 对于Seial MSC, Parallel MSC和Parllel Compactingd而言,触发机制为

    • Old Generation 空间不足
    • Permanent Generation 空间不足
    • Minor GC时的悲观策略
    • Minor GC后在Eden上分配内存仍然失败
    • 执行Heap Dump时
    • 外部调用 System.gc时(可通过*-XX:DisableExplicitGC*来禁止)
  • 对于CMS而言,触发机制为

    • Old Generation 空间使用到一定比率时(如92%)触发。(参考*PrintCMSInitiationiStatistics*和*CMSInitiatingOccupancyFaction*参数)
    • 当Permanent Generation 采用CMS收集且使用到一定比率时触发
    • Hotspot 根据成本计算决定是否需要执行CMS GC
    • 外部调用System.gc(), 且设置了*ExplicitGCInvokesConcurrent*

GC过程

  1. 对象在Eden Space完成内存分配
  2. 当Eden Space满了,再创建对象,会因为申请不到空间,触发Minor GC,进行New(Eden + S0 或 Eden S1) Generation进行垃圾回收
  3. Minor GC时,Eden Space不能被回收的对象被放入到空的Survivor(S0或S1,Eden肯定会被清空),另一个Survivor里不能被GC回收的对象也会被放入这个Survivor,始终保证一个Survivor是空的
  4. 在Step3时,如果发现Survivor区满了,则这些对象被copy到old区,或者Survivor并没有满,但是有些对象已经足够Old,也被放入Old Space。
  5. 当Old Space被放满之后,进行Major GC|Full GC

五. 垃圾回收器

垃圾回收器种类

目前的收集器主要有三种:串行收集器、并行收集器、并发收集器 。

并行:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态

并发:用户线程和垃圾收集线程同时工作,用户程序继续运行,而垃圾收集程序运行于另一个CPU上

  • 串行收集器

    • 使用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。
    • 无法使用多处理器的优势,所以此收集器适合单处理器机器。当然,此收集器也可以用在小数据量(100M 左右)情况下的多处理器机器上。
    • 使用-XX:+UseSerialGC 打开。
  • 并行收集器

    • 对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用-XX:+UseParallelGC .打开。
    • 使用-XX:+UseParallelOldGC 打开对年老代使用。
    • 使用-XX:ParallelGCThreads= 设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等 。
    • -XX:MaxGCPauseMillis= 指定最大垃圾回收暂停(垃圾回收时的最长暂停时间)。为毫秒.
    • 通过-XX:GCTimeRatio= 设定吞吐量。 吞吐量为垃圾回收时间与非垃圾回收时间的比值来设定,公式为1/(1+N)。
  • 并发收集器

    • 可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用。
    • 使用-XX:+UseConcMarkSweepGC 打开。
    • 并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。
    • 通过设置-XX:CMSInitiatingOccupancyFraction= 指定还有多少剩余堆时开始执行并发收集

垃圾回收集实现

1.Serial收集器

  • 采用复制算法
  • Serial收集器是最基本历史最悠久的收集器,它是一个单线程的收集器,它在垃圾收集的时候必须暂停其他所有的工作线程,直到收集完成。
  • 垃圾收集的工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户的正常工作的线程全部停掉,这对很多应用来说都是难以接受的
  • 优点:简单高效 对于限定单个CPU的环境来说,收集器由于没有线程交互的开销,可以获得最高的单线程收集效率。

2.ParNew收集器

  • 采用复制算法
  • ParNew收集器其实就是Serial收集器的多线程版本,包括收集器可用的控制参数、收集算法、暂停所有用户线程、对象分配策略、回收策略等都和Serial收集器完全一样
  • 它是运行在Server模式下的虚拟机中首选的新生代收集器,其中一个原因是,除了Serial收集器,目前只有它能和CMS收集器配合使用。CMS收集器是HotSpot虚拟机上第一款真正意义上的并发收集器
  • ParNew收集器在单CPU环境中不会比Serial收集器有更好的效。

3.Parallel Scavenge收集器

  • 采用复制算法
  • Paraller Scavenge收集器也是一个新生代收集器,使用复制算法的收集器,是一个并行的多线程收集器
  • Parellel Scavenge收集器的特点是它的关注点和其它收集器不同,CMS等收集器尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量
  • 吞吐量是CPU用于运行用户代码的时间与CPU总消耗时间的比值。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
  • 停顿时间越短越适合与用户交互的程序,良好的响应速度能提升用户的体验;高吞吐量可以更高效的利用CPU时间
  • Parallel Scavenge收集器有一种称为GC自适应调用策略,虚拟机根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或最大的吞吐量
  • 自适应策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别

4.Serial Old收集器 

  • 采用标记-整理算法
  • 它是Serial收集器的老年版本,作用于Old Generation单线程收集器。

5.Parallel Old收集器

  • 采用标记-整理算法
  • Parallel Old是Parallel Scavenge收集器的老年版本,使用多线程。

6.CMS收集器

  • 采用标记-**清理**算法
  • CMS收集器是一种以获得最短回收停顿时间为目标的收集器。
  • 它分为四个步骤:初始标记、并发标记、重新标记、并发清除其中初始标记和重新标记两个步骤仍然需要暂停其它线程。
  • 初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段一般比初始标记阶段稍长一些,但远比并发标记的时间短。
  • 由于整个过程中耗时最长的并发标记和并发清理过程中,收集器都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用于线程一起并发执行的。
  • 缺点1:CMS收集器对CPU资源非常敏感,面向并发设计的程序都对CPU资源比较敏感,虽然并发阶段不会导致应用线程停顿,但是会因为占用了一部分线程导致应用程序编码,总吞吐量会降低。
  • 缺点2:CMS是基于标记-清理算法,收集结束的时候会产生大量空间碎片,空间碎片过多的时候将会给大家分配带来很大的麻烦,往往会出现老年代有很大剩余空间,但是无法找打足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
  • 缺点3:需要和serial old垃圾收集器合作,当出现并发失败的时候,会改用serial old垃圾收集器

7.G1收集器

  • 采用标记-整理算法
  • G1收集器是收集器理论进一步发展的产物,它与CMS收集器相比有两个重要的改进:G1收集器基于标记-整理算法,不会产生空间碎片,对于长时间运行的应用程序来说非常重要。二是,它可以非常精确的控制停顿,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
  • G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,它能够避免全区域的垃圾收集。

JVM 相关参数

  • 堆大小

    • -Xmx, -Xms: 最大和最小可用内存
    • -Xmn: 年轻代大小。SUN官方推荐为整个堆的3/8。持久代一般为64m
    • -Xss: 每个线程的堆栈大小
    • -XX:NewRatio: 年轻代与年老代的比值
    • -XX:SurvivorRatio: 年轻代中Eden区与Survivor区(单个,两个Survivor区大小相同)大小比值
    • -XX:MaxPermSize: 持久代大小
    • -XX:MaxTenuringThreshold: 设置垃圾最大年龄,如果设置为0则年轻代对象不经过Servivor区,直接进入年老代
  • 收集器设置

    • -XX:+UseSerialGC:设置串行收集器
    • -XX:+UseParallelGC:设置并行收集器。仅对年轻代有效。
    • -XX:+UseParalledlOldGC:设置并行年老代收集器
    • -XX:+UseConcMarkSweepGC:设置年老代为并发收集器
    • -XX:+UserParNewGC: 设置年轻代为并行收集器
  • 垃圾回收统计信息

    • -XX:+PrintGC
    • -XX:+PrintGCDetails
    • -XX:+PrintGCTimeStamps
    • -Xloggc:filename
  • 并行收集器设置

    • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
    • -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
    • -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
  • 并发收集器设置

    • -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
    • -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。 参考

辅助信息

JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:

  • -XX:+PrintGC
  • -XX:+PrintGCDetails
  • -XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
  • -XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
  • -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用
  • -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
  • -Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。

常用工具

JDK自带工具

Sum jdk自带了若干个内存相关的工具,位于%JAVA_HOME%bin目录下

  • jvsiualvm: Java VsiualVM

    • 该工具也可以单独下载http://visualvm.java.net/
    • 提供在 Java 虚拟机 (Java Virutal Machine, JVM) 上运行的 Java 应用程序的详细信息
    • 包含了jconsole, jps, jinfo, jtack, jmap等工具的功能
  • jps

    • 与unix上的ps类似,用来显示本地的java进程,可以查看本地运行着几个java程序,并显示他们的进程号。
  • jstat

    • 一个极强的监视VM内存工具。可以用来监视VM内存内的各种堆和非堆的大小及其内存使用量。
  • jmap

    • 打印出某个java进程(使用pid)内存内的,所有‘对象’的情况(如:产生那些对象,及其数量)
  • jconsole

    • 一个java GUI监视工具,可以以图表化的形式显示各种数据。并可通过远程连接监视远程的服务器VM
  • jstatd - Virtual Machine jstat Daemon

jstatd是一个RMI服务应用,用于提供接口允许远程监控工具连接到本地运行的JVM上。
如果没有找到已安装的安全管理器则jstatd服务会安装一个RMISecurityPolicy的实例,因此需要指定一个安全策略文件。该文件必须符合默认的"Policy File Syntax"规则。
下面的策略文件允许jstatd 服务运行而不受任何安全限制。

 
  1. grant codebase "file:${java.home}/../lib/tools.jar" {

  2. permission java.security.AllPermission;

  3. };

将上述文本拷贝到文件中,如jstatd.all.policy,并以如下命令运行jstatd服务:

jstatd -J-Djava.security.policy=statd.all.policy

成功启动后,就可以在其它机器上通过JVisualVM的远程主机连到该机器上。

关于jstatd的具体用法可参考oracle 官网说明

第三方工具

MAT: Eclipse内存分析插件 Memory Anal

转载自https://blog.csdn.net/antony9118/article/details/51425581

       https://blog.csdn.net/Scythe666/article/details/518411

猜你喜欢

转载自blog.csdn.net/hezuo1181/article/details/82934135