你的位置:首页 > 信息动态 > 新闻中心
信息动态
联系我们

浓缩JVM内存回收知识

2021/12/30 10:21:27

一、揭开 JVM 内存分配与回收的神秘面纱

把堆内存分为新生代和老年代的目的是更好地回收内存,更快地分配内存

img

1、上图所示的 eden 区、s0(“From”) 区、s1(“To”) 区都属于新生代,tentired 区属于老年代。

2、对象首先在eden区分配,在一次新生代垃圾回收收,如果对象还存活,则会进入s0 或者 s1并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加 到一定程度(默认为 15 岁),就会被晋升到老年代中。

3、Minor GC会一直重复这样的过程: 直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。

4、大对象直接进入老年代。因为大对象就是需要大量连续内存空间(比如:字符串、数组),避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

5、长期存活的对象将进入老年代(15岁了)

二、如何判断对象是否需要被干掉

程序计数器、虚拟机栈、本地方法栈这3个区域随着线程的生存而生存,内存分配和回收都是确定的。两种判断对象是否存活的基础算法:

1、引用计数器计算:

​ 给每个对象添加一个引用计数器,每次引用这个对象的时候计数器+1,引用失效的时候-1.计数器等于0的时候就是不会再次使用的时候。不过这个方法有一种情况就是出现对象的循环引用时GC没法回 收。

2、可达性分析计算:

通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。以下图为例:

img

可作为GC Roots的对象包含以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。(可以理解为:引用栈帧中的本地变量表的所有对象)
  2. 方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象)
  3. 方法区中常量引用的对象(可以理解为:引用方法区中常量的所有对象)
  4. 本地方法栈中(Native方法)引用的对象(可以理解为:引用Native方法的所有对象)

3、引用在判断对象是否需要干掉的作用:

①强引用:

	- 必不可少的生活用品。垃圾回收器绝不会回收它。
	- 当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问 题。

②软引用:

	- 可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以 被程序使用。
	- 软引用可以 加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问 题的产生。		

③弱引用:

	- 可有可无的生活用品。	
	- 但是:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了 只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

④虚引用:

  • 虚引用主要用来跟踪对象被垃圾回收的活动。

  • 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

补充:如何判断一个常量是废弃常量

​ 运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?

​ 假如在常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池。

三、如何宣告一个对象真正死亡

	即使在可达性分析算法中不可达的对象,**也并非是“非死不可”的**,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
	标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。

1、第一次标记并进行一次筛选

筛选的条件是此对象是否有必要执行finalize()方法:
当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。

如果对象有必要执行finalize() 方法,则被放入F-Queue队列中。

2、GC对F-Queue队列中的对象进行二次标记

​ 如果对象在finalize()方法中重新与引用链上的任何一 个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱, 那么只能被回收了。

四、垃圾回收算法

1、标记清除算法

原理:标记出所有需要回收的对象 ==> 标记结束后统一回收

后续:把已经死亡的对象标记为空闲内存,记录在一个空闲列表中,当需要new一个对象时,内存管理模块会从空闲列表中寻找空闲内存来分给新的对象。

缺点:①标记和清除的效率低。

​ ②内存碎片很多,一大块内存里有里有可回收的对象、有存活的对象、有未使用内存。零零散散,当需要一个大内存空间时分配不出来。

2、复制算法

​ 把可用内存分成两等份,每次使用时只使用其中一块(from space),把存活的copy到另一块空间上(to space),交换指针内容。完美解决碎片问题。

缺点:内存缩水。堆内存的使用效率十分低下。

3、标记整理算法

标记可回收对象,让存活的对象都向一端移动,然后直接清理边界外的内存。

4、分代收集算法(问题:HotSpot 为什么要分为新生代和老年代?)

一般把堆内存分为新生代和老年代。根据各个年代的特点采用适当的收集算法。

新生代:每次垃圾回收都有大批对象死亡,少量存活。复制成本低 ===>选用复制算法

老年代:对象存活率高,没有额外的空间对它进行分 配担保,选用标记-清除 或 标记整理

五、垃圾收集器

​ 收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现

1、Serial 收集器

​ Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器。

特点:

	- 单线程收集器
	- 它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。
	- 新生代采用复制算法,老年代采用标记-整理算法。

2、ParNew 收集器

​ ParNew 收集器其实就是 Serial 收集器的多线程版本

​ 新生代采用复制算法,老年代采用标记-整理算法。

3 、Parallel Scavenge 收集器

​ 几乎和ParNew都一样。新生代采用复制算法,老年代采用标记-整理算法。

​ 但是:关注点是吞吐量(高效率的利用 CPU)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。

4、Serial Old 收集器

​ Serial 收集器的老年代版本

5、Parallel Old 收集器

​ Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资 源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

6、 CMS 收集器

​ CMS(Concurrent Mark Sweep)是一种以最短回收停顿时间为目标的收集器,注重用户体验。是一款真正意义上的并发收集器,第一次实现了让垃圾收集线程和用户线程(基本上)同时工作。

运作过程复杂,分为4个步骤

  • 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;

  • **并发标记: **同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。(但在这个阶段结束,这 个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。)

  • **重新标记: **重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动 的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发 标记阶段时间短

  • **并发清除: **开启用户线程,同时 GC 线程开始对为标记的区域做清扫。

**优点:**并发收集、低停顿

缺点:

​ 对 CPU 资源敏感;

​ 无法处理浮动垃圾;

​ 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

7、G1 收集器

​ G1 (Garbage-First) 是一款面向服务器的垃圾收集器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。主要针对配备多颗处理器及大容量内存的机器。

特点:

①并行与并发:

​ G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核 心)来缩短 Stop-The-World 停顿时间。

②分代收集:

​ 虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的 概念。

③空间整合:

​ 与 CMS 的“标记–清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集 器;从局部上来看是基于“复制”算法实现的。

④可预测的停顿:

​ 这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注 点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度 为 M 毫秒的时间片段内。

步骤

①初始标记 ②并发标记 ③最终标记 ④筛选回收

六、JVM调优的一些方面

一般是针对堆内存的调优:

1、调整最大内存和最小内存

2、调整新生代和老年代的比值

3、设置年轻代和老年代的大小

4、调整survivor区和Eden区的比值

七、JVM的栈参数调优

1、调整每个线程栈空间的大小

2、设置线程栈的大小