Java垃圾回收机制
对于Java垃圾回收机制,这个很久前就学过,并且理解过了,好长时间不⽤,就丢到回⽖洼岛了,这⾥还是记下,⽅便以后再次查看,不⽤浪费太多时间。
了解Java垃圾回收机制,就要知道Java各个版本的区别,尤其是随着JDK版本的提升,都⽐较以前版本有哪些改进。最近,尤其是JDK1.7中加⼊了G1,这个是增加的新的回收⽅式,起始在JDK1.6 40左右的版本的时候就已经加⼊实验性的G1了。
线⾯是我转发的博⽂,⾃⼰写没那么多时间,有两篇不错,两篇各有互补点。
第⼀篇:说的⽐较多,但不是太详细
垃圾收集GC(Garbage Collection)是Java语⾔的核⼼技术之⼀,之前我们曾专门探讨过Java 7新增的垃圾回收器G1的新特性,但在JVM 的内部运⾏机制上看,Java的垃圾回收原理与机制并未改变。垃圾收集的⽬的在于清除不再使⽤的对象。GC通过确定对象是否被活动对象引⽤来确定是否收集该对象。GC⾸先要判断该对象是否是时候可以收集。两种常⽤的⽅法是引⽤计数和对象引⽤遍历。
总的来说,可分为两⼤类
引⽤计数收集器
比赛通知
引⽤计数是垃圾收集器中的早期策略。在这种⽅法中,堆中每个对象(不是引⽤)都有⼀个引⽤计数。当⼀个对象被创建时,且将该对象分配给⼀个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引⽤时,计数加1(a = b,则b引⽤的对象+1),但当⼀个对象的某个引⽤超过了⽣命周期或者被设置为⼀个新值时,对象的引⽤计数减1。任何引⽤计数为0的对象可以被当作垃圾收集。当⼀个对象被垃圾收集时,它引⽤的任何对象计数减1。
优点:引⽤计数收集器可以很快的执⾏,交织在程序运⾏中。对程序不被长时间打断的实时环境⽐较有利。
缺点:⽆法检测出循环引⽤。如⽗对象有⼀个对⼦对象的引⽤,⼦对象反过来引⽤⽗对象。这样,他们的引⽤计数永远不可能为0.
跟踪收集器
早期的JVM使⽤引⽤计数,现在⼤多数JVM采⽤对象引⽤遍历。对象引⽤遍历从⼀组对象开始,沿着整个对象图上的每条链接,递归确定可到达(reachable)的对象。如果某对象不能从这些根对象的⼀个(⾄少⼀个)到达,则将它作为垃圾收集。在对象遍历阶段,GC必须记住哪些对象可以到达,以便删除不可到达的对象,这称为标记(marking)对象。
下⼀步,GC要删除不可到达的对象。删除时,有些GC只是简单的扫描堆栈,删除未标记的未标记的对象,并释放它们的内存以⽣成新的对象,这叫做清除(sweeping)。这种⽅法的问题在于内存会分成好多⼩段,⽽它们不⾜以⽤于新的对象,但是组合起来却很⼤。因此,许多GC可以重新组织内存中的对象,并进⾏压缩(compact),形成可利⽤的空间。
为此,GC需要停⽌其他的活动活动。这种⽅法意味着所有与应⽤程序相关的⼯作停⽌,只有GC运⾏。结果,在响应期间增减了许多混杂请求。另外,更复杂的 GC不断增加或同时运⾏以减少或者清除应⽤程序的中断。有的GC使⽤单线程完成这项⼯作,有的则采⽤多线程以增加效率。
实际上GC判断对象是否可达看的是强引⽤。更准确的描述是,⼀个对象存在强引⽤,必定是从其它强引⽤对象的本地变量,静态变量或者其它类似的地⽅直接引⽤过来的。换句话说,如果⼀堆对象通过某个不存活的对象“强引⽤”过来的话,它们会被⼀起回收掉。
⼀些具体常⽤的垃圾收集算法
(1)标记-清除
这种收集器⾸先遍历对象图并标记可到达的对象,然后扫描堆栈以寻找未标记对象并释放它们的内存。这种收集器⼀般使⽤单线程⼯作并停⽌其他操作。并且,由于它只是清除了那些未标记的对象,⽽并没有对标记对象进⾏压缩,导致会产⽣⼤量内存碎⽚,从⽽浪费内存。
(2)标记-压缩
有时也叫标记-清除-压缩收集器,与标记-清除收集器有相同的标记阶段。在第⼆阶段,则把标记对象复制到堆栈的新域中以便压缩堆栈。这种收集器也停⽌其他操作。
(3)复制
这种收集器将堆栈分为两个域,常称为半空间。每次仅使⽤⼀半的空间,JVM⽣成的新对象则放在另⼀半空间中。GC运⾏时,它把可到达对象复制到另⼀半空间,从⽽压缩了堆栈。这种⽅法适⽤于短⽣存期的对象,持续复制长⽣存期的对象则导致效率降低。并且对于指定⼤⼩堆来说,需要两倍⼤⼩的内存,因为任何时候都只使⽤其中的⼀半。
(4) 增量收集器
增量收集器把堆栈分为多个域,每次仅从⼀个域收集垃圾,也可理解为把堆栈分成⼀⼩块⼀⼩块,每次仅对某⼀个块进⾏垃圾收集。这会造成较⼩的应⽤程序中断时间,使得⽤户⼀般不能觉察到垃圾收集器正在⼯作。
(5)分代
复制收集器的缺点是:每次收集时,所有的标记对象都要被拷贝,从⽽导致⼀些⽣命周期很长的对象被来回拷贝多次,消耗⼤量的时间。⽽分代收集器则可解决这个问题,分代收集器把堆栈分为两个或多个域,⽤以存放不同寿命的对象。JVM⽣成的新对象⼀般放在其中的某个域中。过⼀段时间,继续存在的对象(⾮短命对象)将获得使⽤期并转⼊更长寿命的域中。分代收集器对不同的域使⽤不同的算法以优化性能。
并⾏收集器
并⾏收集器使⽤某种传统的算法并使⽤多线程并⾏的执⾏它们的⼯作。在多CPU机器上使⽤多线程技术可以显著的提⾼java应⽤程序的可扩展性。
最后,贴出⼀个⾮常简单的跟踪收集器的例图,以便⼤家加深对收集器的理解:
跟踪收集器的例图
使⽤垃圾收集器要注意的地⽅
下⾯将提出⼀些有关垃圾收集器要注意的地⽅,垃圾收集器知识很多,下⾯只列出⼀部分必要的知识:
(1)每个对象只能调⽤finalize( )⽅法⼀次。如果在finalize( )⽅法执⾏时产⽣异常(exception),则该对象仍可以被垃圾收集器收集。漫画帅哥图片
(2)垃圾收集器跟踪每⼀个对象,收集那些不可触及的对象(即该对象不再被程序引⽤了),回收其占有的内存空间。但在进⾏垃圾收集的时候,垃圾收集器会调⽤该对象的finalize( )⽅法(如果有)。如果在finalize()⽅法中,⼜使得该对象被程序引⽤(俗称复活了),则该对象就变成了可触及的对象,暂时不会被垃圾收集了。但是由于每个对象只能调⽤⼀次finalize( )⽅法,所以每个对象也只可能 "复活 "⼀次。
(3)Java语⾔允许程序员为任何⽅法添加finalize( )⽅法,该⽅法会在垃圾收集器交换回收对象之前被调⽤。但不要过分依赖该⽅法对系统资源进⾏回收和再利⽤,因为该⽅法调⽤后的执⾏结果是不可预知的。
(4)垃圾收集器不可以被强制执⾏,但程序员可以通过调研⽅法来建议执⾏垃圾收集。记住,只是建议。⼀般不建议⾃⼰写,因为会加⼤垃圾收集⼯作量。
详解Java GC的⼯作原理
概要: JVM内存结构由堆、栈、本地⽅法栈、⽅法区等部分组成,另外JVM分别对新⽣代和旧⽣代采⽤不同的垃圾回收机制。
1. ⾸先来看⼀下JVM内存结构,它是由堆、栈、本地⽅法栈、⽅法区等部分组成,结构图如下所⽰。
JVM内存组成结构
1)堆
所有通过new创建的对象的内存都在堆中分配,其⼤⼩可以通过-Xmx和-Xms来控制。堆被划分为新⽣代和旧⽣代,新⽣代⼜被进⼀步划分为Eden和Survivor区,最后Survivor由FromSpace和ToSpace组成,结构图如下所⽰:
JVM内存结构之堆
新⽣代。新建的对象都是⽤新⽣代分配内存,Eden空间不⾜的时候,会把存活的对象转移到Survivor中,新⽣代⼤⼩可以由-Xmn来控制,也可以⽤-XX:SurvivorRatio来控制Eden和Survivor的⽐例旧⽣代。⽤于存放新⽣代中经过多次垃圾回收仍然存活的对象
2)栈安全条幅
每个线程执⾏每个⽅法的时候都会在栈中申请⼀个栈帧,每个栈帧包括局部变量区和操作数栈,⽤于存放此次⽅法调⽤过程中的临时变量、参数和中间结果
3)本地⽅法栈
⽤于⽀持native⽅法的执⾏,存储了每个native⽅法调⽤的状态
4)⽅法区
青春有约
存放了要加载的类信息、静态变量、final类型的常量、属性和⽅法信息。JVM⽤持久代(PermanetGeneration)来存放⽅法区,可通过-XX:PermSize和-XX:MaxPermSize来指定最⼩值和最⼤值。介绍完了JVM内存组成结构,下⾯我们再来看⼀下JVM垃圾回收机制。
2. JVM垃圾回收机制
JVM分别对新⽣代和旧⽣代采⽤不同的垃圾回收机制
新⽣代的GC:
新⽣代通常存活时间较短,因此基于Copying算法来进⾏回收,所谓Copying算法就是扫描出存活的对象,并复制到⼀块新的完全未使⽤的空间中,对应于新⽣代,就是在Eden和FromSpace或ToSpace之间copy。新⽣代采⽤空闲指针的⽅式来控制GC触发,指针保持最后⼀个分配的对象在新⽣代区间的位置,当有新的对象要分配内存时,⽤于检查空间是否⾜够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到survivor,最后到旧⽣代,
⽤javavisualVM来查看,能明显观察到新⽣代满了后,会把对象转移到旧⽣代,然后清空继续装载,当旧⽣代也满了后,就会报outofmemory的异常,如下图所⽰:
outofmemory的异常
在执⾏机制上JVM提供了串⾏GC(SerialGC)、并⾏回收GC(ParallelScavenge)和并⾏GC(ParNew)
1)串⾏GC
在整个扫描和复制过程采⽤单线程的⽅式来进⾏,适⽤于单CPU、新⽣代空间较⼩及对暂停时间要求不是⾮常⾼的应⽤上,是client级别默认的GC⽅式,可以通过-XX:+USerialGC来强制指定
2)并⾏回收GC
在整个扫描和复制过程采⽤多线程的⽅式来进⾏,适⽤于多CPU、对暂停时间要求较短的应⽤上,是rver级别默认采⽤的GC⽅式,可⽤-XX:+UParallelGC来强制指定,⽤-XX:ParallelGCThreads=4来指定线程数
3)并⾏GC
与旧⽣代的并发GC配合使⽤
旧⽣代的GC:
旧⽣代与新⽣代不同,对象存活的时间⽐较长,⽐较稳定,因此采⽤标记(Mark)算法来进⾏回收,所谓标记就是扫描出存活的对象,然后再进⾏回收未被标记的对象,回收后对⽤空出的空间要么进⾏合并,要么标记出来便于下次进⾏分配,总之就是要减少内存碎⽚带来的效率损耗。在执⾏机制上JVM提供了串⾏GC(SerialMSC)、并⾏GC(parallelMSC)和并发GC(CMS),具体算法细节还有待进⼀步深⼊研究。
以上各种GC机制是需要组合使⽤的,指定⽅式由下表所⽰:
GC 机制组合使⽤
第⼆篇:详细介绍了垃圾回收机制的算法
说到垃圾回收(Garbage Collection,GC),很多⼈就会⾃然⽽然地把它和Java联系起来。在Java中,程序员不需要去关⼼内存动态分配和垃圾回收的问题,这⼀切都交给了JVM来处理。顾名思义,垃圾回收就是释放垃圾占⽤的空间,那么在Java中,什么样的对象会被认定
为“垃圾”?那么当⼀些对象被确定为垃圾之后,采⽤什么样的策略来进⾏回收(释放空间)?在⽬前的商业虚拟机中,有哪些典型的垃圾收集器?下⾯我们就来逐⼀探讨这些问题。以下是本⽂的⽬录⼤
纲:
⼀.如何确定某个对象是“垃圾”?
⼆.典型的垃圾收集算法
三.典型的垃圾收集器
⼀.如何确定某个对象是“垃圾”?
在这⼀⼩节我们先了解⼀个最基本的问题:如果确定某个对象是“垃圾”?既然垃圾收集器的任务是回收垃圾对象所占的空间供新的对象使⽤,那么垃圾收集器如何确定某个对象是“垃圾”?—即通过什么⽅法判断⼀个对象可以被回收了。
在java中是通过引⽤来和对象进⾏关联的,也就是说如果要操作对象,必须通过引⽤来进⾏。那么很显然⼀个简单的办法就是通过引⽤计数来判断⼀个对象是否可以被回收。不失⼀般性,如果⼀个对象没有任何引⽤与之关联,则说明该对象基本不太可能在其他地⽅被使⽤到,那么这个对象就成为可被回收的对象了。这种⽅式成为引⽤计数法。
这种⽅式的特点是实现简单,⽽且效率较⾼,但是它⽆法解决循环引⽤的问题,因此在Java中并没有采⽤这种⽅式(Python采⽤的是引⽤计数法)。看下⾯这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13public class Main {
public static void main(String[] args) { MyObject object1 = new MyObject(); MyObject object2 = new MyObject();
化学键教案object1.object = object2;
object2.object = object1;
object1 = null;
object2 = null;
}
}
class MyObject{
14 15 16 public Object object = null; }
最后⾯两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引⽤对⽅,导致它们的引⽤计数都不为0,那么垃圾收集器就永远不会回收它们。
为了解决这个问题,在Java中采取了可达性分析法。该⽅法的基本思想是通过⼀系列的“GC Roots”对象作为起点进⾏搜索,如果在“GC Roots”和⼀个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不⼀定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须⾄少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。
⾄于可达性分析法具体是如何操作的我暂时也没有看得很明⽩,如果有哪位朋友⽐较清楚的话请不吝指教。
下⾯来看个例⼦:
1 2 3 4 5 6 7Object aobj = new Object ( ) ; Object bobj = new Object ( ) ; Object cobj = new Object ( ) ; aobj = bobj;
aobj = cobj;
cobj = null;
小程序>好人好事有哪些aobj = null;
第⼏⾏有可能会使得某个对象成为可回收对象?第7⾏的代码会导致有对象会成为可回收对象。⾄于为什么留给读者⾃⼰思考。 再看⼀个例⼦:
1 2 3String str = new String("hello");
SoftReference<String> sr = new SoftReference<String>(new String("java")); WeakReference<String> wr = new WeakReference<String>(new String("world"));
这三句哪句会使得String对象成为可回收对象?第2句和第3句,第2句在内存不⾜的情况下会将String对象判定为可回收对象,第3句⽆论什么情况下String对象都会被判定为可回收对象。
最后总结⼀下平常遇到的⽐较常见的将对象判定为可回收对象的情况:
1)显⽰地将某个引⽤赋值为null或者将已经指向某个对象的引⽤指向新的对象,⽐如下⾯的代码:
1 2 3 4 5Object obj = new Object(); obj = null;
Object obj1 = new Object(); Object obj2 = new Object(); obj1 = obj2;
2)局部引⽤所指向的对象,⽐如下⾯这段代码:
1 2 3 4 5 6 7 8void fun() {
.....
for(int i=0;i<10;i++) {
Object obj = new Object();
System.out.Class()); }
}
循环每执⾏完⼀次,⽣成的Object对象都会成为可回收的对象。
三年级的语文书 3)只有弱引⽤与其关联的对象,⽐如:
1WeakReference<String> wr = new WeakReference<String>(new String("world"));
⼆.典型的垃圾收集算法
在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进⾏垃圾回收,但是这⾥⾯涉及到⼀个问题是:如何⾼效地进⾏垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个⼚商的虚拟机可以采⽤不同的⽅式来实现垃圾收集器,所以在此只讨论⼏种常见的垃圾收集算法的核⼼思想。
1.Mark-Sweep(标记-清除)算法
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占⽤的空间。具体过程如下图所⽰: