JAVA GC 算法

栈是运行时的单位,而堆是存储的单元。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据,堆解决的是数据存储的问题,即数据怎么放,放在哪儿。
在java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关的信息。包括局部变量、程序运行状态、方法返回值等等,而堆只负责存储对象信息。
为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
1. 从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
2.堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。
3. 栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力,而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
4. 面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,
你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实就是编写了数据结构,也编写了处理数据的逻辑。不得不承认,面向对象的设计,确实很美。
在java中,Main函数就是栈的起始点,也是程序的起始点。
程序要运行总是有一个起点的。同C语言一样,java中的Main就是那个起点。无论什么java程序,找到main就找到了程序执行的入口。

 java中,栈的大小通过-Xss来设置,当栈中存储的数据比较多时,需要适当调大这个值,否则会出现 java.lang.StackOverflowError异常。常见的出现这个异常的是无法返回的递归,因为此时栈中保存的信息都是方法返回的记录点。
 java对象的大小
 基本数据类型的大小是固定的,这里就不多说了,对于非基本类型的java对象,其大小就值得商讨。
 在java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个没有任何属性的对象的大小。看看下面语句:
           Object  ob = new  Object();
 这样在程序中完成了一个java对象的声明,但是它所占的空间为:4byte+8byte。4byte是上面部分所说的java栈中保存引用的所需要空间。
 而那8byte则是java堆中对象的信息。因为所有的java非基本类型的对象都需要默认继承Object对象,因此不论什么样的java对象,其大小都必须是大于8byte。
 有了Object对象的大小,我们就可以计算其他对象的大小了。

可以从不同的角度去划分垃圾回收算法:
按照基本回收策略分
引用计数(Reference Counting)
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
标记-清除(Mark-Sweep)

此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
复制(Copying)

此算法把内存空间划分为两个相等的区域,每次只是用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题??。当然,此算法的缺点也是比较明显的,就是需要两倍内存空间。
标记-整理(Mark-Compact)

此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两个阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆中的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
按分区对待的方式分
增量收集(Incremental Collecting):实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法。
分代收集(Generational Collecting):基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年轻代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。
按系统线程分
串行收集:串行收集使用单线程处理所有垃圾回收工作,因为无需多线程交互,实现容易,而且效率比较高。但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。
并行收集:并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势。
并发收集:GC线程和应用线程大部分时间是并发执行,只是在初始标记(initial mark)和二次标记(remark)时需要stop-the-world,这可以大大缩短停顿时间(pause time),所以适用于响应时间优先的应用,减少用户等待时间。由于GC是和应用线程并发执行,只有在多CPU场景下才能发挥其价值,在执行过程中还会产生新的垃圾floating garbage,如果等空间满了再开始GC,那这些新产生的垃圾就没地方放了,这时就会启动一次串行GC,等待时间将会很长,所以要在空间还未满时就要启动GC。mark和sweep操作会引起很多碎片,所以间隔一段时间需要整理整个空间,否则遇到大对象,没有连续空间也会启动一次串行GC。采用此收集器(如tenured generation),收集频率不能大,否则会影响到cpu的利用率,进而影响吞吐量。。
如何区分垃圾
上面说到的“引用计数”法,通过统计控制生成对象和删除对象是的引用数来判断。垃圾回收程序收集计数为0的对象即可。但是这种方法无法解决循环引用。所以,后来实现的垃圾判断算法中,都是从程序运行的根节点出发,遍历整个对象引用,查找存活的对象,那么在这种方式的实现中,垃圾回收从哪儿开始的呢?即,从哪儿开始查找哪些对象是正在被当前系统使用的,上面分析的堆和栈的区别,其中栈是真正进行程序执行的地方,所以要获取哪些对象正在被使用,则需要从java栈开始。同时,一个栈是与一个线程对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。

    同时,除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。这样,以栈或寄存器中的引用为起点,我们可以找到堆中的对象,又从这些对象找到对堆中其它对象的引用,这种引用逐步扩展,最终以null引用或者基本类型结束,这样就形成了一颗以java栈中引用所对应的对象为根节点的一颗对象树,如果栈中有多个引用,则最终会形成多颗对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收,而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。
    因此,垃圾回收的起点是一些根对象(java栈、静态变量、寄存器...)。而最简单的java栈就是java程序执行的main函数。这种回收方式,也是上面提到的“标记-清除”的回收方式。
    如何处理碎片
    由于不同java对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行内存整理,就会出现零散的内存碎片。碎片最直接的问题就是会导致无法分配大块的内存空间,以及程序运行效率降低。所以,在上面提到的基本垃圾回收算法中 ,“复制”方式和“标记-整理”方式,都可以解决碎片的问题。
    如何解决同时存在的对象创建和对象回收问题
    垃圾回收线程是回收内存的,而程序运行线程则是消耗(或分配)内存的,一个回收内存,一个分配内存,从这点看,两者是矛盾的。因此,在现有的垃圾回收方式中,要进行垃圾回收前,一般都需要暂停整个应用(即:暂停内存的分配),然后进行垃圾回收,回收完成后再继续应用。这种实现方式是最直接,而且最有效的解决二者矛盾的方式。
    但是这种方式有一个很明显的弊端,就是当堆空间持续增大时,垃圾回收的时间也将会相应的持续增大,相应应用暂停的时间也会相应的增大。一些相应时间要求很高的应用,比如最大暂停时间要求是几百毫秒,那么当堆空间大于几个G时,就很有可能超过这个限制,在这种情况下,垃圾回收将会成为系统运行的一个瓶颈。为解决这种矛盾,有了并发垃圾回收算法,使用这种算法,垃圾回收线程与程序运行线程同时运行。在这种方式下,解决了暂停的问题,但是因为需要在新生成对象的同时又要回收对象,算法复杂性会大大增加,系统的处理能力也会相应降低,同时,“碎片”问题将会比较难解决,以后研究的重点!!!!!!。
    为什么要分代
    分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同声明周期的对象可以采取不同的收集方式,以便提高回收效率。
    在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
    是想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是它们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
    如何分代

     如图所示:
     虚拟机中共划分了三个代:年轻代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。
     其中持久代主要存放的是java类的类信息,与垃圾收集要收集的java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
     年轻代:
     所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分为三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活将被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
     年老代:
     在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
     持久代:
     用于存放静态文件,如java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久空间来存放这些运行过程中新增的类。持久代大小通过 -XX:MaxPermSize = <N> 进行设置。
    什么情况下触发垃圾回收
    由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC 和 Full GC
    Scavenge GC
    一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden区能尽快空闲出来。
     Full GC
     对整个堆进行整理,包括Young、Tenured 和 Perm。Full GC 因为需要对整个堆进行回收,所以比 Scavenge GC 要慢,因此应该尽可能减少 Full GC 的次数。在对JVM调优的过程中,很大一部分工作就是对于 Full GC 的调节。

