JVM常见垃圾回收算法

jdk1.7.0_79

  众所周知,Java是一门不用程序员手动管理内存的语言,全靠JVM自动管理内存,既然是自动管理,那必然有一个垃圾内存的回收机制或者回收算法。本文将介绍几种常见的垃圾回收(下文简称GC)算法。

  在Java堆上分配一个内存给实例对象时,此时在虚拟机栈上引用型变量就会存放这个实例对象的起始地址。

Object obj = new Object();
登录后复制

JVM垃圾回收算法与jvm的堆内存中的三个区域-LMLPHP

  现在如果我们将变量赋值为null。

obj = null;
登录后复制

JVM垃圾回收算法与jvm的堆内存中的三个区域-LMLPHP

  此时可以看到Java堆上的实例对象无法再次引用它,那么它就是被GC的对象,我们称之为对象“已死”。那虚拟机栈上的obj变量呢?上文《JVM入门——运行时数据区》提到过,虚拟机栈是线程独占的,也就是说随着线程初始而初始,消亡而消亡,当线程被销毁后,虚拟机栈上的内存自然会被回收,也就是说虚拟机栈上的这块内存空间不在虚拟机GC范围。下图展示了垃圾回收的内存范围:

JVM垃圾回收算法与jvm的堆内存中的三个区域-LMLPHP

  1.对象是否“已死”算法——引用计数器算法

  对象中添加一个引用计数器,如果引用计数器为0则表示没有其它地方在引用它。如果有一个地方引用就+1,引用失效时就-1。看似搞笑且简单的一个算法,实际上在大部分Java虚拟机中并没有采用这种算法,因为它会带来一个致命的问题——对象循环引用。对象A指向B,对象B反过来指向A,此时它们的引用计数器都不为0,但它们俩实际上已经没有意义因为没有任何地方指向它们。所以又引出了下面的算法。

  2.对象是否“已死”算法——可达性分析算法

  这种算法可以有效地避免对象循环引用的情况,整个对象实例以一个树呈现,根节点是一个称为“GC Roots”的对象,从这个对象开始向下搜索并作标记,遍历完这棵树过后,未被标记的对象就会判断“已死”,即为可被回收的对象。

JVM垃圾回收算法与jvm的堆内存中的三个区域-LMLPHP

GC算法

  1.标记-清除算法

  等待被回收对象的“标记”过程在上文已经提到过,如果在被标记后直接对对象进行清除,会带来另一个新的问题——内存碎片化。如果下次有比较大的对象实例需要在堆上分配较大的内存空间时,可能会出现无法找到足够的连续内存而不得不再次触发垃圾回收。

  2.复制算法(Java堆中新生代的垃圾回收算法)

此GC算法实际上解决了标记-清除算法带来的“内存碎片化”问题。首先还是先标记处待回收内存和不用回收的内存,下一步将不用回收的内存复制到新的内存区域,这样旧的内存区域就可以全部回收,而新的内存区域则是连续的。它的缺点就是会损失掉部分系统内存,因为你总要腾出一部分内存用于复制。

  在上文《JVM入门——运行时数据区》提到过在Java堆中被分为了新生代和老年代,这样的划分是方便GC。Java堆中的新生代就使用了GC复制算法。在新生代中又分为了三个区域:Eden 空间、To Survivor空间、From Survivor空间,from survivor 和 to survivor大小相同,且保证一个为empty.。不妨将注意力回到这张图的左边新生代部分,:

JVM垃圾回收算法与jvm的堆内存中的三个区域-LMLPHP

  新的对象实例被创建的时候通常在Eden空间,发生在Eden空间上的GC称为Minor GC,当在新生代发生一次GC后会将Eden和其中一个Survivor空间的内存复制到另外一个Survivor中,如果反复几次有对象一直存活,此时内存对象将会被移至老年代。可以看到新生代中Eden占了大部分,而两个Survivor实际上占了很小一部分。这是因为大部分的对象被创建过后很快就会被GC(这里也许运用了是二八原则)。

  3.标记-压缩算法(或称为标记-整理算法,Java堆中老年代的垃圾回收算法)

  对于新生代,大部分对象都不会存活,所以在新生代中使用复制算法较为高效,而对于老年代来讲,大部分对象可能会继续存活下去,如果此时还是利用复制算法,效率则会降低。标记-压缩算法首先还是“标记”,标记过后,将不用回收的内存对象压缩到内存一端,此时即可直接清除边界处的内存,这样就能避免复制算法带来的效率问题,同时也能避免内存碎片化的问题。老年代的垃圾回收称为“Major GC”。

