JVM学习.02 内存分配和回收策略

简介: 《JVM学习.01 内存模型》篇讲述了JVM的内存布局,其中每个区域是作用,以及创建实例对象的时候内存区域的工作流程。上文还讲到了关于对象存货后,会被回收清理的过程。今天这里就着重讲一下对象实例是如何被清理回收的,以及清理回收的几种算法。

1、前言

《JVM学习.01 内存模型》篇讲述了JVM的内存布局,其中每个区域是作用,以及创建实例对象的时候内存区域的工作流程。上文还讲到了关于对象存货后,会被回收清理的过程。今天这里就着重讲一下对象实例是如何被清理回收的,以及清理回收的几种算法。

2、再谈引用

JDK1.2版本之后,对引用的概念进行了扩充,分为强引用,软引用,弱引用,虚引用。这4种引用关系强度依次减弱。

2.1、Strongly Reference 强引用

强应用是最传统的”引用“定义。这种引用关系,无论任何情况(包括OOM异常),只要强引用关系还存在,GC就不会回收掉被引用对象

声明方式:

Object object = new Object();

2.2、Soft Reference 软引用

一种相对强引用弱化了一些的引用。比如高速缓存就可以用到软引用。当内存足够时就保留,不够时就回收。其中:

  • 当系统内存充足的时候,不会被回收;
  • 当系统内存不足的时,会将这些对象列进回收范围之中进行第二次回收,如果还是内存不足,才会抛出内存溢出异常。

声明方式:

SoftReference softReference = new SoftReference<>(obj);

2.3、Weak Reference 弱引用

弱引用的强度比软引用更弱一些。被弱引用关联的对象,生命周期只能到下一次GC。当GC开始工作,无论当前的内存是否够用,都会会受到被弱引用关联的对象。

声明方式:

WeakReference weakReference = new  WeakReference<>(obj);

2.4、Phantom Reference 虚引用

虚引用是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对该对象的生存时间构成影响,也无法通过虚引用来取得一个对象实例。虚引用的作用主要是用来跟踪对象被垃圾回收的状态。

设值虚引用关联的唯一目的,就是在这个对象被回收的时候收到一个系统通知,或是后续添加进一步的操作处理。

声明方式:

PhantomReference phantomReference = new PhantomReference<>(obj, rq);

2.5、各引用小结

  • 强引用:对象不会被回收,出现OOM
  • 软引用:内存不够时才回收(二次清理)
  • 弱引用:只要GC就回收
  • 虚引用:用于检测对象的GC状态

3、如何判断对象“存活”

3.1、引用计数算法

在JVM中专门开辟一块额外的内存空间,专门用来对实例引用进行技数。一个对象如果在JVM中有被别人引用(关联或持有),则计数器+1;反之,则-1。任何时刻只要计数器为0的对象(没有任何指针对其引用),那么他就是不是存活,需要被清理。

这种的技数方式虽然原理简单,效率也很高,且有不错的案例使用。但是依然存在弊端。

看一段代码:

public class GcReferenceCount {     public void testGC(){         GcObject gcObj1 = new GcObject();          GcObject gcObj2 = new GcObject();          gcObj1.gcObj = gcObj2;         gcObj2.gcObj = gcObj1;         gcObj1 = null;         gcObj2 = null;                  // 假设这里发生了gc         System.gc();     } } class GcObject {     GcObject gcObj; }

上述代码,gcObj1和gcObj2互为引用。就算当gcObj1 = null;gcObj = null;那么计数器永远不可能为0,意味着永远不可能被回收。

image.png

3.2、可达性分析算法

通过一系列被称为“GC Roots”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程所走的路径称为“引用链”。如果某个对象到GC Roots间都没有任何的引用链关联,或者说到GC Roots对象不可达的,则证明此对象是内存垃圾。

通过这种方式可以规避引用计数算法存在的相互指向的问题。也是目前GC中默认的分析标记算法。

网上借来的图:

image.png

3.3、并发的可达性分析

这里的并发指的是用户线程和GC线程同时工作。

3.2中提到的可达性分析算法用来断定对象是否存活。理论上要求标记的全过程都基于一个保障一致性的快照中才能进行(假设一边在进行链路搜索,一边用户线程又在更改对象引用指向,那么起初搜索过的路径就会存在歧义)。且往往需要标记的对象又是大多数,这时候随着堆变大而等比例的增加STW(停顿)时间,那么也将直接影响整个系统。

为了解决或降低用户线程的停顿,即要搞为什么必须要在一个能保证一致性的快照中才能进行。引入了”三色标记“算法作为工具来辅助推导。这里将对象按照”是否访问过“分成三种颜色:

