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

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

  • 类加载器:负责加载*.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的内存,但是元空间用的是系统内存。如果看得有点懵,不要紧,先来我们来一个一个介绍,首先第一部分新生区。

新生区

新生区就是类的诞生、成长、消亡的区域。一个类在这里产生、然后应用,最后被垃圾回收器回收,结束了的生命的过程释放出内存。那么我们来简单说一下,一个类被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的理解,如有不足欢迎提出,谢谢!!!

原文地址:https://www.cnblogs.com/linzepeng/p/12069353.html

时间: 2024-08-27 04:28:59

初步了解JVM第三篇(堆和GC回收算法)的相关文章

java虚拟机学习-JVM调优总结-新一代的垃圾回收算法(11)

垃圾回收的瓶颈 传统分代垃圾回收方式,已经在一定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量推到了一个极限.但是他无法解决的一个问题,就是Full GC所带来的应用暂停.在一些对实时性要求很高的应用场景下,GC暂停所带来的请求堆积和请求失败是无法接受的.这类应用可能要求请求的返回时间在几百甚至几十毫秒以内,如果分代垃圾回收方式要达到这个指标,只能把最大堆的设置限制在一个相对较小范围内,但是这样有限制了应用本身的处理能力,同样也是不可接收的. 分代垃圾回收方式确实也考虑了实时性要求而

03 JVM 从入门到实战 | 简述垃圾回收算法

引言 之前我们学习了 JVM 基本介绍 以及 什么样的对象需要被 GC ,今天就来学习一下 JVM 在判断出一个对象需要被 GC 会采用何种方式进行 GC.在学习 JVM 如何进行垃圾回收方法时,发现所谓的 JVM 垃圾回收思想和现实生活的场景有很多相似的地方.所以本文用餐厅回收餐桌的方式类比 JVM 垃圾回收算法,应该能帮助 JVM 学习的理解和记忆. 经典垃圾回收算 标记-清除(Mark-Sweep) 研发园开了家新餐厅,餐厅老板在考虑如何回收餐盘时首先使用了最简单的方式,那就是服务员在顾客

jvm 调优(2)垃圾回收算法

可以从不同的的角度去划分垃圾回收算法: 按照基本回收策略分 引用计数(Reference Counting): 比较古老的回收算法.原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数.垃圾回收时,只用收集计数为0的对象.此算法最致命的是无法处理循环引用的问题. 标记-清除(Mark-Sweep): 此算法执行分两阶段.第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除.此算法需要暂停整个应用,同时,会产生内存碎片. 复制(Copying): 此算

JVM系列三:内存分配与回收策略

内存分配策略 1.对象优先分配在新生代Eden区 多数情况下,对象分配在新生代的Eden,若Eden区域内存不够,则引发一次Minor GC 2.大对象直接进入老年代 大对象直接分配在老年代,避免新生代里出现从Eden到Survivor频繁的内存复制 可用-XX:PretenureSizeThreshold设置,大于此值的对象直接分配在老年代 典型的大对象有长字符串或者数组等 3.长久存活的对象进入老年代 有的对象经过几次MinorGC都没有被回收,则可以被移入老年代 可用-XX:MaxTenu

JVM内存分配策略,及垃圾回收算法

本人免费整理了Java高级资料,一共30G,需要自己领取;传送门:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q 说起垃圾收集(Garbage Collection, GC),想必大家都不陌生,它是JVM实现里非常重要的一环,JVM成熟的内存动态分配与回收技术使Java(当然还有其他运行在JVM上的语言,如Scala等)程序员在提升开发效率上获得了惊人的便利.理解GC,对于理解JVM和Java语言有着非常重要的作用.并且当我们需要排查各种内存溢

JVM架构和工作原理及GC工作机制

学习java,理解弄懂了JVM和GC,对于学习java开发有很大帮助. 借助前人之鉴博文,这里主要讲四个部分:JVM结构.内存分配.垃圾回收算法.垃圾收集器 一.JVM结构 JVM主要包括四个部分: 1.类加载器(ClassLoader):在JVM启动时或者在类运行时将需要的class加载到JVM中. 2.执行引擎:负责执行class文件中包含的字节码指令. 3.内存区(也叫运行时数据区):是在JVM运行的时候操作所分配的内存区. 方法区(Method Area):用于存储类结构信息的地方,包括

jvm垃圾回收算法整理

java推荐 内存的自动化整理 也就是自动化解决给对象分配内存以及回收对象的内存  ,这两个问题也是主要针对java的内存模型 堆 :有效解决内存丢失等问题: 1.内存分类: 新生代: eden内存 新建的对象存储的位置 survivor0 当eden内存空间存满之后就会将存活的对象进行复制进入survivor0空间,eden内存空间进行一次GC回收 survivor1 当Eden内存和survivor0都存满之后,就会将存活的对象复制进入survivor1空间,eden和survivor0都回

2 - JVM随笔分类(JVM堆的内存回收)

JVM常用的回收算法是: 标记/清除算法 标记/复制算法 标记/整理算法 其中上诉三种算法都先具备,标记阶段,通过标记阶段,得到当前存活的对象,然后再将非标记的对象进行清除,而对象内存中对象的标记过程,则是使用的  “根搜索算法”,通过遍历整个堆中的GC ROOTS,将所有可到达的对象标记为存活的对象的一种方式,则是 “根搜索算法”,其中根是指的“GC ROOTS”,在JAVA中,充当GC ROOTS的对象分别有:“虚拟机栈中的引用对象”,“方法区中的类静态属性引用的对象”,“方法区中的常量引用

SylixOS ARM BSP 第三篇【bspInit.c】

此篇博客为 SylixOS ARM BSP 编写连载的第三篇,主要介绍 bspInit.c 文件具体实现. bspInit.c 为 BSP 操作系统初始化部分代码,通常由 startup.S 初始完基本处理器参数后调用,下面以 S3C2440A 处理器为例,逐块介绍 bspInit.c 代码. SylixOS ARM BSP 第二篇中提到 startup.S 初始化完成会将会调用 bspInit() 函数,此函数用于初始化操作系统,并开始多任务调度. int bspInit (void) {