JVM内存区域划分&垃圾回收

一、JVM基础知识

1.1 并行和并发
  • 并行

并行是指在同一时刻多个线程同时执行。

  • 并发

并发是指在同一时间间隔内,宏观上多个线程同时执行,而微观上多个线程交替执行。

1.2 进程和线程
  • 进程

进程是操作系统分配资源的基本单位。每个独立运行的程序都是一个进程,程序和程序之间相互隔离,进程和进程之间也隔离。

  • 线程

线程是CPU调度的基本单位。每个进程内部至少会有一个或多个线程,每个程序的功能就是由它进程内的所有线程共同完成的。

1.3 JVM、JRE、JDK的关系

JVM(Java Virtual Machine): JVM 就是我们常说的Java虚拟机,是JRE的一部分。它是整个java实现跨平台的最核心的部分,负责解释执行字节码文件,是可运行java字节码文件的虚拟计算机。它能识别后缀为.class的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成相应的操作。
JRE(Java Runtime Environment): JRE 为java提供了运行环境,用于解释执行Java的字节码文件。
JDK(Java SE Development Kit): java标准开发包,它提供了编译、运行Java程序所需的各种工具和资源,包括Java编译器、Java运行时环境,以及常用的Java类库等。
关系:JDK包含JRE,JRE包含JVM

1.4 JVM 的特性

跨平台:我们写的代码,在不同的操作系统上(Linux、Windows、MacOS 等平台)执行,效果是一样,这就是 JVM 的跨平台性。 为了实现跨平台,不同操作系统有对应的 JDK 的版本。

跨语言:JVM 只识别字节码,所以 JVM 其实跟语言是解耦的,也就是没有直接关联,JVM 运行不是翻译 Java 文件,而是识别 class 文件,这个一般称之为字节码。还有像 Groovy 、Kotlin、Scala 等语言,它们其实也是编译成字节码,所以它们也可以在 JVM 上执行,这就是 JVM 的 跨语言特征。

二、JVM的内存区域划分

在这里插入图片描述

2.1 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器在工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。各线程之间计数器独立存储,互不影响(线程私有的)。

注意:因为 JVM 是虚拟机,内部有完整的指令与执行的一套流程,所以在运行 Java 方法的时候需要使用程序计数器(记录字节码执行的地址或行号),如果线程正在执行的是一个 Java 方法,那么这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个方法不是 JVM 来具体执行,所以程序计数器不需要记录了,这个是因为在操作系统层面也有一个程序计数器, 这个程序计数器会记录本地代码的执行地址,所以在执行本地方法时,JVM 中程序计数器的值为空(Undefined)。此内存区域是唯一 一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

2.2 Java虚拟机栈

Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。Java虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

栈的数据结构:先进后出(FILO)的数据结构
虚拟机栈的作用:在 JVM 运行过程中存储当前线程运行方法所需的数据,指令、返回地址。
栈帧:在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦方法完成相应的调用,则出栈。
栈帧的组成:局部变量表、操作数栈、动态连接、返回地址
在这里插入图片描述

  • 局部变量表

顾名思义就是局部变量的表,用于存放我们的局部变量的(方法中的变量)。首先它是一个 32 位的长度,主要存放我们的 Java 的八大基础数据类型,对象引用和returnAddress 类型。一般 32 位就可以存放下,如果是 64 位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的 Object 对象,我们只需要存放它的一个引用地址即可。

  • 操作数栈

存放 java 方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的 java 数据类型,所 以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的。 操作数栈本质上是 JVM 执行引擎的一个工作区,也就是方法在执行,才会对操作数栈进行操作,如果代码不不执行,操作数栈其实就是空的。

  • 动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

  • 返回地址

正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表来确定) 同时,虚拟机栈这个内存也不是无限大,它有大小限制,默认情况下是 1M。 如果我们不断地往虚拟机栈中入栈帧,但是就是不出栈的话,那么这个虚拟机栈就会抛出StackOverflowError。

2.3 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,你甚至可以认为虚拟机栈和本地方法栈是同一个区域,虚拟机规范无强制规定,各版本虚拟机自由实现 ,HotSpot 直接把本地方法栈和虚拟机栈合二为一 。 其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。但本地方法并不是用 Java 实现的,而是由 C 语言实现的。

2.4 Java堆(Heap)

堆(Heap)是 JVM 上最大的内存区域,它是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,我们申请的几乎所有的对象实例都在这里分配内存。我们常说的垃圾回收,操作的对象就是堆。 堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。堆一般设置成可伸缩的。 随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)。

2.5 方法区

方法区(Method Area)是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,例如运行时常量池(Runtime Constant Pool) 字段、方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法 。方法区是 JVM 对内存的“逻辑划分”,在 JDK1.7 及之前很多开发者都习惯将方法区称为“永久代”,是因为在 HotSpot 虚拟机中,使用了永 久代来实现了 JVM 规范的方法区。在 JDK1.8 及以后使用了元空间来实现方法区。

2.6 运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分,是每一个类或接口的常量池的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到运行期解析后才能获得的方法或字段引用。既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

2.7 直接内存

直接内存(Direct Memory),也叫堆外内存。JVM 在运行时,会从操作系统申请大块的堆内存,进行数据的存储;同时还有虚拟机栈、本地方法栈和程序计数器,这块称之为栈区。操作系统剩余的内存也就是堆外内存。它不是虚拟机运行时数据区的一部分,也不是 java 虚拟机规范中定义的内存区域;如果使用了 NIO,这块区域会被频繁使用,在 java 堆内可以用 directByteBuffer 对象直接引用并操作;
这块内存不受 java 堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OutOfMemoryError 异常。

三、GC

3.1 垃圾回收基础知识
  • 新生代

新生代(Young Gen):主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。新生代分成1个 Eden Space 和2个 Survivor Space(命名为 From 和 To)。当对象在堆创建时,将进入新生代的Eden Space。垃圾回收器进行垃圾回收时,扫描 Eden Space 和 From Suvivor Space,如果对象仍然存活,则复制到 To Suvivor Space,如果 To Suvivor Space 已经满,则复制到 Old Gen。同时,在扫描Suvivor Space 时,如果对象已经经过了几次(默认16次)的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到 Old Gen。扫描完毕后,JVM将 Eden Space 和 From Suvivor Space 清空,然后交换 From 和 To 的角色(即下次垃圾回收时会扫描 Eden Space 和 To Suvivor Space。这么做主要是为了减少内存碎片的产生。

  • 老年代

老年代(Tenured Gen):主要存放JVM认为生命周期比较长的对象(经过几次Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。老年代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。

在这里插入图片描述

  • GC执行过程

HostSport将新生代划分为三代,Eden:From:To=8:1:1。划分的主要目的是因为HotSport采用复制算法来回收新生代,设置这个比例是充分利用内存空间,减少浪费。绝大多数刚刚被创建的对象会存放在Eden区分配(大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次 Minor GC。
GC开始时对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)
GC进行时首先 Eden区中所有存活的对象都会被复制到From Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阈值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阈值的对象会被复制到To Survivor区;然后清空Eden区和From Survivor区;最后From Survivor区和To Survivor区会交换它们的角色。即 From Survivor变为To Survivor ;To Survivor 变为 From Survivor。
如果你仔细观察这些步骤就会发现,其中一个幸存者空间必须保持是空的。如果两个幸存者空间都有数据,或者两个空间都是空的,那一定标志着你的系统出现了某种错误。

3.2 垃圾回收机制及算法
3.2.1 标记算法
  • 引用计数算法

引用计数算法是在对象中添加一个引用计数器,然后用一块额外的内存区域来存储每个对象被引用的次数,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻当计数器值为零时,则说明该对象基本不太可能会被再次使用到,那么这个对象就成为可被回收的对象了。通过这种方式我们能快速直观的定位到这些可回收的对象,从而进行清理,但是它很难解决对象之间相互循环引用的问题。
优点:简单,判定效率高
缺点:难以解决对象之间相互循环引用的问题

  • 可达性分析算法

可达性分析算法是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
需要注意的是:一个对象被判定为不可达的对象不一定就会立即成为可回收对象。被判定为不可达的对象要成为可回收对象至少需要经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

在这里插入图片描述

由上图可以判定,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。
在 Java 技术体系里面,哪些可作为GC Roots的对象,在扩展里面有补充,这里就不做说明了。

3.2.2 垃圾回收算法
  • 标记-清除算法

“标记-清除”(Mark-Sweep)算法是最早出现的也是最基础的垃圾回收算法。该算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象所占用的空间,也可以反过来,标记存活的对象,统一回收所有未被标记的对象,具体如下图所示。

在这里插入图片描述

优点:实现起来比较简单
缺点:1. 执行效率不稳定。如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都会随对象数量增长而降低。2. 容易产生内存碎片。标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的连续内存空间而不得不提前触发新的一次垃圾收集动作。

  • 标记-复制算法

为了解决标记-清除算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题,具体如下图所示。

在这里插入图片描述

优点:实现简单,运行效率高且不易产生内存碎片
缺点:对内存空间的使用做出了高昂的代价,能够使用的内存缩减到原来的一半,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

  • 标记-整理算法

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记过程仍然与“标记-清除”算法一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向内存空间一端移动,然后直接清理掉端边界以外的内存,具体如下图所示。

在这里插入图片描述

[了解] 是否需要移动对象
如果移动存活对象,对于老年代而言,每次回收都有大量对象存活,移动存活对象并更新所有引用到这些对象的地方,这将会是一种极为负重的操作,而且这些对象的移动操作必须全程暂停用户应用才能进行,这就更让使用者不得不小心翼翼地权衡其弊端了。如果不移动对象,就像标记-清除算法那样,堆中产生的空间碎片化问题就只能依赖更加复杂的内存分配器和内存访问器来解决。
基于以上两点,是否移动对象都存在弊端,移动则内存回收时会更复杂,不移动则内存分配时会更复杂。1.从垃圾收集的停顿时间来看:不移动对象停顿时间会更短,甚至可以不需要停顿。2.从整个程序的吞吐量来看:移动对象会更划算。因为吞吐量的实质是赋值器与收集器的效率总和。即使不移动对象会使得收集器的效率提升一些,但因内存分配和访问相比垃圾收集频率要高得多,这部分的耗时增加,总吞吐量仍然是下降的。
关注不同选择不同。HotSpot 虚拟机里面关注吞吐量的Parallel Scavenge 收集器是基于标记-整理算法的,而关注延迟的 CMS 收集器则是基于标记-清除算法的。
综合方案:是让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。基于标记-清除算法的 CMS 收集器面临空间碎片过多时采用的就是这种处理方法。

  • 分代收集算法

前面我们说过在Java堆中会划分出不同的区域来【新生代,老年代】,垃圾收集器可以每次只回收其中某一个或者某些部分的区域,于是有了回收类型的划分【“Minor GC”,“Major GC”,“Full GC”】,然后才能够针对不同的区域设计出与里面存储对象存亡特征相匹配的垃圾收集算法【“标记-清除算法”,“标记-复制算法”,“标记-整理算法”】。

部分收集 (Partial GC):垃圾收集的目标不是整个Java 堆,而是堆空间上的部分区域
新生代收集 (Minor GC/Young GC): 垃圾收集的目标只是新生代区域。
老年代收集 (Major GC/Old GC):垃圾收集的目标只是老年代区域。目前只有CMS收集器会有单独收集老年代的行为。
混合收集 (Mixed GC):垃圾收集的目标是整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。

整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

3.3 GC垃圾回收器

并发收集和并行收集的区别

并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,同一时间垃圾收集器线程与用户线程都在运行。
并行(Parallel):并行描述的是多个垃圾收集器线程之间的关系,同一时间有多个这样的线程在协同工作,通常默认此时用户线程是处于等待状态。

3.3.1 Serial 收集器(标记-复制)

Serial 收集器是单线程工作的收集器,但它的单线程的意义并不仅仅说明它只会使用一个处理器或一条垃圾收集线程去完成垃圾收集工作,更重要的是,它在进行垃圾收集的时候,必须暂停其他所有工作线程,直到他收集结束,具体收集过程如下图所示。

在这里插入图片描述

优点:
1.简单高效
2.对于单核处理器或处理器核心数较少的环境来说,Serial 收集器由于没有线程交互的开销,垃圾收集效率可以获得最高的单线程收集效率。

应用场景:
一般运行在 client 模式下的虚拟机。

3.3.2 ParNew 收集器(标记-复制)

ParNew 收集器实质上是 Serial 收集器的多线程并行版本,支持多线程并行收集,它与 Serial 收集器相比并没有太多创新之处,具体收集过程如下图所示。

在这里插入图片描述

应用场景:
在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

3.3.3 Parallel Scavenge 收集器(标记-复制)

Parallel Scavenge 收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集多线程的收集器,跟 ParNew 非常相似,特别之处在于:CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量,主要适合在后台运算而不需要太多交互的分析任务。

应用场景:
以高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互。

3.3.4 Serial Old 回收器(标记-整理)

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。

在这里插入图片描述

应用场景:
主要用于 Client 模式,如果在服务端模式下,它有两种用途:1. 在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配) 2. 作为CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。

3.3.5 Parallel Old 回收器(标记-整理)

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。

在这里插入图片描述

应用场景:
JDK1.6及之后,用来代替老年代的Serial Old收集器;特别是在Server模式,多CPU的情况下;在注重吞吐量以及CPU资源敏感的场景,可以优先考虑 Parallel Scavenge加 Parallel Old 收集器这个组合。

3.3.6 CMS 回收器(标记-清除)

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。从名字上可以看出CMS 收集器是基于标记-清除算法实现的。

具体过程如下:
1)初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,但需要停止用户线程即 “Stop The World”。
2)并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行,但并不能保证可以标记出所有的存活对象。
3)重复标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长,但远比并发标记阶段的时间短,可以采用多线程并行执行来提升效率。
4)并发清除:清理删除掉标记阶段判断的已经死亡的
对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