有如下原因可能导致Full GC:
. 年老代(Tenured)被写满
. 持久代(Perm)被写满
. System.gc()被显式调用
. 上一次GC之后Heap的各域分配策略动态变化
分代垃圾回收流程示意

    选择合适的垃圾收集算法
    串行收集器

    用单线程处理所有垃圾回收工作,因为无需多线程交互,所有效率比较高。但是,也无法使用多处理器的优势,所以此收集器适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。可以使用 -XX:+UseSerialGC打开。
     并行收集器

    对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用 -XX:+UseParallelGC 打开。并行收集器在 J2SE5.0第六6更新上引入,在java SE6.0中进行了增强 --- 可以对年老代进行并行收集。如果年老代不使用并行收集的话,默认是使用单线程进行垃圾回收,因此会制约扩展能力。使用 -XX:+UseParallelOldGC打开。
    使用 -XX:ParallelGCThreads = <N> 设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。
    此收集器可以进行如下配置:
            最大垃圾回收暂停:指定垃圾回收时的最长暂停时间,通过-XX:MaxGCPauseMillis = <N>指定。<N> 为毫秒,如果指定了此值的话,堆大小和垃圾回收相关参数会进行调整以达到指定值。设定此值可能会减少应用的吞吐量。
            吞吐量:吞吐量为垃圾回收时间与非垃圾回收时间的比值,通过-XX:GCTimeRatio = <N> 来设定,公式为 1/(1 + N)。例如,-XX:GCTimeRatio = 19时,表示5%的时间用于垃圾回收。默认情况为99,即1%的时间用于垃圾回收。
    并发收集器
    可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用。使用 -XX:+UseConcMarkSweepGC打开。

    并发收集器主要减少年老代的暂停时间,它在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器会对整个应用进行简短的暂停。在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。
    并发收集器使用处理器换来短暂的停顿时间。在一个N个处理器的系统上,并发收集部分使用 k/N 个可用处理器进行回收,一般情况下 1 <= k <= N / 4。
    在只有一个处理器的主机上使用并发收集器,设置为 incremental mode 模式也可获得较短的停顿时间。
    浮动垃圾:由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。
    Concurrent Mode Failure:并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生“并发模式失败”,此时整个应用将会暂停,进行垃圾回收。
    启动并发收集器:因为并发收集在应用运行时进行收集,所以必须保证收集完成之前有足够的内存空间供程序使用,否则会出现“Concurrent Mode Failure”。通过设置 -XX:CMSInitiatingOccupancyFraction = <N> 指定还有多少剩余堆是开始执行并发收集。
    小结
     串行处理器:
     -- 适用情况:数据量比较小(100M左右),单处理器下并且对相应时间无要求的应用。
     -- 缺点:只能用于小型应用。
     并行处理器:
      -- 适用情况:“对吞吐量有高要求”,多CPU,对应用过响应时间无要求的中、大型应用。举例:后台处理、科学计算。
      -- 缺点:垃圾收集过程中应用响应时间可能加长。
     并发处理器:
      -- 适用情况:“对响应时间有高要求”,多CPU,对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值