深入理解JVM读书笔记二:垃圾收集器与内存分配策略

、判断对象死亡的两种常用算法:        

       在堆里面存放着java世界中几乎所有的实例对象,垃圾收集器在堆进行回收前,第一件事情就是要确定哪些对象还存活着,哪些已经死去。

1、引用计数算法

概念:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。但是主流的java虚拟机并没有选用引用计数算法来管理内存,其中最主要的原因是:它很难解决对象之间相互循环引用的问题。

优点:算法的实现简单,判定效率也高,大部分情况下是一个不错的算法。很多地方应用到它。

缺点:引用和去引用伴随加法和减法,影响性能;于循环引用的对象无法进行回收。

2、根搜索算法:(jvm采用的算法)

概念:根搜索法是通过一系列称为”GC Root”的对象作为起点。以这些节点作为根节点向下搜索,搜索过程中的路径成为“引用链”,当某个对象到”GC Root”没有任何引用链时,也就是“根对象”不可达。就被认为是可以被回收的对象。 

如上图中,GC Root对object1,object2,object3,object4都是仍存活的对象,object5,object6,object7都是GC Root 触及不到的,因此都是要被回收的。     

根(GC Roots):

在JAVA语言中,可以当做GC roots的对象有以下几种:

 1、虚拟机栈(祯栈中的本地变量表)中引用的对象。

 2、方法区中类静态属性引用的对象。

 3、方法区中常量引用的对象。

 4、本地方法栈中JNI(即一般说的Native方法)引用的对象。

二、Java引用的四种状态:

概念:在JDK1.2以前,java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就成这块内存代表着一个引用。在JDK1.2以后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种。这4种引用强度·依此减弱。

1、强引用:

  用的最广。我们平时写代码时,new一个Object存放在堆内存,然后用一个引用指向它,这就是强引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

2、软引用:

  如果一个对象只具有软引用,则内存空间足够时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。(注:如果内存不足,随时有可能被回收。)只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。在JDK1.2以后,提供了SoftReference类来实现软引用。

3、弱引用:

  弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。每次执行GC的时候,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。在JDK1.2以后,提供了WeakReference类来实现弱引用。

4、虚引用:

  “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。在JDK1.2以后,提供了PhantomReference类来实现弱引用。

三、Java对象在内存中的状态:

可达的/可触及的:Java对象被创建后,如果被一个或多个变量引用,那就是可达的。即从根节点可以触及到这个对象。其实就是从根节点扫描,只要这个对象在引用链中,那就是可触及的。

可恢复的:Java对象不再被任何变量引用就进入了可恢复状态。在回收该对象之前,该对象的finalize()方法进行资源清理。如果在finalize()方法中重新让变量引用该对象,则该对象再次变为可达状态,否则该对象进入不可达状态。

不可达的:Java对象不被任何变量引用,且系统在调用对象的finalize()方法后依然没有使该对象变成可达状态(该对象依然没有被变量引用),那么该对象将变成不可达状态。当Java对象处于不可达状态时,系统才会真正回收该对象所占有的资源。

 

四、垃圾回收算法:

1、标记-清除算法:

      概念:标记阶段:先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;清除阶段:清除所有未被标记的对象。

      缺点:标记和清除的过程效率不高(标记和清除都需要从头遍历到尾);标记清除后会产生大量不连续的内存碎片。空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。

2、复制算法:(新生代的GC)

概念:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,然后清除正在使用的内存块中的所有对象。

优点:这样使得每次都是对整个半区进行回收,内存分配时也就不用考虑内存碎片等情况;只要移动堆顶指针,按顺序分配内存即可,实现简单,运行效率高。

缺点:空间的浪费

  从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行。现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块比较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的空间会被浪费。

       当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖于老年代进行分配担保,所以大对象直接进入老年代。整个过程如下图所示:

3、标记-整理算法:(老年代的GC)

      复制算法在对象存活率高的时候要进行较多的复制操作,效率将会降低,所以在老年代中一般不能直接选用这种算法。

概念:标记阶段:先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;整理阶段:将所有的存活对象压缩到内存的一端;之后,清理边界外所有的空间。

优点:不会产生内存碎片。

缺点:在标记的基础之上还需要进行对象的移动,成本相对较高,效率也不高。

