第3章 垃圾收集器与内存分配策略

3.1 概述

  • Lisp语言是最早使用内存分配并GC的语言
  • 因为垃圾收集成为了高并发的瓶颈,并且需要排查内存溢出等复杂问题
  • 程序计数器、虚拟机栈、本地方法三个区域是随着线程生而生,线程灭而灭,所以内存的GC是固定的;我们只关心运行时的内存GC,因为只有在运行时我们才知道需要创建什么对象

3.2 对象已死么

  • java堆当中存放着几乎所有的java对象实例,在GC的时候,需要去判断这些对象实例是否已经死亡

3.2.1 引用计数算法

  • 对象引用的时候,计数器+1,引用失效的时候,计数器-1;但这种算法无法处理对象互相引用的问题
  • 如果两个对象互相引用 ,计数器都不为0,此时GC就不会回收他们
  • 所以在java没有彩计数器的算法来GC

3.2.2 可达性分析算法

  • 通过一个对象的GC来做为Root,向下搜索看看是否存在引用的链路,若存在,说明是对象实例是活着的,无法GC,若不存在,则GC
  • 几个对象互相引用,但如果没有实例化,则会被GC掉
  • java当中可以做为GC ROOT的对象有以下几种 :虚拟机栈当中的对象;方法区表类静态属性引用的对象;方法区当中常量引用的对象;本地方法栈中JNI引用的对象
  • 弊端:当应用工程比较大的时候,在几百兆的内存当中去查询引用链是非常花费时间的;并且不能保证在查找的过程当中,引用链路会发生变化

3.2.3 再谈引用

  • 有一些缓存数据,是没有引用关系的,那么这些如果被GC掉就会非常的可惜,所以java当中将引用分为几个等级;
  • 强引用(Strong Reference) :new 出来的对象,这类对象只要引用存在,则不会被回收掉
  • 软引用(Soft Reference):只有当内存发生溢出的时候才会把这些对象回收掉
  • 弱引用(Weak Reference):只要GC工作的时候,就一定会被回收,存活于两次GC的间隔
  • 虚引用(Phanton Reference):存在的目的就是在对象实例被回收的时候发出一个通知,无法实例化对象,属于最弱的引用

3.2.4 生存还是死亡

  • 一个对象若没有通过GC ROOT来找到对象的引用链,那么此时会被标记并筛选,若对象覆写finalize()方法或finalize()方法已经被虚拟机调用过(只能调用一次,第二次仍然会被GC掉),那么此对象不会被回收
  • 若没有需要执行finalize()方法,那么虚拟机将其放入F-Queue队列当中进行即将回收处理
  • 在即将回收处理的时候,对象可以通过finalize()方法当中通过重新建立引用来拯救自己

3.2.5 回收方法区

  • 方法区当中的内存被称为永久代,永久代当中的内存回收效率很低,因为大家都比较“永久”,所以无法全部回收
  • 废弃的常量的回收:常量存在于常量池当中,当没有一个对象引用此常量的时候,将在GC的时候清理出常量池
  • 无用类的回收:当一个类当中的所有实例都被GC;无法通过反射生成类对象的实例;该类的ClassLoader已经被回收的时候,GC会将这些无用的类回收掉
  • 在操作大量反射的时候,需要注意内存的使用情况,以保证永久代不会溢出

3.3 垃圾收集算法

3.3.1 标记-清除算法

  • 它是最基础的垃圾收集算法
  • 它的不足在于被标记清除的内存会产生大量的碎片,而当内存需要分配大对象的时候,会导致内存不连续而无法分配,需要再次触发垃圾回收机制,所以效率不高

TODO 3.3.2 复制算法

  • 它的不足:对象过多的时候复制的效率就会下降

3.3.3 标记-整理算法

  • 先进行标记清除算法,然后将存活的对象都向内存的一端移动,以保存内存空间可以连续,不产生碎片

