Java 垃圾回收机制学习

##什么是GC##
GC(Garbage Collection),也就是垃圾收集,它可以实现内存的自动回收。一般认为GC是专属于java语言的一个东西,但事实上GC早于java出现,在1960年,Lisp是第一次使用了GC技术。别的不多说了,能看这篇文章的肯定也是知道什么是GC了。

##哪些内存需要回收##

首先我们需要知道jvm在执行程序的过程中,会把它所管理的内存划分为若干个不同的数据区域如下图:
这里写图片描述
各个区域的简单介绍如下:

  • **程序计数器(Program Counter Register)**是一块较小的内存区域,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码命令。由于java虚拟机的多线程是通过线程轮流分配处理器执行时间的方式来实现的,所以一个处理器在一个确定的时候只能处理一条线程的指令,为了能在线程切换后恢复到正确的指令位置,每个线程都需要一个独立的程序计数器来独立存储当前线程的执行位置。此区域是java虚拟机规范中唯一一个没有任何OutOfMemoryError的区域

  • java虚拟机栈 和程序计数器一个,java虚拟机栈也是线程私有的,它的生命周期与线程一致。每个线程的方法在执行时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从调用到结束,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程,示意图如下
    这里写图片描述

  • 本地方法栈和虚拟机栈类似,只不过虚拟机栈是为虚拟机执行java服务,而本地方法栈则为虚拟机使用到的native方法服务。

  • java堆 对于大多数应用来说,java堆(java heap)是java虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟启动时创建。此区域的位移目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。java堆是GC的主要区域。但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,所有对象都在堆上分配这一点已经不在那么绝对。从内存回收的角度来看,java堆又可以分为新生代老年代

  • 方法区和java堆一样,是所有线程公用的一块内存区域,主要存储虚拟机加载的类信息,常量,静态变量,及时编译器编译后的代码等数据

如上所述,其中程序计数器,虚拟机栈,本地方法栈三个区域是线程私有的,随线程而灭。栈中的栈帧随着方法的进入和退出而进行入栈出栈的操作,每一个栈分配的内存基本上在类结构确定下来时就已经确定了,因此这几个区域的内存分配和回收都具有确定性,这几个区域内就不需要过多考虑回收问题,因为方法结束时,内存就自然被回收了。而java堆和方法区则不一样,一个接口中的多个实现类需要的内存不一样,一个方法中多个分支需要的内存也不一样,我们只有在程序运行时才知道会创建哪些对象,这部分的内存分配和回收都是动态的,GC关注的也是这部分的内存
##怎样判断对象已死##
也就是怎样判断对象可以被回收。目前主要有两种方法

  • 引用计数法
  • 可达性分析算法

