JVM专题3: GC 垃圾回收

什么是GC? 为什么要有 GC?

Garbage Collection, 用于内存回收.


简述一下 Java 垃圾回收机制?

那些内存需要回收
虚拟机中程序计数器, 本地方法区, 虚拟机栈随着线程而消亡. 栈中的栈帧随着方法调入和调出而产生和消亡, 垃圾回收主要考虑的是堆和方法区.
堆中的对象实例是主要回收的内容, 需要判断是否不再被使用, 主流的虚拟机都是通过可达性算法来实现. 通过GC Root对象为起点, 从这些节点开始搜索, 走过的路径就是引用链, 如果一个对象没有任何引用链可以连接到GC Root则判断为需要回收.
GC Root包括如下几种:

  • 虚拟机栈中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

以上是判断什么需要回收的条件, 所有的描述都是围绕着“引用”来讨论的, 关于引用又有4中强度的区分

  • 强引用. 只要存在就不会被回收
  • 软引用. 还有用但并非必须的对象, 系统会在要发生内存溢出之前对这些对象进行二次回收. 使用SoftReference实现
  • 弱引用. 有用但并非必须, 对象只能生存到下一次垃圾回收之前. 使用WeakReference实现
  • 虚引用/幻象引用. 最弱的一种引用关系, 无法通过虚引用获取对象实例. 只是在回收的时候收到一个系统通知, 使用PhantomReference来实现.

什么时候回收
对象不可达表示可以回收, 但是不是马上就回收, 只是标记而已, 真正到达回收的过程, 需要两次标注.
发现对象不可达, 进行一次标注并且进行一次筛选: 看对象是否覆盖了finalize(), 如果覆盖了并且虚拟机没有执行过finalize方法, 则放入F-Queue队列中, 表示等待销毁. 在finalize方法中可以拯救对象, 只要这个时候与GC Root产生引用链, 依然可以摆脱回收

方法区也就是永久代也有可能进行垃圾回收。主要是回收废弃的常量和无用的类, 但是效率很差.

  • 判断是否为废弃的常量比较简单, 只要没有常量的引用即可
  • 判断是否为无用的类比较麻烦, 至少需要满足下面的条件
    • 类的所有实例已被回收
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有任何地方被引用, 无法通过反射构造此类

怎么回收

  • 标记-清除算法: 效率不高, 会产生空间碎片
  • 复制算法: 使用一半的内存, 交替使用. 现代虚拟机不使用1:1的内存方式, 使用一块大的Eden空间和两个小的Survivor空间. HotSpot默认的比例是8:1, 当Survivor内存不够的时候, 就会使用老年代进行分配担保.
  • 标记-整理算法: 一般用于老年代
  • 分代收集算法: java的堆分为新生代和老年代, 采用不同的算法. 新生代使用复制算法, 老年代使用标记清除或者标记整理.

如何判断一个对象是否存活?

主流的虚拟机都是通过可达性算法来实现. 通过GC Root对象为起点, 从这些节点开始搜索, 走过的路径就是引用链, 如果一个对象没有任何引用链可以连接到GC Root则判断为需要回收

对象不可达表示可以回收, 但是不是马上就回收, 只是标记而已, 真正到达回收的过程, 需要两次标注.
发现对象不可达, 进行一次标注并且进行一次筛选: 看对象是否覆盖了finalize(), 如果覆盖了并且虚拟机没有执行过finalize方法, 则放入F-Queue队列中, 表示等待销毁. 在finalize方法中可以拯救对象, 只要这个时候与GC Root产生引用链, 依然可以摆脱回收


垃圾回收的优点和原理, 并考虑 2 种回收机制?

垃圾回收器的基本原理是什么?

收集器三大目标memory footprint, throughput, latency是一组三元悖论, 整体随着技术进步而提升, 但这三方面难以同时达成.

  • Throughput Throughput is a measure of the amount of work that can be performed per unit time. A throughput requirement ignores latency or responsiveness. Usually, increased throughput comes at the expense of either an increase in latency and/or an increase in memory footprint.
    An example of a performance throughput requirement is “the application shall execute 2500 transactions per second.”

  • Latency and Responsiveness Latency, or responsiveness, is a measure of the elapsed time between when an application receives a stimulus to do some work and that work is completed. A latency or responsiveness requirement ignores throughput. Usually, increased responsiveness or lower latency, comes at the expense of lower throughput and/or an increase in memory footprint.
    An example of a latency or responsiveness requirement is “the application shall execute trade requests to completion within 60 milliseconds.”

  • Memory Footprint Memory footprint is a measure of the amount of memory required to run an application at a some level of throughput, some level of latency, and/or some level of availability and manageability. Memory footprint is usually expressed as either the amount of Java heap required to run the application and/or the total amount of memory required to run the application. Usually, an increase in memory footprint via an increase in Java heap size can improve throughput or reduce latency, or both. As the memory made available for an application is decreased, throughput or latency is generally sacrificed.

