200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > 《深入理解JAVA虚拟机》学习日志----一 自动内存管理机制(2.垃圾收集器与内存分配策略)

《深入理解JAVA虚拟机》学习日志----一 自动内存管理机制(2.垃圾收集器与内存分配策略)

时间:2024-04-10 21:22:17

相关推荐

《深入理解JAVA虚拟机》学习日志----一 自动内存管理机制(2.垃圾收集器与内存分配策略)

二、垃圾收集器与内存分配策略

前言:讨论的区域集中在Java堆和方法区中,而其他几个区域的内存分配和回收都具备确定性,所以不需过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。在开始讨论之前,我们先来了解一下GC的区域划分:大体可划分为:新生代(def new generation)、老年代(tenured generation)、持久代(也被叫做永久代、永久区)(compacting perm generation)然后新生代又可划分为一块较大的Eden空间和两块较小的Survivor空间,Survivor区可以继续划分为from区和to区那我们怎么样判断对象是处于哪个区域呢?我的理解就是:一个对象只要没有被回收一次,年龄就加一,然后我们可以通过年龄来判断属于哪个区域。

HotSpot虚拟机默认每一个Survivor区大概占新生代的1/10,Eden区占8/10

2.1、判断对象的情况

垃圾收集器在对堆进行回收前,第一件事就是要确定这些对象之中那些还“存活”着,哪些已经“死去”。

2.1.1 引用计数算法

很多教科书判断对象是否存活的算法:给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加1;引用失效时,值就减1;任何时刻计数器为0的对象就是不可能再被使用的。但是主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。下面这个例子就是如此,此时da和db两个对象已经不可能再被访问了,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器去回收它们。

控制台中的GC日志显示"4704K->515K",意味着虚拟机并没有因为这两个对象互相引用就不回收它们,这也从侧面说明虚拟机并不是通过引用计数算法来判断对象是否存活的。

4704K->515K(7680K), 0.0038929 secs 的的含义:GC前该内存区域已使用内存->GC后该内存区域使用容量(该内存区域总容量),GC所用的时间

还有一些其他的含义大家可以自行百度。

2.1.2 可达性分析算法

目前主流的商用语言(Java、C#)都是通过可达性分析来判定对象是否存活的。如图,此算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说,就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。对象object5、6、7虽然互相有关联,但是它们到 GC Roots 是不可达的,所以它们将会被判定为是可回收的对象。

Java语言中,可作为 GC Roots 的对象包括下面几种:1、虚拟机栈(栈帧中的本地变量表)中引用的对象。2、方法区中类静态属性引用的对象。3、方法区中常量引用的对象。4、本地方法栈中JNI(即一般说的 Native 方法)引用的对象。

2.1.3 四种引用

1、强引用:就平时经常用到的,类似“Object obj = new Object()” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。2、软引用:描述一些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。3、弱引用:强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。4、虚引用:也被称作幽灵引用或者幻影引用,非常非常弱的引用关系。完全不会对对象的生存时间构成影响,也无法通过虚引用来取得一个对象实例。那虚引用的目的在哪呢?就是能在这个对象被收集器回收时收到一个系统通知。

2.1.4 回收方法区

虽然方法区(或者HotSpot虚拟机中的永久代)垃圾收集效率很低,但其实也是存在的。方法区(永久代)主要回收两部分内容:废弃常量和无用的类。1、废弃常量:没有其他地方引用了这个字面量就回收2、无用的类:①该类所有实例都被回收,也就是堆中不存在该类的任何实例。②加载该类的 ClassLoader 已经被回收。③该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。满足上面三个条件就可以进行无用类的回收了,但也仅仅是“可以”,是否对其进行回收,虚拟机提供了一些相关的参数。在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

2.2 垃圾收集算法

因为垃圾收集算法涉及大量的程序细节,而且各个平台的虚拟机操作内存的方法又各不相同,这里只简单的介绍几种算法的思想及其发展过程。

2.2.1 标记–清除算法

这个算法是最基础的收集算法,如同其名,先标记再清除。首先标记需要回收的对象,然后再统一进行清除。至于为什么说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。主要不足有两点:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,如上图所示,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2.2.2 复制算法

下面是我对复制算法的总结:目的:解决效率问题。过程:将可用内存按容量划分为大小两块,如上图所示,一块是保留紫色的保留区域,其他部分就是另外一块。我们可以看到上图的“回收前状态”,当表格的左半边区域内存使用完时,虚拟机会先把左边还存活着的对象复制到右半边的保留区域,并且按顺序排好,然后会把左边已使用过的内存空间一次性全部清理掉。优点:每次只对一半的区域进行内存回收,内存分配时也不用考虑内存碎片等复杂情况,只需要移动存活着的对象堆顶的指针,将其按顺序分配内存即可。实现简单,运行高效。缺点:将内存缩小为原来的一半,未免太高了些。但是在新生代中可以避免这个缺点(具体原因👇👇👇)。运用:基本都采用这种算法来回收新生代,IBM公司研究表明,新生代中的对象98%都是“朝生夕死”的,这也是为什么新生代的Eden区占了很多空间,而Survivor只占了一点点空间。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。这样的话就只有一块Survivor的内存空间(大约占新生代的10%)浪费掉。当然也可能出现这块空闲的Survivor空间不够用的情况,这时,我们就需要找其他内存(这里指老年代)帮忙。(分配担保)

2.2.3 标记–整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的时,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。根据老年代的特点,有人提出了另外一种“标记-整理”算法,标记过程和“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

2.3 垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下面大概介绍几种常见的收集器。新生代收集器:Serial、ParNew、Parallel Scavenge老年代收集器:CMS、Serial Old(MSC)、Parallel Old

2.3.1 Serial 收集器

概述:最基本、发展历史最悠久的收集器。是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。优点:简单而高效(与其他收集器的单线程比),依然是虚拟机运行在Client模式下的默认新生代收集器。缺点:当遇到大的应用时,会造成停顿时间过长。

2.3.2 ParNew 收集器

概述:其实就是Serial收集器的多线程版本,除了多线程收集之外,其他与Serial收集器相比并没有太多创新之处。比较:ParNew收集器在单CPU的环境中绝对不会有比Serial更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能100%地保证可以超越Serial。当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境下,其可以限制垃圾收集的线程数。

注意:我们在这里先解释两个名词:并发和并行。这两个名词都是并发编程中的概念。

并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

2.3.3 Parallel Scavenge 收集器

概述:和其他收集器不一样,别的收集器关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而它的目标则是达到一个可控制的吞吐量。吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99% 不同:停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序地运算任务,主要适合在后台运算而不需要太多交互的任务。

2.3.4 Serial Old 收集器

概述:这是Serial的老年代版本,也是一个单线程收集器,跟Serial功能差不多,只是作用的区域不同。作用:①在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用;②作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。这两点具体在后面会说。

2.3.5 Parallel Old 收集器

和 Parallel Scavenge 收集器搭配使用,因为Parallel Scavenge无法和CMS搭配工作,而与Serial Old搭配效率又低,所以出现了此收集器。

2.3.6 CMS 收集器

概述:是一种以获取最短回收停顿时间为目标的收集器,总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行。过程:整体分成四个步骤:初始标记、并发标记、重新标记、并发清除。其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记了一下GC Roots能直接关联到的而对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。优点:并发收集、低停顿。缺点:①对CPU资源非常敏感。②无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。

2.3.7 G1 收集器

概述:是当今收集器计数发展的最前沿成果之一,是一款面向服务端应用的垃圾收集器。优点:①并行与并发:充分利用多CPU、多核环境下的硬件优势。②分代收集:与其他收集器一样,分代概念在G1中依然得以保留,而且G1不需要其他收集器配合就能独立管理整个GC堆。③空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。④可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。区别:其他收集器进行收集的范围都是整个新生代或者老年代,而在使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域,虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

2.4 内存分配与回收策略

前面我们一直在讲回收内存,接下来说说给对象分配内存。

2.4.1 对象优先在Eden分配

基本上对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

Minor GC 和 Full GC的区别:Minor GC(新生代GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。Full GC(Major GC/老年代GC):指发生在老年代的GC。Major GC的速度一般会比Minor GC慢10倍以上。

2.4.2 大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。写程序时应该避免一群“朝生夕灭”的“短命大对象”,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

2.4.3 长期存活的对象将进入老年代

怎么判断是否是长期存活呢?虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。

这一章终于看完了,太难顶了

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。