三种算法的比较:

(1)效率:复制算法 > 标记/整理算法 > 标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。

(2)内存整齐度:复制算法=标记/整理算法>标记/清除算法。

(3)内存利用率:标记/整理算法=标记/清除算法>复制算法。

注1:标记-整理算法不仅可以弥补标记-清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。

注2:可以看到标记/清除算法是比较落后的算法了,但是后两种算法却是在此基础上建立的。

注3:时间与空间不可兼得。

4、分代收集算法:

  当前商业虚拟机的GC都是采用的“分代收集算法”,这并不是什么新的思想,只是根据对象的存活周期的不同将内存划分为几块儿。一般是把Java堆分为新生代和老年代:短命对象归为新生代,长命对象归为老年代。

1、存活率低:少量对象存活,适合复制算法:在新生代中,每次GC时都发现有大批对象死去,只有少量存活(新生代中98%的对象都是“朝生夕死”),那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成GC。

2、存活率高:大量对象存活,适合用标记-清理/标记-整理:在老年代中,因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-清理”/“标记-整理”算法进行GC。

注:老年代的对象中,有一小部分是因为在新生代回收时,老年代做担保,进来的对象;绝大部分对象是因为很多次GC都没有被回收掉而进入老年代。

五、垃圾收集器:

      如果说收集算法时内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。虽然我们在对各种收集器进行比较,但并非为了挑出一个最好的收集器。因为直到现在位置还没有最好的收集器出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器

1、Serial收集器:(新生代 – 串行GC)

      这个收集器是一个单线程的收集器,但它的单线程的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程(Stop-The-World:将用户正常工作的线程全部暂停掉),直到它收集结束。收集器的运行过程如下图所示:

     

图中:

  • 新生代采用复制算法,Stop-The-World
  • 老年代采用标记-整理算法,Stop-The-World

       优点:简单而高效(与其他收集器的单线程相比),在限于单个CPU的环境,没有额外的线程交互的开销,所以能获得最高效率。

       当它进行GC工作的时候,虽然会造成Stop-The-World,但它存在有存在的原因:正是因为它的简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,没有线程交互的开销,专心做GC,自然可以获得最高的单线程手机效率。所以Serial收集器对于运行在client模式下是一个很好的选择(它依然是虚拟机运行在client模式下的默认新生代收集器)。

2、ParNew收集器:Serial收集器的多线程版本(新生代 – 并行GC)

  ParNew收集器是Serial收集器的多线程版本。它是运行在server模式下的首选新生代收集器,除了Serial收集器外,目前只有它能与CMS收集器配合工作。CMS收集器是一个被认为具有划时代意义的并发收集器,因此如果有一个垃圾收集器能和它一起搭配使用让其更加完美,那这个收集器必然也是一个不可或缺的部分了。收集器的运行过程如下图所示:

图中:

  • 新生代采用复制算法,Stop-The-World
  • 老年代采用标记-整理算法,Stop-The-World

优点:在多CPU的环境下可以发挥更高而效率,并且是唯一一个可以和CMS收集器搭配工作的新生代并行GC。

3、ParNew Scanvenge收集器(新生代 – 并行回收GC)

    概念: 类似ParNew,但更加关注吞吐量。目标是:达到一个可控制吞吐量的收集器。停顿时间和吞吐量不可能同时调优。我们一方买希望停顿时间少,另外一方面希望吞吐量高,其实这是矛盾的。因为:在GC的时候,垃圾回收的工作总量是不变的,如果将停顿时间减少,那频率就会提高;既然频率提高了,说明就会频繁的进行GC,那吞吐量就会减少,性能就会降低。

      吞吐量:CPU用于用户代码的时间/CPU总消耗时间的比值,即=运行用户代码的时间/(运行用户代码时间+垃圾收集时间)。比如,虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

4、Serial Old收集器(老年代 – 串行GC)

  Serial Old是Serial收集器的年老代版本,同样是一个单线程收集器,使用”标记-整理”算法。

  适用:Client模式下虚拟机使用;在Server模式有两大用途:与Parallel Scavenge收集器搭配使用,作为CMS收集器的后备预案。

5、Parallel Old收集器(老年代 – 并行GC)

  Parallel Old是Parallel Scavenge收集器的老年代版本,为了配合Parallel Scavenge的面向吞吐量的特性而开发的对应组合。

  适用:在注重吞吐量以及CPU资源敏感的场合采用。

