初步了解JVM第三篇(堆和GC回收算法)

《初步了解JVM第一篇》《初步了解JVM第二篇》中,分别介绍了:html

  • 类加载器:负责加载*.class文件,将字节码内容加载到内存中。其中类加载器的类型有以下:执行引擎:负责解释命令,提交给操做系统执行。
    • 启动类加载器(Bootstrap)
    • 扩展类加载器(Extension)
    • 应用程序类加载器(AppClassLoader)
    • 用户自定义加载器(User-Defined) 
  • 执行引擎:负责解释命令,提交给操做系统执行。
  • 本地接口:目的是为了融合不一样的编程语言提供给Java所用,可是企业中已经不多会用到了。
  • 本地方法栈:将本地接口的方法在本地方法栈中登记,在执行引擎执行的时候加载本地方法库
  • PC寄存器:是线程私有的,记录方法的执行顺序,用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。
  • 方法区:存放类的架构信息,ClassLoader加载的class文件内容存放在方法区中。
  • 栈:线程私有,用来管理Java程序的运行。

进行简单的回顾后,接下来为你们介绍Java中的堆。算法

堆(Heap)编程

你们可会分不清栈和堆,其实能够简单记住一句话:栈管运行,堆管存储。堆是线程共享的,而栈是线程私有的。那么什么是堆呢?多线程

在一个JVM实例中,堆内存只存在一个。对内存的大小是能够进行调节的,类加载器读取了类文件以后须要把类、方法、常变量放到堆内存中,保存全部引用类型的真实信息,以便执行器执行。架构

首先抛给一个大的概念给你们先,为你们介绍堆内存的三大部分(这里咱们讲的以JDK8的版本为准,也就是将永久代变改成元空间):并发

  • 新生区:咱们new出来的对象的存放地址,而新生区又分为三部分:
    • Eden(伊甸区)
    • Survivor 0 Space(幸存者0区)
    • Survivor 1 Space(幸存者1区)
  • 养老区:新生区的对象通过15次的GC回收(垃圾回收)以后存活下来的对象就放在这里,养老区若是满了也会进行GC回收,只不过发生的频率小于新生区
  • 元空间:元空间咱们上一篇已经讲过了,主要是用来存放类的结构信息,相似一个模板。

 

上以就是堆内存的大三部分:伊甸区、养老区、元空间。上图是逻辑上的结构,可是在物理上只有新生区和养老区,并且咱们须要区分新生代和养老代用的是JVM的内存,可是元空间用的是系统内存。若是看得有点懵,没关系,先来咱们来一个一个介绍,首先第一部分新生区。编程语言

新生区spa

新生区就是类的诞生、成长、消亡的区域。一个类在这里产生、而后应用,最后被垃圾回收器回收,结束了的生命的过程释放出内存。那么咱们来简单说一下,一个类被new出来以后从开始到消亡的一个过程:操作系统

  • 1)假设有一个程序是一直不断在new对象,那么new出来的对象首先就是存放在新生区的伊甸区,(注意:通常new的对象是放在新生区的伊甸区的,大的对象会特殊处理)。
  • 2)伊甸区的内存也是有限,程序一直在不断的new对象,终于!!!在某一个时刻,伊甸园的空间快没有地方能够存放新的对象了。也就是达到伊甸区存放对象的阈值。这时候,注意!!!伊甸区就开始进行垃圾回收,也就是咱们常说的轻GC,将大部分再也不使用的对象Kill掉!!留下还在使用的对象。由于堆内存里面的对象绝大多数都是临时对象,因此一次垃圾回收会Kill掉90%以上的对象,能存活下来的数量很是少。
  • 3)存活下来的对象就从伊甸区移到了幸存者0区注意幸存者0区还有一个别名就作From。
  • 4)虽然垃圾回收会Kill掉大部分的对象,可是咱们仍是不能排除有个别现象存在伊甸区和幸存者0区再一次满了的状况,由于程序new的速度确定是比Kill的速度快的,终于又在某一时刻!!!伊甸区又达到了必定的阈值,再次进行垃圾回收,这时候就会将伊甸区和幸存者0区(注意:迁移的对象包括幸存者0区)存活下来的对象迁移到幸存者1区(幸存者1区的另一个别名为To)。
  • 5)一直如此反复,等到幸存者1区也满了,就将存活的对象移到养老区进行养老,能到养老区的通常都一些长期使用的对象。那养老区怎么肯定哪些才是长期使用的对象呢?在新生区中,一个对象通过每次垃圾回收以后幸存下来的,都会进行计数,通过了15次垃圾回收以后依然存在的,就会进入到养老区。

(注意:讲到这里,是大部分对象消亡了,可是仍是有通过15次垃圾回收以后存活下来的对象进入了养老区)线程

养老区

在新生区中,咱们已经描述了一个类从开始到消亡或者进入养老区的过程,要么就是被kill了,要么就是进入了养老区。进入养老区以后就能够舒舒服服的摸鱼了吗?你想得太简单了,接下来看看,养老区又有怎么样的一番搏斗呢:

  • 1)重新生区幸存下来的幸运儿来到了养老区养老,养老区就至关一个养老院,可是一个养老院也会满员。这时候,没办法了,只能清出一部分老人,让新的一批重新生区来的老人入住,这时候就发生了垃圾回收,也就是咱们说的重GC。
  • 2)虽然在养老区也会发生垃圾回收机制,可是仍是会有一天,这个养老院实在是腾不出空位了,即便是进行重GC也腾不出几个空间,这时候没办法了!!!表明已经没有内存了,玩不转了,因此系统就会报错,也就是咱们常看到的OOM(“OutOfMemoryError”):对内存溢出。
  • 3)因而乎,程序就异常中止了,全部对象都消亡了,这个就是程序中一个对象从开始到消亡的整个过程。

堆的内存大小分配:

 注:

  • From就是上面说的幸存者0区的别名
  • To就是上面说的幸存者1区的别名

这个比例咱们必定要记住,很是重要,这是在GC时选取何种算法的一个依据之一,新生代跟老年代是1:2,而新生代中的三个分区中分别是8:1:1。

看完了堆内存的结构,接下来咱们就要讲讲GC垃圾回收算法了。在上面咱们描述了一个对象从开始到结束的过程,中间会发生GC回收,其中:

  • 新生代:发生的GC叫作轻GC也叫MinorGC,所用的算法叫作复制算法。
  • 老年代:发生的GC叫作重GC也叫Full GC,所用的算法叫作标记清除算法和标记压缩算法

  这里过个眼熟,下面咱们在GC垃圾回收算法的时候会讲到。

垃圾回收算法

在进行垃圾回收的时候,JVM须要根据不一样的堆内存和结构去选取适合的算法来提升垃圾回收的效率,而垃圾回收算法主要有:

  • 引用计数法
  • 复制算法
  • 标记清除算法
  • 标记压缩算法

1)引用计数算法

原理:给对象中每个对象分配一个引用计数器,每当有地方引用该对象时,引用计数器的值加一,当引用失效时,引用计数器的值减一,无论何时,只要引用计数器的值等于0了,说明该对象不可能再被使用了。

优势:

  • 实现原理简单,并且断定效率很高。大部分状况下都是一个不错的算法。

缺点:

  • 每次对对象复制时均要维护引用计数器,且计数器自己也有必定的消耗。
  • 较难处理循环引用。

在JVM中通常不采用这种方式实现,因此就不展开来说了。

2)复制算法(Copying)——新生代使用

在新生代中的GC,用的主要算法就是复制算法,并且发生GC的过程当中From区和To区会发生一次交换(请记住这句话)。在堆的内存分配图中JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(别名叫From和To)。默认比例为8:1:1,通常状况下,新建立的对象都会被分配到Eden区(一些大对象特殊处理),当Eden区进行了GC还存留下来的就会被移到Survivor区。对象在Survivor区每通过一轮GC存留下来年龄就会加1。直到它存活到了必定岁数的是时候就会被移到养老区。因为新生区中的绝大部分对象都是临时对象,不会存活过久,因此通过每一轮的GC以后存活下来的对像都很少,因此新生区所用的GC算法就是复制算法。

复制算法原理:

首先先给你们介绍一个名词叫作根集合(GC Root):

  • 经过System Class Loader或者Boot Class Loader加载的class对象,经过自定义类加载器加载的class不必定是GC Root
  • 处于激活状态的线程
  • 栈中的对象
  • JNI栈中的对象
  • JNI中的全局对象
  • 正在被用于同步的各类锁对象
  • JVM自身持有的对象,好比系统类加载器等

有了上面的了解咱们就能够来学学复制算法:

  • 复制算法从根集合(GC Root)开始,从From区中找到通过GC存活下来的对象(注意:虽说是From区,可是这里的From区是包括了伊甸区和幸存者1区(别名From),因此你们不要认为From区就是单单包括From区而已)。拷贝到To中;
  • 上面咱们说过From和To会发生一次交换就是发生在这里,From将幸存下来的对象拷贝到To以后,这时From区就没有对象,空出来了,而To如今不是空的,存放了From的幸存的对象(默认状态是From有对象,To是空的)。这时候From和To就会发生身份的互换,下次内存分配从To开始。也就是说发生一次GC以后From就会变成To,To就会变成From(当谁是空的,谁就是To)
  • 一直这样反复GC,一直再一次发生GC的时候,From存活的对象拷贝到To时,To会被填满,这时候就会把这些对象(知足年龄为15的对象,这个值能够经过-XX:MaxTenuringThreshold来设置,默认是15)移动到养老区。

  下面咱们用一张图来描述一下复制算法发生的过程:

咱们一直都在反复强调,Eden区的对象存活率是比较低的,因此通常就是拿两块10%的内存做为空闲区(To)和活动区(From),拿80%的内存来存储新建的对象。一但GC事后,就会将这10%的活动区和80%的Eden区存留下来的对象移到空闲区(To)中。而后以前的内存就获得了释放,依次类推。

复制算法的缺点:

  • 复制的时候须要耗费通常的内存,内存消耗大(可是效率的快的,并且新生区的存活效率低,并不须要复制太多的对象,因此新生区用这种算法效率是比咱们下面要讲的算法效率高的)。
  • 若是对象的存活率很高,须要复制的对象太多,这时候效率就大大下降了。

复制算法的优势:

  • 没有标记和清除的过程,效率高。
  • 由于是直接对对象进行复制的,因此不会产生内存碎片。

3)标记清除算法(Mark-Sweep)

老年代主要由标记清除算法和标记压缩算法混合使用。

标记算法的步骤从名字其实就能够看出来是怎么回事了:

  • 标记须要清除的对象
  • 清除标记的对象

在复制算法中咱们就说了它的缺点是浪费空间,因此为了解决这个问题,就不将对象进行复制了,由于复制一份须要同等大小的内存。标记清除算法采用标记的方式,将要清除的对象进行标记而后直接清除掉,这样就就大大节省了空间了。同上,继续来经过一张图来理解:

上图就是标记清除算法的过程,从过程当中能够看出一些问题:

因为回收的对象是进行标记后直接删除的,因此就像上图回收后所展现的同样,内存空间是不连续的,也就是会有内存碎片的产生。第二个问题是复制算法是直接复制的,可是标记清除算法是须要扫描两次,耗时严重。

标记清除算法的优势:

  • 对须要回收的对象进行标记清除,不须要额外的空间。

标记清除算法的缺点:

  • 效率低,在进行GC时,须要中止整个程序。
  • 清理出来的内存空间是不连续的,存在内存碎片。因为空间不连续,查找的效率也会下降

可是因为养老区存活下来的对象会比新生区的对象多,因此用标记清除是比复制算法好的。

4)标记压缩算法(Mark-Compact)

理解了标记清除算法后,其实这一个算法就比较简单理解了。就是多了一步整理的阶段,清除内存碎片使空间变得连续。过程以下图:

标记压缩算法的优势:

  • 能够看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当咱们须要给新对象分配内存时,JVM只须要持有一个内存的起始地址便可,这比维护一个空闲列表显然少了许多开销。 
  • 标记/整理算法不只能够弥补标记清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。

标记压缩算法的缺点:

  • 虽然这个算法解决了上两个算法的一些缺点,可是这个算法倒是耗时最长的。从效率来看是低于标记清除算法和复制算法的。

以上就是GC的四大算法,固然出了这四大算法还有标记清除压缩算法(Mark-Sweep-Compact),这个也很好理解就是在整理阶段再也不是GC一次就整理一次,而是每隔一段时间整理一次,减小移动对象的成本。

分代收集算法:

当有人问你哪一个算法是最好的时候,你的回答应该是:无,没有最好的算法,只有最合适的算法。使用哪一个算法应该看GC发生在什么地方:

  • 新生代:复制算法
    • 缘由:存活率低,须要复制的对象不多,所须要用到的空间不是不少。另一方面,新生代发生的频率是很是高的,而复制算法的效率在新生代是最高的,因此新生代用复制算法是最合适的。
  • 老年代:标记清除和标记压缩算法混合使用
    • 缘由:存在大量存活率高的对像,复制算法明显变得不合适。通常是由标记清除或者是标记清除与标记整理的混合实现。
    • Mark阶段的开销与存活对像的数量成正比,这点上说来,对于老年代,标记清除或者标记整理有一些不符,但能够经过多线程利用,对并发、并行的形式提升标记效率。
    • Sweep阶段的开销与所管理区域的大小成正相关,可是清除“就地处决”的特色,回收的过程没有移动对象。使其相对其它有移动对像步骤的回收算法,仍然是效率最好的。可是须要解决内存碎片问题。
    • Compact阶段的开销与存活对像的数据成开比,如上一条所描述,对于大量对像的移动是很大开销的,作为老年代的第一选择并不合适。
    • 基于上面的考虑,老年代通常是由标记清除或者是标记清除与标记整理的混合实现。以hotspot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对像的回收效率很高,而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施:当内存回收不佳(碎片致使的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。

终于写完了,以上即是本人对JVM的理解,若有不足欢迎提出,谢谢!!!

相关文章
相关标签/搜索