3.3.4 分代收集算法

  • 按照对象的存活周期进行分配,将java堆内存分为新生代和老年代两块;
  • 新生代里面对象经过GC以后存活的很少,采用复制算法进行回收
  • 老年代里面对象经过GC以后存活的很多,彩标记-整理算法进行回收

3.4 HotSpot算法实现

3.4.1 枚举根节点

  • 可达性分析的弊端:工程比较大的时候,查找引用链比较费时;不能保证在查找过程当中引用链不发生变化
  • 目前主流的虚拟机都采用准确式GC,当系统停顿下来之后,可以快速计算出对象之间的引用
  • HotSpot通过OopMap的数据结构来记录,当类加载器加载完成后,将对象内什么偏移量是什么数据类型记录下来,同时在特定位置记录在栈和寄存器当中哪些位置是引用的;在GC的时候可以直接获取这些信息

3.4.2 安全点

  • 但是每次引用链发生变化,都需要生成OopMap数据结构,导致需要大量的额外空间,怎么解决这样的问题呢?
  • 通过设定Safepoint,当程序到达安全点的时候,程序会停顿下来进行GC处理,才会生成OopMap数据结构,防止OopMap数据结构频繁地变化
  • Safepoint的值设置多大合适呢?长时间执行的程序才会产生Safepoint,像循环跳转,方法调用,异常跳转等;
  • 另外一个问题,如何保证程序在停顿的时候,所有的线程都停止?一种方式是抢先式中断,将所有的线程都中断,若发现某线程没有到到Safepoint,再进行恢复,但是基本没有虚拟机采用此种方式;另一种方法,设置标志在安全点的位置,线程在执行过程当中轮询此标志,若为true,则自动中断挂起

3.4.3 安全区域

  • 当线程处于Sleep或者Blocked状态的时候,无法进入安全点,那么就不会被GC,此时就需要使用安全区域来解决这样的问题
  • 安全区域是指一段代码在一段时间里面,引用关系没有发生变化,那么在这个区域里面的任何地方开始GC都是安全的;
  • 虚拟机开始GC的时候,不会处理被标记为Safe Region的线程,当线程要离开Safe Region时,如果已经完成了GC,那么线程继续捃地,否则一直等待到可以安全离开为止

3.5 垃圾收集器

  • 如果收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现;虚拟机里面不止一种收集器
  • Hotspot里面有七种垃圾收集器,新生代的有:Serial、ParNew、ParallelScavenge;老年代的有:CMS、Parallel Old、Serial Old(MSC);还有G1是跨新生代和老年代
  • 没有完美的收集器

3.5.1 Serial收集器

  • 发展历史最悠久的收集器,采用标记整理的算法
  • 单线程的收集器;在GC的时候把其他所有的线程都暂停,直接GC结束,即Stop The World;
  • 这样的实现方式会给应用带来不好的体验;每运行1个小时,应用暂停5分钟?但若不暂停所有的线程怎么可以完全的进行GC呢?这是一个合理的矛盾,所以收集器的发展就是不断缩小暂停线程的时间
  • 在Client模式下,分配给桌面应用的内存不会太大,并且如果是单CPU的运行环境,Serial收集器的效率非常的高,优于其他的收集器;

3.5.2 ParNew收集器

  • ParNew就是Serial收集器的多线程版本,它可以对单个的线程进行GC;默认开启的收集线程数与CPU数量相等,可以通过—XX ParallelGCThreads来调整GC的线程数
  • 因为是多线程的操作,所以在Server模式下GC是首先的收集器;
  • 在单CPU下面,因为需要开启线程而带来一定的消耗,所以在单CPU环境下,没有Serial优越
  • Parallel意为并行,多条收集器与线程并行运行,但是是线程仍处理等待状态
  • Concurrent意为并发,多条收集器与线程并发运行,线程与收集器运行于不同的CPU,保证线程不等待,可以正常进行

3.5.3 Parallel Scavenge收集器

  • Parallel Scavenge的目的达到一个可控制的吞吐量,而不是缩小线程暂停的时间; 吞吐量 = 运行用户代码的时间 / (运行用户代码的时间 + GC时间)
  • 控制最大垃圾收集停顿时间: -XX:MaxGCPauseMillis;停顿时间越小,则新生代就越小,则GC对小新生代的回收就越快
  • 设置吞吐量大小: -XX: GCTimeRadio; 参数值区间[0-100);默认值是99,即 1/(1+99)=1%,说明默认设置的允许最大的垃圾收集时间为1%;
  • 自动根据系统适配收集器相关参数的值: -XX: +UseAdaptiveSizePolicy;虚拟机会根据GC自动调节策略去分配新生代大小(-Xmn)等参数信息,用户就无需再关注这些参数的设置
  • 它无法配和CMS工作

3.5.4 Serial Old收集器

  • 是Serial收集器的老年代版本,彩复制的算法
  • 主要存在的意义在于client模式下GC使用
  • 配合Parallel Scavenge和CMS使用

3.5.5 Parallel Old收集器

  • 它是Parallel Scavenge的老年代版本,采用多线程和标记-整理算法
  • 在Parallel Old收集器出现之前,只能使用Serial Old,而Serial Old无法使用多CPu环境的资源,导致性能比较差;
  • Parallel Scavenge与Parallel Old配合使用,保证吞吐量优先的垃圾收集器

3.5.6 CMS收集器

  • CMS(Concurrent Mark Sweep)是一种以缩短系统停顿时间为目标的收集器,基于标记-清除算法实现,又称为并发低停顿收集器(Concurrent Low Pause Collector)
  • 分四个步骤进行
    1. 初始标记(CMS initial Mark)
    1. 并发标记(CMS Concurrent Mark)
    1. 重新标记(CMS remark)
    1. 并发清除(CMS Concurrent sweep)
  • 步骤1和步骤2还需要Stop the World
  • 重新标记是标记那些修改了对象引用而产生的变化引用链
  • 标记时间停顿长度: 并发标记<重新标记<初始标记
  • CMS的缺点
    1. 对CPU资源敏感,当CPU数量少的时候,占用资源会比较多,应用就会变得非常的慢
    1. 无法处理浮动垃圾,即在GC过程当中,线程又创建出来的对象,此时无法被清除掉,所以只能等到下一次的GC
    1. 算法为标记-清除,会造成大量的碎片

3.5.7 G1收集器

  • G1(Garbage-First)是目前收集器的最前沿的技术,始于JDK 6u14,直到JDK 7u14才被商用
  • G1收集器的特点
  • 并行与并发:充分利用多核来缩短停顿的时间,通过并发的方式让Java其他线程可以继续运行
  • 分代收集
  • 空间整合:G1当中是 标记-整理和复制两种方式,所以不会产生碎片
  • 可预测停顿:建立预测停顿时间的模型
  • 分四个步骤进行:
    1. 初始标记
    1. 并发标记
    1. 最终标记
    1. 筛选回收

3.5.8 理解GC日志

  • 收集器日志形式都是由收集器本身的实现所决定的,每个收集器的日志格式都可以不一样
  • 启动工程的时候添加jvm参数,打开GC的文件-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/gc.log
  • 查看log信息以获取gc的日志
