《java performance》读书笔记之 jvm 垃圾回收

          jvm 的垃圾收集器基于以下两个在java应用中成立的假设:1是大部分分配的对象几乎马上就没有被引用到了,2是从老对象到新对象的引用变得越来越少(老对象很少依赖新创建的对象)。基于这两点,hotspot vm设计了minor gc和fgc两类分别正对上述两种情况的垃圾回收方式。HotSpot 虚拟机把堆内存划分为几个物理块:年轻代、年老代、永久代。
          年轻代:
          大部分新分配的对象被分配在年轻代当中,这部分对象的典型特征是对象较小并且回收频繁,被年轻代GC(也即monor gc)回收过之后存活的对象会非常少。总得来说,因为minor gc专注于小并且包含大量垃圾对象的区域,所以monor gc是很有效的。
          年老代:
          对于长时间存活的对象,会被晋升到年老代。这个代要比年轻代更大,并且增长得更慢。因此年老代gc,也就是full gc是更不频繁的,但是一旦fgc发生,那会持续相当长的时间。
          永久代:
          这个区域不会用来放置用户生成的java对象,它不应该被视为内存分代结构中的一部分。它只是被用于jvm自身来保存一些元数据,比如加载入虚拟机的表示java类的数据结构,被interned的string对象等。
          卡表:
          minor gc时要考虑存在年老代对年轻代中对应的引用,这种年轻代中的对象要保证不被回收,识别的方式是通过遍历年老代的对象查看是否有指向年轻代的对象。为了保证minor gc足够快速,垃圾收集器要做到在不扫描整个年老代(因为年老代可能会变得相当大)的情况下来识别年轻代当中的存活对象,hotspot 使用了一种叫做卡表的机制。
卡表的机制是将“老生代”以512字节为单位进行划分,划分得到的每个区域叫做一个卡。每个卡在卡表中有占用一个一个字节的标识。java代码在执行的过程中JVM一旦发现“老生代”的对象引用了或者释放了“新生代”中的 对象,那么JVM就要将与之对应的卡表中的状态置为相应的值。这样在收集的时候只遍历被标记为“脏”的卡,以便知道哪些“新生代”的对象被引用中,是不可以进行回收的。
          分代收集:
          分代进行垃圾收集的好处是可以根据每个代的具体特点为其设定不同的垃圾收集算法。在新生代中往往使用速度较快的垃圾收集算法,因为次要收集的频率比较高。这种算法在内存的使用效率上没有优势,好在“新生代”的空间占整个JVM内存堆比例较小,尚不能对性能构成大的问题。而内存使用效率高的算法往往用在“老生代”的垃圾收集上。因为“老生代”占据着JVM堆的很大部分。虽然“老生代”中进行的主收集每次的收集时间相对于次收集要长好多,但是主收集在频率上要比次收集少很多,故对性能的影响也不大。正是这种新、“老生代”的相互补很好的平衡JVM垃圾收集中的瓶颈。

          年轻代详细描述:
          年轻代被分为三个独立的区域:一个eden区、两个survivor区。
          eden区:大部分的对象会被直接分配到eden区当中,一些特别大的对象除外(会直接被分配到年老代中)。eden区通常会在minor gc之后被清空。
survivor区:survivor区保存了一些最少一次被minor gc 之后的对象,不直接晋升到年老代的原因是垃圾回收器希望再等一下这些对象,或许这批对象就会变得无用了,这样就减少了对象进入年老代的机会。

          一次minor gc的过程大致是这样的:eden区中还在使用当中的对象会被拷贝到两个survivor区当中的to区,其余没用的对象将被清理;对于survivor的from区中的对象,一部分没用的会直接清理掉,一部分被垃圾回收器认为足够老的对象会被晋升到年老代上,另外一部分还不是足够老的对象垃圾回收期会再给它们一次机会,把它们迁移到to区当中。这样,minor gc完成之后,eden区就变空了,survivor中的from区和to区就互换了角色,之前from区被清空,变成了to区,而之前的to区接收了从eden区和from区拷贝过来的对象,就变成了from区。minor gc的垃圾回收器被叫做copying 垃圾回收器。这是一种用空间来换取时间的算法。copying算法的详细情况如下图所示:


          minor gc可能导致的一个问题是:当一次minor gc进行完,survivor中的to区不够用来支持整个从eden和from区迁移过来的对象,这个时候,剩余的对象会被迁移到年老代当中。这种现象被叫做提前晋升。这可能导致一个严重的性能问题。如果这个时候年老代也不足以容纳要迁移过来的对象,这就会导致fgc的发生。fgc会收集整个java堆,这是一种晋升失败的情况。

          快速分配内存:

          copying的算法使得jvm可以使用bump-the-pointer的技术来分配内存,因为内存区块内已使用和未使用的内存块是独立并且内部连续的,所以只要使用这种算法,在需要新分配对象的时候找到目前已分配的最大地址,再看一下这个最大地址到eden区结束地址之间能否满足当前的分配内存请求,如果满足则直接分配内存,不满足就会直接在old区内分配。

          此外,为了减少多线程应用运行时多个线程需要同时分配内存要使用全局的锁的情况,hotspot jvm在内存分配时还使用了线程级别隔离机制:Thread-Local Allocation Buffers(TLABs)。这种机制是给了每一个线程一块分配内存的缓冲区,即TLAB。每个线程都会把内存分配到TLAB上,这样线程之间就不需要采用锁的机制来进行同步保护了。在每个线程的TLAB内部,都可以使用上面提到的bump-the-pointer技术了进行内存分配。不过当TLAB满了之后,线程中的内存分配还是需要采取全局锁的保护。

          串行GC

          minor gc使用如上所述的copying算法,fgc使用mark-sweep-compact算法,特点是串行,stop-the-world。在单核、吞吐量和响应时间要求都不高的系统使用。

          详细情况如下图所示:

