1.jvm结构图
1.1 整体结构图
1.2 执行流程图
1.3 内存结构图
2. jvm 内存分区
Java 8 及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
- Young Generation Space 新生区 Young/New 又被划分为 Eden 区和 Survivor 区
- Tenure generation space 养老区 Old/Tenure
- Meta Space 元空间 Meta
Java 堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen)
其中年轻代又可以划分为 Eden 空间、Survivor0 空间和 Survivor1 空间(有时也叫做 from 区、to 区)
配置新生代与老年代在堆结构的占比。
- 默认
-XX:NewRatio=2
,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3 - 可以修改
-XX:NewRatio=4
,表示新生代占 1,老年代占 4,新生代占整个堆的 1/5
在 HotSpot 中,Eden 空间和另外两个 survivor 空间缺省所占的比例是 8:1:1
几乎所有的 Java 对象都是在 Eden 区被 new 出来的。绝大部分的 Java 对象的销毁都在新生代进行了。
3.GC介绍
3.1 GC概述
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序的结束,被保留的空间无法被其它对象使用,甚至可能导致内存溢出。
jvm垃圾回收是自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险。自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发。
3.2 GC作用区域
GC关注的主要区域:方法区 和堆中的垃圾收集。
垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收。Java堆是垃圾收集器的工作重点。
- 频繁收集Young区
- 较少收集Old区
- 基本不收集Perm区(元空间)
4. GC算法
4.1 GC算法之标记阶段算法--判断对象存活的方式
引用计数算法( JVM 中采用的不是引用计数器的算法) :
对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
优点:
实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性
缺点:
它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
可达性分析算法:
可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)。
如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
GC Roots包括以下几类元素:
- 虚拟机栈中引用的对象 :各个线程被调用的方法中使用到的参数、局部变量等。
- 本地方法栈内JNI(通常说的本地方法)引用的对象
- 方法区中类静态属性引用的对象:Java类的引用类型静态变量
- 方法区中常量引用的对象 :字符串常量池(String Table)里的引用
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用:基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。
- 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
4.2 GC算法之清除阶段:标记-清除算法
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
目前在JVM中比较常见的三种垃圾收集算法是:
- 标记一清除算法(Mark-Sweep)
- 当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
- 标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
- 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收
- 缺点:
- 标记清除算法的效率不算高
- 在进行GC的时候,需要停止整个应用程序,用户体验较差
这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表。
- 复制算法(copying)
为了解决标记-清除算法在垃圾收集效率方面的缺陷。核心思想是:将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:
没有标记和清除过程,实现简单,运行高效
复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点:
此算法的缺点也是很明显的,就是需要两倍的内存空间。
对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小
注意:
如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。
使用场景:
在新生代,对常规应用的垃圾回收,一次通常可以回收70% - 99% 的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代
- 标记-压缩算法(Mark-Compact)
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
标记一清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。标记-压缩(Mark-Compact)算法由此诞生。
执行过程:
第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
第三阶段 清理边界外所有的空间
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
优点:
消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
消除了复制算法当中,内存减半的高额代价。
缺点:
从效率上来说,标记-整理算法要低于复制算法。
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
移动过程中,需要全程暂停用户应用程序。即:STW
4.3 算法对比
Mark-Sweep | Mark-Compact | Copying |
---|---|---|
中等 | 最慢 | 最快 |
少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍空间(不堆积碎片) |
否 | 是 | 是 |
4.4 分代收集算法
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
目前几乎所有的GC都采用分代收集算法执行垃圾回收的
4.5 对象的finalization机制
Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
注:永远不要主动调用某个对象的finalize()方法I应该交给垃圾回收机制调用。
- 在finalize()时可能会导致对象复活。
- finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
- 一个糟糕的finalize()会严重影响Gc的性能。
5. GC流程
概述
JVM 在进行 GC 时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
针对 Hotspot VM 的实现,它里面的 GC 按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)
-
部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为:
-
新生代收集(Minor GC / Young GC):只是新生代的垃圾收集
-
老年代收集(Major GC / Old GC):只是老年代的圾收集。
- 目前,只有 CMSGC 会有单独收集老年代的行为。
- 注意,很多时候 Major GC 会和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。
-
混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集。
- 目前,只有 G1 GC 会有这种行为
-
-
整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集。
年轻代 GC(Minor GC):
- 当年轻代空间不足时,就会触发 MinorGC,这里的年轻代满指的是 Eden 代满,Survivor 满不会引发 GC。(每次 Minor GC 会清理年轻代的内存。)
- 因为Java 对象大多都具备朝生夕灭的特性.,所以 Minor GC 非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
- Minor GC 会引发 STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
老年代 GC(Major GC / Full GC):
-
指发生在老年代的 GC,对象从老年代消失时,我们说 “Major GC” 或 “Full GC” 发生了
-
出现了 Major Gc,经常会伴随至少一次的 Minor GC(但非绝对的,在 Paralle1 Scavenge 收集器的收集策略里就有直接进行 MajorGC 的策略选择过程)
- 也就是在老年代空间不足时,会先尝试触发 Minor Gc。如果之后空间还不足,则触发 Major GC
-
Major GC 的速度一般会比 Minor GC 慢 10 倍以上,STW 的时间更长
-
如果 Major GC 后,内存还不足,就报 OOM 了
Full GC 触发机制
触发 Full GC 执行的情况有如下五种:
- 调用 System.gc()时,系统建议执行 Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存
- 由 Eden 区、survivor space0(From Space)区向 survivor space1(To Space)区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
说明:Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些
6. jvm调优
6.1 概述
为什么要调优
防止出现OOM,解决OOM,减少Full GC出现的频率。
监控的依据
运行日志,异常堆栈,GC日志,线程快照,堆转储快照
调优的大方向
合理地编写代码,充分并合理的使用硬件资源,合理地进行JVM调优
6.2 性能优化的步骤
第1步:性能监控
GC频繁,cpu load过高,OOM,内存泄露,死锁,程序响应时间较长
第2步:性能分析
- 打印GC日志,通过GCviewer或者 gceasy.io 来分析异常信息
- 灵活运用命令行工具、jstack、jmap、jinfo等
- dump出堆文件,使用内存分析工具分析文件
- 使用阿里Arthas、jconsole、JVisualVM来实时查看JVM状态
- jstack查看堆栈信息
第3步:性能调优
- 适当增加内存,根据业务背景选择垃圾回收器
- 优化代码,控制内存使用
- 增加机器,分散节点压力
- 合理设置线程池线程数量
- 使用中间件提高程序效率,比如缓存、消息队列等
- 其他……
停顿时间(或响应时间)参考:
提交请求和返回该请求的响应之间使用的时间,一般比较关注平均响应时间。常用操作的响应时间列表:
操作 | 响应时间 |
---|---|
打开一个站点 | 几秒 |
数据库查询一条记录(有索引) | 十几毫秒 |
机械磁盘一次寻址定位 | 4毫秒 |
从机械磁盘顺序读取1M数据 | 2毫秒 |
从SSD磁盘顺序读取1M数据 | 0.3毫秒 |
从远程分布式换成Redis 读取一个数据 | 0.5毫秒 |
从内存读取 1M数据 | 十几微妙 |
Java程序本地方法调用 | 几微妙 |
网络传输2Kb数据 | 1 微妙 |
6.3 监控和诊断方式
(1) 命令行方式
jps [options] [hostid] ---查看正在运行的Java进程
显示指定系统内所有的HotSpot虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程。
-q:仅仅显示LVMID(local virtual machine id),即本地虚拟机唯一id。不显示主类的名称等
-l:输出应用程序主类的全类名 或 如果进程执行的是jar包,则输出jar完整路径
-m:输出虚拟机进程启动时传递给主类main()的参数
-v:列出虚拟机进程启动时的JVM参数。比如:-Xms20m -Xmx50m是启动程序指定的jvm参数。
jstat - [-t] [-h] [ []] ----查看 JVM 统计信息
用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。常用于检测垃圾回收问题以及内存泄漏问题。
查看命令相关参数: jstat-h 或 jstat-help
-class:显示ClassLoader的相关信息:类的装载、卸载数量、总空间、类装载所消耗的时间等
垃圾回收相关的:
-gc:显示与GC相关的堆信息。包括Eden区、两个Survivor区、老年代、永久代等的容量、已用空间、GC时间合计等信息。
-gccapacity:显示内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间。
-gcutil:显示内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比。
-gccause:与-gcutil功能一样,但是会额外输出导致最后一次或当前正在发生的GC产生的原因。
-gcnew:显示新生代GC状况
-gcnewcapacity:显示内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间
-geold:显示老年代GC状况
-gcoldcapacity:显示内容与-gcold基本相同,输出主要关注使用到的最大、最小空间
-gcpermcapacity:显示永久代使用到的最大、最小空间。
jinfo [options] pid -----实时查看和修改 JVM 配置参数
jinfo(Configuration Info for Java):查看虚拟机配置参数信息,也可用于调整虚拟机的配置参数。在很多情况卡,Java应用程序不会指定所有的Java虚拟机参数。而此时,开发人员可能不知道某一个具体的Java虚拟机参数的默认值。在这种情况下,可能需要通过查找文档获取某个参数的默认值。这个查找过程可能是非常艰难的。但有了jinfo工具,开发人员可以很方便地找到Java虚拟机参数的当前值。
-flag name 输出对应名称的参数
-flag [+-]name 开启或者关闭对应名称的参数 只有被标记为manageable的参数才可以被动态修改
-flag name=value 设定对应名称的参数
-flags 输出全部的参数
-sysprops 输出系统属性
jmap ---导出内存映像文件&内存使用情况
jmap [option]
jmap [option] <executable
jmap [option] [server_id@]
jmap(JVM Memory Map):作用一方面是获取dump文件(堆转储快照文件,二进制文件),它还可以获取目标Java进程的内存相关信息,包括Java堆各区域的使用情况、堆中对象的统计信息、类加载信息等。开发人员可以在控制台中输入命令“jmap -help”查阅jmap工具的具体使用方式和一些标准选项配置。
-dump 生成dump文件(Java堆转储快照),-dump:live只保存堆中的存活对象
-heap 输出整个堆空间的详细信息,包括GC的使用、堆配置信息,以及内存的使用信息等
-histo 输出堆空间中对象的统计信息,包括类、实例数量和合计容量,-histo:live只统计堆中的存活对象
-J <flag> 传递参数给jmap启动的jvm
-finalizerinfo 显示在F-Queue中等待Finalizer线程执行finalize方法的对象,仅linux/solaris平台有效
-permstat 以ClassLoader为统计口径输出永久代的内存状态信息,仅linux/solaris平台有效
-F 当虚拟机进程对-dump选项没有任何响应时,强制执行生成dump文件,仅linux/solaris平台有效
jhat ---JDK自带堆分析工具
jhat(JVM Heap Analysis Tool):Sun JDK提供的jhat命令与jmap命令搭配使用,用于分析jmap生成的heap dump文件(堆转储快照)。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,用户可以在浏览器中查看分析结果(分析虚拟机转储快照信息)。
-stack false|true 关闭|打开对象分配调用栈跟踪
-refs false|true 关闭|打开对象引用跟踪
-port port-number 设置jhat HTTP Server的端口号,默认7000
-exclude exclude-file 执行对象查询时需要排除的数据成员
-baseline exclude-file 指定一个基准堆转储
-debug int 设置debug级别
-version 启动后显示版本信息就退出
-J <flag> 传入启动参数,比如-J-Xmx512m
jstack -----打印 JVM 中线程快照
用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合。
生成线程快照的作用:可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用jstack显示各个线程调用的堆栈情况。
-F 当正常输出的请求不被响应时,强制输出线程堆栈
-l 除堆栈外,显示关于锁的附加信息
-m 如果调用本地方法的话,可以显示C/C++的堆栈
jcmd ----多功能命令行
在JDK 1.7以后,新增了一个命令行工具jcmd。它是一个多功能的工具,可以用来实现前面除了jstat之外所有命令的功能。比如:用它来导出堆、内存使用、查看Java进程、导出线程信息、执行GC、JVM运行时间等。
jcmd拥有jmap的大部分功能,并且在Oracle的官方网站上也推荐使用jcmd命令代jmap命令
jcmd -l: 列出所有的JVM进程
jcmd 进程号 help: 针对指定的进程,列出支持的所有具体命令
jcmd 进程号 具体命令: 显示指定进程的指令命令的数据
jstatd -----远程主机信息收集
一些监控工具也支持对远程计算机的监控(如jps、jstat)。为了启用远程监控,则需要配合使用jstatd 工具。命令jstatd是一个RMI服务端程序,它的作用相当于代理服务器,建立本地计算机与远程监控工具的通信。jstatd服务器将本机的Java应用程序信息传递到远程计算机。
(2) GUI 工具
命令行工具或组合能帮您获取目标Java应用性能相关的基础信息。JDK提供了一些内存泄漏的分析工具
JDK自带的工具:
- jconsole:JDK自带的可视化监控工具。查看Java应用程序的运行概况、监控堆信息、永久区(或元空间)使用情况、类加载情况等
- Visual VM:Visual VM是一个工具,它提供了一个可视界面,用于查看Java虚拟机上运行的基于Java技术的应用程序的详细信息。
- JMC:Java Mission Control,内置Java Flight Recorder。能够以极低的性能开销收集Java虚拟机的性能数据。
第三方工具:
- MAT:MAT(Memory Analyzer Tool)是基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
- JProfiler:商业软件,需要付费。功能强大
- Arthas: 是Alibaba开源的Java诊断工具,它可以帮助我们解决在线环境的以下问题
监控到JVM的实时运行状态(涵盖Jps ,jstat ,jinfo ,jstack ,部分Jmap功能)。
在不需要重新部署服务的情况下修改业务代码。
以局视角来查看系统的运行状况。