ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

深入浅出JVM,看这一篇就够了!

深入浅出JVM,看这一篇就够了! 1. 类的加载过程1.1 加载过程介绍如果 JVM 想要执行这个 .class 文件我们需要将其装进一个类加载器中它就像一个搬运工一样会把所有的 .class 文件全部搬进 JVM 里面来。重点知识Java 文件经过编译后变成 .class 字节码文件字节码文件通过类加载器被搬运到 JVM 虚拟机中虚拟机主要的 5 大块方法区堆都为线程共享区域有线程安全问题栈和本地方法栈和计数器都是独享区域不存在线程安全问题而 JVM 的调优主要就是围绕堆栈两大块进行。1.2 类加载流程类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中加载、验证、准备和初始化这四个阶段发生的顺序是确定的而解析阶段则不一定它在某些情况下可以在初始化阶段之后开始这是为了支持 Java 语言的运行时绑定也成为动态绑定或晚期绑定。另外注意这里的几个阶段是按顺序开始而不是按顺序进行或完成因为这些阶段通常都是互相交叉地混合进行的通常在一个阶段执行的过程中调用或激活另一个阶段。加载查找并加载类的二进制数据在 Java 堆中也创建一个 java.lang.Class 类的对象连接连接又包含三块内容验证、准备、初始化1验证文件格式、元数据、字节码、符号引用验证2准备为类的静态变量分配内存并将其初始化为默认值3解析把类中的符号引用转换为直接引用。初始化为类的静态变量赋予正确的初始值使用new出对象程序中使用卸载执行垃圾回收。1.3 类加载器加载一个 Class 类的顺序也是有优先级的类加载器从最底层开始往上的顺序是这样的BootStrap ClassLoaderrt.jarExtention ClassLoader加载扩展的 jar 包App ClassLoader指定的 classpath 下面的 jar 包Custom ClassLoader自定义的类加载器2. 垃圾回收2.1 如何确定对象已死通常判断一个对象是否被销毁有两种方法引用计数算法为对象添加一个引用计数器每当对象在一个地方被引用则该计数器加 1每当对象引用失效时计数器减 1但计数器为 0 的时候就表明该对象没有被引用。可达性分析算法通过一系列被称之为 “GC Roots” 的根节点开始沿着引用链进行搜索凡是在引用链上的对象都不会被回收。就像上图的那样绿色部分的对象都在 GC Roots 的引用链上就不会被垃圾回收器回收灰色部分的对象没有在引用链上自然就被判定为可回收对象。2.2 垃圾回收算法2.2.1 标记--清除算法见名知义“标记--清除算法” 就是对无效的对象进行标记然后清除。2.2.2 复制算法标记--复制算法就是把 Java 堆分成两块每次垃圾回收时只使用其中一块然后把存活的对象全部移动到另一块区域。2.2.3 标记--整理算法“标记--整理算法” 算是一种折中的垃圾收集算法在对象标记的过程和前面两个执行的是一样步骤。进行标记之后存活的对象会移动到堆的一端然后直接清理存活对象以外的区域就可以了。这样既避免了内存碎片也不存在堆空间浪费的说法了。但是每次进行垃圾回收的时候都要暂停所有的用户线程特别是对老年代的对象回收则需要更长的回收时间这对用户体验是非常不好的。2.3 垃圾收集器2.3.1 Serial 收集器Serial 收集器是最基础、历史最悠久的收集器是一个单线程工作的收集器。使用 Serial收集器无论是进行 Minor gc 还是 Full GC 清理堆空间时所有的应用线程都会被暂停。2.3.2 ParNew 收集器ParNew 收集器实质上是 Serial 收集器的多线程并行版本。除了同时使用多条线程进行垃圾收集之外其余的行为包括 Serial 收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一致。2.3.3 Parallel Scavenge 收集器Parallel Scavenge 收集器也是一款新生代收集器基于标记——复制算法实现能够并行收集的多线程收集器和 ParNew 非常相似。Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量Throughput。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。如果虚拟机完成某个任务用户代码加上垃圾收集总共耗费了 100 分钟其中垃圾收集花掉 1 分钟那吞吐量就是 99%。2.3.4 Serial Old 收集器Serial Old 是 Serial 收集器的老年代版本它同样是一个单线程收集器使用 “标记-整理算法”。2.3.5 Parallel Old 收集器Parallel Old 是 Parallel Scavenge 收集器的老年代版本支持多线程并发收集基于标记-整理算法实现。2.3.6 CMS收集器CMS 收集器设计的初衷是为了消除 Parallel 收集器和 Serial 收集器 Full gc 周期中的长时间停顿。CMS收集器在 Minor gc 时会暂停所有的应用线程并以多线程的方式进行垃圾回收。2.3.7 垃圾收集器对比3. 运行时数据区域什么是运行时数据区域Java 程序在运行时会为 JVM 单独划出一块内存区域而这块内存区域又可以再次划分出一块运行时数据区运行时数据区域大致可以分为五个部分3.1 Java 堆Heap栈管运行堆管存储虚拟机栈负责运行代码而虚拟机堆负责存储数据。Java 堆区具有下面几个特点存储的是我们 new 来的对象不存放基本类型和对象引用由于创建了大量的对象垃圾回收器主要工作在这块区域线程共享区域因此是线程不安全的能够发生 OutOfMemoryError。其实Java 堆区还可以划分为新生代和老年代新生代又可以进一步划分为 Eden 区、Survivor 1 区、Survivor 2 区。3.2 虚拟机栈JVM StacksJava 虚拟机栈也是一块被开发者重点关注的地方同样先把干货放上来Java 虚拟机栈是线程私有的每一个线程都有独享一个虚拟机栈它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型每个方法被执行的时候都会同时创建一个栈帧Stack Frame用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。存放基本数据类型boolean、byte、char、short、int、float、long、double以及对象的引用reference 类型它不等同于对象本身根据不同的虚拟机实现它可能是一个指向对象起始地址的引用指针也可能指向一个代表对象的句柄或者其他与此对象相关的位置和 returnAddress 类型指向了一条字节码指令的地址。这个区域可能有两种异常如果线程请求的栈深度大于虚拟机所允许的深度将抛出 StackOverflowError 异常如果虚拟机栈可以动态扩当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError异常。3.3 本地方法栈Native Method Stacks本地方法栈与虚拟机栈所发挥的作用是非常相似的其区别不过是虚拟机栈为虚拟机执行 Java 方法也就是字节码服务而本地方法栈则是为虚拟机使用到的 Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定因此具体的虚拟机可以自由实现它。甚至有的虚拟机譬如 Sun HotSpot 虚拟机直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。3.4 方法区Method Area方法区也是一块被重点关注的区域主要特点如下线程共享区域因此这是线程不安全的区域。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时将抛出 OutOfMemoryError 异常。3.5 程序计数器Program Counter Register它的作用就是记录当前线程所执行的位置。这样当线程重新获得 CPU 的执行权的时候就直接从记录的位置开始执行分支、循环、跳转、异常处理也都依赖这个程序计数器来完成。4. JVM堆内存4.1 堆内存结构Java 堆区可以划分为新生代和老年代新生代又可以进一步划分为 Eden 区、Survivor 1 区、Survivor 2 区。具体比例参数的话可以看一下这张图。4.2 GC类型Minor GC/Young GC针对新生代的垃圾收集Major GC/Old GC针对老年代的垃圾收集。Full GC针对整个 Java 堆以及方法区的垃圾收集。4.3 Minor GC 工作原理通常情况下初次被创建的对象存放在新生代的 Eden 区当第一次触发 Minor GCEden 区存活的对象被转移到 Survivor区 的某一块区域。以后再次触发 Minor GC 的时候Eden区的对象连同一块 Survivor 区的对象一起被转移到了另一块 Survivor 区。可以看到这两块 Survivor 区我们每一次只使用其中的一块这样也仅仅是浪费了一块 Survivor 区。4.4 Full GC 工作原理老年代是存储长期存活的对象的占满时就会触发我们最常听说的 Full GC期间会停止所有线程等待 GC 的完成。所以对于响应要求高的应用应该尽量去减少发生 Full GC 从而避免响应超时的问题。5. GC 日志5.1 GC 日志开启偷个懒直接贴网上的内容5.2 理解 GC 日志Minor GC 日志Full GC 日志6. JVM 常用命令Sun JDK监控和故障处理命令有 jps、jstat、jmap、jhat、jstack、jinfo。6.1 jpsJVM Process Status Tool显示指定系统内所有的 HotSpot 虚拟机进程。6.2 jstatjstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据。6.3 jmapdump 堆到文件可用于对文件的分析。6.4 jhatjhat(JVM Heap Analysis Tool)命令是与 jmap 搭配使用用来分析 jmap 生成的 dumpjhat 内置了一个微型的 HTTP/HTML 服务器生成 dump 的分析结果后可以在浏览器中查看。在此要注意一般不会直接在服务器上进行分析因为 jhat 是一个耗时并且耗费硬件资源的过程一般把服务器生成的 dump 文件复制到本地或其他机器上进行分析。6.5 jstackjstack 用于生成 java 虚拟机当前时刻的线程快照。线程快照是当前 java 虚拟机内每一条线程正在执行的方法堆栈的集合生成线程快照的主要目的是定位线程出现长时间停顿的原因如线程间死锁、死循环、请求外部资源导致的长时间等待等。线程出现停顿的时候通过 jstack 来查看各个线程的调用堆栈就可以知道没有响应的线程到底在后台做什么事情或者等待什么资源。7. 性能检测工具7.1 jconsoleJconsoleJava Monitoring and Management Console是从 java5 开始在 JD K中自带的 java 监控和管理控制台用于对 JVM 中内存线程和类等的监控是一个基于 JMXjava management extensions的 GUI 性能监测工具。jconsole 使用 jvm 的扩展机制获取并展示虚拟机中运行的应用程序的性能和资源消耗等信息。概览包括堆内存使用情况、线程、类、CPU 使用情况四项信息的曲线图。线程相当于可视化的 jstack 命令同时也可以点击 “检测死锁” 来检查线程之间是否有死锁的情况。7.2 VisualVMVisualVMAll-in-One Java Troubleshooting Tool是功能最强大的运行监视和故障处理程序之一曾经在很长一段时间内是 Oracle 官方主力发展的虚拟机故障处理工具。相比一些第三方工具VisualVM 有一个很大的优点不需要被监视的程序基于特殊 Agent 去运行因此它的通用性很强对应用程序实际性能的影响也较小使得它可以直接应用在生产环境中。Visual GC 是常常使用的一个功能需要通过插件可以明显的看到年轻代、老年代的内存变化以及 gc 频率、gc 的时间等感觉这个插件很酷监控的主页其实也就是cpu、内存、类、线程的图表这里面可以执行堆 dump。最后就是堆 dump8. JVM 调优8.1 选择合适的垃圾回收器CPU 单核那么毫无疑问 Serial 垃圾收集器是你唯一的选择CPU 多核关注吞吐量 那么选择 PSPO 组合CPU 多核关注用户停顿时间JDK 版本 1.6 或者 1.7那么选择 CMSCPU 多核关注用户停顿时间JDK1.8 及以上JVM 可用内存 6G 以上那么选择 G1。参数配置//设置Serial垃圾收集器新生代 开启-XX:UseSerialGC //设置PSPO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器 开启 -XX:UseParallelOldGC //CMS垃圾收集器老年代 开启 -XX:UseConcMarkSweepGC //设置G1垃圾收集器 开启 -XX:UseG1GC8.2 调整内存大小现象垃圾收集频率非常频繁。原因如果内存太小就会导致频繁的需要进行垃圾收集才能释放出足够的空间来创建新的对象所以增加堆内存大小的效果是非常显而易见的。注意如果垃圾收集次数非常频繁但是每次能回收的对象非常少那么这个时候并非内存太小而可能是内存泄露导致对象无法回收从而造成频繁 GC。参数配置//设置堆初始值 指令1-Xms2g 指令2-XX:InitialHeapSize2048m //设置堆区最大值 指令1-Xmx2g 指令2 -XX:MaxHeapSize2048m //新生代内存配置 指令1-Xmn512m 指令2-XX:MaxNewSize512m8.3 设置符合预期的停顿时间现象程序间接性的卡顿原因如果没有确切的停顿时间设定垃圾收集器以吞吐量为主那么垃圾收集时间就会不稳定。注意不要设置不切实际的停顿时间单次时间越短也意味着需要更多的 GC 次数才能回收完原有数量的垃圾.参数配置//GC停顿时间垃圾收集器会尝试用各种手段达到这个时间 -XX:MaxGCPauseMillis8.4 调整内存区域大小比率现象某一个区域的GC频繁其他都正常。原因如果对应区域空间不足导致需要频繁GC来释放空间在JVM堆内存无法增加的情况下可以调整对应区域的大小比率。注意也许并非空间不足而是因为内存泄造成内存无法回收从而导致 GC 频繁。参数配置//survivor区和Eden区大小比率 指令-XX:SurvivorRatio6 //S区和Eden区占新生代比率为1:6,两个S区2:6 //新生代和老年代的占比 -XX:NewRatio4 //表示新生代:老年代 1:4 即老年代占整个堆的4/5默认值28.5 调整对象升老年代的年龄现象老年代频繁 GC每次回收的对象很多。原因如果升代年龄小新生代的对象很快就进入老年代了导致老年代对象变多而这些对象其实在随后的很短时间内就可以回收这时候可以调整对象的升级代年龄让对象不那么容易进入老年代解决老年代空间不足频繁 GC 问题。注意增加了年龄之后这些对象在新生代的时间会变长可能导致新生代的 GC 频率增加并且频繁复制这些对象新生的 GC 时间也可能变长。配置参数//进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值默认值7 -XX:InitialTenuringThreshol78.6 调整大对象的标准现象老年代频繁 GC每次回收的对象很多,而且单个对象的体积都比较大。原因如果大量的大对象直接分配到老年代导致老年代容易被填满而造成频繁 GC可设置对象直接进入老年代的标准。注意这些大对象进入新生代后可能会使新生代的 GC 频率和时间增加。配置参数//新生代可容纳的最大对象,大于则直接会分配到老年代0代表没有限制。 -XX:PretenureSizeThreshold10000008.7 调整GC的触发时机现象CMSG1 经常 Full GC程序卡顿严重。原因G1 和 CMS 部分 GC 阶段是并发进行的业务线程和垃圾收集线程一起工作也就说明垃圾收集的过程中业务线程会生成新的对象所以在 GC 的时候需要预留一部分内存空间来容纳新产生的对象如果这个时候内存空间不足以容纳新产生的对象那么JVM就会停止并发收集暂停所有业务线程STW来保证垃圾收集的正常运行。这个时候可以调整GC触发的时机比如在老年代占用 60% 就触发 GC这样就可以预留足够的空间来让业务线程创建的对象有足够的空间分配。注意提早触发 GC 会增加老年代 GC 的频率。配置参数//使用多少比例的老年代后开始CMS收集默认是68%如果频繁发生SerialOld卡顿应该调小 -XX:CMSInitiatingOccupancyFraction //G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65% -XX:G1MixedGCLiveThresholdPercent658.8 调整 JVM本地内存大小现象GC 的次数、时间和回收的对象都正常堆内存空间充足但是报 OOM原因JVM 除了堆内存之外还有一块堆外内存这片内存也叫本地内存可是这块内存区域不足了并不会主动触发 GC只有在堆内存区域触发的时候顺带会把本地内存回收了而一旦本地内存分配不足就会直接报 OOM 异常。注意本地内存异常的时候除了上面的现象之外异常信息可能是 OutOfMemoryErrorDirect buffer memory。解决方式除了调整本地内存大小之外也可以在出现此异常时进行捕获手动触发 GCSystem.gc()。配置参数XX:MaxDirectMemorySize9. JVM 调试实战9.1 网站流量浏览量暴增后网站反应页面响很慢1、问题推测在测试环境测速度比较快但是一到生产就变慢所以推测可能是因为垃圾收集导致的业务线程停顿。2、定位为了确认推测的正确性在线上通过 jstat -gc 指令 看到 JVM 进行 GC 次数频率非常高GC 所占用的时间非常长所以基本推断就是因为 GC 频率非常高所以导致业务线程经常停顿从而造成网页反应很慢。3、解决方案因为网页访问量很高所以对象创建速度非常快导致堆内存容易填满从而频繁 GC所以这里问题在于新生代内存太小所以这里可以增加 JVM 内存就行了所以初步从原来的 2G 内存增加到 16G 内存。4、第二个问题增加内存后的确平常的请求比较快了但是又出现了另外一个问题就是不定期的会间断性的卡顿而且单次卡顿的时间要比之前要长很多。5、问题推测之前的优化加大了内存所以推测可能是因为内存加大了从而导致单次 GC 的时间变长从而导致间接性的卡顿。6、定位还是通过 jstat -gc 指令 查看到 的确 FGC 次数并不是很高但是花费在 FGC 上的时间是非常高的根据 GC 日志 查看到单次 FGC 的时间有达到几十秒的。7、解决方案因为 JVM 默认使用的是 PSPO 的组合PSPO 垃圾标记和收集阶段都是 STW所以内存加大了之后需要进行垃圾回收的时间就变长了所以这里要想避免单次 GC 时间过长所以需要更换并发类的收集器因为当前的 JDK 版本为 1.7所以最后选择 CMS 垃圾收集器根据之前垃圾收集情况设置了一个预期的停顿的时间上线后网站再也没有了卡顿问题。9.2 后台导出数据引发的 OOM问题描述公司的后台系统偶发性的引发 OOM 异常堆内存溢出。1、因为是偶发性的所以第一次简单的认为就是堆内存不足导致所以单方面的加大了堆内存从 4G 调整到 8G。2、但是问题依然没有解决只能从堆内存信息下手通过开启了 -XX:HeapDumpOnOutOfMemoryError 参数 获得堆内存的 dump 文件。3、VisualVM 对堆 dump 文件进行分析通过 VisualVM 查看到占用内存最大的对象是 String 对象本来想跟踪着 String 对象找到其引用的地方但 dump 文件太大跟踪进去的时候总是卡死而 String 对象占用比较多也比较正常最开始也没有认定就是这里的问题于是就从线程信息里面找突破点。4、通过线程进行分析先找到了几个正在运行的业务线程然后逐一跟进业务线程看了下代码发现有个引起我注意的方法导出订单信息。5、因为订单信息导出这个方法可能会有几万的数据量首先要从数据库里面查询出来订单信息然后把订单信息生成 excel这个过程会产生大量的 String 对象。6、为了验证自己的猜想于是准备登录后台去测试下结果在测试的过程中发现到处订单的按钮前端居然没有做点击后按钮置灰交互事件结果按钮可以一直点因为导出订单数据本来就非常慢使用的人员可能发现点击后很久后页面都没反应结果就一直点结果就大量的请求进入到后台堆内存产生了大量的订单对象和 EXCEL 对象而且方法执行非常慢导致这一段时间内这些对象都无法被回收所以最终导致内存溢出。7、知道了问题就容易解决了最终没有调整任何 JVM 参数只是在前端的导出订单按钮上加上了置灰状态等后端响应之后按钮才可以进行点击然后减少了查询订单信息的非必要字段来减少生成对象的体积然后问题就解决了。
返回列表