深入理解Java虚拟机(二)——GC

1 概述

垃圾收集器(Garbage Collection)简称GC,1960年诞生与MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。上一节《深入理解Java虚拟机(一)——JVM内存结构》中了解到内存运行时区域的各个部分,其中程序计数器、本地方法栈、虚拟机栈3个区域随着线程而生存死亡,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理。所以GC主要的收集区域为Java堆和方法区,这部分的内存区域的分配和回收都是动态的。

虚拟机如何进行回收的呢,主要从三个方面考虑。

  • 哪些内存是需要回收的(如何判断对象为垃圾对象)?
  • 什么时候回收?
  • 怎样进行回收?

2 如何判断对象为垃圾对象

GC如何确定哪些对象还”存活“,哪些对象”死去“(即对象不再被任何对象使用)。主要通过以下两种方式:

  1. 引用计数法:在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效的时候计数器的值就-1。具有实现简单,判断效率高好处。但是这种算法不能实现对象之间的相互引用问题。
/**
 * 
 * @desc 对象 objA 和 objB 相互引用,引用计数均不为 0,除此之外无其他引用,因此如果用引用计数法则GC无法回收
 * VM Args -XX:+PrintGCDetails
 * -XX:+PrintGCDetails 打印gc详情信息 
 * @author lb
 * @date 2019年3月18日
 */
public class ReferenceCountGC {
	public Object instance = null;
	// 创建一个2M的内存
	private byte[] size = new byte[2 * 1024 * 1024];

	public static void main(String[] args) {
		ReferenceCountGC objA = new ReferenceCountGC();
		ReferenceCountGC objB = new ReferenceCountGC();
		objA.instance = objB;
		objB.instance = objA;
		objA = null;
		objB = null;

		// 手动调用GC
		System.gc();
	}
}

运行结果:
[GC (System.gc()) [PSYoungGen: 6717K->720K(76288K)] 6717K->728K(251392K), 0.0007462 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 720K->0K(76288K)] [ParOldGen: 8K->512K(175104K)] 728K->512K(251392K), [Metaspace: 2505K->2505K(1056768K)], 0.0042440 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 76288K, used 1966K [0x000000076b500000, 0x0000000770a00000, 0x00000007c0000000)
  eden space 65536K, 3% used [0x000000076b500000,0x000000076b6eba30,0x000000076f500000)
  from space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)
  to   space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000)
 ParOldGen       total 175104K, used 512K [0x00000006c1e00000, 0x00000006cc900000, 0x000000076b500000)
  object space 175104K, 0% used [0x00000006c1e00000,0x00000006c1e80198,0x00000006cc900000)
 Metaspace       used 2514K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 269K, capacity 386K, committed 512K, reserved 1048576K

从运行结果看可以看到,GC 执行后有内存从6717k到720k,所以表示Jvm没有因为对象相互引用而不回收,因此,Jvm并不是使用引用计数法判断对象是否为垃圾对象

  1. 可达性分析法:利用 GC Roots 向下链路查找,搜索的路径称为下路链,如果查不到则将查不到的对象及以下链路全部回收。可做为 GC Roots 的对象:虚拟机栈(局部变量表)中引用的对象、方法区的类属性所引用的对象、方法区中常量所引用的对象、本地方法栈中所引用的对象