不积跬步,无以至千里;不积小流,无以成江海。

  • 1.JVM的堆栈

栈:在jvm中栈用来存储一些对象的引用、局部变量以及计算过程的中间数据,在方法退出后那么这些变量也会被销毁。它的存储比堆快得多,只比CPU里的寄存器慢

堆:用来存储程序中的一些对象,比如你用new关键字创建的对象,它就会被存储在堆内存中,但是这个对象在堆内存中的首地址会存储在栈中。

栈内存在JVM中默认是1M,可以通过下面的参数进行设置

-Xss
登录后复制
  • 1

最小堆内存在JVM中默认物理内存的64分之1,最大堆内存在JVM中默认物理内存4分之一,且建议最大堆内存不大于4G,并且设置-Xms=-Xmx避免每次GC后,调整堆的大小,减少系统内存分配开销

-Xms

-Xmx
登录后复制
  • 1

  • 2

  • 3

在jvm的堆内存中有三个区域:

1.年轻代:用于存放新产生的对象。

2.老年代:用于存放被长期引用的对象。

3.持久带:用于存放Class,method元信息。

如图:

JVM垃圾回收算法与jvm的堆内存中的三个区域-LMLPHP

  • 一.年轻代

年轻代中包含两个区:Eden 和survivor,并且用于存储新产生的对象,其中有两个survivor区如图:

JVM垃圾回收算法与jvm的堆内存中的三个区域-LMLPHP

可以使用参数配置年轻代的大小,如果配置它为100M那么就相当于2*survivor+Eden = 100M

-Xmn
登录后复制
  • 1

可以配置Eden 和survivor区的大小,这里配置的是比值,jvm中默认为8,意思就是Eden区的内存比上survivor的内存等于8,如果年轻代的Xmn配置的100M,那么Eden就会被分配80M内存,每个survivor分配10M内存

-XX:SurvivorRatio
登录后复制
  • 1

还可以配置年轻代和老年代的比值,这里需要注意:老年代的内存就是通过这个比值设置,jvm没有给你直接设置老年代内存大小的参数;如果整个堆内存设为100M并且在这里设置年轻代和老年代的比值为7,如果持久代占用了10M,那么100M-10M=90M这里的90M就是老年代和年轻代的内存总和,且年轻代占用(90/(7+1)*7)的内存,老年代就占用(90/(7+1)*1)的内存。

-XX:NewRatio
登录后复制
  • 1

  • 二.老年代

年轻代在垃圾回收多次都没有被GC回收的时候就会被放到老年代,以及一些大的对象(比如缓存,这里的缓存是弱引用),这些大对象可以不进入年轻代就直接进入老年代(1.防止新生代有大量剩余的空间,而大对象创建导致提前发生GC;2.防止在eden区和survivor区的大对象复制造成性能问题),这个可以通过如下参数设置,表示单个对象超过了这个值就会直接到老年带(默认为0):

-XX:PretenureSizeThreshold
登录后复制
  • 1

并且大的数组对象也会直接放到老年代,比如array和arrayList(底层用数组实现),因为数组需要连续的空间存储数据。

  • 三.持久代

持久代用来存储class,method元信息,大小配置和项目规模,类和方法的数量有关,一般配置128M就够了,设置原则是预留30%空间,它可以通过如下参数进行大小配置:

-XX: PermSize 
-XX: MaxPermSize
登录后复制
  • 1

  • 2

持久代也可能会被GC回收,如果持久代理的常量池没有被引用以及一些无用的类信息和类的Class对象也会被回收。

相关文章:

jvm垃圾回收算法

Java 详解垃圾回收与对象生命周期

相关视频:

javascript初级教程

以上就是JVM垃圾回收算法与jvm的堆内存中的三个区域的详细内容,更多请关注Work网其它相关文章!

09-15 10:07