JVM系列--内存回收

简介: JVM系列--内存回收

在前边的文章中我们介绍了Java的虚拟机是如何进行内存管理的,以及一个对象的内存分配过程。今天这篇文章将会介绍下关于jvm里面的垃圾回收过程相关细节点。


如何判断一个对象是否存活


简单来讲,判断一个对象是否存活的算法主要有以下两类:


  • 引用计数算法
  • 根可达算法


引用计数算法

根据一个引用计数器来计算对象被引用的次数,如果引用加一,则计数器+1,反之计数器-1。


这个算法虽然实现思路比较简单,但是默认的jvm并不使用这种算法,因为无法解决相互引用的问题。


网络异常,图片无法展示
|


根可达算法


通过一系列名为“GC Roots”的对象作为起始点,从“GC Roots”对象开始向下搜索,如果一个对象到“GC Roots”没有任何引用链相连,说明此对象可以被回收。


这里我通过这张图来带大家理解下什么是根可达。


网络异常,图片无法展示
|


这张图中,可以看到有一系列的对象存活于根集合当中,他们会和被使用的对象进行引用关联。从GC ROOT作为顶部顺藤摸瓜下去,如果能够找到相关引用的对象则不进行回收,如果没有引用,则该对象需要被回收掉处理。如对象e就是需要被回收处理。


怎么理解什么是ROOT呢


关于这一点的理解,我用一个简单的代码案例来介绍下:


public class Test {
    public static void main(String[] args) {
        List<String> test = new ArrayList<>();
    }
}
复制代码


代码里的test集合就可以理解为是一个ROOT对象。


被选择为GC Root对象可以是一些存储在公共内存区域的对象,例如:


  • 方法区中的常量
  • 虚拟机栈中引用的常量
  • 局部变量
  • 本地方法中的JNI
  • 同步锁中绑定的对象


在Hotspot虚拟机中关于GC ROOT部分的包含了对于jvm中其他对象引用部分的数据存储和管理。内部的实现中使用了Oop Map(Ordinary Object Pointer Map)集合来进行管控,在类加载的时候,就直接将一些类信息的引用给记录到OopMap当中存储,后续jvm直接可以从这里获取所需要的数据内容。


网络异常,图片无法展示
|


垃圾收集


如今比较多的垃圾收集器都是基于分代假说来进行设计的,这种分代假说有什么不足点?


大多数情况下,使用简单的分代假说存在跨代引用的不足点。假设一个对象存在于年轻代,但是它在老年代也有被引用,那么就可能会有该对象存在跨代引用的情况发生。


算法演变思路


每次做年轻代对象回收的时候,都需要遍历老年代,判断是否有引用关系。这种方式虽然实现简单,但是实际运行起来比较损耗性能。


后期进行了设计改良,单独设置了一个全局内存区域,专门记录老年代的哪些模块存在跨代引用,当垃圾回收的时候,合适地进行回收即可。


三大垃圾收集思路


从早期的Hotspot虚拟机一直到现在,主要的垃圾收集思路一直是围绕着以下几点进行迭代。


Mark-Sweep (标记清除)


这个算法比较好理解,就是对每个对象都做一个标记,记录该对象是否可以进行回收。


网络异常,图片无法展示
|


不足点:


每个对象都需要标记是否可回收,当对象数目一旦变多了之后,性能损耗提升。

回收对象之后,内存碎片化严重。


优点:


简单容易理解,实现难度低。


Semispace Copying (标记复制)


标记复制算法主要是将存活的对象进行标记,然后将空闲内存移向一侧,另一侧用于存放存活对象。这个过程会涉及到较多的对象内存地址移动操作,所以性能损耗也不小。


网络异常,图片无法展示
|


不足点:


1.需要有两倍的内存空间用于进行垃圾回收,占用内存。假设我的jvm有2g空间,那么其中至多只有1g空间用于存储对象


当对象数目非常多的时候,移动频率会比较高,比较耗费性能。例如说老年代中的对象,大部分都是体积比较庞大,并且移动起来比较困难的对象,所以使用这种算法在老年代进行使用会比较吃亏。


优点:


解决了之前内存碎片化的一些问题


早期的时候,“Appel式回收算法”对标记复制进行了完善,也是较早提出分代设计的一次尝试。它将年轻代划分为了Eden区和Survivor区域。将Eden区和Survivor区中存活的对象直接一次性复制到另一个区域中。


ps:分代设计中不会直接沾满整个年轻代的内存空间,实际上会预留一部分空间大小用于做“冗余”,当minior gc 之后的存活对象在年轻代没有足够空间存放的时候就会被挪置老年代。


Mark Compact 标记整理


之前提到的垃圾回收方式是基于标记清除的算法,这是一种非移动式的回收算法。标记整理更多是将固定内存区域中存活的对象往同一个方向去挪动,尽量地保证内存区域中的内存碎片化概率给降低。


