Java教程

JVM_自动内存管理

本文主要是介绍JVM_自动内存管理,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

本篇为《深入理解Java虚拟机 第3版》读书笔记

文中,《Java虚拟机规范》简称《规范》

内存区域与内存溢出异常

运行时数据区域

JVM_自动内存管理_1

程序计数器

字节码解释器工作时,通过改变这个计数器的值,来选取下一条需要执行的字节码指令

程序计数器特点

  • 每条线程有一个独立的程序计数器「即线程私有」
  • 执行Java方法,计数器记录正在执行的虚拟机字节码指令的地址;执行Native方法,计数器的值为空
  • 内存区域中,唯一一个《规范》中没有规定任何OutOfMemoryError「OOM」情况的区域

虚拟机栈

Java方法被执行的时候,JVM会创建一个栈帧,一个Java方法的被调用到执行完毕,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。通常,Java讲的堆栈中的栈就是指「虚拟机栈」,或者指「虚拟机栈」中的局部变量表

栈帧存储:局部变量表、操作数栈、动态连接、方法出口等信息

局部变量表存储:

  • 编译期可知的八种基本数据类型,即boolean、short、int、float、long、double
  • 对象引用reference
  • 返回地址returnAddress

方法参数(包括实例方法中的隐藏参数“this”)、显式异常处理程序的参数(Exception Handler Parameter,就是try-catch语句中catch块中所定义的异常)、方法体中定义的局部变量都需要依赖局部变量表来存放

局部变量表的存储空间以局部变量槽来表示,对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64位的数据类型则需要两个变量槽来存放

关于「this」:在任何实例方法里面,都可以通过this关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现非常简单,仅仅是通过在Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个变量槽位来存放对象实例的引用,注意这个处理只对实例方法有效.

实例的构造方法中,也可以使用this,神奇!

虚拟机栈特点

  • 线程私有,生命周期与线程相同
  • JVM实现虚拟机栈时,可以实现为可以动态扩展,或者实现为不支持动态扩展
  • 《规范》规定了两类异常
    • StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的最大深度,抛出该异常
    • OutOfMemoryError:如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,抛出该异常

HotSpot虚拟机栈的容量是不可以动态扩展的

关于上面的两类异常,可以分为两种情况去思考它

一、栈容量允许动态扩展的情况,在这种情况下,如果栈容量无法容纳新的栈帧,这时候栈会申请更多的内存,如果申请不到,就会抛出OOM异常,这种情况下不会抛出StackOverFlow异常

二、栈容量不支持动态扩展的情况,在这种情况下,如果栈容量无法容纳新的栈帧,因为无法进行扩展,所以这时候会抛出StackOverFlow异常,而不会抛出OOM异常

补充:在创建新的线程时没有足够的内存去创建对应的Java虚拟机栈,也会抛出OOM异常。

本地方法栈

与虚拟机栈的作用类似,区别是虚拟机栈为执行Java方法服务,本地方法栈为执行Native方法服务

本地方法栈特点

  • 线程私有
  • JVM实现本地方法栈时,可以实现为可以动态扩展,或者实现为不支持动态扩展
  • 《规范》规定了两类异常
    • StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的最大深度,抛出该异常
    • OutOfMemoryError:如果栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,抛出该异常

HotSpot虚拟机把虚拟机栈和本地方法栈合二为一,在HotSpot中,不区分虚拟机栈和本地方法栈,其栈容量都实现为不可动态扩展的

关于上面的两类异常,可以分为两种情况去思考它

一、栈容量允许动态扩展的情况,在这种情况下,如果栈容量无法容纳新的栈帧,这时候栈会申请更多的内存,如果申请不到,就会抛出OOM异常,这种情况下不会抛出StackOverFlow异常

二、栈容量不支持动态扩展的情况,在这种情况下,如果栈容量无法容纳新的栈帧,因为无法进行扩展,所以这时候会抛出StackOverFlow异常,而不会抛出OOM异常

补充:在创建新的线程时没有足够的内存去创建对应的本地方法栈,也会抛出OOM异常。

Java堆

JVM所管理的内存中最大的一块,虚拟机启动时创建,用于存放对象实例。Java堆可以划分出多个线程私有的分配缓冲区,以提升对象分配时的效率

