psscavengejava_Java垃圾回收机制
垃圾回收机制
垃圾回收(GC)是Java虚拟机(JVM)垃圾回收器提供的⼀种⽤于在空闲时间不定时回收⽆任何对象引⽤的对象占据的内存空间的⼀种机制。
PS:Java 中的垃圾回收⼀般是在 Java 堆中进⾏,因为堆中⼏乎存放了 Java 中所有的对象实例。垃圾回收回收的是⽆任何引⽤的对象占据的内存空间⽽不是对象本⾝。换⾔之,垃圾回收只会负责释放那些对象占有的内存。对象是个抽象的词,包括引⽤和其占据的内存空间当对象没有任何引⽤时其占据的内存空间随即被收回备⽤,此时对象也就被销毁。
垃圾回收机制的意义
Java语⾔中⼀个显著的特点就是引⼊了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃⽽解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有“作⽤域”的概念,只有对象的引⽤才有“作⽤域”。垃圾回收可以有效的防⽌内存泄露,有效的使⽤空闲的内存。也就是说,在系统运⾏过程中,会产⽣⼀些⽆⽤的对象,这些对象占据着⼀定的内存,如果不对这些对象清理回收,可能会导致内存的耗尽。
PS:内存泄露是指该内存空间使⽤完毕之后未回收,在不涉及复杂数据结构的⼀般情况下,Java 的内存泄露表现为⼀个内存对象的⽣命周期超出了程序需要它的时间长度,我们有时也将其称为“对象游离”。
对象引⽤
谈到 Java 堆中的垃圾回收,⾃然要谈到对象引⽤。在 JDK1.2 之前,Java 中的引⽤定义很很纯粹:如果 reference 类型的数据中存储的数值代表的是另外⼀块内存的起始地址,就称这块内存代表着⼀个引⽤。但在 JDK1.2 之后,Java 对引⽤的概念进⾏了扩充,将其分为强引⽤(Strong Reference)、软引⽤(Soft Reference)、弱引⽤(Weak Reference)、虚引⽤(Phantom Reference)四种,引⽤强度依次减弱。
强引⽤:如“Object obj = new Object()”,这类引⽤是 Java 程序中最普遍的。只要强引⽤还存在,垃圾收集器就永远不会回收掉被引⽤的对象。
软引⽤:它⽤来描述⼀些可能还有⽤,但并⾮必须的对象。在系统内存不够⽤时,这类引⽤关联的对象将被垃圾收集器回收。JDK1.2 之后提供了 SoftReference 类来实现软引⽤。
弱引⽤:它也是⽤来描述⾮需对象的,但它的强度⽐软引⽤更弱些,被弱引⽤关联的对象只能⽣存到
下⼀次垃圾收集发⽣之前。当垃圾收集器⼯作时,⽆论当前内存是否⾜够,都会回收掉只被弱引⽤关联的对象。在 JDK1.2 之后,提供了 WeakReference 类来实现弱引⽤。
虚引⽤:最弱的⼀种引⽤关系,完全不会对其⽣存时间构成影响,也⽆法通过虚引⽤来取得⼀个对象实例。为⼀个对象设置虚引⽤关联的唯⼀⽬的是希望能在这个对象被收集器回收时收到⼀个系统通知。JDK1.2 之后提供了 PhantomReference 类来实现虚引⽤。
垃圾对象的判定
Java 堆中存放着⼏乎所有的对象实例,垃圾收集器对堆中的对象进⾏回收前,要先确定这些对象是否还有⽤,判定对象是否为垃圾对象。但Java语⾔规范没有明确地说明JVM使⽤哪种垃圾回收算法,但是任何⼀种垃圾回收算法⼀般要做2件基本的事情:(1)找到所有存活对象;(2)回收被⽆⽤对象占⽤的内存空间,使该空间可被程序再次使⽤。
引⽤计数算法
给对象添加⼀个引⽤计数器,每当有⼀个地⽅引⽤它时,计数器值就加 1,当引⽤失效时,计数器值就减1,任何时刻计数器都为 0 的对象就是不可能再被使⽤的。
引⽤计数算法的实现简单,判定效率也很⾼,在⼤部分情况下它都是⼀个不错的选择,但Java 语⾔并
没有选择这种算法来进⾏垃圾回收,主要原因是它很难解决对象之间的相互循环引⽤问题,也就是在循环引⽤的时候不能够正确把对象当成垃圾。
根搜索算法
Java 和 C# 中都是采⽤根搜索算法来判定对象是存活。这种算法的基本思路是通过⼀系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所⾛过的路径称为引⽤链,当⼀个对象到 GC Roots 没有任何引⽤链相连时,就证明此对象是不可⽤的,会被标记为“GC Roots”不可达的对象,也就是说程序不会再⽤到它们了。这些就是垃圾对象,回收器将会在接下来的阶段中清除它们。
PS:所谓根集(Root Set)就是正在执⾏的Java程序可以访问的引⽤变量(注意:不是对象)的集合。程序可以使⽤引⽤变量访问对象的属性和调⽤对象的⽅法。在 Java ⾥,可作为 GC Roots 的对象包括下⾯⼏种:
effort可数吗虚拟机栈(栈帧中的本地变量表)中引⽤的对象。
⽅法区中的类静态属性引⽤的对象。
⽅法区中的常量引⽤的对象。
本地⽅法栈中 JNI(Native ⽅法)的引⽤对象。超好听英文歌曲推荐
活跃的线程。
开始进⾏标记前,需要先暂停应⽤线程,否则如果对象图⼀直在变化的话是⽆法真正去遍历它的。暂停应⽤线程以便JVM可以尽情地收拾家务的这种情况⼜被称之为安全点(Safe Point),这会触发⼀次Stop The World(STW)暂停。暂停时间的长短并不取决于堆内对象的多少也不是堆的⼤⼩,⽽是存活对象的多少。因此,调⾼堆的⼤⼩并不会影响到标记阶段的时间长短。
实际上,在根搜索算法中,要真正宣告⼀个对象死亡,⾄少要经历两次标记过程:如果对象在进⾏根搜索后发现没有与 GC Roots 相连接的引⽤链,那它会被第⼀次标记并且进⾏⼀次筛选,筛选的条件是此对象是否有必要执⾏ finalize()⽅法。当对象没有覆盖 finalize()⽅法,或 finalize()⽅法已经被虚拟机调⽤过,虚拟机将这两种情况都视为没有必要执⾏。如果该对象被判定为有必要执⾏ finalize()⽅法,那么这个对象将会被放置在⼀个名为 F-Queue 队列中,并在稍后由⼀条由虚拟机⾃动建⽴的、低优先级的 Finalizer 线程去执⾏ finalize()⽅法。finalize()⽅法是对象逃脱死亡命运的最后⼀次机会(因为⼀个对象的 finalize()⽅法最多只会被系统⾃动调⽤⼀次),稍后 GC 将对 F-Queue 中的对象进⾏第⼆次⼩规模的标记,如果要在 finalize()⽅法中成功拯救⾃⼰,只要在 finalize()⽅法中让该对象重引⽤链上的任何⼀个对象建⽴关联即可。⽽如果对象这时还没有关联到任何链上的引⽤,那它就会被回收掉。
PS:F-Queue是⼀个⾼性能、基于磁盘持久存储的队列消息系统。兼容memcached协议,能⽤memcached的语⾔都可以良好的与它通信。了解更多F-Queue。
垃圾回收算法differentfrom
判定除了垃圾对象之后,便可以进⾏垃圾回收了。下⾯介绍⼀些垃圾回收算法的实现思想。
标记—清除算法(Tracing算法)
标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:⾸先标记出所需回收的对象,在标记完成后统⼀回收掉所有被标记的对象,它的标记过程其实就是前⾯的根搜索算法中判定垃圾对象的标记过程。标记—清除算法的执⾏情况如下图所⽰:
回收前状态:
回收后状态:
优点:不需要进⾏对象的移动,并且仅对不存活的对象进⾏处理,在存活对象⽐较多的情况下极为⾼效。
缺点:(1)标记和清除过程的效率都不⾼。(这种⽅法需要使⽤⼀个空闲列表来记录所有的空闲区域以及⼤⼩。对空闲列表的管理会增加分配对象时的⼯作量)。(2)标记清除后会产⽣⼤量不连续的内存碎⽚。虽然空闲区域的⼤⼩是⾜够的,但却可能没有⼀个单⼀区域能够满⾜这次分配所需的⼤⼩,因此本次分配还是会失败 —— 在Java中就是⼀次OutOfMemoryError。
标记—整理算法(Compacting算法)
复制算法⽐较适合于新⽣代,在⽼年代中,对象存活率⽐较⾼,如果执⾏较多的复制操作,效率将会变低,所以⽼年代⼀般会选⽤其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程⼀样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进⾏清理,⽽是让所有的对象都向⼀端移动,然后直接清理掉端边界以外的内存。在基于Compacting算法的收集器的实现中,⼀般增加句柄和句柄表。标记—整理算法的回收情况如下所⽰:
回收前状态:
回收后状态:
优点:(1)经过整理之后,新对象的分配只需要通过指针碰撞便能完成(Pointer Bumping),相当简单。(2)使⽤这种⽅法空闲区域的位置是始终可知的,也不会再有碎⽚的问题了。
缺点:GC暂停的时间会增长,因为你需要将所有的对象都拷贝到⼀个新的地⽅,还得更新它们的引⽤地址。
Copying算法(Copying Collector)
该算法的提出是为了克服句柄的开销和解决堆碎⽚的垃圾回收。它将内存按容量分为⼤⼩相等的两块,每次只使⽤其中的⼀块(对象⾯),当这⼀块的内存⽤完了,就将还存活着的对象复制到另外⼀块内存上⾯(空闲⾯),然后再把已使⽤过的内存空间⼀次清理掉。韩语论坛
复制算法⽐较适合于新⽣代(短⽣存期的对象),在⽼年代(长⽣存期的对象)中,对象存活率⽐较⾼,如果执⾏较多的复制操作,效率将会变低,所以⽼年代⼀般会选⽤其他算法,如标记—整理算法。⼀种典型的基于Coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区,在对象区与空闲区的切换过程中,程序暂停执⾏。Copying算法的回收情况如下所⽰:
优点:(1)标记阶段和复制阶段可以同时进⾏。(2)每次只对⼀块内存进⾏回收,运⾏⾼效。(3)只需移动栈顶指针,按顺序分配内存即可,实现简单。(4)内存回收时不⽤考虑内存碎⽚的出现(得活动对象所占的内存空间之间没有空闲间隔)。
缺点:需要⼀块能容纳下所有存活对象的额外的内存空间。因此,可⼀次性分配的最⼤内存缩⼩了⼀半。化妆品常识
Generation算法(Generational Collector)
分代的垃圾回收策略,是基于这样⼀个事实:不同的对象的⽣命周期是不⼀样的。因此,不同⽣命周期的对象可以采取不同的回收算法,以便提⾼回收效率。Java的堆内存基于Generation算法(Generational Collector)划分为新⽣代、年⽼代和持久代。
堆内存分配策略明确以下三点:
sdfg对象优先在Eden分配。
⼤对象直接进⼊⽼年代。
长期存活的对象将进⼊⽼年代。
PS:Java的内存空间除了堆内存还有其他部分:
栈 每个线程执⾏每个⽅法的时候都会在栈中申请⼀个栈帧,每个栈帧包括局部变量区和操作数栈,⽤于存放此次⽅法调⽤过程中的临时变量、参数和中间结果。
本地⽅法栈 ⽤于⽀持native⽅法的执⾏,存储了每个native⽅法调⽤的状态。
⽅法区 存放了要加载的类信息、静态变量、final类型的常量、属性和⽅法信息。
JVM⽤持久代(PermanetGeneration)来存放⽅法区,可通过-XX:PermSize和-XX:MaxPermSize来指定最⼩值和最⼤值。所有通过new 创建的对象的内存都在堆中分配,其⼤⼩可以通过-Xmx和-Xms来控制。详细可以参考:Java内存区域和内存溢出。
年轻代(Young Generation)
所有新⽣成的对象⾸先都是放在年轻代的。年轻代的⽬标就是尽可能快速的回收掉那些⽣命周期短的对象。
新⽣代内存按照8:1:1的⽐例分为⼀个eden区和两个survivor(survivor0,survivor1)区(⼀般⽽⾔)。⼤部分对象在Eden区中⽣成。回收时先将eden区存活对象复制到⼀个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另⼀个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。当survivor1区不⾜以存放 eden和survivor0的存活对象时,就将存活对象直接存放到⽼年代。若是⽼年代也满了就会触发⼀次Full GC,也就是新⽣代、⽼年代都进⾏回收
新⽣代发⽣的GC也叫做Minor GC,MinorGC发⽣频率⽐较⾼(不⼀定等Eden区满了才触发)
年⽼代(Old Generation)
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年⽼代中。因此,可以认为年⽼代中存放的都是⼀些⽣命周期较长的对象。
关于教师节的黑板报内存⽐新⽣代也⼤很多(⼤概⽐例是1:2),当⽼年代内存满时触发Major GC即Full GC,Full GC发⽣频率⽐较低,⽼年代对象存活时间⽐较长,存活率标记⾼。
bless是什么意思
持久代(Permanent Generation)
⽤于存放静态⽂件,如Java类、⽅法等。持久代对垃圾回收没有显著影响,但是有些应⽤可能动态⽣成或者调⽤⼀些class,例如Hibernate 等,在这种时候需要设置⼀个⽐较⼤的持久代空间来存放这些运⾏过程中新增的类。
当前商业虚拟机的垃圾收集 都采⽤分代回收,它根据对象的存活周期的不同将内存划分为⼏块,⼀般是把 Java 堆分为新⽣代和⽼年代。在新⽣代中,每次垃圾收集时都会发现有⼤量对象死去,只有少量存活,因此可选⽤复制算法来完成收集,⽽⽼年代中因为对象存活率⾼、没有额外空间对它进⾏分配担保,就必须使⽤标记—清除算法或标记—整理算法来进⾏回收。
垃圾回收器(GC)
按执⾏机制划分Java有四种类型的垃圾回收器
串⾏垃圾回收器(Serial Garbage Collector)
并⾏垃圾回收器(Parallel Garbage Collector)
并发标记扫描垃圾回收器(CMS Garbage Collector)
G1垃圾回收器(G1 Garbage Collector)
每种类型都有⾃⼰的优势与劣势,在很⼤程度上有 所不同并且可以为我们提供完全不同的应⽤程序性能。重要的是,我们编程的时候可以通过向JVM传递参数选择垃圾回收器类型。每种类型理解每种类型的垃圾回收器并且根据应⽤程序选择进⾏正确的选择是⾮常重要的。
串⾏垃圾回收器
串⾏垃圾回收器通过持有应⽤程序所有的线程进⾏⼯作。它为单线程环境设计,只使⽤⼀个单独的线程进⾏垃圾回收,通过冻结所有应⽤程序线程进⾏⼯作,所以不适合服务器环境。它最适合的是简单的命令⾏程序(单CPU、新⽣代空间较⼩及对暂停时间要求不是⾮常⾼的应⽤)。是client级别默认的GC⽅式。通过JVM参数-XX:+USerialGC可以使⽤串⾏垃圾回收器。
并⾏垃圾回收器
并⾏垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串⾏垃圾回收器不同,它使⽤多线程进⾏垃圾回收。相似的是,当执⾏垃圾回收的时候它也会冻结所有的应⽤程序线程。
适⽤于多CPU、对暂停时间要求较短的应⽤上,是rver级别默认采⽤的GC⽅式。可⽤-XX:+UParallelGC来强制指定,⽤-
XX:ParallelGCThreads=4来指定线程数。
并发标记扫描垃圾回收器
并发标记垃圾回收使⽤多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下⾯两种情况持有应⽤程序所有线程。
当标记的引⽤对象在Tenured区域;
在进⾏垃圾回收的时候,堆内存的数据被并发的改变。
相⽐并⾏垃圾回收器,并发标记扫描垃圾回收器使⽤更多的CPU来确保程序的吞吐量。如果可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相⽐并发垃圾回收器。通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
G1垃圾回收器
G1收集器是当今收集器技术发展最前沿的成果,它是⼀款⾯向服务端应⽤的收集器,它能充分利⽤多CPU、多核环境。因此它是⼀款并⾏与并发收集器,并且它能建⽴可预测的停顿时间模型。
G1垃圾回收器适⽤于堆内存很⼤的情况,他将堆内存分割成不同的区域,并且并发的对其进⾏垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进⾏压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第⼀块垃圾最多的区域。
通过JVM参数 –XX:+UG1GC 使⽤G1垃圾回收器。
以上各种GC机制是需要组合使⽤的,指定⽅式由下表所⽰:
垃圾回收的JVM配置
运⾏的垃圾回收器类型:
GC的优化配置:
使⽤JVM GC 参数的例⼦:
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+USerialGC -jar java-application.jar
Java8给出了全部的⼏种Java垃圾回收器,需要根据应⽤场景,硬件性能和吞吐量需求来决定使⽤哪⼀种。
新⽣代收集器使⽤的收集器:Serial、PraNew、Parallel Scavenge
⽼年代收集器使⽤的收集器:Serial Old、Parallel Old、CMS
Serial收集器(复制算法)新⽣代单线程收集器,标记和清理都是单线程,优点是简单⾼效。
Serial Old收集器(标记-整理算法)⽼年代单线程收集器,Serial收集器的⽼年代版本。
规则英文>中译西ParNew收集器(停⽌-复制算法)新⽣代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着⽐Serial更好的表现。