网络异常,图片无法展示
|


不足点:


需要有两倍的内存空间用于进行垃圾回收,占用内存。假设我的jvm有2g空间,那么其中至多只有1g空间用于存储对象


当对象数目非常多的时候,移动频率会比较高,比较耗费性能。例如说老年代中的对象,大部分都是体积比较庞大,并且移动起来比较困难的对象,所以使用这种算法在老年代进行使用会比较吃亏。


优点:


解决了之前内存碎片化的一些问题,早期的时候,“Appel式回收算法”对标记复制进行了完善,也是较早提出分代设计的一次尝试。它将年轻代划分为了Eden区和Survivor区域。将Eden区和Survivor区中存活的对象直接一次性复制到另一个区域中。


ps:分代设计中不会直接沾满整个年轻代的内存空间,实际上会预留一部分空间大小用于做“冗余”,当minior gc 之后的存活对象在年轻代没有足够空间存放的时候就会被挪置老年代。


Mark Compact 标记整理


之前提到的垃圾回收方式是基于标记清除的算法,这是一种非移动式的回收算法。标记整理更多是将固定内存区域中存活的对象往同一个方向去挪动,尽量地保证内存区域中的内存碎片化概率降低。


网络异常,图片无法展示
|


通常情况下,整理算法重排堆中对象时采用下述三种策略:


任意顺序(Arbitrary) :对象的移动方式与它们的原始排序顺序和引用关系无关,可以把对象移动到任意位置。其实现简单且执行快速,但这种整理方式可能会把原来相邻的对象分散到不同的高速缓存行或虚拟内存页,从而降低空间的局部性(locality)。


线性(Linearising) :将具有关联关系的对象排列在一起,比如具有引用关系的对象,或同一数据结构中的相邻对象。其最关键的问题是,很难评估将具有什么样关联关系的对象排在一起有更好的性能。所以,你几乎看不到使用这种策略实现的整理算法。


滑动(Sliding) :将存活对象滑动到堆的一端,挤出垃圾,保证原有顺序。由于它不改变对象的相对排列熟悉,不会影响赋值器的局部性,所以现代的标记-整理回收器均使用这一策略。


使用标记整理中关于整理对象的这种思路的算法实际上有很多类,这里我查阅了一些资料介绍其中的双指针算法:


双指针算法是 Robert A.Saunders 在1974年提出的,它采用任意顺序策略,需要遍历两次堆空间,第一次遍历的目的是整理内存,即移动对象;第二次遍历的目的则是更新所有指向被移动对象的指针。


双指针算法的最佳适用场景为只包含固定大小对象的区域。其实现原理非常简单,大致的示意图如下所示,图中字母表示内存地址,数字为对象标识。


网络异常,图片无法展示
|


算法的第一次遍历(如上图)


细节步骤如下


在算法初始阶段,指针free指向区域开始,指针scan指向区域末尾,在第一次遍历过程中,指针free不断向前移动,指针scan不断向后移动;


指针free不断向前移动,直到遇到一个空闲位置,指针scan不断向后移动,直到遇到一个存活对象;


当指针free和指针scan分别指向空闲位置和存活对象时,准备将 scan 指向的对象移动到 free 的位置;


将 scan 指向的对象移动到 free的位置,scan 指向的位置记录下原对象移动到了哪里(图中为B位置),并将这块内存标记为空闲;


当 指针free和指针scan发生交错,遍历结束。


算法的第二次遍历初始化时,指针scan指向区域的起始位置;


然后开始遍历,如果指针scan指向的对象中包含指向空闲位置的指针p,则p指向的内存块中必定记录着对象移动后的地址,然后将p指向这个地址。比如,图中对象3有一个指针指向对象4,对象4移动后,其原来的内存块F中记录着其移动后的地址B,那么需要将这个指针修改为指向B;


继续遍历,直到指针scan指向的位置为空闲内存,遍历结束。


小结


双指针算法的优势是实现简单且执行速度快,但它打乱了堆中对象的原有顺序,这会破坏程序局部性,而且还对分配内存大小有严格限制,所以其应用范围有限。


这也算是采用任意顺序策略的整理算法的通病,可以想象一下,如果对象大小不固定,遍历存活对象并找到合适大小的空闲内存,该如何遍历堆,又会遍历多少次?到这儿也就能够理解为什么采用任意顺序策略的整理算法只能处理单一大小对象,或只能对不同大小的对象分别进行整理的原因了。

目录
相关文章
|
27天前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
211 1
|
2月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
40 4
|
16天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
25天前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
26天前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
21 3
|
27天前
|
存储 缓存 监控
Elasticsearch集群JVM调优堆外内存
Elasticsearch集群JVM调优堆外内存
45 1
|
1月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
1月前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
20 1
|
1月前
|
程序员 开发者
分代回收和手动内存管理相比有何优势
分代回收和手动内存管理相比有何优势
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
79 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS