JVM内存结构与垃圾回收器介绍

1、运行时数据区
在这里插入图片描述

JVM管理的最大的一块内存区域,存放着对象的实例,是线程共享区。
堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”.

JAVA堆的分类:
从内存回收的角度上看,可分为新生代(Eden空间,From Survivor空间、To Survivor空间)及老年代(Tenured Gen);
从内存分配的角度上看,为了解决分配内存时的线程安全性问题,线程共享的JAVA堆中可能划分出多个线程私有的分配缓冲区(TLAB)。

JAVA堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可;
可通过参数 -Xmx -Xms 来指定运行时堆内存的大小,堆内存空间不足也会抛OutOfMemoryError异常。

方法区

  • 方法区也是线程共享区,用于存储虚拟机加载的类信息(类的版本、字段、方法、接口),常量,静态变量,即时编译器编译后的代码等数据;
  • 方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”;
  • HotSpot虚拟机使用永久代来实现方法区,使得HotSpot虚拟机的垃圾收集器可以像管理堆内存一样来管理这部分内存,能省去专门为方法区编写内存管理代码工作。所以开发者喜欢将方法区称为永久代,本质上两者并不等价,对于其他虚拟机来说不存在永久代的概念;
  • 方法区可选择不实现垃圾收集,一般来说,这个区域对内存回收的条件较为苛刻,但是这部分区域的回收确实是必要的;
  • 当方法区无法满足内存分配需求时,将会抛OutOfMemoryError异常。

运行时常量池

  1. 运行时常量池是方法区的一部分;
  2. class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后加入方法区的运行时常量池中存放。
  3. 运行时常量池相于class文件中的常量池所不同的是其具备了动态性。class文件中常量池中的常量在编译期间就已经定义好了,而运行时常量池在程序运行期间也可以将常量放入该常量池中,最常见的做法就是调用String类的intern()方法。

虚拟机栈

每个线程有一个私有的栈,随着线程的创建而创建,生命周期与线程相同。

虚拟机栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表、操作数栈、动态链接、方法出口等信息。

  • 局部变量表存放了编译期可知的各种基本数据类型和对象引用类型。通常我们所说的“栈内存”指的就是局部变量表这一部分。
  • 64位的long和double类型的数据会占用2个局部变量空间,其余的数据类型只占用1个。
  • 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧分配多少内存是固定的,运行期间不会改变局部变量表的大小。

方法的调用到执行完毕,对应的就是栈帧的入栈和出栈的过程。

栈的大小可以固定也可以动态扩展。

  • 在固定大小的情况下,当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError异常。
  • 在动态扩展的情况下,若扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

  • 和虚拟机栈类似,两者的区别就是虚拟机栈是为虚拟机执行java方法服务,本地方法栈为虚拟机执行native方法服务 。
  • HotSpot虚拟机不区分虚拟机栈和本地方法栈,两者是一块的。
  • 与虚拟机栈一样,本地方法栈也会抛StackOverflowError和OutOfMemoryError异常。

程序计数器

  • 程序计数器是一块较小的空间,它可以看作是当前线程所执行的字节码的行号指示器。
  • 如果线程执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址(可以理解为上图所示的行号),如果正在执行的是native方法,这个计数器的值为undefined。
  • JVM的多线程是通过线程轮流切换并分配CPU执行时间片的方式来实现的,任何一个时刻,一个CPU都只会执行一条线程中的指令。为了保证线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程间的程序计数器独立存储,互不影响。
  • 此区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域,因为程序计数器是由虚拟机内部维护的,不需要开发者进行操作。

2、JVM的垃圾回收算法有哪些

常用的垃圾回收算法有如下四种:标记-清除、复制、标记-整理和分代收集。

标记-清除算法

从算法的名称上可以看出,这个算法分为两部分,标记和清除。首先标记出所有需要被回收的对象,然后在标记完成后统一回收掉所有被标记的对象。
这个算法简单,但是有两个缺点:一是标记和清除的效率不是很高;二是标记和清除后会产生很多的内存碎片,导致可用的内存空间不连续,当分配大对象的时候,没有足够的空间时不得不提前触发一次垃圾回收。

复制算法

这个算法将可用的内存空间分为大小相等的两块,每次只是用其中的一块,当这一块被用完的时候,就将还存活的对象复制到另一块中,然后把原已使用过的那一块内存空间一次回收掉。这个算法常用于新生代的垃圾回收。

复制算法解决了标记-清除算法的效率问题,以空间换时间,但是当存活对象非常多的时候,复制操作效率将会变低,而且每次只能使用一半的内存空间,利用率不高。

标记-整理算法

这个算法分为三部分:一是标记出所有需要被回收的对象;二是把所有存活的对象都向一端移动;三是把所有存活对象边界以外的内存空间都回收掉。

标记-整理算法解决了复制算法多复制效率低、空间利用率低的问题,同时也解决了内存碎片的问题。

分代收集算法

根据对象生存周期的不同将内存空间划分为不同的块,然后对不同的块使用不同的回收算法。一般把Java堆分为新生代和老年代,新生代中对象的存活周期短,只有少量存活的对象,所以可以使用复制算法,而老年代中对象存活时间长,而且对象比较多,所以可以采用标记-清除和标记-整理算法。

3、垃圾回收器
Oracle 的 HotSpot 虚拟机所使用的垃圾回收器,如下图所示:
在这里插入图片描述

  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、Parallel Old、CMS
  • 整堆回收器:G1

新生代(Young Generation)