6、CMS收集器:(老年代 – 并发GC

      CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。

CMS收集器运行过程:(着重实现了标记的过程)

(1)初始标记

  根可以直接关联到的对象

  速度快

(2)并发标记(和用户线程一起)

  主要标记过程,标记全部对象

(3)重新标记

  由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正

(4)并发清除(和用户线程一起)

  基于标记结果,直接清理对象

上图中,初始标记和重新标记时,需要stop the world。整个过程中耗时最长的是并发标记和并发清除,这两个过程都可以和用户线程一起工作。 

优点:并发收集,低停顿

缺点:

(1)导致用户的执行速度降低。

(2)无法处理浮动垃圾。因为它采用的是标记-清除算法。有可能有些垃圾在标记之后,需要等到下一次GC才会被回收。如果CMS运行期间无法满足程序需要,那么就会临时启用Serial Old收集器来重新进行老年代的手机。

(3)由于采用的是标记-清除算法,那么就会产生大量的碎片。往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次full GC。

疑问:既然标记-清除算法会造成内存空间的碎片化,CMS收集器为什么使用标记清除算法而不是使用标记整理算法:

答案:CMS收集器更加关注停顿,它在做GC的时候是和用户线程一起工作的(并发执行),如果使用标记整理算法的话,那么在清理的时候就会去移动可用对象的内存空间,那么应用程序的线程就很有可能找不到应用对象在哪里。

7、G1收集器

  G1(Garbage First)收集器是当前收集器技术最前沿成果,与之前的CMS相比有两个显著改进:基于”标记-整理”算法实现收集器和精确控制停顿。能够在基本不牺牲吞吐量的前提下完成低停顿的内存回收。      

      优点:

     1、结合了空间整合,不会产生大量的碎片,也降低了进行gc的频率。

  2、可以让使用者明确指定指定停顿时间。(可以指定一个最小时间,超过这个时间,就不会进行回收了)

它有了这么高效率的原因之一就是:对垃圾回收进行了划分优先级的操作,这种有优先级的区域回收方式保证了它的高效率。

如果你的应用追求停顿,那G1现在已经可以作为一个可尝试的选择;如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处。

注:以上所有的收集器当中,当执行GC时,都会stop the world,但是CMS收集器却不会这样。

六、Java堆内存划分:

根据对象的存活率(年龄),Java对内存划分为3种:新生代、老年代、永久代。

1、新生代:

      比如我们在方法中去new一个对象,那这方法调用完毕后,对象就会被回收,这就是一个典型的新生代对象。 

     现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块比较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的空间会被浪费。

      当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖于老年代进行分配担保,所以大对象直接进入老年代。同时,长期存活的对象将进入老年代(虚拟机给每个对象定义一个年龄计数器)。

Minor GC和Full GC:

GC分为两种:Minor GC和Full GC

Minor GC:

  Minor GC是发生在新生代中的垃圾收集动作,采用的是复制算法。对象在Eden和From区出生后,在经过一次Minor GC后,如果对象还存活,并且能够被to区所容纳,那么在使用复制算法时这些存活对象就会被复制到to区域,然后清理掉Eden区和from区,并将这些对象的年龄设置为1,以后对象在Survivor区每熬过一次Minor GC,就将对象的年龄+1,当对象的年龄达到某个值时(默认是15岁,可以通过参数 --XX:MaxTenuringThreshold设置),这些对象就会成为老年代。但这也是不一定的,对于一些较大的对象(即需要分配一块较大的连续内存空间)则是直接进入老年代。

Full GC:

  Full GC是发生在老年代的垃圾收集动作,采用的是标记-清除/整理算法。老年代里的对象几乎都是在Survivor区熬过来的,不会那么容易死掉。因此Full GC发生的次数不会有Minor GC那么频繁,并且做一次Full GC要比做一次Minor GC的时间要长。另外,如果采用的是标记-清除算法的话会产生许多碎片,此后如果需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次GC。

2、老年代:

    在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。而且大对象直接进入老年代。

3、永久代:

    即方法区。

猜你喜欢

转载自blog.csdn.net/qq_26891141/article/details/84997997
今日推荐