黑马JVM教程——自学笔记

Posted linklate2022

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了黑马JVM教程——自学笔记相关的知识,希望对你有一定的参考价值。

三、垃圾回收

3.1、如何判断对象可以回收

3.1.1 引用计数法

image

弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放

3.1.2 可达性分析算法

  • JVM中的垃圾回收器通过可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看能否沿着GC Root对象为起点的引用链找到该对象,如果找不到,则表示可以回收
  • 可以作为GC Root的对象(使用eclipse的Java Memory Analyzer工具(MAT)分析哪些是gc root )
    • 虚拟机栈(栈帧中的本地变量表)中引用的Java对象。 
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(即一般说的Native方法)引用的对象

3.1.3 四种引用

image

强引用

只有GC Root都不引用该对象时,才会回收强引用对象

  • 如上图B、C对象都不引用A1对象时,A1对象才会被回收
软引用(SoftReference)

当GC Root指向软引用对象时,在内存不足时,会回收软引用所引用的对象

  • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象

  • 可以配合引用队列来释放软引用对象

  • 如上图如果B对象不再引用A2对象且内存不足时,软引用所引用的A2对象就会被回收

软引用的使用

public class Demo1 {
	public static void main(String[] args) {
		final int _4M = 4*1024*1024;
		//使用软引用对象 list对SoftReference是强引用,而SoftReference对byte数组则是软引用
		List<SoftReference<byte[]>> list = new ArrayList<>();
		SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
	}
}

如果在垃圾回收时发现内存不足,在回收软引用所指向的对象时,软引用本身不会被清理

如果想要清理软引用,需要使用引用队列

public class Demo1 {
	public static void main(String[] args) {
		final int _4M = 4*1024*1024;
		//使用引用队列,用于移除引用为空的软引用对象
		ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
		//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
		List<SoftReference<byte[]>> list = new ArrayList<>();
		SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);

		//遍历引用队列,如果有元素,则移除
		Reference<? extends byte[]> poll = queue.poll();
		while(poll != null) {
			//引用队列不为空,则从集合中移除该元素
			list.remove(poll);
			//移动到引用队列中的下一个元素
			poll = queue.poll();
		}
	}
}

大概思路为:查看引用队列中有无软引用,如果有,则将该软引用从存放它的集合中移除(这里为一个list集合)

弱引用(WeakReference)

仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用所引用的对象

  • 如上图如果B对象不再引用A3对象,则A3对象会被回收

弱引用的使用和软引用类似,只是将 SoftReference 换为了 WeakReference

可以配合引用队列来释放弱引用队列自身

虚引用(PhantomReference)

当虚引用对象所引用的对象被回收以后,虚引用对象就会被放入引用队列中,会有一个专门的线程来调用虚引用的方法,进而释放直接内存

必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由ReferenceHandler线程调用虚引用相关方法来释放直接内存

  • 虚引用的一个体现是释放直接内存所分配的内存,当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中,然后调用Cleaner的clean方法来释放直接内存
  • 如上图,B对象不再引用ByteBuffer对象,ByteBuffer就会被回收。但是直接内存中的内存还未被回收。这时需要将虚引用对象Cleaner放入引用队列中,然后调用它的clean方法来释放直接内存
终结器引用(FinalReference)

所有的类都继承自Object类,Object类有一个finalize方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize方法。调用以后,该对象就可以被垃圾回收了

无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由FinalizeHandler线程通过终结器引用找到被引用对象并调用他的Finalize方法,第二次GC时才能被引用对象

  • 如上图,B对象不再引用A4对象。这时终结器对象就会被放入引用队列中,引用队列会根据它,找到它所引用的对象。然后调用被引用对象的finalize方法。调用以后,该对象就可以被垃圾回收了,主要注意的是,FinalizeHandler线程的优先级较低

3.2、垃圾回收算法

3.2.1 标记清除

Mark Sweep

  • 优点:速度快

image

  • 缺点:容易产生内存碎片,即内存不连续。清理内存后不会再对内存进行整理。