《java performance》读书笔记之 jvm 垃圾回收_第1张图片

          并行GC
          minor gc和fgc的算法和串行gc类似,只是充分利用了多核机制,并行进行gc操作,在有吞吐量要求的系统中使用。
          并发GC
          在响应时间要求高的系统使用Concurrent Mark-Sweep GC (CMS)。在这种gc策略中,minor gc和并行串行gc是一样的行为。不过在进行fgc的时候,cms会使用一个并发的算法来进行gc工作,使得整个fgc过程当中只有两个很短的暂停。
          CMS GC的执行过程是:先进行initial mark,它的作用是识别在old以为可达的对象,这个过程会导致一个短的暂停。接着进入concurrent marking阶段,它标记所有间接可达的存活对象,因为在这个并发标记的阶段应用还在运行,所以在执行完成这个阶段之后,并不能保证所有的存活对象都被标记过了。为了解决这个问题,应用会再次被暂停,即remark阶段,并再次标记在并发标记阶段被引用到的对象。在这个阶段当中,卡表的机制再次被使用来减少扫描的范围。
          为了减少remark阶段的暂停时间,垃圾回收器又引入了并发的pre-cleaning阶段,它在并发标记阶段之后remark阶段之前进行,它的作用是做一些在remark阶段会被做到的事情,比如重新访问在并发标记阶段被修改的对象。pre-cleaning阶段可以很可观得减少remark阶段需要去标记的对象,从而减少remark时的暂停时间。
          在remark阶段结束时,所有在java堆中的存活对象都已经被标记上了。但是pre-cleaning阶段和remark阶段明显增加了垃圾回收器在垃圾回收时的工作量,但这是所有致力于减少垃圾回收暂停时间的一个权衡。
          在识别了所有年老代中的存活对象之后,cms fgc的最后一个阶段就是concurrent sweeping 阶段了,这个阶段把java堆中的垃圾对象全部清除(但是没有重新整理)。使用这种gc之后,堆内存的使用是不连续的,因此这个垃圾收集器需要一个新的数据结构(在Hotspot vm中叫做free list)来记录没被使用的内存块的地址空间。因此在old区内分配内存时,就不能使用bump-the-pointer机制来分配内存。这就使得在old区内分配内存变得非常昂贵,这就给ygc带来了额外的压力,因为大部分的old内的内存分配都来源于          ygc时的对象晋升。
          CMS方式gc通常会带来的另外一个坏处是可能会导致更大的java堆的需求。首先,cms gc比stop-the-world的gc持续的时间更长,而只有再sweeping阶段才会真正得回收掉垃圾内存。因为在所有标记的阶段应用程序都是不会暂停的,也就是依然有可能继续在分配内存,所以在标记阶段old区的占用率会持续得增加,直到最后的sweeping阶段。其次,虽然垃圾回收器保证了在标记阶段会识别所有的存活对象,但它不保证能够识别所有的垃圾对象:在垃圾回收器正在标记时成为垃圾的对象就不会在这个周期被清理掉,这部分垃圾会留到下一个周期才不回收,这种的垃圾对象被称为漂浮垃圾。
          最后,由于缺少了整理这个阶段,内存碎片的问题也导致了垃圾回收器不能有效的使用内存。此外,如果在一次回收时,年老代中已经满了,CMS会把垃圾回收方式调整成和并行gc、串行gc类似的gc方式。

你可能感兴趣的:(《java performance》读书笔记之 jvm 垃圾回收)