3 垃圾收集算法

  1. 标记 - 清除算法:首先标记出需要回收的对象,在标记完成后,统一回收被标记的对象。存在不足:效率不高、空间问题(标记清除后会产生大量不连续的内存碎片,在后续产生大对象时,无法找到足够大的连续空间)
  2. 复制算法:将内存等分为两块,每次使用一块,当内存使用完了,就将存活的对象复制当另一块,然后将已使用的内存空间一次清理掉。实现简单、效率高,但是内存使用浪费太大。
    堆内存主要分为Eden空间和两个较小的Survivor空间,每次使用Eden和一个Survivor。Hotspot虚拟机默认Eden区域和Survivor区域为(8:1:1),也就是每次使用的内存为90%。当每次进行GC时,将Eden区的存活对象及Survivor的存活对象复制到另一块在Survivor区域中,当Survivor空间不够时,需要依赖其它内储存(老年代)进行分配担保。
  3. 标记 - 整理算法:主要使用在老年代的计算方式,标记过程与”标记 - 清除算法“一样,但是后续不是直接回收死亡的对象,而是让存活的对象向一端移动,然后直接清理掉端边界以外的内存。
  4. 分代收集算法:将Java堆分为新生代、老年代,根据不通特点采用不同的收集算法。新生代中,每次收集都会有大量的对象死去,选用复制算法。老年代对象存活率高,采用标记-清除或标记-整理算法。

4 垃圾收集器

4.1 Serial

  • Serial是最基本、单线程的收集器。进行垃圾收集时,必须停下来其它工作线程,直至收集结束。这种收集方式具有简单高效的优点。对于桌面应用场景,收集比较少的新生代内存,效率也是很高的。Serial 收集器也是虚拟机在Client模式下默认的新生代收集器。
    Serial运行示意图

4.2 ParNew

  • ParNew 就是 Serial 收集器的多线程版本,在性能无关的前提下,ParNew 是作为Server模式下Jvm首选的新生代收集器。
    ParNew运行示意图

4.3 Paraller Scavenge

  • Paraller Scavenge为新生代收集器,采用的为复制算法,也是并行的多线程收集器,与 ParNew 基本一致。主要的特点为达到可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)),-XX:MaxGCPauseMillis 垃圾收集器停顿的最大时间(大于0的毫秒数) -XX:GCTimeRatio 吞吐量大小(0,100)

4.4 CMS

  • CMS 收集器是一种以获取最短回收停顿时间为目标的收集器。是基于”标记 - 清除“算法实现的,主要包含初始标记、并发标记、重新标记、并发清除四个过程。优点:并发收集、低停顿,缺点:并发消耗CPU资源、无法收集浮动垃圾(并发标记的过程中,会有新的垃圾产生)、”标记 - 清除“算法的清除后空间不连续确定
    cms运行示意图

4.5 G1

  • G1 收集器是一款面向服务端应用的垃圾收集器,优点:并发与并行、分代收集、空间整合(采用"标记 - 整理"算法,不会产生内存空间碎片)、可预测的停顿(降低停顿时间)。主要包含初始标记、并发标记、最终标记、筛选回收四个步骤。
    G1运行示意图

5 垃圾收集器常用参数

jdk1.7中各种垃圾回收器参数使用:

参数 描述
UseSerialGC 虚拟机运行在Client模式下的默认值,打开此开关后,使用 Serial+Serial Old 的收集器组合进行内存回收
UseParNewGC 打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收
UseConcMarkSweepGC 打开此开关后,使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为 CMS 收集器出现 Concurrent Mode Failure 失败后的后备收集器使用
UseParallelGC 虚拟机运行在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器组合进行内存回收
UseParallelOldGC 打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收
SurvivorRatio 新生代中 Eden 区域与 Survivor 区域的容量比值,默认为8,代表 Eden : Survivor = 8 : 1
PretenureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
MaxTenuringThreshold 晋升到老年代的对象年龄,每个对象在坚持过一次 Minor GC 之后,年龄就增加1,当超过这个参数值时就进入老年代
UseAdaptiveSizePolicy 动态调整 Java 堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 区的所有对象都存活的极端情况
ParallelGCThreads 设置并行GC时进行内存回收的线程数
GCTimeRatio GC 时间占总时间的比率,默认值为99,即允许 1% 的GC时间,仅在使用 Parallel Scavenge 收集器生效
MaxGCPauseMillis 设置 GC 的最大停顿时间,仅在使用 Parallel Scavenge 收集器时生效
CMSInitiatingOccupancyFraction 设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集,默认值为 68%,仅在使用 CMS 收集器时生效
UseCMSCompactAtFullCollection 设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用 CMS 收集器时生效