  • 白色:该对象没有被GC访问过。
  • 黑色:该对象被GC访问过,他是安全存活的,且这个对象所有引用都被扫描过。
  • 灰色:该对象被GC访问过,但这个对象至少存在一个引用还没被扫描过。

关于并发可达性分析算法,可能存在两个问题:

1、原本消亡的对象被错误标记为存活,这个是可以容忍的。只不过产生了一点浮动垃圾而已,等待下次回收就可以了。

2、原本存活的对象被错误标记为消亡,这个可能会导致系统的致命错误。

关于并发出现”对象消失“问题示意图:

image.png

同时满足两个条件时,就会出现”对象消失“的问题:

1、赋值器插入一条或多条从黑色对象到白色对象的新引用;

2、赋值器删除了全部从灰色对象到该白色对象的直接或间接引用;

解决方式:

1、增量更新。破坏第一个条件。当黑色对象插入新的指向白色对象的引用时,把这个新的引用记录下来,等并发标记结束之后,再扫描一次这个记录。比如用一个队列记录下来。可以理解为,黑色对象一旦新插入白色对象的引用之后,它就变回灰色对象了。

2、原始快照。破坏第二个条件。当灰色对象要删除指向白色对象的引用时,就把这个要删除的引用记录下来,等并发标记结束之后,再以这个记录里的灰色对象为根,重新扫描一次。

4、内存回收策略

4.1、标记 - 清除算法

标记:标记处所有需要回收的对象(也可以反过来,标记存活的对象)。

清除:在标记完成后,统一回收所有被标记的对象(如果标记的是需要被回收的对象的话,否则反之)。

网上借的图:

image.png

主要缺点:

1、执行效率不稳定。如果Java堆中包含大量对象,且其中大部分是需要被回收的。必须进行大量的标记动作,导致执行效率会随着对象数量增加而降低;

2、空间碎片化。标记,清除后会产生大量不连续的内存碎片。空间碎片太多会导致后面大对象分配时无法找到足够的连续空间。

4.2、标记 - 复制算法

将内存分为大小相等的两块空间,每次只使用其中一块。

标记:标记处所有需要回收的对象(也可以反过来,标记存活的对象)。

复制:当其中一块的内存不足时,将存活的对象复制到另一块内存中。然后把这块的对象清理。

网上借的图:

image.png

主要缺点:

1、空间利用率低。以空间换时间的做法,造成空间浪费;其间始终有一块内存没有被使用。

2、效率问题。如果对象有大量都是存活的,那么复制的对象很多,效率自然也会低下。

主要优点:

适合大量对象都是短生命周期的。一次性收集后存活对象很少的情况。同时也避免了空间碎片的问题。

4.3、标记 - 整理算法

结合了标记清除和标记复制的优缺点。

标记:标记处所有需要回收的对象(也可以反过来,标记存活的对象)。

整理:当被标记对象需要被清理时,对存活的对象不进行复制,而是统一向一端移动,然后清理掉端边界外部的内存空间。

网上借的图:

image.png

主要缺点:

1、效率问题。每次存活对象的移动,都带来大量的内存重新寻址的计算量, 执行效率较低。甚至低于复制算法。

主要优点:

不会造成空间碎片和空间浪费问题。

4.4、分代收集原则

到目前为止,大多数的回收器都遵循分代垃圾收集原则。

新生代:以标记复制算法居多。大部分对象生命周期较短,采用复制算法可以避免一定的空间碎片问题,且效率比较高

老年代:标记清除或标记整理算法。因为对象的存活时间比较长。

5、小结

到这里,讲述了JVM中的内存回收,以及引用如何被垃圾收集器回收的一些算法。对JVM的内存使用更加了解。其实JVM相关内容看过很多次,但是从来没有过系统性的整理,大部分都停留在脑子中。第一次尝试整理这些内容,一方面可以加深自己的印象,另一方面,通过搜索其他的参考资料,可以发现很多以前忽略的地方。或许这个就是写技术博客的魅力吧。虽然千篇一律,但都是自己手敲原创。respect!

相关文章
|
7天前
|
NoSQL 算法 Redis
redis内存淘汰策略
Redis支持8种内存淘汰策略,包括noeviction、volatile-ttl、allkeys-random、volatile-random、allkeys-lru、volatile-lru、allkeys-lfu和volatile-lfu。这些策略分别针对所有键或仅设置TTL的键,采用随机、LRU(最近最久未使用)或LFU(最少频率使用)等算法进行淘汰。
21 5
|
8天前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
26天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
24天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
19 1
|
27天前
|
存储 分布式计算 算法
1GB内存挑战:高效处理40亿QQ号的策略
在面对如何处理40亿个QQ号仅用1GB内存的难题时,我们需要采用一些高效的数据结构和算法来优化内存使用。这个问题涉及到数据存储、查询和处理等多个方面,本文将分享一些实用的技术策略,帮助你在有限的内存资源下处理大规模数据集。
25 1
|
1月前
|
程序员 开发者
分代回收和手动内存管理相比有何优势
分代回收和手动内存管理相比有何优势
|
28天前
|
存储 监控 Java
深入理解计算机内存管理:优化策略与实践
深入理解计算机内存管理:优化策略与实践
|
2月前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
51 10
|
2月前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
2月前
|
算法 Java 程序员
内存回收
【10月更文挑战第9天】
50 5