image

  • 当分配一个较大的对象需要一段连续的数组时,找不到这样的内存空间,导致内存溢出

3.2.2 标记整理

Mark Compact

  • 缺点:速度慢
  • 优点:没有内存碎片
  • 在清理垃圾的过程中,会把可用的对象向前移动,使得内存更紧凑,连续的内存更多

image

3.2.3 复制

Copy

  • 不会有内存碎片
  • 需要占用双倍的空间

image

image

image

image

3.3、分代垃圾回收

image-20210512211805138

回收流程

新创建的对象都被放在了新生代的伊甸园

image

当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC

Minor GC 会将伊甸园和幸存区FROM存活的对象复制到 幸存区 TO中(复制算法), 并让其寿命加1,再交换两个幸存区

image

image

image

再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1

image

如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代

image

如果新生代老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收

总结:

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的
    对象年龄加 1并且交换 from to
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
    当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时
    间更长

3.3.1 相关JVM参数

image

GC分析

大对象处理策略

当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代

线程内存溢出

某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行

这是因为当一个线程抛出OOM异常后它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常

3.4、垃圾回收器

相关概念

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

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

吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%

3.4.1 串行

  • 单线程
  • 堆内存较小,适合个人电脑(CPU核数较少)

image

安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象

因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

Serial 收集器

Serial收集器是最基本的、发展历史最悠久的收集器

特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)

ParNew 收集器

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

特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题

Serial Old 收集器

Serial Old是Serial收集器的老年代版本

特点:同样是单线程收集器,采用标记-整理算法

3.4.2 吞吐量优先

  • 多线程

  • 堆内存较大,多核CPU

  • 单位时间内,STW(stop the world,停掉其他所有工作线程)时间最短,0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高

  • JDK1.8默认开启使用的垃圾回收器(两个开启任意一个即可,另一个会自动开启)

    image

Parallel Scavenge 收集器

与吞吐量关系密切,故也称为吞吐量优先收集器

特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与ParNew收集器类似)

该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别

GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

Parallel Scavenge收集器使用两个参数控制吞吐量

  • XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间,默认值是200ms
  • XX:GCTimeRatio=ratio gc时间所占程序运行总时间的比例,设置吞吐量的大小,公式:1/(1+ratio);ratio默认值为99;当gc时间比例超过ratio时会调大堆的内存,借此降低gc的次数;ratio一般设置为19
  • 上述两个参数相互矛盾,相互制约;堆内存大了就会使单次gc暂停时间增大,所以要寻找一个平衡点
Parallel Old 收集器

是Parallel Scavenge收集器的老年代版本

特点:多线程,采用标记-整理算法(老年代没有幸存区)

3.4.3 响应时间优先

  • 多线程

  • 堆内存较大,多核CPU

  • 尽可能让单次STW时间变短(尽量不影响其他线程运行)0.1 0.1 0.1 0.1 0.1 = 0.5

    image

CMS 收集器

Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器;与之配合工作的是ParNew收集器,工作在新生代;当CMS并发失败时,会退化为SerialOld单线程收集器

特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务

CMS收集器的运行过程分为下列4步:

初始标记:只标记GC Roots的直接关联对象。速度很快但是仍存在Stop The World问题

并发标记:执行GC Roots Tracing算法 ,进行跟踪标记,找出存活对象且用户线程可并发执行,不需要STW

重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题

  • 重新标记过程只追踪在并发标记过程中产生变动的对象

并发清除:对标记的对象进行清除回收

CMS收集器的内存回收过程是与用户线程一起并发执行

  • -XX: ParallelGCThreads 一般设置为CPU核数
  • -XX: ConcGCThreads 一般设置为ParallellGCThreads数的四分之一
  • -XX: CMSInitiatingOccupancyFraction:执行CMS垃圾回收的内存占比;设为80,即老年代内存占到80%时就开启垃圾回收,为了预留空间给浮动垃圾;早期默认值为65
    • 由于在并发清理阶段,用户线程还有可能产生垃圾,这部分垃圾只能等到下一次CMS时才能清理,这部分垃圾称作浮动垃圾;即:cms过程中还会产生新的垃圾,故不能等到老年代全部满了再垃圾回收
  • -XX: +CMSScavageBeforeRemark :再重新标记之前,先对新生代做一次垃圾回收,减轻重新标记时的压力
  • 碎片过多时,并发失败,会退化为SerialOld收集器,进行一次标记-整理回收。

3.4.4 G1

定义:Garbage First

JDK 9以后默认开启使用,而且替代了CMS 收集器

image

适用场景

  • 同时注重吞吐量(ThroughPut)和低延迟(Low latency)(响应时间),默认的暂停时间是200ms
  • 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域(Region)
  • 整体上是标记-整理算法,两个区域之间是复制算法

相关参数:JDK8 并不是默认开启的,所需要参数开启

  • -XX : +UseG1GC
  • -XX : G1HeapRegionSize=size 必须设为1,2,4,8,16这样的大小
  • -XX : MaxGCPauseMillis = time

1)G1垃圾回收阶段

image

新生代伊甸园垃圾回收—–>当老年代内存不足,新生代垃圾回收+并发标记—–>混合收集:回收新生代伊甸园、幸存区、老年代内存——>新生代伊甸园垃圾回收(重新开始)

2)Young Collection

分区算法region

分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间

E:伊甸园 S:幸存区 O:老年代

  • 会STW

image

新生代对象通过复制算法复制到幸存区:

image

当幸存区中对象也比较多了或者辛存区对象年龄达到阈值,会再次触发新生代垃圾回收,部分幸存区对象会晋升老年代

image

3)Young Collection + CM

CM(Concurrent Marking):并发标记

  • 在 Young GC 时会对 GC Root 进行初始标记

  • 在老年代占用堆内存的比例达到阈值(默认45%)时,进行并发标记(不会STW),阈值可以根据用户来进设定

image

4)Mixed Collection

会对E S O 进行全面的回收

  • 最终标记(Remark):会STW
  • 拷贝存活(Evacuation):会STW

-XX:MaxGCPauseMills:xxx 用于指定最长的停顿时间;G1会根据MaxGCPauseMills有选择的进行垃圾回收

:为什么有的老年代被拷贝了,有的没拷贝?

因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)

image

5)Full GC

  • SerialGC 串行

    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC 并行

    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS 并发

    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足 - 分情况
  • G1 并发

    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足 - 分情况

G1为例:

G1在老年代内存不足时(老年代所占内存超过阈值)

  • 如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理
  • 如果垃圾产生速度快于垃圾回收速度,便会触发Full GC

6)Young Collection 跨代引用

  • 新生代回收的跨代引用(老年代引用新生代)问题
    • 很多gc root在老年代中,遍历老年代查找gc root太耗时

image

  • 卡表与Remembered Set
    • Remembered Set 存在于E中,用于记录保存新生代对象对应的脏卡
      • 脏卡:O被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡
  • 在引用变更时通过post-write barried + dirty card queue
  • concurrent refinement threads 更新 Remembered Set

image

7)Remark

重新标记阶段

在垃圾回收时,收集器处理对象的过程中

黑色:已被处理,需要保留的

灰色:正在处理中的

白色:还未处理的

image

但是在并发标记过程中,有可能A被处理了以后未引用C,但该处理过程还未结束,在处理过程结束之前A引用了C,这时就会用到remark重新标记

过程如下

  • 当对象C的引用发生改变时,JVM就会给C加一个写屏障,写屏障的指令会被执行,将C加 入一个队列当中,并将C变为 处理中 状态
  • 并发标记阶段结束以后,进入重新标记阶段,重新标记阶段会STW,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它

image

image

image

image

8)JDK 8u20 字符串去重

-XX:+UseStringDeduplication

