《初步了解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的理解,如有不足欢迎提出,谢谢!!!

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

  1. JVM内存模型及GC回收算法

    该篇博客主要对JVM内存模型以及GC回收算法以自己的理解和认识做以记录. 内存模型 GC垃圾回收 1.内存模型 从上图可以看出,JVM分为 方法区,虚拟机栈,本地方法栈,堆,计数器 5个区域.其中最为 ...

  2. JVM系列(三):java的垃圾回收机制

    java垃圾回收机制介绍    上一篇讲述了JVM的内存模型,了解了到了绝大部分的对象是分配在堆上面的,我们在编码的时候并没有显示的指明哪些对象需要回收,但是程序在运行的过程中是会一直创建对象的,之所 ...

  3. 学习一下 JVM (三) -- 了解一下 垃圾回收

    一.简单了解几个概念 1.什么是垃圾(Garbage)?什么是垃圾回收(Garbage Collection,简称 GC)? (1)什么是垃圾(Garbage)? 这里的垃圾 指的是 在程序运行过程中 ...

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

    垃圾回收的瓶颈 传统分代垃圾回收方式,已经在一定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量推到了一个极限.但是他无法解决的一个问题,就是Full GC所带来的应用暂停.在一些对实时性要 ...

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

    引言 之前我们学习了 JVM 基本介绍 以及 什么样的对象需要被 GC ,今天就来学习一下 JVM 在判断出一个对象需要被 GC 会采用何种方式进行 GC.在学习 JVM 如何进行垃圾回收方法时,发现 ...

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

    可以从不同的的角度去划分垃圾回收算法: 按照基本回收策略分 引用计数(Reference Counting): 比较古老的回收算法.原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数. ...

  7. JVM知识(五):垃圾回收算法

    在介绍垃圾回收算法之前,我们需要先了解一个词“stop the world”,stop the world会在执行某一个垃圾回收算法的时候产生,JVM为了执行垃圾回收,会暂时java应用程序的执行,等 ...

  8. JVM调优-Jva中基本垃圾回收算法

    从不同的的角度去划分垃圾回收算法. 按照基本回收策略分 引用计数(Reference Counting) 比较古老的回收算法.原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数.垃圾回 ...

  9. JVM 第三篇:Java 类加载机制

    本文内容过于硬核,建议有 Java 相关经验人士阅读. 1. 什么是类的加载? 类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 ...

随机推荐

  1. /etc/security/limits.conf 详解与配置

    目录 一. /etc/security/limits.conf 详解 /etc/security/limits.conf 配置解析 /etc/security/limits.d/ 目录 二. ulim ...

  2. Java中的compareTo()方法,compareToIgnoreCase()方法

    1.compareTo(String)方法: Java中String类有一个compareTo方法,该方法返回一个int类型的数据.其比较规则是:拿出字符串的第一个字符与参数的第一个字符进行比较,如果 ...

  3. 使用FastReport报表工具生成图片格式文档

    之前我在随笔<使用FastReport报表工具生成报表PDF文档>介绍过使用FastReport.Net来根据报表模板进行生成PDF,以及随笔<使用FastReport报表工具生成标 ...

  4. Spring Boot整合Mybatis报错InstantiationException: tk.mybatis.mapper.provider.base.BaseSelectProvider

    Spring Boot整合Mybatis时一直报错 后来发现原来主配置类上的MapperScan导错了包 由于我使用了通用Mapper,所以应该导入通用mapper这个包

  5. python-面向对象之封装

    封装 面向对象三大特性: 继承 封装 多态 隐藏对象的属性和实现细节,仅对外提供公共访问方法 广义上的封装 : 把方法和变量都封装在类中 狭义上的封装 : 在类的外部干脆不能调用了 优点 将变化隔离 ...

  6. JVM的类加载机制全面解析

    什么是类加载机制 JVM把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被JVM直接使用的Java类型,这就是JVM的类加载机制. 如果你对Class文件的结 ...

  7. Gitlab用户信息批量导出

    前言 因运维体系中涉及到用户权限管理及统计,需将Gitlab用户数据提取出来并录入到公司内部自建的权限统计平台. 本文将对Gitlab的用户信息数据批量导出进行操作说明! 思路 A)要对数据进行批量的 ...

  8. Prometheus PromQL 基础

    目录 时序 4 种类型 Counter Gauge Histogram Summary Histogram vs Summary 操作符 时序 4 种类型 Prometheus 时序数据分为 Coun ...

  9. Linux权限基础

    Linux权限基础 四种权限大类 基本权限 特殊权限 隐藏权限 file ACL权限 三种基本权限 r(read) 读是基本权限,没有读权限其他的一切操作都会被限制 读 4 w(write) 写 2 ...

  10. 使用 font-spider 对 webfont 网页字体进行压缩

    原文链接:使用 font-spider 对 webfont 网页字体进行压缩 随着当前 Web 技术的日新月异,网页界面内容越来越丰富,让人眼花缭乱,其中就包括了网页中的各种自定义字体. 例如,个人博 ...