第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)
- 分四个步骤进行
-
- 初始标记(CMS initial Mark)
-
- 并发标记(CMS Concurrent Mark)
-
- 重新标记(CMS remark)
-
- 并发清除(CMS Concurrent sweep)
- 步骤1和步骤2还需要Stop the World
- 重新标记是标记那些修改了对象引用而产生的变化引用链
- 标记时间停顿长度: 并发标记<重新标记<初始标记
- CMS的缺点
-
- 对CPU资源敏感,当CPU数量少的时候,占用资源会比较多,应用就会变得非常的慢
-
- 无法处理浮动垃圾,即在GC过程当中,线程又创建出来的对象,此时无法被清除掉,所以只能等到下一次的GC
-
- 算法为标记-清除,会造成大量的碎片
3.5.7 G1收集器
- G1(Garbage-First)是目前收集器的最前沿的技术,始于JDK 6u14,直到JDK 7u14才被商用
- G1收集器的特点
- 并行与并发:充分利用多核来缩短停顿的时间,通过并发的方式让Java其他线程可以继续运行
- 分代收集
- 空间整合:G1当中是 标记-整理和复制两种方式,所以不会产生碎片
- 可预测停顿:建立预测停顿时间的模型
- 分四个步骤进行:
-
- 初始标记
-
- 并发标记
-
- 最终标记
-
- 筛选回收
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的意义