过程

  • 将所有新分配的字符串(底层是char[])放入一个队列
  • 当新生代回收时,G1并发检查是否有重复的字符串
  • 如果字符串的值一样,就让他们引用同一个字符串对象
  • 注意,其与String.intern的区别
    • intern关注的是字符串对象
    • 字符串去重关注的是char[]
    • 在JVM内部,使用了不同的字符串标

优点与缺点

  • 节省了大量内存
  • 新生代回收时间略微增加,导致略微多占用CPU

9)JDK 8u40 并发标记类卸载

在并发标记阶段结束以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类

-XX:ClassUnloadingWithConcurrentMark 默认启用

10)JDK 8u60 回收巨型对象

  • 一个对象大于region的一半时,就称为巨型对象
  • G1不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

11)JDK 9 并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDK 9 可以动态调整
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间

3.5、GC垃圾回收调优

明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则

查看虚拟机参数命令

> "F:\\JAVA\\JDK8.0\\bin\\java" -XX:+PrintFlagsFinal -version | findstr "GC"

可以根据参数去查询具体的信息

3.5.1 调优领域

  • 内存
  • 锁竞争
  • CPU占用
  • IO
  • GC

3.5.2 确定目标

低延迟/高吞吐量? 选择合适的GC

  • CMS,G1,ZGC(java 12)
  • ParallelGC
  • Zing

3.5.3 最快的GC

最快的GC是不发生GC

首先排除减少因为自身编写的代码而引发的内存问题

  • 查看Full GC前后的内存占用,考虑以下几个问题
    • 数据是不是太多?
      • resultSet = statement.excuteQuery("select * from 大表 limit n")
    • 数据表示是否太臃肿
      • 对象图
      • 对象大小 16 Integer24 int4
    • 是否存在内存泄漏?
      • 第三方缓存实现

3.5.4 新生代调优

  • 新生代的特点

    • 所有的new操作分配内存都是非常廉价的
      • TLAB thread-local allocation buffer
    • 死亡对象回收零代价
    • 大部分对象用过即死(朝生夕死)
    • MInor GC 所用时间远小于Full GC(1到2个数量级)
  • 新生代内存越大越好么?

    • 不是
      • 新生代内存太小:频繁触发Minor GC,会STW,会使得吞吐量下降
      • 新生代内存太大:老年代内存占比有所降低,会更频繁地触发Full GC。而且触发Minor GC时,清理新生代所花费的时间会更长
    • 新生代内存设置为内容纳[并发量*(请求-响应)]的数据为宜

幸存区调优

  • 幸存区需要大到能够保存 当前活跃对象+需要晋升的对象

  • 晋升阈值配置得当,让长时间存活的对象尽快晋升

  • -XX:MaxTenuringThreshold=threshold 最大晋升阈值,可以相对调小
    -XX:+PrintTenuringDistribution 打印晋升详细信息

3.5.5 老年代调优

以 CMS 为例

  • CMS 的老年代内存越大越好

  • 先尝试不做调优,如果没有 Full GC 那么已经...,否则先尝试调优新生代

  • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3

    • -XX:CMSInitiatingOccupancyFraction=percent 开启Full Gc时老年代空间使用比例

3.5.6 案例

1、案例1 Full GC 和 Minor GC频繁

新生代内存过小,先增大新生代内存,同时增大幸存区空间和晋升阈值,使生命周期较短的对象尽可能留在新生代,

2、案例2 请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)

查看gc日志,看cms哪个阶段耗时最长,发现重新标记阶段耗时最长,所以打开CMSScavageBeforeRemark开关,在重新标记之前对新生代先进行一次gc

3、案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)

1.7以前是永久代,永久代空间设小了就会触发整个堆的一次Full GC

以上是关于黑马JVM教程——自学笔记的主要内容,如果未能解决你的问题,请参考以下文章

黑马程序员JVM教程笔记完整目录

黑马程序员JVM教程笔记完整目录

黑马程序员SSM框架教程_Spring+SpringMVC+MyBatisPlus笔记(自学用,持续更新)

黑马程序员Java自学资源汇总放送

Vue基础自学系列 | 前端工程化

Vue基础自学系列 | webpack中的插件