CMS的目标是低延迟, ps/old的目标是高吞吐量, 把它们强行糅在一块确实没有必要. 如果一定要这样做, 就只能牺牲复杂度了, 例如G1, 在延迟和吞吐量上表现都还可以, 官方称其为 fully-featured garbage collector 也是意有所指, 但这时候它内存占用就相当可观了.

https://www.jianshu.com/p/bad434203ea2
Serial Garbage Collector
单线程, 会暂停所有的工作线程进行垃圾回收, 默认用于Java client中。设置参数:-XX:+UseSerialGC
Serial Garbage Collector Old
Serial收集器的老年代版本, 作为CMS收集器的后备预案, 在并发收集发生Concurrent Mode Failure时使用

ParNew
Serial的多线程版本, 也会暂停所有的工作线程, 是CMS的默认ParNew新生代收集器. 设置参数
-XX:+UseConcMarkSweepGC, 此时默认使用
-XX:+UseParNewGC, 强制使用
-XX:ParallelGCThreads, 指定垃圾收集的线程数量, ParNew默认开启的收集线程与CPU的数量相同

Parallel Scavenge
新生代的收集器, 使用复制算法, 并行的多线程收集器。是一种以最大吞吐量为目标的方案.
-XX:MaxGCPauseMillis, 用于设置回收器的停顿时间;
-XX:GCTimeRatio, 用于设置吞吐量的大小;
-XX:+UseAdaptiveSizePolicy, 打开这个参数则系统会自动设置很多参数, 根据GC运行的情况进行调节(自省是这个收集器的主要特点)
Parallel Garbage Collector Old
Parallel Scavenge收集器的老年代版本. 因为代码上的原因, parallel scavenge/old没有遵循hotspot早期的分代框架, 就是那堆Generation类(DefNewGeneration, ParNewGeneration, etc), 导致它不好与其他早期收集器搭配协作, 这点也是CMS能与ParNew配合却无法与ps配合的原因.

CMS
CMS是一种以最小停顿为目标的方案. 基于标记-清除的算法. GC过程暂停短, 适合对latency要求高, 用户线程不允许长时间停顿的服务. 其新生代GC默认使用ParNew, 老年代GC失败时使用Serial GC Old

-XX:+UseConcMarkSweepGC, 指定使用CMS收集器
-XX:+UseCMSCompactAtFullCollection, 打开这个参数则在需要Full GC的时候使用内存碎片的合并整理过程
-XX:+CMSFullGCsBeforeCompaction, 设置执行多少次不压缩的Full GC后, 来一次压缩整理, 为减少合并整理过程的停顿时间, 默认值为0
过程步骤:

  1. Idling
  2. Initial Marking
  3. Marking
  4. Precleaning Abortable Prelean
  5. Final Marking
  6. Sweeping
  7. Resizing
  8. Resetting

对象在标记过程中, 根据标记情况, 分成三类:

  • 白色对象, 表示自身未被标记;
  • 灰色对象, 表示自身被标记, 但内部引用未被处理;
  • 黑色对象, 表示自身被标记, 内部引用都被处理;

CMS调优详解 https://backstage.forgerock.com/knowledge/kb/article/a35746010

G1
https://medium.com/@hoan.nguyen.it/how-did-g1gc-tuning-flags-affect-our-back-end-web-app-c121d38dfe56
https://backstage.forgerock.com/knowledge/kb/article/a35746010
https://backstage.forgerock.com/knowledge/kb/article/a75965340
G1 garbage collector is used for large heap memory areas. It separates the heap memory into regions and does collection within them in parallel. G1是一种适用于超大内存JVM的收集器, 在大内存环境下依然能保证较低的停顿, 以及较高的吞吐量. 基于的算法是并行与并发, 分代收集, 空间整合, 可预期的停顿.