在这里插入图片描述

优点:并发收集、低停顿
缺点:
1)无法处理“浮动垃圾”,在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉,这一部分垃圾就称为“浮动垃圾”。
2)对cpu 资源敏感,并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。
3)又由于CMS 是基于“标记-清除”算法,所以收集结束时会产生大量空间碎片。

3.3.7 Garbage First 回收器

G1是一款主要面向服务端应用的垃圾收集器,是垃圾收集器技术发展历史上的里程碑式的成果。在G1收集器出现之前的其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

基本思想:
G1回收器仍遵循分代收集理论设计,但堆内存与其它回收器不同的是,G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

具体过程如下:
1)初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
2)并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
3)最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
4)筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条收集器线程并行完成的。

在这里插入图片描述

优点:
1)并行与并发:充分利用多CPU并行来缩短"Stop The World"停顿时间;也可以并发让垃圾收集与用户程序同时进行。
2)分代收集:能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;能够采用不同方式处理不同时期的对象;虽然保留分代收集的设计,但Java堆的内存布局有很大差别;它将整个堆划分为多个大小相等的独立区域(Region);新生代和老年代不再是物理隔离,它们都是Region的一部分(不需要连续)。
3)整合空间:从整体来看,它是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间都不会产生内存碎片,有利于程序长时间运行,不会提前触发一次GC。
4)可预测停顿时间:低停顿的同时实现高吞吐量,还能建立可预测的停顿时间模型。

四、扩展

4.1 内存泄露和内存溢出
  • 内存泄露

内存泄露是指,当程序内部的对象在使用了空间后本应该将内存资源释放掉,但是没有释放,导致这片内存空间一直无法被再次分配出去。内存泄露在不断增加的情况下最终会导致内存溢出。
总结:内存泄露是指程序在申请内存后,无法释放已申请的内存空间。

  • 内存溢出