6 内存分配和回收策略

  • 优先分配到Eden
  • 大对象直接分配到老年代:-XX:PretenureSizeThreshold 指定多大的对象分配到老年代,默认值是虚拟机根据内存计算
  • 长期存活的对象分配到老年代:-XX:MaxTenuringThreshold 指定垃圾回收多少次进入老年代,默认15次
  • 空间分配担保:-XX:+HandlePromotionFailure 是否允许使用空间分配担保
  • 逃逸分析及栈上分配

7 虚拟机工具

7.1 jdk 命令行工具

名称 主要作用
jps JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程
jstat JVM Statistics Monitoring Tool,用于收集HotSpot虚拟机各方面的运行数据
jinfo Configuration Info for Java,显示虚拟机配置信息
jmap Memory Map for Java,生成虚拟机的内存转储快照(heapdump文件)
jhat JVM Heap Dump Browser,用于分析heapdump文件,他会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果
jstack Stack Trace for Java,显示虚拟机的线程快照

7.1.1 jps 虚拟机进程状况工具

  • jps(JVM Process Status Tool)可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID。
命令格式 	
 	jps [options] [hostid]
 	 	
option参数
	-l : 输出主类全名或jar路径
	-q : 只输出LVMID,省略主类名称
	-m : 输出JVM启动时传递给main()的参数
	-v : 输出JVM启动时显示指定的JVM参数
	-mlv :  全部参数
	
其中[option]、[hostid]参数也可以不写。

7.1.2 jstat 虚拟机统计信息监控工具

  • jstat(JVM statistics Monitoring)用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
命令格式	
 	jstat [option] LVMID [interval] [count]
 	
参数
	[option] : 操作参数
	LVMID : 本地虚拟机进程ID
	[interval] : 连续输出的时间间隔
	[count] : 连续输出的次数

官方文档介绍(具体参数详解):https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
在这里插入图片描述
7.1.3 jinfo java配置信息工具

  • jinfo(JVM Configuration info)作用是实时查看和调整虚拟机运行参数。jps -v口令只能查看到显示指定的参数,如果想要查看未被显示指定的参数的值就要使用jinfo口令。
命令格式
	jinfo [option] [args] LVMID
	
option参数
	-flag : 输出指定args参数的值
	-flags : 不需要args参数,输出所有JVM参数的值
	-sysprops : 输出系统属性,等同于System.getProperties()

7.1.4 jmap java内存印象工具

  • jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)。jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。
命令格式
	 jmap [option] LVMID

option参数
	dump : 生成堆转储快照,格式为:-dump:[live, ] format=b,file=<filename>,其中live子参数说明是否只dump出存活的对象。
	finalizerinfo : 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象,只在Linux / Solaris平台有效
	heap : 显示Java堆详细信息,如使用哪种回收器、参数设置、分代状况等。只在Linux / Solaris平台有效
	histo : 显示堆中对象的统计信息,包括类、实例数量、合计容量
	permstat : 以ClassLoader为统计口径显示永久带内存状态。只在Linux / Solaris平台有效
	F : 当-dump没有响应时,强制生成dump快照。只在Linux / Solaris平台有效

7.1.5 jhat 虚拟机堆转储快照分析工具

  • jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在浏览器中查看。
命令格式
	jhat [option] [dumpfile]