###引用计数法###
此方法会给对象添加一个计数器,每当增加一个引用,计数器就加一,删除一个引用,计数就减一,当数值为0时对象就没有再被引用了。此算法的优点是实现简单,判断效率高,但是它有一个主要的缺点就是很难解决相互引用问题。举个例子,对象A中只有一个成员变量instance,对象B中也只有一个变量instance,然后将对象B赋值给A的instance,把对象A赋值给B的instance,初此之外再无其他地方对这两个对象有引用,这样的话实际上对象A和对象B根本不可能再被访问,但是由于引用计数不为0,所以导致这两个对象无法被回收。
###可达性分析算法###
这个算法的基本思想是通过一系列成为"GC roots"的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象没有被任何gc roots相连,则认为这个对象不可用。如图所示,虽然object 6,object 7,object 8有相互引用,但是他们都没有和gc roots相连,所以这三个对象将被回收。在主流的商用程序语言(java,C#)中使用的都是可达性分析算法
这里写图片描述
那GC roots又是什么呢?在java语言中可以作为GC roots的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中的类静态属性引用对象
  • 方法区中常量引用对象
  • 本地方法栈中JNI已用对象

###对象引用方式###
在java中存在四种引用方式,分别是强引用(strong reference),软引用(soft reference),弱引用(weak reference),虚引用(phantom reference),介绍如下

  • 强引用是使用最广的引用方式,"Object o = new Object"这种用法就是强引用,这类的引用只要强引用关系存在,则对象就不会被回收。
  • 软引用用来引用一些目前还有用,但不是必须的对象。被软引用的对象,在系统将要发生OOM之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收后还没有足够内存空间,才会抛出OOM的异常,软引用一般用SoftReference实现。
  • 弱引用用来引用那些非必须的对象,比软引用更弱,被弱引用关联的对象只能生存到下次GC发生之前。也就是说放GC发生的时候,不管当前内存是否足够,都会回收弱引用的对象,用WeakReference实现。
  • 虚引用是更弱的引用,也成为幽灵引用,一个对象是否有虚引用存在根本就不会印象它是否可以被回收,也无法通过虚引用来获取对象(通过get方法会返回null),为一个对象设置虚引用的唯一目的就是能在这个对象被GC回收时收到一个系统通知

了解了不同的引用方式后,我们在平时的开发过程中可以更加灵活的使用对象。

###Finalize()方法###
当一个对象在可达性算法中被判定为不可达的时候,并不是肯定会被回收。因为GC在回收的时候,会有两次标记的过程,第一次标记的时候把不可达的对象找不出来,然后进行筛选,筛选的条件是看当前对象是否有有必要执行finalize()方法,而没有必要执行的判断条件是:1.当前对象没有覆盖finalize()方法,2.finalize已经被调用过。如果对象被判定为有必要执行finalize()方法,则该对象会被放在一个叫做F-Queue的队列中,并在一个由虚拟机建立,低优先级的Finalize线程中去执行。但这里的执行并不是一定要等finalize方法执行结束,只是触发他而已,因为如果某个对象的finalize执行缓慢,或者死循环,这样会导致其他对象永远在等待。然后GC会对F-Queue中的对象进行第二次标记,这次标记完后,这些对象将被回收。如果我们在finalize方法中将当前对象的赋值给其他引用,则可以逃过被回收的命运。
需要注意的是,一个对象的finalize只会被调用一次

另外一般情况下不建议使用finalize方法,因为它的运行代价高,不确定性大,无法保证各个对象的运行顺序。

##垃圾搜集算法##
前面知道了哪些对象会被回收,那么GC在回收对象的时候具体是怎样的实现呢?这里不放代码,只讲算法,代码我也不会。

###标记-清除算法###
标记清除算法分为两个阶段,标记和清除:首先标记出所有需要回收的对象,然后在标记完成后统一回收所有被标记的对象。这个算法是最基础的算法,后面其他算法都是基于此进行改进的。此算法主要有两个缺点:1.效率问题,标记和清除两个过程效率都不高;2.空间问题,清除完成后会产生大量不连续的内存碎片,这样会导致以后要申请一块大内存的时候,找不到符合要求的区域而又出发一次GC。标记-清除算法的示意图如下:
这里写图片描述

###复制算法###
复制算法将可用内存分为大小相等的两块,每次只使用其中的一块,当一块的内存用完的时候,将存活的对象全部复制到另一块空的内存,只剩下需要回收的内存,然后把这些内存一次性清理掉。这种算法不要考虑内存碎片问题,因为每次都是正片清除,实现简单,运行高效,但是这种算法的内存使用缩小为原来的一半,导致代价过高。示意图如下:
这里写图片描述
不过现在的商业虚拟机都是用这种算法来回收新生代。因为研究发现,新生代的对象大部分的生命周期都很短,所以并不需要按照1:1来划分内存空间,而是将内存分为一块较大的Eden空间两块较小的Survivor空间,这里称之为Survivor1和Survivor2,每次使用Eden和其中一块Survivor(这里假设使用Survivor1)。当回收时,将Eden和Survivor1上还存活的对象复制到Survivor2上,然后清理掉Eden和Survivor1。目前HotSpot虚拟机上默认Eden和Survivor大小的比例为8:1,如下图:
这里写图片描述
但是有个问题就是不是每次存活的对象都只有不多于10%,当存活对象大于10%的时候Survivor2就放不下了,那怎么办呢?这时候就需要把这些对象方法哦老年代去。

###标记-整理算法###
复制算法在对象存活率低的新生代使用比较有效,但是遇到对象存活率很高的老年代就不适用了,因为老年代大部分对象都是不需要回收的,而复制算法有需要开辟一开空间去存放复制的对象,这样会很容易导致内存空间不足,所以在老年代不能采用复制算法,有人提出了一种“标记-整理”算法,这个算法的标记过程和新生代的“标记-清除”算法一样,但是在回收阶段不是直接对对象进行清理,而是将所有存活的对象移到一端,然后清理掉边界意外的内存,如图所示:
这里写图片描述

##垃圾收集器##
前面讲的算法如果不用代码实现,都没什么卵用,当然目前肯定已经有实现了这些算法的程序了,我们叫它为垃圾收集器。不同的厂商,不同的虚拟机提供的垃圾收集器是不一样的,而且一般都会提供参数供用户根据自己的需求来组合出各个年代所使用的垃圾收集器组合。本文讨论的收集器是基于hotspot在jdk1.7之后使用的收集器,包含的虚拟机如下图:
这里写图片描述
图中收集器处于哪个取就说明该收集器是专门作用于哪个区,两个收集器之间有连线,说明这两款可以搭配使用。要说明一点是,并没有最好的垃圾收集器,我们在选择垃圾收集器的时候要根据具体的应用场景来决定。接下来就是对各个垃圾收集器做一下介绍。

  • Serial 收集器 是历史最悠久的收集器,从名字可以看出,它是一款串行收集器,不但是垃圾收集使用单线程,更重要的是Serial在回收垃圾的时候会暂停其他所有线程(stop the world),直到收集结束。serial收集器示意图如下
    这里写图片描述
    虽然听上去感觉Serial收集器很差,但是实际上它仍然是jvm在client模式下默认的垃圾收集器,因为它简单而高效。在单个cpu环境下,由于没有线程切换带来的开销,而且在桌面应用一般分配给虚拟机的内存不会很大,搜集一次花费的时间也不会太长,基本只有几十毫秒,这对用户来说是可以是接受的。

  • ParNew收集器
    ParNew收集器可以说是Serial的多线程版本,它在做垃圾回收的时候是用多线程去做,在多处理器环境下会比Serial效率更好。事实上ParNew和Serial共用了相当多的代码。可以说除了多线程进行垃圾回收,其他行为和Serial完全一样。示意图如下
    这里写图片描述
    ParNew虽然与Serial相比没有太多创新,但是它却是许多运行在server模式下虚拟机首选的新生代收集器,其中一个与性能无关的原因是,除了Serial收集器外,目前只有ParNew可以和CMS收集器搭配使用。而CMS收集器又是很牛逼的收集器,它作用于老年代,可以实现垃圾收集的同时用户程序继续运行,也就是不用全部挂起。但是它不能和Parallel Scavenge收集器搭配使用,所以在老年代使用CMS的虚拟机上新生代只能选择ParNew。

  • Parallel Scavenge收集器是一个新生代收集器,使用复制算法,而且是多线程收集器,那么它和ParNew有什么区别呢。一般的收集器关注的点都是怎样缩短GC时用户线程的暂停时间,而Parallel Scavenge的目的则是达到一个可控制的吞吐量。吞吐量是指CPU用于运行用户代码的时间与CPU总运行时间的比值,比如虚拟机总共运行了100秒,其中垃圾收集花了1秒,那么吞吐量就是99%。吞吐量越高说明用户代码使用cpu的效率越高。Parallel Scavenge有两个参数可以精确控制吞吐量,分别是-XX:MaxGCPauseMillis和-XX:GCTimeRatio,前者用于指定最大垃圾收集停顿时间,后者用于直接设置吞吐量。Parallel Scavenge还有一个参数-XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开之后就不需要手工指定新生代的大小(-Xmn),Eden与Survivor区的比例(-XX:SurvivorRatio),晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机就会根据系统当前的运行情况动态调整参数以提供最合适的停顿时间或者最大吞吐量,这种调节方式称作GC自适应调节策略(GC Ergonomics)。

  • Serial Old收集器是Serial收集器的老年代版本,同样是单线程的,使用标记-整理算法。这个收集器主要是给在client模式下的虚拟机使用,如果是在server模式下使用,那么主要有两个用途:1.在JDK1.5及之前的版本中与Parallel Scavenge配合使用;2.作为CMS的后背方案,当CMS发生Concurrent Mode Failure时,就会使用Serial Old收集器。

  • Parallel Old收集器Parallel Scavenge的老年代版本,使用多线程和"标记-处理"算法。

  • CMS收集器是一种以获取最短停顿事件为目标的收集器。目前大多数的java应用程序都使用在服务器上,所以对响应速度要求很高,CMS很好的满足了这个要求
    CMS是基于标记-清除算法,它的工作过程分为四个步骤
    1.初始标记
    2.并发标记
    3.重新标记
    4.并发清除

其中,初始标记和重新标记仍然需要“stop the world"
初始标记只是标记了GC roots的直接关联对象,速度很快
并发标记阶段就是进行GC Roots tracing的过程,而重新标记阶段是为了修正在并发标记期间,由于用户代码
继续运行而导致标记产生变动的那一部分对象的标记记录。这部分时间比初始标记花费的事件稍长,但远比并发标记快。
而由于耗时最长的并发标记和并发清除过程是和用户代码同时进行,所以从总体来说CMS的收集过程是和用户代码同时进行的
CMS的示意图如下:这里写图片描述
CMS虽然很优秀,但是也有一些缺点,主要如下:

  • CMS对CPU资源特别敏感。CMS默认开启的收集线程数是(cpu+3)/4,当cpu数量大于4的时候,并发回收垃圾线程不少于25%的cpu资源,随着cpu数量的增加这个比例越来越低,但是如果cpu数量小于4,比如2,那么回收垃圾的线程相当于占了一半的cpu资源,这样会严重影响用户 代码的执行效率,因为多线程之间切换是需要耗费资源的。

  • CMS无法处理浮动垃圾,可能出现"Concurrent Mode Fail"失败而导致full gc的产生。由于在CMS收集过程中,用户的代码还在继续运行,这样 会产生新的内存对象,而这些内存对象只能在下次GC的时候进行回收,这些对象就是”浮动对象“。由于CMS垃圾收集器和用户代码是同时运行,所以必须在 老年代留出一部分内存空间给用户代码使用,在jdk1.5下,CMS默认会在老年代被使用到68%的时候出发CMS收集.这个值是可以通过-XX:CMSinitiatingOccupanyFraction来指定的。如果程序运行期间,预留的内存无法满足程序的需求,就会出现一次"Concurrent Mode Fail",这时虚拟机就会使用后备方案:Serial Old垃圾收集器。这样停顿时间就更长了。

  • 由于CMS是基于标记-清除算法的,这个算法会产生内存碎片,如果程序找不到足够大的连续内存,则会出发一次full gc,为了解决这个问题CMS,提供了一个参数XX:+UseCMSCompactAtFullCollection,这个参数的作用是,放虚拟机发现内存不足要进行full gc的时候,CMS对内存碎片进行整理由于整理的过程是不能并发的,所以用户的程序执行会被影响。

  • **G1收集器(Garbage-First)**是当前最前沿的收集器,主要用于服务端应用,与其他收集器相比,它有如下优点:

  • 并行与并发:G1能充分利用cpu资源,既可以使用多线程并行收集垃圾,还可以和用户程序并发进行

  • 分代收集:G1可以处理新生代和老年代,并对不同存活时间的对象分别对待处理,以获取更好的收集效果

  • 空间整合:整体上使用标记整理算法,局部又是复制清理算法,这就导致它不会产生内存碎片

  • 可预测的停顿:G1可以指定在M长的时间内,花费在GC上的所用的时间。

G1可以回收老年代和年轻代,它将内存划分成大小相等的若干个独立区域(Region),虽然还有年轻代和老年代的概念,但是已经不是物理隔离的了,而是 一些Region的集合。
G1可预测停顿的实现是这样的,它不再对整个内存区域进行垃圾收集,而是跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小和回收所需的时间的经验值) 然后维护一个优先级列表,根据用户指定的时间,优先回收价值高的Region,这种方式可以在有限的时间内实现内存回收的最大效率。