-XX:+UseG1GC, 指定使用G1收集器;
-XX:G1NewSizePercent=30
-XX:InitiatingHeapOccupancyPercent, 当整个Java堆的占用率达到参数值时, 开始并发标记阶段;默认为45;
-XX:ParallelGCThreads=8, 并发数量, 根cpu逻辑核数一致 according to Oracle, equals to 5/8 of logical processors
-XX:ConcGCThreads=2, roughly equals to 1/4 of parallel GC threads
-XX:MaxGCPauseMillis, 为G1设置暂停时间目标, 默认值为200毫秒;
-XX:G1HeapRegionSize, 设置每个Region大小, 范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region
尽量让JVM不要产生Full GC, 而是通过Mixed GC解决问题
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

其中 -XX:+UseG1GC 为开启G1垃圾收集器, -Xmx32g 设计堆内存的最大内存为32G, -XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms.
如果我们需要调优, 在内存大小一定的情况下, 我们只需要修改最大暂停时间即可.

G1将新生代, 老年代的物理空间划分取消了, 这样我们再也不用单独的空间对每个代进行设置了, 不用担心每个代内存是否足够. 取而代之的是将堆划分为若干个Region, 这些Region的一部分包含新生代, 新生代的垃圾收集依然采用暂停所有应用线程的方式, 将存活对象拷贝到老年代或者Survivor空间; 老年代也分成很多Region, G1收集器通过将对象从一个Region复制到另外一个Region 完成清理工作. 这就意味着, 在正常的处理过程中, G1完成了堆的压缩, 这样也就不会有cms内存碎片问题的存在了.
在G1中还有一种特殊的Region 叫 Humongous Region, 如果一个对象占用的空间超过了分区容量50%以上, G1收集器就认为这是一个巨型对象, 这些巨型对象默认直接会被分配在年老代, 但是如果它是一个短期存在的巨型对象, 就会对垃圾收集器造成负面影响, 为了解决这个问题, G1划分了一个Humongous Region, 用来专门存放巨型对象. 如果一个H区装不下一个巨型对象, 那么G1会寻找连续的H分区来存储, 为了能找到连续的H区, 有时候不得不启动Full GC

如果仅仅GC 新生代对象, 我们如何找到所有的根对象呢? 老年代的所有对象都是根么? 那这样扫描下来会耗费大量的时间。于是, G1引进了RSet的概念。它的全称是Remembered Set, 作用是跟踪指向某个heap区内的对象引用. 在CMS中, 也有RSet的概念, 在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out, 在进行Young GC时, 扫描根时, 仅仅需要扫描这一块区域, 而不需要扫描整个老年代.


垃圾回收器可以马上回收内存吗? 有什么办法主动通知虚拟机进行垃圾回收?

在 Java 中, 对象什么时候可以被垃圾回收?

要看是哪一种垃圾回收器, 有些是分步骤进行的, 会逐渐回收
可以手动执行 System.gc() 和 Runtime.getRuntime().gc() 通知GC运行, 但是Java语言规范并不保证GC一定会执行


简述Minor GC 和 Major GC?

Minor GC: collecting garbage from Young space (consisting of Eden and Survivor spaces)
Major GC is cleaning the Tenured space.
Full GC is cleaning the entire Heap – both Young and Tenured spaces.


Java 中垃圾收集的方法有哪些?

JDK 7, JDK8 默认使用的是ParallelGC, Parallel Scavenger(新生代) 和 Parallel Old(老年代)

JDK9 默认使用的是G1


System.gc() 和 Runtime.gc() 会做些什么?

java.lang.System.gc()只是java.lang.Runtime.getRuntime().gc()的简写, 两者的行为没有任何不同。唯一要能说有什么不同那就是在字节码层面上调用前者比调用后者短一点点, 前者是1条字节码而后者是2条.
System.gc() 就是呼叫java虚拟机的垃圾回收器运行回收内存的垃圾, 这个命令只是建议JVM安排GC运行, 还有可能完全被拒绝. GC本身是会周期性的自动运行的.

要触发GC, 一个是进程内外皆可用的, 基于JMX(Java Management Extensions)的方式java.lang.management.MemoryMXBean.gc() 例如说JConsole里的“GC”按钮就是用这个方式实现的, 参考 http://hllvm.group.iteye.com/group/topic/35871
另一个是Oracle/Sun系的JVM所支持的attach API, 从Java 6开始可以用. 例如说通过jmap -histo:live这样会触发一次full GC, 这个就是基于attach API来实现的.


什么是分布式垃圾回收DGC, 它是如何工作的?

RMI (Java Remote Method Invocation)子系统实现基于引用计数的“分布式垃圾回收DGC, 以便为远程服务器对象提供自动内存管理设施. 当客户机创建序列化远程引用时, 会在服务器端 DGC 上调用 dirty(), 当客户机完成远程引用后, 它会调用对应的 clean() 方法.
针对远程对象的引用由持有该引用的客户机租用一段时间, 租期从收到 dirty() 调用开始, 在此类租约到期之前, 客户机必须通过对远程引用额外调用 dirty() 来更新租约, 如果客户机不在租约到期前进行续签, 那么分布式垃圾收集器会假设客户机不再引用远程对象.


串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?

这两者都会冻住所有线程, 只是一个单线程, 一个多线程, 对于G1, 大部分时间只会冻住部分线程

Serial garbage collector
works by holding all the application threads. It is designed for the single-threaded environments. It uses just a single thread for garbage collection. The way it works by freezing all the application threads while doing garbage collection may not be suitable for a server environment. It is best suited for simple command-line programs.
Turn on the -XX:+UseSerialGC JVM argument to use the serial garbage collector.

Parallel Garbage Collector is also called as throughput collector.
It is the default garbage collector of the JVM(JDK7 & JDK8, not JDK9). Unlike serial garbage collector, this uses multiple threads for garbage collection. Similar to serial garbage collector this also freezes all the application threads while performing garbage collection.


讲讲你理解的性能评价及测试指标?

性能评价: 在给定条件(如并发数)下的吞吐量, 响应时间错误率. 反过来, 根据最佳的吞吐量和响应时间组合, 决定节点的最大并发量.

web服务: 响应时间(平均值, 90%线, 95%线, 99%线), 错误率error rate, 吞吐量throughput(TPS), 数据流量(KB/s)

1 可以通过 top 和 vmstat 查看load状况
2 通过ps -eLf | grep java | wc –l 统计java线程 通过ps -eLf | grep httpd | wc –l 统计 apache线程 这样可以判断是否是机器在超负荷运转. 也可通过日志大小判断.
3 通过filter日志判断系统慢在什么地方.
4 通过debug日志判断cache, 数据库或者依赖的其他系统是否正常.
5 通过dump 线程查看线程都在干什么.
6 通过jstat 查看java gc状况.
7 通过jmap dump内存 查看java 内存是否存在泄漏.
8 通过sar看看机器历史记录有助问题排查.


jstack如何使用

jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。

通过top命令定位到cpu占用率较高的线程之后,继续使用jstack pid命令查看当前java进程的堆栈状态. jstack命令生成的thread dump信息包含了JVM中所有存活的线程,为了分析指定线程,必须找出对应线程的调用栈,应该如何找?

在top命令中,已经获取到了占用cpu资源较高的线程pid,将该pid转成16进制的值,在thread dump中每个线程都有一个nid,找到对应的nid即可
隔段时间再执行一次stack命令获取thread dump, 区分两份dump是否有差别, 在nid=0x246c的线程调用栈中, 发现该线程一直在执行JstackCase类第33行的calculate方法,得到这个信息,就可以检查对应的代码是否有问题。

通过thread dump分析线程状态

除了上述的分析,大多数情况下会基于thead dump分析当前各个线程的运行情况,如是否存在死锁、是否存在一个线程长时间持有锁不放等等。

在dump中,线程一般存在如下几种状态:
1、RUNNABLE,线程处于执行中
2、BLOCKED,线程被阻塞
3、WAITING,线程正在等待


常用的性能优化方式有哪些?

确定优化的方向和指标, 是要吞吐量, 还是要延迟, 设置一个错误率下限

分离静态的, 第三方的资源

增加CPU, 内存, 提高磁盘IO

设置固定大小的Xms, MetaspaceSize, 观察在多次Major GC, 稳定后, HEAP里老年代tenured gen的大小, 将Young gen设置为1~1.5倍大小, 将老年代设为至少2倍大小, 整体Xms, Xmx应该至少为3~3.5倍大小

拆分, 使用多个服务节点

数据库分库

使用缓存

使用队列减小峰值压力


什么是GC调优?

减小gc频率, 减小gc停顿时间, 尽量避免major gc失败, 避免full gc造成服务整体的停顿, 限制gc的时间预期, 保证gc时间不会超过指定的长度
调节的参数主要是根据应用场景和内存大小选择合适的GC收集器, 配置触发百分比, 配置并发线程数, 配置同步线程数, 以及其他跟收集器相关的参数, 例如G1里的延迟上限和region大小

posted on 2022-01-15 21:16  Milton  阅读(123)  评论(0编辑  收藏  举报

导航