第三章垃圾收集器和内存分配策略

一、GC算法

知道的GC算法有四种:标记-清除算法、复制算法、标记整理、分代收集算法。

 

标记-清除算法:

        分“标记”和“清除”两个阶段:先标记出所有需要回收的对象,标记完成后统一回收。

        最基础的收集算法,有两个缺陷:1.效率不高  2.会产生大量不连续的内存碎片,导致之后分配大对象空间不够,而不得又触发垃圾回收。

 

复制算法:

        将可用内存按照容量划分为大小相等的两块,每次只使用其中一块。第一块用完了,就将第一块里存活的复制到第二块,再回收掉第一块全部。

        这样不用考虑内存碎片且运行高效。但是代价是缩小一半可用,代价大。

        然后实际上会把可用内存空间按8:1:1分为Eden和两块Survivor 并称为新生代,每次使用Eden和一块Survivor也就是90%,回收时把存活的对象放到剩余的那块Survivor上,再回收。减少浪费。如果剩余那块Survivor放不下,就会放到Java堆的老年代里去。

 

标记-整理算法:

        由于可能存在大量对象存活的情况,那么复制算法复制较多效率会变低。

        而根据老年代存活对象较多的特点,用标记-整理算法,与标记清除算法区别在于标记后得把活着的整理到一端,再把另一端回收。

 

分代收集算法:

        并没有新的收集算法,而是把Java堆按2:1分为新生代和老年代,根据特点选用收集算法。新生代采用复制算法,老年代采用标记-整理或标记-清除。

 

二、GC回收器

第三章垃圾收集器和内存分配策略

Serial收集器:

        单线程收集器,只会使用一条线程去完成收集,在收集时必须暂停其它所有工作线程。

缺点很明显,但优点在于简单而高效,没有线程交互开销

在clien的用户程序模式下,暂停几十毫秒,用户感受不到,可以接收。

 

ParNew收集器:

Serial的多线程版本,多线程去垃圾回收.

Server模式下虚拟机首选的新生代收集器,原因在于除Serial收集器外只要它可以和CMS这款强大的并发老年代收集器配合。

 

Parallel Scavenge收集器:

        特点在于关注点和其它收集器不同,CMS等收集器关注点在于尽可能缩短用户线程的停顿时间,而Parallel Scavenge收集器目的是控制吞吐量,即控制 用户代码时间在用户代码时间+GC时间中的比例。

 

Serial Old收集器:

        Serial收集器的老年代版本,采用标记-整理算法。主要给客户端模式下的虚拟机使用。在Server模式下两个用途:jdk1.5之前和Parallel Scavenge收集器搭配使用,其次是作为CMS收集器的后备预案

 

Parallel Old收集器:

        Parallel Scavenge收集器的老年版本。与新生代收集器Parallel Scavenge 配合实现吞吐量的控制和利用多处理器能力。

 

CMS收集器:

        以最短回收停顿时间为目标的收集器,“标记-清除”算法,过程分为四个步骤:

  1. 初始标记:标记GC Roots能直接关联到的对象
  2. 并发标记:顺着GC Roots寻找的过程
  3. 重新标记:修改并发标记期间程序运行而改动的对象
  4. 并发清除

 

其中初始标记、重新标记需要停止其它工作线程,并发标记和并发清除不需要。

由于整个过程中耗时最长的并发标记和并发清除过程是可以和用户线程一起的,所以总体上CMS收集器是可以和用户线程一起的。

优点:并发收集、低停顿

缺点:

  1. 占用大量CPU资源
  2. CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生。

 

浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生,这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC中再清理。这些垃圾就是“浮动垃圾”。

  1. 标记-清除容易产生空间碎片,容易提前触发Full GC。

 

G1收集器:

        希望未来可以替代CMS的收集器。将Java堆划分为多个大小相等的独立区域。

  1. 并行和并发
  2. 分代收集,且G1就可以独立管理整个Java堆
  3. 整体上采用“标记-整理”、局部(两个独立区域之间)上是复制算法。
  4. 可以指定某个时间片段上,垃圾收集不超过的时间:维护了独立区域的回收价值优先列表,优先回收回收价值高的(Garbage First名字由来),实现指定时间效率。

 

回收步骤:

  1. 初始标记(标记GC Roots能直接关联到的对象)
  2. 并发标记(顺着GC Roots寻找的过程,只有这个是并发的,表示和用户一起)
  3. 最终标记(修改并发标记期间程序运行而改动的对象)
  4. 筛选回收

 

三、对象的创建

  java是面向对象的语言,因此对象的创建无时无刻都存在。在语言层面,使用new关键字即可创建出一个对象。但是在虚拟机中,对象创建的创建过程则是比较复杂的。

  首先,虚拟机运到new指令时,会去常量池检查是否存在new指令中包含的参数,比如new People(),则虚拟机首先会去常量池中检查是否有People这个类的符号引用,并且检查这个类是否已经被加载了,如果没有则会执行类加载过程。

  在类加载检查过后,接下来为对象分配内存当然是在java堆中分配,并且对象所需要分配的多大内存在类加载过程中就已经确定了。为对象分配内存的方式根据java堆是否规整分为两个方法:1、指针碰撞(Bump the Pointer),2、空闲列表(Free List)。指针碰撞:如果java堆是规整的,即所有用过的内存放在一边,没有用过的内存放在另外一边,并且有一个指针指向分界点,在需要为新生对象分配内存的时候,只需要移动指针画出一块内存分配和新生对象即可;空闲列表:当java堆不是规整的,意思就是使用的内存和空闲内存交错在一起,这时候需要一张列表来记录哪些内存可使用,在需要为新生对象分配内存的时候,在这个列表中寻找一块大小合适的内存分配给它即可。而java堆是否规整和垃圾收集器是否带有压缩整理功能有关。

  在为新生对象分配内存的时候,同时还需要考虑线程安全问题。因为在并发的情况下内存分配并不是线程安全的。有两种方案解决这个线程安全问题,1、为分配内存空间的动作进行同步处理;2、为每个线程预先分配一小块内存,称为本地线程分配缓存(Thread Local Allocation Buffer, TLAB,哪个线程需要分配内存,就在哪个线程的TLAB上分配。

  内存分配后,虚拟机需要将每个对象分配到的内存初始化为0值(不包括对象头),这也就是为什么实例字段可以不用初始化,直接为0的原因。

  接来下,虚拟机对对象进行必要的设置,例如这个对象属于哪个类的实例,如何找到类的元数据信息。对象的哈希吗、对象的GC年代等信息,这些信息都存放在对象头之中。

  执行完上面工作之后,所有的字段都为0,接着执行<init>指令,把对象按照程序员的指令进行初始化,这样一个对象就完整的创建出来。