Java堆特点

  • 所有线程共享
  • 垃圾收集器管理的内存区域,故也称作GC堆
  • 不需要连续的物理内存,但在逻辑上被视为连续的
    • 一些大对象,比如数组对象,多数虚拟机出于实现简单、存储高效的考虑,会要求连续的物理内存空间
  • Java堆可以实现为固定大小、也可以是可扩展的,主流JVM按照可扩展实现
  • 异常
    • 需要内存完成实例分配时,如果内存不够且堆无法再扩展,那么JVM抛出OutOfMemoryError

Idea的虚拟机启动参数中,可以将对容量设置为不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展) --书本P55

方法区

别名Non-Heap,方法区存储JVM加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

类型信息:对象的类型、父类、实现的接口、方法描述等

HotSpot方法区发展历史

  • JDK7及之前的JDK版本,方法区采用永久代实现,方便内存管理
  • JDK6,HotSpot开发团队有放弃永久代、逐步采用本地内存来实现方法区的计划
  • JDK7,把原本放在永久代的字符串常量池、静态变量等移出
  • JDK8,完全废弃永久代的概念,采用元空间代替

方法区特点

  • 所有线程共享
  • 可以选择不实现垃圾收集;该区域内存回收的目标主要是针对常量池的回收和类型的卸载;该区域的回收效果一般比较难令人满意
  • 不需要连续的物理内存
  • 方法区可以实现为固定大小、也可以是可扩展的
  • 《规范》规定,方法区无法满足新的内存分配需求时,抛出OOM

运行时常量池

运行时常量池是方法区的一部分,Class文件的常量池表在类加载后存放到方法区的运行时常量池中

PS:Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用

  • 字面量:较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
  • 符号引用:属于编译原理方面的概念,主要包括下面几类常量:
    • 被模块导出或者开放的包(Package)
    • 类和接口的全限定名(Fully Qualified Name)
    • 字段的名称和描述符(Descriptor)
    • 方法的名称和描述符
    • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
    • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

所在区域

  • 在JDK6或更早之前的HotSpot虚拟机中,运行时常量池分配在永久代中
  • 自JDK7起,原本存放在永久代的字符串常量池被移至Java堆之中

Class文件的常量池表与运行时常量池的区别与联系

  • JVM对于Class文件每一部分包括常量池表的格式有严格规定,但对于运行时常量池,《规范》没有做任何细节的要求
  • Class文件的常量池表会存放于运行时常量池,还有常量池表的符号引用翻译出来的直接引用也会存储在运行时常量池中
  • 运行时常量池相比于Class文件常量池,具备动态性,除了编译期Class文件中常量池的内容可以进入运行时常量池,运行期间也可以将新的常量放入运行时常量池当中,比如通过Stringintern方法

运行时常量池特点

  • 受到方法区内存的限制,当运行时常量池无法再申请到内存时会抛出OOM

永久代和元空间

永久代:PermGen「Permanent Generation」

元空间:Metaspace

HotSpot虚拟机,在JDK8以前,方法区是通过永久代来实现的,从JDK8开始,废弃了永久代的概念,方法区采用元空间实现。方法区是JVM的规范,永久代和元空间是JVM规范的一种实现,并且只有HotSpot虚拟机,才有永久代的概念。在方法区中,存储有类的相关信息,而永久代有着大小的限制,通过参数-XX:MaxPermSize设置,即使不设置也有默认大小,那么动态生成大量的类的情况下,容易出现永久代内存溢出的情况。

JDK7中,字符串常量池从永久代转移到了Java Heap中,类的静态变量也从永久代转移到了Java Heap中,符号引用则从永久代转移到了Native Heap

在《规范》所定义的概念模型中,所有Class相关的信息都应该存放在方法区之中,但方法区应该如何实现,《规范》并未做出规定,这就成了一件允许不同虚拟机自己灵活把握的事情。JDK7及其以后版本的HotSpot虚拟机选择把「静态变量」与「类型在Java语言一端的映射的Class对象」存放在一起,存储于Java堆之中,也就是类的静态变量从永久代转移到了Java Heap中 --参考书P156

JDK8中,完全废弃了永久代的概念,采用元空间实现方法区,元空间并不在虚拟机中,它使用的是本地内存,默认情况下,元空间的大小仅受本地内存大小的限制

可以采用参数-XX:MaxMetaspaceSize来指定元空间的最大空间,默认是没有限制的

将字符串常量池从PermGen剥离到Java Heap中,将类型信息从PermGen转移到MetaSpace的好处

  • 应用程序的String::intern()的调用是不可预测和不可控的,类及方法的信息比较难确定其大小,所以永久代的大小比较难确定,太小容易出现永久代溢出,如果设置的太大,那么其它区域的可用空间就减小了

  • 字符串常量池若在PermGen中实现,容易有内存溢出的问题,而在Java Heap中不容易出现这个问题

  • 将字符串常量池从PermGen分离出来,与类元数据分开,提升类元数据的独立性

  • 将元数据从PermGen剥离出来到Metaspace,可以提升对元数据的管理同时提升GC效率

在PermGen中元数据可能会随着每一次Full GC发生而进行移动。HotSpot虚拟机的每种类型的垃圾回收器都需要特殊处理PermGen中的元数据,分离出来以后可以简化Full GC

  • 便于将JRockit虚拟机的优秀功能移植到HotSpot虚拟机上面

PermGen是HotSpot的实现特有的,JRockit并没有PermGen一说

类变量在方法区中,但要注意方法区本身是一个逻辑上的区域,在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了

书P272

参考

  1. Java8内存模型—永久代和元空间
  2. 聊聊jvm的PermGen与Metaspace

直接内存

该区域不属于运行时数据区的一部分,由于频繁使用,也放到这里讲解

直接内存特点

  • 直接内存的分配不会受到Java堆大小的限制,但会受到本机总内存和处理器寻址空间的限制
  • 可能会抛出OOM

HotSpot虚拟机对象探秘

这里探讨HotSpot虚拟机的Java堆中,对象分配、布局和访问的全过程

对象的创建

在Java中,使用关键字new,即可创建一个对象,在虚拟机中,该对象的创建需要经历下面的过程

  1. 虚拟机遇到一条new指令的字节码时,先检查该指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、连接和初始化过。如果没有,那么就执行相应的类加载过程
  2. 为新生对象分配内存,对象所需的内存大小在类加载完成后便可完全确定,分配内存有两种方法
    • 指针碰撞:Java堆中的内存是绝对规整的情况下
    • 空闲列表:Java堆中的内存并不是规整的

选择哪种分配方式由Java堆是否规整决定,Java堆是否规整由所采用的垃圾收集器是否带有空间压缩整理的能力决定

在为对象分配内存的时候,要考虑并发情况,使得内存可以正常分配,有两种可选方案

  • 第一种方案:对分配内存空间的动作进行同步处理
  • 第二种方案:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就先在线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定
  1. 为分配到的内存空间初始化零值(不包括对象头),保证在Java代码中,可以不赋初始值就使用对象的实例字段(访问到这些字段的数据类型所对应的零值)
  2. 设置对象头的信息,根据虚拟机当前运行状态的不同,比如是否启用偏向锁等,对象头会有不同的设置方式
  3. 执行Class文件中的()方法,即通过构造函数对对象进行初始化。到了这里,一个对象就算完全被构造出来了

对象的内存布局

对象在堆内存的存储布局可以划分为三个部分:对象头、实例数据、对齐填充

对象头

对象头包含两类信息

  • 第一类:用于存储对象自身的运行时数据,包括哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等

官方称这部分数据为“Mark Word”,Mark Word采取动态定义的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间

  • 第二类:类型指针,即对象指向它的类型元数据的指针,Java虚拟机可以通过这个指针,确定该对象是哪个类的实例

并不是所有的虚拟机都实现为在对象数据上保留类型指针,也就是说,查找对象的元数据信息并不一定要经过对象本身

  • 另外,如果对象是一个Java数组,对象头还需要记录数组长度,以便确定该数组对象的大小

普通Java对象的元数据信息可以确定Java对象的大小

实例数据

存储我们在程序代码里面所定义的各种类型的字段内容

对齐填充

HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是任何对象的大小都必须是8字节的整数倍,对齐填充用于填充数据,起占位符的作用

对象的访问定位

Java程序通过栈上的references数据来操作堆上的具体对象,关于引用如何访问到对象,有两种方式

  • 句柄
    • 优点:对象被移动时(如垃圾收集时移动对象),只需改变句柄中的实例数据指针,references本身无需修改
  • 直接指针
    • 优点:访问对象的实例数据时,节省了一次指针定位的时间开销

HotSpot虚拟机采用「直接指针」的方式,进行对象访问

垃圾回收与内存分配策略

程序计数器、虚拟机栈和本地方法栈随线程而生,随线程而灭。这三个区域的内存分配和回收具备确定性,首先是栈帧,每一个栈帧分配多少内存基本是在类结构确定下来时就已知的,栈中的栈帧随着方法的进入和退出,执行着入栈和出栈的操作;当方法结束或者线程结束时,内存就自然被回收了。

Java堆和方法区则有着显著的不确定性,这里讨论的内存分配与回收主要指这一部分内存。

判断对象存活

垃圾回收之前,要先判断哪些对象是存活的,哪些对象是死去的

引用计数器算法

思路:每个对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;计数器的值为零就表示这个对象不可能再被使用了

优点:原理简单、判定高效

缺点:该算法需要考虑很多例外情况,需配合大量额外处理才能保证正确工作,例如单纯的引用计数无法解决对象循环引用的问题

主流的Java虚拟机没有选用引用计数算法来管理内存

可达性分析算法

思路:通过一系列GC Roots的根对象为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

固定可以作为GC Roots的集合

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

当前主流的商用程序语言的内存管理子系统,都是通过可达性分析算法来判定对象是否存活的

方法区的回收

书P74

垃圾收集算法

垃圾收集算法分为两大类

  • 引用式计数器垃圾收集
  • 追踪式垃圾收集

主流Java虚拟机未涉及「引用式计数器垃圾收集」算法,这里讨论「追踪式垃圾收集」算法,包含"标记"阶段是所有「追踪式垃圾收集」算法的共同特征

分代收集理论

后面提到的「标记-复制算法」「标记-清除算法」「标记-整理算法」,都始于分代收集理论

分代收集理论建立在三条假说之上

  1. 弱分代假说:绝大多数对象都是朝生夕灭的。
  2. 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
  3. 跨代引用假说:跨代引用相对于同代引用来说仅占极少数

根据前两条假说,垃圾收集器有下面的设计原则:收集器将堆划分为不同的区域,回收对象依据其年龄分配到不同的区域中存储。如果一个区域中大多数对象都是朝生夕灭,每次回收时只关注如何保留少量存活的对象,而不是去标记那些大量要被回收的对象,就能以较低代价回收到大量的空间;如果一个区域都是难以消亡的对象,虚拟机可以使用较低的频率回收这个区域。

分代收集理论具体放到现在的商用Java虚拟机里,设计者一般至少会把Java堆划分为新生代和老年代两个区域 。顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。

第三条假说与跨代引用有关,书P76

标记-清除算法

方法:算法分为「标记」和「清除」两个阶段,首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程,这在前一节讲述垃圾对象标记判定算法时其实已经介绍过了。

缺点:

  • 执行效率不稳定,如果Java堆中包含大量需要被回收的对象,这时必须进行大量标记和清除的动作,也就是说,标记和清除两个过程的执行效率都随对象数量增长而降低

  • 内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

这是最基础的收集算法,后续的收集算法大多都是以「标记-清除」算法为基础,对其缺点进行改进而得到的

算法示意图:

JVM_自动内存管理_2

标记-复制算法

「标记-复制」算法主要用于新生代的垃圾回收,老年代的回收一般不直接选用该算法

方法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着
的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象

优点

  • 每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况
  • 实现简单,运行高效

缺点

  • 可用内存缩小为了原来的一半,空间浪费过多

算法示意图

JVM_自动内存管理_3.png

算法改进:分代收集理论中,提到大多对象是朝生夕灭,其实新生代中有98%对象熬不过第一轮收集,因此不需要按照1∶1的比例来划分新生代的内存空间。一种策略是将新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。

在HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,这样的话,每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的,由于没有保证每次回收都只有不多于10%的对象存活,因此有一个「逃生门」的安全设计,需要依赖其它内存区域进行内存的分配担保。

当执行一次Minor GC(新生代收集)时,Eden空间的存活对象会被复制到To Survivor空间,并且之前经过一次Minor GC并在From Survivor空间存活的仍年轻的对象也会复制到To Survivor空间。有两种情况Eden空间和From Survivor空间存活的对象不会复制到To Survivor空间,而是晋升到老年代。一种是存活的对象的分代年龄超过-XX:MaxTenuringThreshold(用于控制对象经历多少次Minor GC才晋升到老年代)所指定的阈值。另一种是To Survivor空间容量达到阈值。当所有存活的对象被复制到To Survivor空间,或者晋升到老年代,也就意味着Eden空间和From Survivor空间剩下的都是可回收对象,这个时候GC执行Minor GC,Eden空间和From Survivor空间都会被清空,新生代中存活的对象都存放在To Survivor空间。接下来将From Survivor空间和To Survivor空间互换位置,也就是此前的From Survivor空间成为了现在的To Survivor空间,每次Survivor空间互换都要保证To Survivor空间是空的,这就是"标记-复制算法"在新生代中的应用。

标记-整理算法

老年代一般采取「标记-整理算法」.

「标记-复制算法」不适合老年代的原因

  • 老年代存活的对象较多,而标记-复制算法在存活对象较多的情况下,需要进行较多的复制操作,效率低
  • 如果只使用内存空间的50%,会造成大量的空间浪费;如果想使用更多的内存空间,就需要前面提到内存的分配担保,而老年代的存活对象比较多,可能要频繁的进行内存的分配担保,不好

「标记-整理算法」的步骤

  1. 标记:与「标记-清除算法」的标记过程一样
  2. 整理:让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存

「标记-整理算法」是移动式的,「标记-清除算法」是非移动式的,即它们区别在于是否是移动式的,移动式的利与弊如下

  • 利:空间是连续的,降低了内存访问和分配时的难度
  • 弊:老年代大多对象是存活的,这意味着要移动大量的对象,并且更新所有引用这些对象的地方,这是一个比较重的操作

「非移动式」的「利与弊」与上面的相反。由于内存访问是程序最频繁的操作,假如在这个环节上增加额外的负担,势必会直接影响应用程序的吞吐量,从这一个角度来看,移动式更胜一筹。

HotSpot虚拟机里面关注吞吐量的Parallel Old收集器是基于「标记-整理算法」的,而关注延迟的CMS收集器则是基于「标记-清除算法」。实际上,CMS收集器中,是结合了「标记-整理算法」和「标记-清除算法」的.

一种“和稀泥式”解决方案可以不在内存分配和访问上增加太大额外负担,做法是让虚拟机平时多数时间都采用「标记-清除算法」,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用「标记-整理算法」收集一次,以获得规整的内存空间。前面提到的基于「标记-清除算法」的CMS收集器面临空间碎片过多时采用的就是这种处理办法

算法示意图

JVM_自动内存管理_4

HotSpot虚拟机内存回收流程

这一部分是看书后自己总结的,不一定准确~~

HotSpot虚拟机内存回收主要有下面的几个步骤

  1. 枚举根节点:这里主要做的是找出GC Roots,在找GC Roots的过程中,是需要Stop The World(STW),暂停用户线程的,为了减少这里的耗时,HotSpot中,使用了一组称为OopMap的数据结构,采用OopMap数据结构,HotSpot可以快速准确地完成GC Roots枚举,OopMap的信息在线程运行到安全点的时候记录,线程运行到安全点时,会中断挂起,这时虚拟机会开始垃圾回收,当GC Roots枚举完成时,线程可以恢复运行。对于虚拟机的垃圾回收来说,这只是第一步(与安全点对应的,还有一个概念叫做安全区域)
  2. 接着就需要从GC Roots,往下遍历对象图,这时候用户线程和对象图的遍历是可以并发执行的,为了解决并发执行中出现的一些问题,有「原始快照」和「增量更新」两种方法。在对象图遍历的时候,虚拟机当然也会考虑到强引用、软引用、弱引用、虚引用这几种不同的引用
  3. 在对象图遍历完成后,这时候就已经找到哪些对象是存活的,哪些对象是死亡的,接着就可以执行前面提到的垃圾收集算法--「标记-清除算法」「标记-复制算法」「标记-整理算法」进行垃圾的回收了

这三种垃圾收集算法都是「追踪式垃圾收集」算法,都包含"标记"阶段,而"标记"阶段是在第2步完成的

垃圾收集器的职责

垃圾收集器的职责不仅仅是垃圾收集,描述它的职责的一个更贴切的名词是「自动内存管理子系统」,自动内存管理最根本的目标是自动化地解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存。

一个垃圾收集器除了垃圾收集的本职工作外,还要负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责。其中至少堆的管理和对象的分配这部分功能是Java虚拟机能够正常运作的必要支持。

参考书P121和P129

参考

《深入理解Java虚拟机 第3版》

这篇关于JVM_自动内存管理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!