G1收集器的回收大致可以分为如下几步:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

前三个步骤与CMS差不多,在第四个筛选回收阶段,会先对被标记Region进行价值排序,然后再根据用户指定是的时间按价值从高到低的顺序回收Region中的内存。

##理解GC日志##
以下内容来自博客Java GC 介绍并有所补充
了解GC日志可以帮助我们更好地排查一些线上问题,如OOM、应用停顿时间过长等等。GC日志对我们进行JVM调优也是很有帮助的。采用不同的GC收集器所产生的GC日志的格式会稍微不同,但虚拟机设计者为了方便用户阅读,将各个收集器的日志都维持一定的共性。

具有一定共性的的GC日志格式大致如下所示:

<datestamp>:[GC[<collector>:<start occupancy1>-><end occupancy1>(total size1),<pause time1> secs]<start occupancy2>-><end occupancy2>(total size2),<pause time2> secs] [Times:<user time> <system time>, <real time>]
  • datestamp : 表示GC日志产生的时间点,如果指定的jvm参数是-XX:+PrintGCTimeStamps,那么输出的是相对于虚拟机启动时间的时间戳,如果指定的是-XX:+PrintGCDateStamps,那么输出的是具体的时间格式,可读性更高
  • GC : 表示发生GC的类型,有GC(代表MinorGC)和FullGC两种情况
  • collector : 表示GC收集器类型,取值可能是DefNew、ParNew、PSYoungGen、Tenured、ParOldGen、PSPermGen等等
  • start occupancy1 : 表示发生回收之前占用的内存空间
  • end occupancy1 : 表示发生回收以后还占用的内存空间
  • total size1 : 该堆区域所拥有的总内存空间
  • pause time1 : 发生垃圾收集的时间
  • start occupancy2 : 表示回收前Java堆内存总占用空间
  • end occupancy2 : 表示回收后Java堆内存还占用的总空间
  • total size2 : 表示Java堆内存总空间
  • pause time2 : 表示整个堆回收消耗时间
  • Times:具体时间数据,user:用户态消耗的时间,sys:内核态消耗的时间,real:操作从开始到结束所经过的墙钟时间。