参数
	-stack false|true 关闭对象分配调用栈跟踪(tracking object allocation call stack)。 如果分配位置信息在堆转储中不可用. 则必须将此标志设置为 false. 默认值为 true.
	-refs false|true 关闭对象引用跟踪(tracking of references to objects)。 默认值为 true. 默认情况下, 返回的指针是指向其他特定对象的对象,如反向链接或输入引用(referrers or incoming references), 会统计/计算堆中的所有对象。
	-port port-number 设置 jhat HTTP server 的端口号. 默认值 7000.
	-exclude exclude-file 指定对象查询时需要排除的数据成员列表文件(a file that lists data members that should be excluded 	from the reachable objects query)。 例如, 如果文件列列出了 java.lang.String.value , 那么当从某个特定对象 Object o 计算可达的对象列表时, 引用路径涉及 java.lang.String.value 的都会被排除。
	-baseline exclude-file 指定一个基准堆转储(baseline heap dump)。 在两个 heap dumps 中有相同 object ID 的对象会被标记为不是新的(marked as not being new). 其他对象被标记为新的(new). 在比较两个不同的堆转储时很有用.
	-debug int 设置 debug 级别. 0 表示不输出调试信息。 值越大则表示输出更详细的 debug 信息.
	-version 启动后只显示版本信息就退出
	-J  因为 jhat 命令实际上会启动一个JVM来执行, 通过 -J 可以在启动JVM时传入一些启动参数. 例如, -J-Xmx512m 则指定运行 jhat 的Java虚拟机使用的最大堆内存为 512 MB. 如果需要使用多个JVM启动参数,则传入多个 -Jxxxxxx.
示例:
jhat 快照地址(jmap后保存在硬盘的位置)
浏览器打开 http://localhost:7000

7.1.6 jhat Java堆栈跟踪工具

  • jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者等待着什么资源。
命令格式
	stack [option] LVMID
	
option参数

	-F : 当正常输出请求不被响应时,强制输出线程堆栈
	-l : 除堆栈外,显示关于锁的附加信息
	-m : 如果调用到本地方法的话,可以显示C/C++的堆栈

7.2 jdk 可视化工具

7.2.1 JConsole Java监视与管理控制台

  • JConsole 是一种基于 JMX 的可视化监控管理工具。

启动JConsole

  • 通过jdk/bin/下的“jconsole.exe”来启动,启动后将搜索出本机运行的所有虚拟机进程,不需要再使用 jps 来查询。双击进程即可开始监控。
    在这里插入图片描述
    在这里插入图片描述

7.2.2 VisualVM 多合一故障处理工具

  • VisualVM(All-in-One Java Troubleshooting Tool)是到目前为止随JDK发布的功能最强大的运行监视和故障处理程序,并且可以遇见在未来一段时间内都是官方主力发展的虚拟机故障处理工具。官方在VisualVM的软件说明中写上了“All-in-One”的描述字样,预示着他除了运行监视、故障处理外,还提供了很多其他方面的功能。如性能分析(Profiling),VisualVM的性能分析功能甚至比起JProfiler、YourKit等专业且收费的Profiling工具都不会逊色多少,而且VisualVM还有一个很大的优点:不需要被监视的程序基于特殊Agent运行,因此他对应用程序的实际性能的影响很小,使得他可以直接应用在生产环境中。这个优点是JProfiler、YourKit等工具无法与之媲美的。

下载地址:https://visualvm.github.io/download.html

软件示例图:
在这里插入图片描述

8 性能调优

jvm调优没有固定的方式,根据不同的服务器、应用部署情况选择恰当的参数设置,执行最优的性能。以下为调优建议:

  • 当服务器性能高、内存大,应用中创建大对象过多,此时大对象会直接放入老年代,当老年代溢出时,会进行垃圾回收(Full GC),如果服务器的堆内存过大,则会回收时间很长,出现卡顿现象。解决方式:可部署多个web节点,每个web节点适当减少堆内存的大小。
  • 处理不对等数据处理时,例如一台服务器不断被请求,请求的速度大于了处理请求的速度,请求被积压在堆中,导致内存溢出。解决方式:可在两台设备中间添加一个消息队列,实现异步处理。
  • 创建对象尽量在方法内,而不是使用用成员变量。

猜你喜欢

转载自blog.csdn.net/qq_34929019/article/details/88636133
今日推荐