CommandLine flags: -XX:InitialHeapSize=1073741824 -XX:MaxHeapSize=3221225472 -XX:MaxPermSize=262144000 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:ReservedCodeCacheSize=67108864 -XX:+UseCodeCacheFlushing -XX:+UseCompressedOops -XX:+UseParallelGC
63.754: [GC [PSYoungGen: 188934K->4025K(888832K)] 300900K->159627K(1588224K), 0.0921650 secs] [Times: user=0.16 sys=0.03, real=0.09 secs] 
63.847: [Full GC [PSYoungGen: 4025K->0K(888832K)] [ParOldGen: 155602K->150933K(699392K)] 159627K->150933K(1588224K) [PSPermGen: 74746K->74697K(149504K)], 1.3261620 secs] [Times: user=2.64 sys=0.13, real=1.32 secs] 
  • InititalHeapSize: 初始堆内存
  • MaxHeapSize: 最大堆内存
  • MaxPermSize: 最大永久代内存
  • UseParallelGC: 使用Parallel Scavenge收集器
  • 63.754: GC发生的时间,jvm从开始运行到gc的时间
  • GC/Full GC: 收集器的停顿类型,用于区别老年代还是新生代,如果有Full,说明这次GC发生了Stop-The-World
  • PSYoungGen: 新生代(不同的收集器显示的名称不一样,这里为Parallel Scavenge收集器显示)
  • ParOldGen: 老年代
  • PSPermGen: 永久代
  • [PSYoungGen: 188934k-> 4025k(888832k)]: GC前该内存区域已经使用内存大小-> GC后该内存区域已使用内存大小(该内存区域总容量)
  • 300900k->159627k(15588224k): 方括号外表示 GC前Java Heap已经使用容量-> GC后Java Heap已经使用容量(Java Heap总容量)
  • 0.0921650 secs: 表示内存区域GC占用时间,单位是秒
  • [Times: user=0.16 sys=0.03 real=0.09 secs]: 详细的时间,user(消耗cpu的时间) sys(内核消耗cpu时间) real(操作开始到结束所经过的墙钟时间)

Serial ParNew Parallel Scavenge


DefNew ParNew PSYoungGen

3.5.9 垃圾收集器参数总结

3.6 内存分配与回收策略

  • 对象的内存分配一般都分配在堆上

3.6.1 对象优先在Eden分配

  • Eden是什么?Eden是新生代区域的一块,还有两块是From和To;Eden是存放新生的对象;From和To称为Survivor,GC后存活下来的对象
  • Eden满了,那么jvm将发起一次Minor GC,GC后存活下来的对象copy到 Survivor当中;而当Survivor满了,就将GC存活下来的对象copy到老年代当中
  • 每次GC后,Eden都会被清空掉
  • Minor GC: 新生代的GC,回收频繁,速度快
  • Major GC: 老年代的GC(Full GC)比Minor GC慢10倍以上

3.6.2 大对象直接进入老年代

  • 大对象指需要大量连续内存空间的java对象,大的字符串、数组等;短命的大对象不好处理
  • -XX:pretenureSizeThreshold参数 可以设置大对象直接进入老年代
  • 这样做可以避免在新生代回收的时候,使用复制的回收算法而耗费大量的时间

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

  • jvm为每个对象定义了一个对象年龄计数器;在Eden新生成的对象,Age为0;经过GC后,复制到Survivor当中的对象,Age值为1;
  • 而在Survivor当中经过一次GC,Age就加1;当Age的值大于 MaxTenuringThreshold设置的值(默认值为15)时,就会copy到老年代当中

3.6.4 动态对象年龄判定

  • jvm并不是永远按照MaxTenuringThreshold的值来将对象晋升为老年代的
  • 若Suvrivor当中相同年龄的对象的内存总和大于Survivor空间的一半,此时大于或等于此年龄的对象都会进入老年代

3.6.5 空间分配担保

  • Suvrivor经过GC后的对象进入老年代,老年代里面是否可以分配是无法确定的,有可能没有足够的容量,那么就要进行一次Full GC
  • HandlePromotionFailure参数来控制老年代分配内存是否允许失败,若允许失败,则进行一次Full GC;

3.7 本章小结

  • 本章介绍了垃圾收集的算法
  • jdk1.7 垃圾收集器特点和运作原理
  • jvm内存分配及回收规则
  • GC日志查看
  • 一些jvm options的意义