JVM&GC

JVM&GC

JVM 常用参数设置积累

# 堆的初始值,默认物理内存的1/64
-Xms:
# 堆的最大值,默认物理内存的1/4
-Xmx:
# 年轻代大小「在整个堆内存大小确定的情况下,增大年轻代将会减小年老代,反之亦然。此值关系到 JVM 垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的3/8」
-Xmn:
# 设置年轻代初始值为 1024 M
-XX:NewSize=1024
# 设置年轻代最大值为 1024 M
-XX:MaxNewSize=1024m
# 设置线程栈大小,设置越小,说明一个线程栈里面能分配的栈帧数就越少,但对于 JVM 来讲,能开启的线程数就越多;
-Xss128k
# 方法区大小设置「jdk1.8 之后使用元空间替换了方法区,也使用了其他命令」
-XX:MaxPermSize
# 元空间大小设置
-XX:MetaspaceSize
-XX:MaxMetaspaceSize
# 设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代「只在 Serial 和ParNew两个收集 器下有效」
-XX:PretenureSizeThreshold=1000000
# 设定对象晋升到老年代的年龄阈值「设定经历 10 次拷贝后,如果对象依旧存活则晋升至老年代,反之则清理掉,实际上绝大多数的对象都是朝生夕死」
-XX:MaxTenuringThreshold=10
# jdk1.8 默认设置了下述参数,设置该参数,就会在每次 minor gc 之前看看老年代的可用内存大小,是否大于之前每一次 minor gc 后进入老年代的对象的平均大小,如果小于则那么就会触发一次 Full gc 
-XX:-HandlePromotionFailure

JVM 排查问题命令积累

# 查询实例个数和占用空间大小
jmap -histo pid
# 导出堆内存信息
jmap -dump:format=b,file=test.hprof pid
# 查找死锁,打印出线程的状态
jstack pid
# 查看当前运行 java 应用的扩展参数
jinfo pid
# 查看内存中各个部分的使用情况「eden、survivor、old」
jstat -gc pid
# 堆内存统计
jstat -gccapacity pid
# 新生代内存统计
jstat -gcnewcapacity pid
# 新生代垃圾回收统计
jstat -gcnew pid
# 老年代内存统计
jstat -gcoldcapacity pid
# 老年代垃圾回收统计
jstat -gcold pid
# 元数据空间统计
jstat -gcmetacapacity pid
# 总结垃圾回收统计
jstat -gcutil pid

JVM 的运行模式有三种:

  1. 解释模式「-Xint」:只使用解释器,执行一行字节码就编译一次机器码「不会去缓存」;
  • 优点:启动块;
  • 缺点:整体执行相比编译模式慢;
  1. 编译模式「-Xcomp」:只使用编译器,现将所有的字节码文件一次性编译为机器码,然后一次性去执行所有的机器码;
  • 优点:好处是执行快;
  • 缺点:启动比解释模式慢;
  1. 混合模式:依旧采用解释模式执行代码,但是对于一些“热点”代码采用编译模式执行,JVM 一般采用混合模式执行代码;
  • 优点:相比解释模式,执行会快,相比编译模式,启动会快;

针对混合模式,JVM 有对应的技术去实现,比如 JIT,也就是即时编辑技术。

JVM 内存分配与回收

  • jvm 内存区域图
graph LR runTime[JVM 运行时内存区域] --> B[堆] & mateData[方法区/元空间] & T[虚拟机栈] & time[程序计数器] & method[本地方法栈] B --> B1[堆内内存] B --> B2[堆外内存] mateData --> 常量[常量] mateData --> 静态变量[静态变量] mateData --> 类元信息[类信息] B1 --> young[年轻代] B1 --> old[老年代] T --> stack[栈帧] time[程序计数器] --> time1[线程私有,记录程序执行的位置,为线程切换用] method[本地方法栈] --> method1[线程私有,面向 native 方法] young --> eden[伊甸园] young --> survivor[survivor 区] survivor --> survivor1[survivor from 区] survivor --> survivor2[survivor to 区]

avatar

  1. 堆内内存:堆内内存 = 年轻代 + 老年代
  • 优点:
  • 缺点:
  1. 堆外内存:把内存对象分配在 Java 虚拟机的堆以外的内存「比如:java.nio.DirectByteBuffer」
  • 优点:
  1. 减少了垃圾回收机制(GC 会暂停其他的工作);
  2. 加快了复制的速度「堆内在flush到远程时, 会先复制到直接内存(非堆内存), 然后再发送,而堆外内存(本身就是物理机内存)几乎省略了该步骤」。
  • 缺点:
  1. 内存难以控制「使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难」。

年轻代

  • 伊甸园:survivor from 区:survivor to 区 = 8:1:1
伊甸园
  • 大部分对象都在这里诞生;
  • 当 Eden 区满时, 依然存活的对象将被复制到 Survivor 区,当一个 Survivor 区满时,此区的存活对象将被复制到另外一个 Survivor 区;
survivor 区
  • survivor from 区
  • survivor to 区

