目录
为什么会有GC
背景:
理解:
可达性分析算法
基本思路:
标记清除算法
讲解
一 点睛
二 执行过程
三 什么是清除
四 缺点
STW
对象与分代
GC时对象在内存池中的迁移
GC Roots
总结
默认算法
标记-清除算法(Mark-Sweep)
复制算法(Copying)
标记-整理算法(Mark-Compact)
视频
为什么会有GC
其最本质的原因是因为内存资源的稀缺性。我们计算机最核心的资源是CPU和内存,CPU是随着计算机一直存在的东西,核数有限但是一直存在;但内存比较稀缺,A占满了,B就不能用了,我们怎么可以共享使用这个内存呢,这就是GC产生的原因了。
背景:
在传统的编程语言中,程序员需要手动分配和释放内存空间。这样的内存管理方式容易出现内存泄漏(Memory Leak)和空指针异常(NullPointerException)等问题,同时也增加了编程的复杂性和难度。
为了解决这些问题,Java引入了垃圾回收机制。垃圾回收器负责自动检测和释放不再使用的对象,将它们占用的内存空间返还给系统。这样,程序员无需关心对象的生命周期,也不需要手动释放内存,大大简化了程序的开发和维护工作。
Java中的GC背景还包括以下几个方面:
1.动态内存分配:Java中的对象都是在堆(Heap)上分配内存的,而非栈(Stack)上。堆是动态分配和回收的,允许程序在运行时创建和销毁对象,不需要静态定义和预先分配内存空间。
2.分代垃圾回收:Java的垃圾回收机制根据对象的生命周期进行不同的处理。一般来说,新创建的对象往往生命周期较短,而已经存活一段时间的对象可能会存活更长时间。为了提高垃圾回收的效率,Java将内存分为不同的代(Generation),并采用不同的回收策略来处理。
3.垃圾回收算法:Java的垃圾回收机制采用的是可达性分析算法。该算法通过判断对象是否可达(即是否可以通过根对象(如局部变量、类的静态变量等)追溯到)来确定其是否存活。如果对象不可达,则被认为是垃圾,可以被回收。
总之,Java中的垃圾回收机制背后有着动态内存分配、分代垃圾回收和可达性分析等背景。它大大简化了程序员对内存管理的工作,提高了程序的可靠性和开发效率。
理解:
GC是垃圾收集的意思(Garbage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。
所以,Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放。
对于Java程序员来说,分配对象使用new关键字;释放对象时,只要将对象所有引用赋值为null,让程序不能够再访问到这个对象,我们称该对象为"不可达的".GC将负责回收所有"不可达"对象的内存空间。
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的".当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。但是,为了保证GC能够在不同平台实现的问题,Java规范对GC的很多行为都没有进行严格的规定。例如,对于采用什么类型的回收算法、什么时候进行回收等重要问题都没有明确的规定。因此,不同的JVM的实现者往往有不同的实现算法。这也给Java程序员的开发带来许多不确定性。本文研究了几个与GC工作相关的问题,努力减少这种不确定性给Java程序带来的负面影响。
可达性分析算法
引用链法(可达性分析法)
基本思路:
可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上到下的⽅式搜索被根对象集合所连接的⽬标对象是否可达;
使⽤可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所⾛过的路径称为引⽤链(ReferenceChain);
如果⽬标对象没有任何引⽤链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象;
在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活对象。
标记清除算法
引用计数法在JVM垃圾回收算法中逐渐被废弃,很简单,如果存在对象之间的循环引用,则计数器的count值永远不会清0,如此对象将会一直存在内存中得不到释放。最后就导致我们从内存泄漏到内存溢出。
优化:引用计数->引用追踪(标记清除算法)
讲解
一 点睛
当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
标记-清除算法(Mark-Sweep)是一种非常基础和常见的垃圾收集算法,该算法被 J.McCarthy 等人在1960年提出并并应用于Lisp语言。
二 执行过程
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
标记:Collector 从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的 Header 中记录为可达对象。标记的是引用的对象,不是垃圾!!
清除:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header中 没有标记为可达对象,则将其回收
三 什么是清除
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放覆盖原有的位置。
如果内存规整
- 采用指针碰撞的方式进行内存分配
如果内存不规整
- 虚拟机需要维护一个列表
- 空闲列表分配
四 缺点
标记清除算法的效率不算高。
在进行GC的时候,需要停止整个应用程序,用户体验较差。
在GC标记-清除算法的使用过程中会逐渐产生被细化的分块,不久后就会导致无数的小分块散布在堆的各处。我们称这种状况为碎片化(fragmentation)。
STW
Stop-the-World,简称STW。
暂停原因:STW的主要目的是为了在垃圾回收期间保证应用程序的一致性。当Java虚拟机启动垃圾回收时,它会暂停所有的应用线程,以防止在垃圾回收期间发生数据竞争或并发错误。
1、指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应, 有点像卡死的感觉,这个停顿称为STW。
(1)可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。 ① 分析工作必须在一一个能确保一 致性的快照中进行 ② 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上 ③ 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
(2)被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样, 所以我们需要减少STW的发生。
2、STW事件和采用哪款GC无关,所有的GC都有这个事件。
3、哪怕是G1也不能完全避免stop-the-world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
4、STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。开发中不要用System.gc() ;会导致stop-the-world的发生。
减少系统的停顿时间(STW),这个是我们后面优化算法统一的核心目标。
注意:这里压缩不是说的标记清除算法而是以后的其它算法。比如标记-压缩算法(Mark-Compact Algorithm)和标记-整理算法(Mark-Sweep-Compact Algorithm)等。
对象与分代
java中的对象分代是一种内存管理策略,它将堆内存中的对象划分为不同的代(Generation),并针对不同代的对象使用不同的垃圾回收算法和策略。Java虚拟机通常将堆内存分为年轻代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation 或者说是Metaspace)。
-
年轻代(Young Generation):年轻代是新创建的对象的分配区域,通常包含三个区域:Eden区、Survivor区From和Survivor区To。新创建的对象首先分配在Eden区,当Eden区满时,会触发Minor GC(Minor Garbage Collection)。Minor GC的目标是清理年轻代的垃圾对象,并将存活的对象复制到Survivor区中的一个区域。经过多次Minor GC后,仍然存活的对象会被晋升到老年代。
-
老年代(Old Generation):老年代用于存放生命周期较长的对象,包括年轻代晋升的对象和直接在老年代分配的大对象。当老年代的空间不够时,会触发Full GC(Full Garbage Collection),Full GC会遍历整个堆内存,清理全部的垃圾对象。
-
永久代(Permanent Generation 或者说是Metaspace):永久代主要用于存放类的元数据(metadata)和常量池(constant pool)等。在Java 8之后,永久代被Metaspace取代,Metaspace使用本地内存进行类的元数据存储。
通过分代的概念,Java虚拟机可以根据对象的生命周期使用不同的垃圾回收算法和策略。例如,年轻代通常使用复制算法进行垃圾回收,每次清理少量的对象,而老年代由于对象生命周期较长且稳定,通常使用标记-整理算法或标记-清除-整理算法进行垃圾回收。
分代的设计目的是为了提高垃圾回收的效率。通常,大部分对象都有较短的生命周期,而只有少部分对象拥有较长的生命周期。通过将对象按照生命周期划分到不同的代中,可以针对不同代使用适当的垃圾回收算法和策略,从而提高垃圾回收的效率和系统的性能。
GC时对象在内存池中的迁移
-
发生Minor GC时,首先将Eden区和Survivor区From区域中的存活对象复制到空的Survivor区To区域。
-
复制完成后,清空Eden区和Survivor区From区域,将它们作为空闲空间。
-
下一次Minor GC时,将Eden区和Survivor区To区域中的存活对象复制到Survivor区From区域或者老年代(取决于对象的年龄)。
-
此后,对象会在From区和To区之间进行复制,年龄达到一定阈值(通常是15岁)的对象将会被晋升到老年代。
-
新生代Eden区满时,发生Minor GC,通过GC Roots引用链找存活的对象,回收垃圾对象;老年代内存满时,发生Full GC(Major GC),回收老年代的垃圾对象;
-
这两种垃圾回收的目标和作用范围是不同的。Full GC(Major GC)会遍历和清理整个堆内存,而Minor GC主要涉及到年轻代的Eden区和Survivor区。
-
如果Full GC仍然无法释放足够的内存空间,可能会抛出OutOfMemoryError异常。
为什么是复制,不是移动?
复制算法的优势:复制算法相对于移动操作的主要优势在于简单性和效率。由于复制操作是将存活对象复制到一个空的区域,因此不需要对整个堆内存进行扫描和标记。这样可以减少标记和清除的开销,提高垃圾回收的效率。同时,由于每次垃圾回收后都是对整个区域进行清空,可以保证内存分配的连续性,解决了内存碎片问题。
移动操作的劣势:移动对象需要更新所有对该对象的引用,涉及到对堆内存中所有引用该对象的地方进行更新,这对于大型对象或者引用链较长的情况来说开销较大,并且容易引入安全问题。而复制操作只涉及到存活对象的复制,不需要修改引用的指向,不会涉及到全局的引用更新,因此更加高效且安全。
对象销毁过程
Java对象的销毁指的是释放对象占用的内存空间,JVM通过GC机制实现内存的自动回收
GC Roots
可以作为 GC Roots 的对象
1.当前正在执行的方法里的局部变量和输入参数
2.活动线程 (Active threads)
3.所有类的静态字段 (static field)
4.JNI 引用
此阶段暂停的时间,与堆内存大小,对象的总数没有直接关系,而是由存活对象 (aliveobiects) 的数量来决定。所以增加堆内存的大小并不会直接影响标记阶段暂停的时间。
总结
默认算法
标记-清除算法(Mark-Sweep)
标记---清除算法(Mark-Sweep)是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并并应用于Lisp语言。标记清除的执行过程是先标记,再清除。
特点:实现简单
缺点:每次清除的时候都需要停机、存在内存空间太强片化问题。
复制算法(Copying)
复制(Copying)算法是为了解决标记-清除算法,的效率和收集的时间空间不连续等问题。主要的实现是将空间分为两份,将存活的对象移到另外一份,标记完后,将原来的空间清除,这样的话空间是连续的,并且效率较高。
特点:空间连续无碎片化、清除高效;
缺点:
压缩一半空间,垃圾清楚的时候一半空间不可用。
对存活对象较多的老年代下,交率较差。
标记-整理算法(Mark-Compact)
由于复制算法的高效性是建立在存活对象少,垃圾对象多的前提下的,对于新生代来说比较适合,但是针对老年代来说,很多对象是一直存活的,所以就不能用复制算法,这样会导致每次回收的垃圾很少,会造成大量的复制。所以标记-整理算法主要是针对老年代来设计的。其原理主要是:分为两个阶段,第一个阶段与标记-清理算法一样,先从根节点标记哪些是被对象引用的,第二阶段将所有存活的对象压缩移动到内存的另一端,按顺序排放,最后清除所有边界以外的空间。
注意:在JDK8默认的配置下使用 新生代,老年代的垃圾回收策略,新生代区域使用标记-复制算法,老年代区域使用标记-整理算法。
对比名称 | 标记-清除 | 标记-整理 | 标记-复制 |
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(会产生碎片) | 少(不会产生碎片) | 需要对象2倍大小 |
移动对象 | 否 | 是 | 是 |
视频
链接:https://www.aliyundrive.com/s/RTG4PHVxq5w
今天就到这里吧,感觉有用的小伙伴可以点个赞,你的支持就是我更新的最大动力!