具体logsample可以参考博客Java GC 介绍

###垃圾收集相关参数###

  • -XX:+UseSerialGC
    虚拟机运行在client模式下的默认值,使用这个参数表示虚拟机将使用Serial + Serial Old收集器组合进行垃圾回收。
    -XX:+UseSerialGC表示使用这个设置,而-XX:-UseSerialGC表示禁用这个设置。
  • -XX:+UseParNewGC
    使用这个设置以后,虚拟机将使用ParNew + Serial Old收集器组合进行垃圾回收。
  • -XX:+UseConcMarkSweepGC
    使用这个设置以后,虚拟机将使用ParNew + CMS + Serial Old的收集器组合进行垃圾回收。注意Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器来进行回收(将会整理内存碎片)。
  • -XX:+UseParallelGC
    虚拟机运行在server模式下的默认值。使用这个设置,虚拟机将使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器组合进行垃圾回收。
  • -XX:+UseParallelOldGC
    使用这个设置以后,虚拟机将使用Parallel Scavengen + Parallel Old的收集器组合进行垃圾回收。
  • -XX:PretenureSizeThreshold
    设置直接晋升到老年代的对象大小,大于这个参数的对象将直接在老年代分配,而不是在新生代分配。注意这个值只能设置为字节,如-XX:PretenureSizeThreshold=3145728表示超过3M的对象将直接在老年代分配。
  • -XX:MaxTenuringThreshold
    设置晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就会加1,当超过这个值时就进入老年代。默认设置为-XX:MaxTenuringThreshold=15。
  • -XX:ParellelGCThreads
    设置并行GC时进行内存回收的线程数。只有当采用的垃圾回收器是采用多线程模式,包括ParNew,Parallel Scavenge、Parallel Old、CMS,这个参数的设置才会有效。
  • -XX:CMSInitiatingOccupancyFraction
    设置CMS收集器在老年代空间被使用多少(百分比)后触发垃圾收集。默认设置-XX:CMSInitiatingOccupancyFraction=68表示老年代空间使用比例达到68%时触发CMS垃圾收集。仅当老年代收集器设置为CMS时候这个参数才有效。
  • -XX:+UseCMSCompactAtFullCollection
    设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅当老年代收集器设置为CMS时候这个参数才有效。
  • -XX:CMSFullGCsBeforeCompaction
    设置CMS收集器在进行多少次垃圾收集后再进行一次内存碎片整理。如设置-XX:CMSFullGCsBeforeCompaction=2表示CMS收集器进行了2次垃圾收集之后,进行一次内存碎片整理。仅当老年代收集器设置为CMS时候这个参数才有效。
  • -XX:SurvivorRatio
    设置新生代中Eden区与survivor区的容量比值,默认为8,表示eden:survivor=8:1
  • -XX:UseAdaptiveSizePolicy
    动态调整各个区域大小和进入老年代的年龄
  • GCTimeRatio
    GC时间占总时间比率,默认值为99,即允许%1的GC时间,仅在使用Parallel Scavenge时生效
  • MaxGCPauseMillis设置GC最大停顿时间,仅在Parallel Scavenge时生效

###GC日志相关###

  • -XX:+PrintGCDetails
    表示输出GC的详细情况。
  • -XX:+PrintGCDateStamps
    指定输出GC时的时间格式,比指定-XX:+PrintGCTimeStamps可读性更高。
  • -Xloggc
    指定gc日志的存放位置。如-Xloggc:/var/log/myapp-gc.log表示将gc日志保存在磁盘/var/log/目录,文件名为myapp-gc.log。

参考资料如下:
《深入理解java虚拟机》周志明
java GC介绍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值