老年代

方法区/元空间

  • 在 jdk1.8 之后取消了方法区,命名为元空间

Minor GC & Young GC & Major GC & Full GC 的区别:

Minor GC 发生在 Eden 区域,Young GC 发生在 Eden 、from 和 to 区域,Major GC 发生在 old 区域,Full GC 发生在整个堆空间,包括年轻代和老年代;

avatar

线程栈

什么场景下对象会进入老年代

  1. 即将存储的大对象在 Eden 区域是发现存储不下「就算 Minor gc 之后还是存储不下」;
  2. 长期存活下来的对象;
  3. Minor gc 后存活的对象 Survivor 区放不下;

什么是老年代空间分配担保机制

​ 年轻代每次 Minor gc 之前 JVM 都会计算下老年代剩余可用空间,如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象)就会看一个 “-XX:-HandlePromotionFailure”(jdk1.8 默认就设置了)的参数是否设置了,如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次 Minor gc 后进入老年代的对象的平均大小。如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次 Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生 OOM,当然,如果 Minor gc 之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发 Full gc,Full gc 完之后如果还是没用空间放 Minor gc 之后的存活对象,则也会发生 “OOM”。

触发 Full gc 的时机

  1. 老生代内存不足的时候;「使用率达到 92%」
  2. 即将要放进老年代的对象过大,需要进行老年代回收;「和上面类似」
  3. 老年代空间分配担保机制中有可能触发;「也就是老年代空间分配担保失败」
  4. 执行 jmap -histo:live 或者 jmap -dump:live 的时候;

如何判断对象可以被回收

  1. 引用计数法

给对象添加一个引用计数器,没增加一个地方引用它,计数器就加一,减少一个,计数器就减一,但是解决不了循环引用的问题「会导致内存泄露」,主流的虚拟机都没有使用这个。

  1. 可达性分析

通过一系列的称为 GC Roots 的对象作为起点,从这些节点开始向下搜索,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。

GC Roots 根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等。

具体操作:从gc root根往下搜索,然后三色标记,黑灰白,刚开始是白色,如果搜索到A节点,A节点的子节点还没被搜索,则A节点是灰色,A节点包括子节点全部搜索完毕标记为黑色,到最后白色的就回收了

  1. 依据引用类型
    java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用。
  • 强引用
    普通的变量引用
public static Person person = new Person();
  • 软引用
    将对象用 SoftReference 软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
public static SoftReference<Person> person = new SoftReference<Person>(new Person());
  • 弱引用
    将对象用 WeakReference 软引用类型的对象包裹,弱引用跟没引用差不多,GC 会直接回收掉,很少用。
public static WeakReference<Person> person = new WeakReference<Person>(new Person());
  • 虚引用

虚引用也称为幽灵引用或者幻影引用,是最弱的一种引用关系,几乎不用。
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。虚引用和弱引用对关联对象的回收都不会产生影响,如果只有虚引用活着弱引用关联着对象,那么这个对象就会被回收。它们的不同之处在于弱引用的get方法,虚引用的get方法始终返回null,弱引用可以使用ReferenceQueue,虚引用必须配合ReferenceQueue使用。
jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存,而直接内存是在堆内存之外(其实是内存映射文件,自行去理解虚拟内存空间的相关概念),所以直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收,相应的用户线程会收到通知并对直接内存进行清理工作。

  1. 通过 finalize() 方法最终判定对象是否存活

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

  • 第一次标记并进行一次筛选。
    筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize 方法,对象将直接被回收。
  • 第二次标记
    如果这个对象覆盖了 finalize 方法,finalize 方法是对象脱逃死亡命运的最后一次机会,如果对象要在 finalize() 中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

垃圾回收算法

graph A[垃圾回收算法] --> B[标记清除算法] & C[标记整理算法] & D[复制算法] & E[分代回收算法]

1、标记-清除算法

分为两个阶段,即标记和清除,首先会标记所有需要被回收的对象,在标记完成后统一对已经标记的对象进行回收,是最基础的收集算法。

  • 优点
  1. 实现简单
  • 缺点
  1. 内存碎片化
  2. 效率不高
  • 使用场景:主流虚拟机不使用

2、标记-整理算法「也叫标记-压缩算法」

针对老年代进行回收的一种算法,标记的过程和『标记-清除算法』一样,只是在清除完成后,会将还存活的对象朝着一个方向移动,然后固定的清理靠近边界的对象。

  • 优点
  1. 解决了碎片化
  • 缺点
  1. 效率不高
  2. 移动了对象地址,需要更新对象的引用
  • 使用场景

用于老年代垃圾回收

3、复制算法「比标记清理和标记整理快 10 倍以上」

能解决「标记-清理算法」带来碎片化问题,复制算法首先将内存分为大小相同的两块,每次只使用其中的一块,但这一块被使用完后「或者是没法提供所需的连续长度的内存」,就会将这一块的内存复制到另一块去,然后再一次性将这块的内存空间全部清理掉。

  • 优点
  1. 解决了碎片化
  2. 效率高
  • 缺点
  1. 内存使用率不高「总有至少一半内存空间没有使用」
  • 使用场景

用于年轻代垃圾回收

4、分代回收算法

​ 这种算法不是新鲜的算法,而是针对不同的内存分区,采用不同的回收算法,比如在新生代中,每次收集都会有大量对象(近 99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保「老年代多是大对象,很可能是需要连续内存地址的对象」,所以我们必须选择「标记清除算法」或「标记整理算法」进行垃圾收集。

垃圾收集器「回收算法的具体实现」

graph A[垃圾收集器] --> B[Serial 收集器] & C[ParNew 收集器] & D[Parallel 收集器] & E[CMS 收集器] & G[G1 收集器]

1、Serial 收集器「-XX:+UseSerialGC -XX:+UseSerialOldGC」

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

Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程「也就是应用程序线程」,直到它收集结束。

Serial 收集器执行过程

avatar

  • 优点
  1. 没有多线程交互,单线程实现简单;
  2. 相比其他单线程收集器,效率最高「当然是比不上多线程收集器」;
  • 缺点:
  1. STW 时间长,用户体验不好
  • 使用场景:

​ 一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

2、ParNew 收集器「-XX:+UseParNewGC」

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

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。默认的收集线程数跟 CPU 核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。

ParNew 收集器执行过程

avatar

  • 优点:
  1. 相比 Serial 效率高
  • 缺点:
  1. 实现稍复杂
  • 使用场景:

3、Parallel 收集器「-XX:+UseParallelGC(年轻代) -XX:+UseParallelOldGC(老年代)」

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

Parallel Scavenge 收集器关注点是吞吐量(高效率的利用CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。

avatar

  • 优点:

  • 缺点:

  • 使用场景:

Parallel 收集器执行过程

4、CMS 收集器「-XX:+UseConcMarkSweepGC(old)」

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

CMS(Concurrent Mark Sweep)以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

CMS 收集器执行过程

初始标记-》并发标记-》重新标记-》并发清理-》并发重置

其中只有『初始标记』不能和用户线程并发,其他的四个是可以的。

avatar

  • 优点:
  1. 并发收集、低停顿;
  2. 吞吐量高;
  • 缺点:
  1. 对 CPU 资源敏感(会和服务抢资源);
  2. 无法处理浮动垃圾(即在并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次 GC 再清理了);
  3. 它使用的回收算法“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数 -XX:+UseCMSCompactAtFullCollection 可以让 jvm 在执行完标记清除后再做整理;
  4. 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发 Full gc,也就是“concurrent mode failure”,此时会进入 stop the world,使用 serial old 垃圾收集器来回收;
  • 使用场景

注重用户体验的系统,低延时。

何为 concurrent mode failure 错误

5、G1 收集器「-XX:+UseG1GC」

一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。会预测的停顿的时间,以及一些抉择,比如 200ms 回收 10MB 和 50ms 回收 20MB 两种选择,会选择第二种,用以达到有限时间内最大的回收效率;

  • 优点:
  • 缺点:
  • 使用场景:

逃逸分析

public void test() {
		Person person = new Person();
}

上面的代码,会经历如下几个步骤:

  1. 加载 Person.class 到内存上;
  2. 在栈中开辟一段空间,用于 test 方法的入栈,然后在 test 方法的栈空间分配一个变量 p;
  3. 在堆内存中创建一块区域空间,分配内存地址「new 关键字的作用」;
  4. 对空间的属性空间分配,默认初始化;
  5. 构造函数初始化;
  6. 将分配的地址赋值给变量 p,即 p 指向了刚刚划分并且初始化好的堆地址;

按照上面的步骤,每个对象的分配,对象会直接分配在堆上,但如果需要分配的对象非常多,并且生命周期都比较短,比如在某个循环中一直 new 某一个类的对象,并且创建的对象不会作为返回值『或者是返回值的一部分 』,返回给调用者,那么这些数量多且生命周期短的对象,将会占用较多的堆空间,这些被占用的会由 GC 定时去清理,但如果有一种手段,尽量的让这些对象都存储在栈里面,也就是方法栈,这些对象的销毁会随着方法的出栈而消亡,就不再需要 GC 去耗费宝贵的时间和资源去回收堆内存了,STW 的时间自然也会短,GC 的次数也会少,这种手段就是逃逸分析,在 JDK8 中逃逸分析是默认开启。
『但一种手段的出现,肯定是有利也有弊,开启逃逸分析也会耗费时间和资源,就需要我们自己去测试分析,手上的项目是否合适,不能保证逃逸分析的性能收益必定高于它的消耗』

逃逸分析的分类

  • 方法逃逸
  • 线程逃逸

方法逃逸

当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其它方法中。

线程逃逸

这个对象甚至可能被其它线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量。

逃逸分析总结

当一个对象,在其生命周期内,被其他对象所持有,那么就会发生逃逸。

posted @ 2021-07-02 19:31  码上猿梦  阅读(77)  评论(0编辑  收藏  举报