内存溢出是指,当程序申请内存的时候,由于剩余的内存空间不足,经过一系列操作后(如:回收垃圾),仍然无法满足分配的需要,就会出现内存溢出。
总结:内存溢出是指程序在申请内存时,没有足够的内存空间供其使用。

4.2 对象引用级别
  • 强引用

强引用是最常见的普通对象引用,无论任何情况下,只要强引用关系还存在,则垃圾收集器就永远不会回收掉被引用的对象。

  • 软引用

软引用是一种相对强引用弱化一些的引用,用来描述一些还有用,但非必须的对象。可以让对象豁免一些垃圾收集,只有当JVM认为内存不足时,才会去试图回收软引用指向的对象,JVM会确保在抛出OutOfMemoryError之前,清理软引用指向的对象,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

  • 弱引用

弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。弱引用并不能使对象豁免垃圾收集,在垃圾收集器线程扫描它所管辖的内存区域的过程中,一旦发现只具有弱引用的对象时,不管当前内存空间是否充足,都会回收掉只被弱引用关联的对象。
弱引用和软引用大致相同,区别在于,只具有弱引用的对象拥有更短暂的生命周期。

  • 虚引用

虚引用也称为“幽灵引用”或者“幻象引用”,它是最弱的一种引用关系。与其他几种引用都不同,虚引用并不会决定对象的生命周期,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用存在的唯一目的,是为了能在这个对象被收集器回收时收到一个系统通知(用来跟踪对象被垃圾回收器回收的活动)。

4.3 哪些可作为GC Roots的对象

1、虚拟机栈中引用的对象,例如:方法堆栈中使用到的参数、局部变量表、临时变量等。
2、方法区中类静态属性引用的对象,例如:Java类的引用类型静态变量。
3、方法区中常量引用的对象,例如:字符串常量池里的引用。
4、本地方法栈中 JNI(即一般说的 Native 方法)引用对象。
5、Java虚拟机内部的引用,如基本数据类型对应Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
6、所有被同步锁(synchronized关键字)持有的对象。

本文是学习了《深入理解Java虚拟机》之后的总结,主要内容都来自于书中,也有作者的一些理解。一是为了梳理知识点,归纳总结,二是为了分享交流,如有理解错误之处还望各位高人指出。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很抱歉,作为文本形式的AI助手,无法直接提供图形展示。但是,我可以通过文字描述来帮助您理解JVM内存划分JVM内存划分通常可以用下面这张图来表示: ``` +------------------------+ | JVM Memory | +------------------------+ | Heap | +------------------------+ | Young Generation | | +--------------+ | | | Eden | | | +--------------+ | | | Survivor 1 | | | +--------------+ | | | Survivor 2 | | | +--------------+ | +------------------------+ | Old Generation | +------------------------+ | Method Area | +------------------------+ | Native Method Stack | +------------------------+ | Java Virtual Stack | +------------------------+ | Program Counter | +------------------------+ ``` 上面的图示了JVM内存划分,以下是各部分的解释: - JVM内存:整个JVM占用的内存空间。 - 堆(Heap):存放动态分配的对象、数组和类实例的区域。堆被划分为年轻代和老年代。 - 年轻代(Young Generation):包括Eden空间和Survivor空间。新创建的对象首先被分配到年轻代。 - Eden空间:刚刚创建的对象被分配到Eden空间。 - Survivor空间:当对象在Eden空间经过一次垃圾回收后仍然存活,会被移动到Survivor空间。 - 老年代(Old Generation):存放长时间存活的对象。 - 方法区(Method Area):存储类的信息、常量、静态变量和编译器编译后的代码等。 - 本地方法栈(Native Method Stack):为本地方法服务。 - Java虚拟机栈(Java Virtual Stack):每个线程在运行时都有一个虚拟机栈,用于存储局部变量、方法参数、返回值等信息。 - 程序计数器(Program Counter):指示当前线程执行的字节码指令的地址指示器。 这些部分共同构成了JVM内存划分。请注意,具体的内存结构可能因不同的JVM实现和版本而有所不同,上述图示仅为一般情况下的示意。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值