绝大数新创建的对象都会存放在新生代,除非是大对象会直接进入老生代。新生代采用的是复制算法,这样可以更高效的回收内存空间。
新生代有细分为:Eden、Form Survivor、To Survivor 三个区域,默认的比例是 8:1:1,可以通过 -XX:SurvivorRatio 来设定

新生代垃圾回收的执行过程:

  1. Eden 区 + From Survivor 区存活着的对象复制到 To Survivor 区;
  2. 清空 Eden 和 From Survivor 分区;
  3. From Survivor 和 To Survivor 分区交换(From 变 To,To 变 From)。

老生代(Tenured Generation)

老生代垃圾回收的频率比新生代低,存放的主要对象是:

  1. 新生代对象经过 N 次 GC 晋升到老年代
    可以通过设置 -XX:MaxTenuringThreshold=5 来设置,默认值是 15 次。
  2. 大对象直接存储到老生代
    所谓的“大对象”指的是需要连续存储空间的对象,比如:数组。
    当大对象在新生代存储不下的时候,就需要分配担保机制,把当前新生代的所有对象复制到老年代中,因为分配担保机制需要涉及大量的复制,会导致性能问题,所有最好的方案是直接把大对象存储到老生代中。
    通过参数 -xx:PretrnureSizeThreshold 来设定大对象的值。
    注意:该参数只有 Serial 和 ParNew 垃圾回收器有效。

Serial

Serial 最早的垃圾回收器,JDK 1.3.1 之前新生代唯一的垃圾回收器,使用的是单线程串行回收方式,在单 CPU 环境下性能较好,因为单线程执行不存在线程切换。
线程类型: 单线程
使用算法: 复制算法
指定收集器: -XX:+UseSerialGC

Serial Old

Serial 收集器的老年代版本,同样也是单线程的。它有一个实用的用途作为CMS收集器的备选预案,后面介绍CMS的时候会详细介绍。
线程类型: 单线程
使用算法: 标记-整理
指定收集器: -XX:+UseSerialGC

ParNew

ParNew 其实就是 Serial 的多线程版本,可以和 Serial 共用很多控制参数,比如:-XX:SurvivorRatio , ParNew 可以和 CMS 配合使用;
线程类型: 多线程
使用算法: 复制
指定收集器: -XX:+UseParNewGC

Parallel Scavenge

Parallel 和 ParNew 收集器类似,也是多线程的,但 Parallel 是吞吐量优先的收集器,GC停顿时间的缩短是以吞吐量为代价的,比如收集 100MB 的内存,需要 10S 的时间,CMS 则会缩短为 7S 收集 50 MB 的内存,这样停顿的时间确实缩少了,但收集的频率变大了,吞吐量就变小了。
线程类型: 多线程
使用算法: 复制
指定收集器: -XX:+UseParallelGC

Parallel Old

Parallel Old 是 Parallel 的老生代版本,同样是吞吐量优先的收集器。
线程类型: 多线程
使用算法: 标记-整理
指定收集器: -XX:+UseParallelOldGC

CMS

CMS(Concurrent Mark Sweep)一种以获得最短停顿时间为目标的收集器,非常适用B/S系统。
使用 Serial Old 整理内存。
在这里插入图片描述

1、初始标记, 标记 GC Roots 直接关联的对象,需要 Stop The World 。
2、并发标记, 从 GC Roots 开始对堆进行可达性分析,找出活对象。
3、重新标记, 重新标记阶段为了修正并发期间由于用户进行运作导致的标记变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,也需要 Stop The World 。
4、并发清除 除垃圾对象。

CMS 缺点:

1、对 CPU 资源要求敏感。
CMS 回收器过分依赖于多线程环境,默认情况下,开启的线程数为(CPU 的数量 + 3)/ 4,当 CPU 数量少于 4 个时,CMS
对用户本身的操作的影响将会很大,因为要分出一半的运算能力去执行回收器线程。
2、CMS无法清除浮动垃圾。
浮动垃圾指的是CMS清除垃圾的时候,还有用户线程产生新的垃圾,这部分未被标记的垃圾叫做“浮动垃圾”,只能在下次 GC 的时候进行清除。
3、CMS 垃圾回收会产生大量空间碎片。
CMS 使用的是标记-清除算法,所有在垃圾回收的时候回产生大量的空间碎片。
注意:CMS 收集器中,当老生代中的内存使用超过一定的比例时,系统将会进行垃圾回收;当剩余内存不能满足程序运行要求时,系统将会出现
Concurrent Mode Failure,临时采用 Serial Old 算法进行清除,此时的性能将会降低。

线程类型: 多线程
使用算法: 标记-清除
指定收集器: -XX:+UseConcMarkSweepGC

G1

  • G1 GC 这是一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项。G1 可以直观的设定停顿时间的目标,相比于CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。

  • G1 GC 仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 region。Region 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更加明显。

  • G1 吞吐量和停顿表现都非常不错,并且仍然在不断地完善,与此同时 CMS 已经在 JDK 9 中被标记为废弃(deprecated),所以 G1 GC 值得深入掌握。
    在这里插入图片描述
    G1 运行过程:

  1. 初始标记
    标记 GC Roots 直接关联的对象,需要 Stop The World 。
  2. 并发标记
    从 GC Roots 开始对堆进行可达性分析,找出活对象。
  3. 重新标记
    重新标记阶段为了修正并发期间由于用户进行运作导致的标记变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,也需要
    Stop The World 。
  4. 筛选回收
    首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC
    停顿时间来制定回收计划。这个阶段可以与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的。

线程类型: 多线程
使用算法: 复制、标记-整理
指定收集器: -XX:+UseG1GC(JDK 7u4 版本后可用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值