什么是GC垃圾回收
垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。
注意:垃圾回收回收的是无任何引用的对象占据的内存空间而不是对象本身。换言之,垃圾回收只会负责释放那些对象占有的内存。
分析
引用:如果Reference
类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。(引用都有哪些?对垃圾回收又有什么影响呢?)
垃圾:无任何对象引用的对象。(怎么通过算法找到这些垃圾呢?)
回收:清理垃圾”垃圾“占用的内存空间而非对象本身(通过怎样的算法实现垃圾回收呢?)
发生地点:一般发生在堆内存空间中,因为大部分对象都存储在堆内存空间中(堆内存为了配合垃圾回收进行了不同区域的划分,各个区域有什么不同呢?)
发生时间:程序空间不定时回收(回收的执行机制是什么?是否可以通过显式调用函数方式来确定回收过程)
这些都是我们需要解决的问题。
Java中对象引用
- 强引用(Strong Reference):如”Object obj = new Object()“,这类引用是Java程序中最普遍的。只要强引用还存在,垃圾回收机制就永远不会回收掉被引用的对象。
- 软引用(Soft Reference):它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用的时候,这类引用关联的对象将会被垃圾收集器回收。JDK1.2之后提供了
SoftReference
类来实现软引用。 - 弱引用(Weak Reference):它也是用来描述非必须的对象的,但它的引用强度比软引用更加弱一些,被弱引用关联的对象只能生存到下一次垃圾回收发生之前。在垃圾回收开始时,无论内存空间是否足够,都会回收只被弱引用关联的对象。在JDK1.2之后,提供了
WeakReference
来实现弱引用。 - 虚引用(Phantom Reference):最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2之后提供了
PhantomReference
类来实现虚引用。
判断对象是否是需要回收的垃圾算法
引用计数算法(Reference Counting Collector)
堆中每个对象(不是引用)都有一个计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当每一个地方引用,计数器值就会加1(a=b, b b被引用,则b的引用计数加一)。当引用失效时(一个对象的某个引用超过生命周期(出作用域范围)或者被设置为一个新值时),计数器值就减一,任何引用计数为0的对象可以被当作垃圾收集。
优点:引用计数收集器执行简单,判定效率高,交织在程序中运行。对程序不被长时间打断的环境比较有利(OC的内存管理使用该算法)
缺点:难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销,所以Java语言并没有选择这种算法进行垃圾回收。
根搜索算法(Tracing Collector)
首先了解一个概念:根集(Root Set)
所谓根集(Root Set)就是正在执行的Java程序可以访问的引用变量(注意:不是对象)的集合(包括局部变量,参数,类变量),程序可以使用引用变量访问对象属性和调用对象的方法。
这种算法的基本思路:
- 通过一系列名为”GC Roots“的对象作为起点,寻找对应的引用节点。
- 找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点
- 重复第二的步骤
- 搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连时,证明此对象不可用。
Java和C#都是采用根搜索算法来判定对象是否存活的。
垃圾回收器将某些特殊的对象定义为GC Roots根对象。
- 虚拟机栈中引用对象(栈帧中本地变量表)
- 方法区中常量引用对象
- 方法区中类静态属性引用的对象
- 本地方法栈中JNI(Native方法)引用对象
- 活跃线程
接下来,垃圾回收器会对内存中的整个对象图进行遍历,它先从GC根对象开始,然后是根对象引用其他对象,比如实例变量。回收器将访问到所有的对象都标记为存活。
存活对象在上图中都标记为蓝色。当标记阶段完成了之后,所有存活的对象都已经被标记完了,其他的那些(上图中灰色的那些)也就是GC根对象不可达的对象,也就是说你的应用不会再用到它了,这些就是垃圾对象,回收器将会再接下来的阶段清除他们。
关于标记阶段有几个关键点是值得注意的
- 开始进行标记前,需要先暂停应用线程,否则如果对象图一直在变化的话是无法真正去遍历它的。暂停应用线程以便JVM可以尽情地收拾家务地情况称之为安全点(Safe Point),这里会触发一次Stop The World(STW)暂停。触发安全点地原因有许多,但最常见地就是垃圾回收了
- 暂停时间地长短并不取决于堆内对象地多少也不是堆地大小,而是存活对象地多少。因此,调高堆地大小并不会影响到标记时间地长短。
- 在根搜集算法中,要真正宣告一个对象死亡,至少需要两个过程:
如果对象在进行根搜索后发现没有于GC Roots相连接地引用链,那它会被第一次标记并且进行一次筛选。筛选条件是此对象是否有必要执行finalize()
方法。当对象没有覆盖finalize()
方法,或finalize()
方法已经被虚拟机调用过,虚拟机将这种情况都视为没有必要执行。
如果该对象被判定为有必要执行finalize()
方法,那么这个对象将会被放置在一个名为F-Queue
队列中,并在稍后由一条由虚拟机自动建立地,低优先级地finalize
线程去执行finalize()
方法。finalize()
方法是对象逃脱死亡命运地最后一次机会(因为一个对象地finalize()
方法最多只会被系统调用一次),稍后GC将对F-Queue
中地对象进行小规模地标记,如果要在finalize()
方法上拯救自己,只要在finalize()
方法中让该对象重新引用一个对象即可。而如果这时还没有关联到任何链上引用,那么它就会被回收掉。
- 实际上GC判断对象是否可达看到是强引用。
- 当标记阶段完成后,GC开始进入下一个阶段,删除不可达对象。
垃圾回收发生地点和垃圾回收的算法
垃圾回收发生地点
在JVM中存在垃圾回收主要是堆空间中,因为大部分对象都是存储在堆空间当中。
Java内存空间除了对空间还有其他部分:
- 栈:每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量和操作数栈,用于存放此方法调用过程中的临时变量、参数和中间结果。
- 本地方法栈:用于支持native方法的执行,存储了每个native方法调用的状态
- 方法区:存放了要加载的类信息、静态变量,final类型的常量、属性和方法信息,JVM用持久代(PermanetGeneration)来存放方法区,可通过设置
-XX:PermSize
和-XX:PermSize
来指定最小值和最大值
在堆空间中主要分为下面几个部分(jdk1.8之后永久代被称为元空间)
年轻代(Young Generation,采用复制算法进行GC)
几乎所有的新生成的对象都放在了年轻代。新生代内存按照8:1:1的比例分为了Eden
区和两个Survivor(Survivor0,Survivor1)区。大部分对象在Eden
区生成。当新的对象生成,Eden
空间申请失败(因为空间不足等),则会发起一次GC(Scavenge GC),回收后将Eden
存活的对象移动到Survivor0
区,然后清空Eden
区,持续这个步骤,当Eden
和Survivor0
区都满了进行GC将这两个区域任然存活的对象采用复制算法
移动到另外一个Survivor1
区域中,清空Eden
和Survivor0
。此时Survivor0
是空的,我们的jvm
会将Survivor0
和Survivor1
区域互换,始终保持Survivor1
是一个空闲的空间,如此往复。
当Survivor1
不足以存放Eden
和Survivor0
存活下来的对象,就会直接将其放入老年代。
当对象在Survivor
区躲过了一次GC的话,其年龄便会加1,默认情况下,如果对象年龄到达15岁就会移动到老年代。
如果老年代也满了,那么会触发Full GC
,也就是新生代,老年代都进行回收。
新生代的大小也可以由-Xmn
来控制,也可以用-XX:SurvivorRatio
来控制Eden
和Survivor
比例
-XX:MaxTenuringThreshold
— 设置对象在新生代中存活的次数
老年代(Old Generation, 采用标记清除和标记整理算法)
在年轻代中经历N次GC后任然存活的对象,就会被放到老年代。因此,可以认为老年代都是一些生命周期比较长的对象。内存也比新生代大很多(大概是1:2),当老年代内存满的时候出发Major GC
也就是Full GC
,Full GC
发生的频率较低,老年代存活时间较长,存活率较高。一般来说大对象会被直接分配到老年代。所谓大对象是指需要大量连续存储空间的对象,最常见的就是这种大数组。
byte[] array = new byte[4 * 1024 * 1024];
这种一般直接分配到老年代,当然这种分配也不是固定的。
永久代(Permanent Generation)
用于存放静态文件(class类、方法)和常量。永久代对垃圾回收没有显著的影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate
等,这种时候需要设置一个比较大的永久代来存放运行过程中新增的类。对永久代的回收主要是:废弃的常量和无用的类。
永久代空间在Java SE8特性中已经被移除。取而代之的是元空间(MetaSpace)。因此不会再出现“java.lang.OutOfMemoryError: PermGen error”错误。
堆内存分配策略明确以下三点
- 对象优先分配到
Eden
区 - 大对象直接进入老年代
- 长期存活的对象直接进入老年代
垃圾回收机制说明
- 新生代GC(Minor GC/Scavenge GC):发生在新生代的垃圾收集动作。因为Java对象大多都具有朝生夕灭的特点,因此
Minor GC
非常频繁(不一定等Eden
满才触发),一般回收速度也比较快。在新生代中,每次垃圾收集都会发现大量对象死去,只有少量存活,因此可以使用复制算法
- 老年代GC(Major GC/Full GC):发生在老年代的垃圾回收动作。
Major GC
,经常会伴随着至少一次Minor GC
。由于老年代中对象生命周期较长,因此Major GC
并不频繁,一般都是等老年代满了后才进行一次Full GC
,而且其速度一般会比Minor GC
慢上10倍以上,另外,如果分配了Direct Memory
,在老年代中进行Full GC
时会顺便清理掉Direct Memory
中废弃的对象。而老年代中因为对象存活率高、没有额外的空间对它进行分配担保,就必须使用标记-清除
或者标记整理
算法。
垃圾回收算法
复制算法
Minor GC
会把Eden
中的所有活的对象都移到Survivor区域中,如果Survivor
区中放不下,那么剩下的活的对象就被移到Old generation
中,也即一旦收集后,Eden
是就变成空的了。
当对象在 Eden
( 包括一个 Survivor
区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC
后,如果对象还存活,并且能够被另外一块 Survivor
区域所容纳( 上面已经假设为 from
区域,这里应为 to
区域,即 to
区域有足够的内存空间来存储 Eden
和 from
区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor
区域 ( 即 to
区域 ) 中,然后清理所使用过的 Eden
以及 Survivor
区域 ( 即 from
区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,通过-XX:MaxTenuringThreshold 来设定参数),这些对象就会成为老年代。
-XX:MaxTenuringThreshold — 设置对象在新生代中存活的次数
复制算法的缺点
- 很明显,它要浪费出一部分内存空间
- 如果对象的存活率很高,我们可以举一个很极端的例子,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有的引用地址重置一遍,复制这个工作需要一定的时间。所以
复制算法
必须在对象存活率低的地方使用
标记清除算法
用通俗的话解释一下标记清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将要回收的对象标记一遍,最终统一回收这些对象,完成标记清理工作接下来便让应用程序恢复运行。
主要进行两项工作,第一项则是标记,第二项则是清除。
标记清除算法缺点
- 首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲
- 其次,主要的缺点则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
标记整理算法
在整理压缩阶段,不再对标记的对像做回收,而是通过所有存活对像都向一端移动,然后直接清除边界以外的内存。
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价
标记整理算法缺点
标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
从效率上来说,标记/整理算法要低于复制算法。
垃圾回收执行时间和注意事项
GC分为Minor GC
和Full GC
Minor GC
:发生在Eden
区的垃圾回收
Full GC
:对整个堆进行整理,包括新生代,老年代,永久代。Full GC
因为要对整个堆回收,所以比Minor GC
要慢,因此尽可能减少Full GC
次数,在对JVM调优的过程中,很大部分就是对于Full GC
的调节。
有如下原因可能导致Full GC
:
- 老年代(Tenured)被写满
- 持久代(Perm)被写满
- System.gc()被显示调用
- 上一次GC之后Heap的各域分配策略动态变化
与垃圾回收时间有关的两个函数
System.gc();
命令行参数监视垃圾收集器的运行:
使用System.gc()
可以不管JVM使用哪一种垃圾回收算法,都可以请求Java的垃圾回收。在命令行中有一个参数-verbosegc
可以查看Java使用的堆内存的情况,他的格式是:
java -verbosegc classfile
需要注意的是,调用System.gc()
也仅仅是一个建议。jvm接收这个请求后,并不是立即做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收更加容易发生,或提早发生,或回收较多。
finalize()
在JVM垃圾回收器收集一个对象之前,一般要求程序调用适当的方法释放资源。但没有明确释放资源的情况下,Java提供了缺省机制来终止该对象以释放资源这个方法就是finalize()
protected void finalize() throws Throwable
在finalize()
方法返回之后,对象消失,垃圾收集开始执行,throws Throwable表示它可以抛出任何类型的异常。
当 finalize() 方法被调用时,JVM 会释放该线程上的所有同步锁。
触发GC主条件
- 当程序空闲时,即没有应用程序运行时,GC会被调用。因此GC在优先级最低的线程中进行,所以当应用忙时GC线程不会被调用,但以下情况例外:
- Java堆内存不足时,GC会被调用。当应用程序在运行,并在运行过程中创建对象,这时内存不足,jvm就会强制调用GC线程,以便回收内存用于新的分配。如果一次GC不能满足需求,jvm会尝试进行第二次GC,如果任然不能满足需求,则会报出
outOfMemory
的错误,程序停止。 - 在编译过程中作为一种优化技术,Java编译器能选择性赋值
null
,从而标记可以回收。
减少GC开销措施
- 不要显示调用
System,gc()
此函数建议JVM进行GC,虽然知识建议而非一定,但很多情况下它会触发GC,从而增加GC频率,也增加了间歇性暂停。 - 尽量减少临时变量的使用
临时变量在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少垃圾的产生 - 对象不用时最好显示赋值为
null
一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。 - 尽量使用StringBuffer,而不用String来累加字符串
由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。 - 能用基本数据类型就不要用包装类型
基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。 - 尽量少用静态对象变量
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar
详细信息可以参考这篇博文Java虚拟机