该文章为科普文,所以很多细节涉及不到,旨在指引入门,同事在聊的时候不至于插不上话,顺带回顾部分JVM相关知识。准备好了吗,开始发车。如有不正确的地方,欢迎批评指正。
目录
JVM调优调的到底是什么
回顾相关JVM的知识
我的对象在哪里?运行时数据区
堆
Methoad Area
栈
PC Register
本地方法栈
直接内存
我的对象分配在哪?内存分配策略
凭什么我是垃圾,他不是。什么算垃圾
引用计数法
可达性分析法
垃圾是如何被回收的?
垃圾回收触发条件
垃圾回收算法
垃圾的终结者,垃圾收集器
JVM调优调的到底是什么
不会还有人不知道什么是JVM吧?
调优的最终目的都是为了应用程序使用最小的硬件消耗来承载更大的吞吐量。所以我们可以通过调整堆分配的大小以及垃圾回收对象的方式,让其高效释放掉内存空间达到我们的目的。
一般情况下我们的调优目标是:
- 堆内存使用率 <= 70%;
- 老年代内存使用率<= 70%;
- avg pause <= 1秒;
- Full GC 次数 0 或 avg pause interval >= 24小时 ;
- 创建更多的线程
如果程序运行时jvm的内存与我们的调优目标不符或者过于离谱,我们则需要调优。
jvm调优是不得已手段,一般来说jvm默认配置是够我们用的,我们在调优之前要优先架构调优和代码调优。
小结:我们调优调的是内存分配 + 垃圾回收。
回顾相关JVM的知识
我的对象在哪里?运行时数据区
堆
供各线程共享的运行时内存区域,对于大多数应用来说,是 Java 虚拟机所管理的内存区域中最大的一块。几乎所有的对象实例和数组实例都要在 Java 堆上分配,但随着 JIT 编译器及逃逸分析技术的发展,也可能会被优化为栈上分配。
Heap 中除了作为对象分配使用,还包含字符串字面量 常量池(Internd Strings) 。 除此之外 Heap 中还包含一个 新生代(Yong Generation) 、一个 老年代(Old Generation) 。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。
新生代分三个区,一个Eden区,两个Survivor区,大部分对象在Eden区中生成。Survivor 区总有一个是空的。
老年代中保存一些生命周期较长的对象,当一个对象经过多次的 GC 后还没有被回收,那么它将被移动到老年代。
Java 堆不需要连续内存,并且可以通过动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
Methoad Area
方法区的数据由所有线程共享,因此为安全的使用方法区的数据,需要注意线程安全问题。
方法区主要保存类级别的数据,包括:
-
ClassLoader Reference
-
Runtime Constant Pool
- 数字常量
- 类属性引用
- 方法引用
-
Field Data:每个类属性的名称、类型等
-
Methoad Data:每个方法的名称、返回值类型、参数列表等
-
Methoad Code:每个方法的字节码、本地变量表等
栈
当线程启动时,都会分配一个独立的运行时栈,用以保存方法调用。每个方法调用,都会在栈顶增加一个栈帧(Stack Frame)。
每个栈帧都保存三个引用:本地变量表(Local Variable Array) 、 操作数栈(Operand Stack) 和 当前方法所属类的运行时常量池(Runtime Constant Pool) 。
栈的大小可以动态扩展,默认1MB,但是如果一个线程需要的栈大小超过了允许的大小,就会抛出 StackOverflowError
。
PC Register
每个 JVM 线程,当线程启动时,都会有一个独立的 PC(Program Counter) 计数器,用来保存当前执行的代码地址(方法区中的内存地址)。如果当前方法是 Native 方法,PC 的值为 NULL。一旦执行完成,PC 计数器会被更新为下一个需要执行代码的地址。
本地方法栈
本地方法栈和 Java 虚拟机栈的作用相似,Java 虚拟机栈执行的是字节码,而本地方法栈执行的是 native
方法。
直接内存
使用过NIO的小伙伴们都知道这个玩意,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer
对象作为这块内存的引用进行操作。
我的对象分配在哪?内存分配策略
大多数情况下,对象在新生代 Eden 区分配。因为有JIT优化的逃逸分析,未逃逸的对象也会分配到栈上。
对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组,直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
凭什么我是垃圾,他不是。什么算垃圾
jvm如何鉴别你的对象是不是垃圾以便回收。
引用计数法
给每个对象添加一个引用计数器,当一个地方引用它的时候,计数器值加1;当引用失效后,计数器值减1。但是这种方法有一个致命的缺陷,当两个对象相互引用时会导致这两个都无法被回收。
可达性分析法
追踪从根结点开始的 引用图。起点即 GC Root,GC Root 根据 JVM 实现不同而不同,但是总会包含以下几个方面(堆外引用):
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中的类静态属性引用的变量。
- 方法区中的常量引用的变量。
- 本地方法 JNI 的引用对象。
引用图是一个有向图,其中节点是各个对象,边为引用类型。JVM 中的引用类型分为四种:强引用(StrongReference) 、软引用(SoftReference) 、弱引用(WeakReference) 和 虚引用(PhantomReference) 。
- 强:禁止引用目标被垃圾收集器收集
- 软:垃圾收集器在要发生内存溢出前将这些对象列入回收范围中进行回收
- 弱:垃圾收集器在 GC 的时候会回收所有的 WeakReference
- 虚:所有的虚引用都必须由程序明确的清除。同时也不能通过虚引用来取得一个对象的实例,使用虚引用的目的就是为了得知对象被GC的时机。虚引用有一个很重要的用途就是用来做堆外内存的释放,DirectByteBuffer就是通过虚引用来实现堆外内存的释放的。你不可不知的Java引用类型之——虚引用 - 弗兰克的猫 - 博客园 (cnblogs.com)
垃圾是如何被回收的?
垃圾回收触发条件
Young GC(Minor GC) :只收集 Young Gen 的 GC,当 Eden 区空间不够时,发起 Minor GC。
Old GC:只收集 Old Gen 的 GC。只有 CMS的 Concurrent Collection 是这个模式。它主要是定时去检查 Old Gen 的使用量,当使用量超过了触发比例就会启动一次 GC,对 Old Gen做并发收集。
Mixed GC:收集整个 Young Gen 以及部分 Old Gen 的 GC。只有 G1 有这个模式。
Full GC(Major GC) :收集整个堆,包括 Young Gen、Old Gen、Perm Gen(如果存在的话)等所有部分的 GC 模式。1、当准备要触发一次 Young GC 时,如果发现之前 Young GC 的平均晋升大小比目前 Old Gen剩余的空间大,则不会触发 Young GC 而是转为触发 Full GC。2、调用 System.gc()。3、老年代空间不足。
垃圾回收算法
- 复制回收算法:内存使用率低,将可用内存分为大小相等的两份,在同一时刻只使用其中的一份。当这一份内存使用完了,就将还存活的对象复制到另一份上,然后将这一份上的内存清空。复制算法能有效避免内存碎片。
- 标记清除算法:产生大量不连续的内存碎片。先暂停整个程序的全部运行线程,让回收线程以单线程进行扫描标记,并进行直接清除回收,然后回收完成后,恢复运行线程。
- 标记整理算法:可以集成空闲空间,与标记清除算法类似,但是回收期间同时会将保留的存储对象搬运汇集到连续的内存空间
- 增量回收:将内存空间分成若干分区(Region)。程序运行所需的存储对象会分布在这些分区中,每次只对其中一个分区进行回收操作,从而避免程序全部运行线程暂停来进行回收,降低回收时间,增加程序响应速度。
- 分代回收思想:还记得上边说过的新生代和老年代吗,就是对于不同生命周期的对象进行回收,不同的生命周期也可以采用不同的垃圾回收算法,以提高效率。
垃圾的终结者,垃圾收集器
连线表示垃圾收集器可以配合使用,常见的配合下图所示
简单介绍
-
Serial 收集器:它是单线程的收集器,只会使用一个线程进行垃圾收集工作。它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。它是 Client 模式下的默认新生代收集器。
-
ParNew 收集器:它是 Serial 收集器的多线程版本。是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
-
Parallel Scavenge 收集器:与 ParNew 一样是多线程收集器。其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
-
Serial Old收集器:是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。
-
Parallel Old 收集器:是 Parallel Scavenge 收集器的老年代版本。
-
CMS 收集器:
-
四个流程如下:
- 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除: 不需要停顿
- 具有以下缺点:
- 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC
-
- G1 收集器:是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。
- 流程:
- 初始标记
- 并发标记
- 最终标记: 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收: 首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
- 特点:
- 空间整合: 整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿: 能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
- 流程: