JVM专栏
目录
一、JVM内存结构
1.JDK1.8内存结构
JDK1.8与JDK1.7内存结构比较
程序计数器
虚拟机栈
本地方法栈
堆
元空间(Metaspace)
配置参数与异常
2.内存溢出
内存溢出与内存泄漏
OOM:Java heap space
OOM:Metaspace
OOM:StackOverflowError
3.实战技巧
记一次大量请求积压导致内存溢出
二、垃圾回收
1.判断对象是否可回收
引用计数
可达性分析
两次标记
2.垃圾收集算法
标记-清除算法
复制算法
标记-整理算法
分代收集算法
3.内存分配与回收策略
对象优先在Eden分配
大对象直接进入老年代
长期存活的对象将进入老年代
动态对象年龄判定
空间分配担保
3.四次引用
强引用 Strong Reference
软引用 Soft Reference
弱引用 WeakReference
虚引用 PhantomReference
4.垃圾收集器
新生代收集器
老年代收集器
整堆收集器G1
5.实战技巧
YoungGC和FullGC触发条件
G1日志查看
为什么YGC比FullGC慢很多
新生代和老年代比例问题调优(作为一个思路,G1不需要关注新生代老年代比例)
三、性能调优
1.JVM性能优化总方向
2.GC调优总体步骤
3.调优工具
调优命令
JDK自带可视化工具
第三方工具
4.实战技巧
记一次微服务运行一段时间后CPU过高的问题
四、类加载
1.什么是类加载
类加载定义
什么时候加载一个类
从哪里加载.class文件
2.类加载的过程
加载
验证
准备
解析
初始化
3.类加载器
双亲委派模型
自定义类加载器
线程上下文类加载器
Tomcat违背双亲委派模型
五、编译优化
1.Java编译器种类
2.前端编译器
3.运行期优化
解释器和编译器
即时编译对象
两种即时编译器
运行期优化总结
一、JVM内存结构
1.JDK1.8内存结构
JDK1.8与JDK1.7内存结构比较
1.8同1.7比,最大的差别就是:元空间取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。(永久代是1.7方法区的实现,元空间是1.8方法区的实现)
为何废弃永久代?方法区里存储的内容又叫做永久代,它们的生命周期往往较长,难以被回收,导致永久代内存溢出。
JDK1.7字符串常量池位置从方法区搬到了堆中;运行时常量池还在方法区。JDK1.8字符串常量池位置还在堆中,运行时常量池位置变成了元空间。类元信息、字段、静态属性、方法、常量等移至元空间。
JDK1.7和JDK1.8内存结构对比(绿色代表线程隔离;空白代表线程共享):
结合类加载的知识便是下图:
程序计数器
程序计数器是一块较小的内存空间,它可以是看作当前线程所执行的字节码的行号指示器。此区域不会发生内存溢出。
虚拟机栈
线程私有,生命周期与线程相同。包括局部变量表、操作数栈、动态连接、方法返回地址。
本地方法栈
本地方法栈和虚拟机栈所发挥的作用是很相似的,区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。
堆
堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在对上进行分配。1.7后,字符串常量池也存放在堆中。
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 )。
(虚拟机栈和堆内存联系见下方右图,方法里new一个对象,如果方法执行完了,中间箭头就没了,可以回收堆中的这个对象,这样与垃圾回收串起来了;类的静态变量在方法区,指向的实例在堆的老年代)
元空间(Metaspace)
元数据区和永久代本质上都是方法区的实现。元空间存放类元信息、字段、静态属性、方法、常量等。
配置参数与异常
名称 | 配置参数 | 异常 |
---|---|---|
程序计数器 | 无 | 无 |
虚拟机栈 | -Xss | 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError |
本地方法栈 | 无 | 同虚拟机栈 |
堆 | -Xms -Xmx -Xmn(新生代大小) | OOM:Java heap space |
方法区 |
1.7: -XX:PermSize=64M -XX:MaxPermSize=64M 1.8: -XX:MetaspaceSize=64M - XX:MaxMetaspaceSize=64M |
1.7: OOM:PermGen space 1.8: OOM:Metaspace |
直接内存 | -XX: MaxDirectMemorySize (默认与-Xmx一样) |
OOM:Direct buffer memory |
2.内存溢出
内存溢出与内存泄漏
内存溢出:GC过后还是没有空间,放不下新对象。两个主要原因:第一,并发量大,导致对象都是存活的;第二,内存泄漏
内存泄漏:对象都是存活的,没有及时取消对它们的引用。
OOM:Java heap space
大体方向(核心是分析堆dump:-XX: HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,内存溢出时打印堆dump信息)
1.检查Xms(初始堆大小)和Xmx(最大堆大小)设置是否合理,如不合理,增加;
2.检查错误日志(一般日志的异常栈会打印内存溢出的类)同时结合一些命令或工具查看gc情况(jstat -gc查看gc情况;jmap -histo查看大对象(程序运行中用此,挂了用堆dump);或者分析堆dump查看大对象),还是确定不了的话,走查和分析代码,找出可能发生内存溢出的位置。产生原因:
a.内存中加载的数据量过于庞大,如一次从数据库取出过多数据、文件上传(尽量分块或者分批处理)
b.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收
c.代码中存在死循环或循环产生过多重复的对象实体
3.如果上述1、2找不到原因,利用工具检查内存泄露。
内存泄漏
排查工具:
1.jmap,首先配置JVM启动参数,让JVM在遇到OOM时自动生成Dump文件;jmap -dump:format=b,file=/path/heap.bin pid;最后借助jhat或者MAT可视化查看
2.jconsole和VisualVM
3.Arthas
内存泄漏产生原因:(本质上是长生命周期的对象持有短生命周期对象的引用,导致短生命周期对象无法回收)
1.静态集合类持有短生命周期对象的引用
2.连接未释放(数据库连接、网络连接和IO连接)
3.监听器和回调
4.单例模式持有外部对象
5.内部类持有外部模块的引用(尽量使用静态内部类)
堆dump分析
核心就是找到大对象,谁在引用这个大对象,哪个线程在引用,找到对应的代码。
1.查看总体分析报告
2.查看详情,确定大对象
3.查看线程栈,确定错误代码位置
OOM:Metaspace
产生原因是系统的代码非常多或引用的第三方包非常多或者通过cglib等技术动态生成一些类,代码没控制好,导致生成的类过多,导致元空间的内存占用很大。
解决方法:通过MAT工具打开dump分析
OOM:StackOverflowError
产生原因是递归调用形成死循环。
解决方法:一般查看应用日志就能看出是递归调用导致异常
3.实战技巧
记一次大量请求积压导致内存溢出
1.查看应用日志,初步确定是Tomcat工作线程导致内存溢出
Exception in thread "http-nio-8080-exec-1089" java.lang.OutOfMemoryError: Java heap space
2.进一步结合应用日志,发现之前有大量Timeout Exception,因此确定了是请求后端rpc时连接超时
3.事后通过MAT工具分析堆dump确定大对象(800*byte[](10M)=8G),右击对象可以查看对象的引用者
MAT工具发现有400个Tomcat线程存在,因此怀疑是这400个线程创建了这800个byte[]
二、垃圾回收
1.判断对象是否可回收
引用计数
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。缺点:无法解决对象相互循环引用的问题。
可达性分析
对象有没有到GC Roots的引用链。
GC Roots包括:
1.虚拟机栈中引用的对象
2.本地方法栈中JNI引用的对象
3.方法区中类静态属性实体引用的对象
4.方法区中常量引用的对象
两次标记
第一次标记:可达性分析该对象没有引用链与GC Roots相连,重写了finalize()方法并没有执行过,则进入F-Queue
队列。稍后虚拟机自建的低优先级的finalizer
线程会去执行finalize()。
第二次标记:第一标记的对象执行完finalize()还是没有与GC ROOTS
连接,则直接回收;如果连接上则逃脱了。
2.垃圾收集算法
标记-清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
缺点:效率低;内存碎片
复制算法
内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
效率高;内存为原来一半
标记-整理算法
标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。
解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域问题,但效率上比复制算法要差很多。
分代收集算法
Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
YGC流程:把Eden区存活对象和放着上一次YGC后存活对象的Survivor区内的存活对象,转移到另外一块Survivor区去。(因为每次YGC可能存活下来的对象就1%,所以8:1:1)
FullGC:触发OldGC,同时也会进行YGC和元数据(永久代)的GC。
3.内存分配与回收策略
对象优先在Eden分配
大多数情况下,对象在新生代的Eden区分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次YGC。YGC后,Eden区中绝大部分对象会被回收,而那些存活对象会被移送到Survivor区(From或To区)。
大对象直接进入老年代
所谓大对象是指需要大量连续内存空间的Java对象。
长期存活的对象将进入老年代
每个对象有个计数器,每次YGC都会加1,超过某个阈值(默认15,可通过-XX:MaxTenuringThreshold设置)进入老年代。等于说最多可以在Survivor区的From和To之间交换14次。
动态对象年龄判定
如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold要求的值。
空间分配担保
YGC前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,确保安全。
若不安全,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,若允许失败,则继续查看虚拟机会检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于就YGC,小于或者HandlePromotionFailure设置值不允许担保失败,则Full GC。
3.四次引用
强引用 Strong Reference
强引用就是类似“Object obj = new Object()”的引用,强引用永远不会被垃圾收集器回收。
软引用 Soft Reference
用来描述一些还有用但是但是并非必须的对象。内存溢出之前回收。很多系统缓存都会使用该类型的引用。类似缓存作用,提升速度。
弱引用 WeakReference
也用来描述非必须对象,但是其强度比软引用弱,其关联的对象只能生存到下一次垃圾收集之前,当垃圾收集器开始回收内存时,无论内存是否够用,这些弱引用关联的对象都会被回收掉。检测对象是否被标记。
虚引用 PhantomReference
也称幽灵引用或者幻影引用,是最弱的引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。设置虚引用的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统的通知。检测对象是否从内存中删除。
4.垃圾收集器
JDK7/8后,HotSpot虚拟机所有收集器及组合(连线,G1之前的常用组合:ParNew CMS、Parallel Scavenge Parallel Old),如下图。另外所有的垃圾收集器都会有部分操作是需要STW的,即需要暂停用户线程的。
新生代收集器
Serial
单线程收集,需“STW”(Stop The World:会把用户正常的工作线程全部停掉,即GC停顿)
ParNew
Serial收集器的多线程版本,除Serial外,目前只有它能与CMS收集器配合工作
Parallel Scavenge
多线程收集,关注系统吞吐量(适合于后台处理型应用的例如:批量处理、订单处理、工资支付、科学计算)
老年代收集器
Serial Old
Serial收集器的老年代版本,单线程收集
Parallel Old
Parallel Scavenge收集器的老年代版本,多线程收集(这样在注重吞吐量以及CPU资源敏感的场景就有了Parallel Scavenge Parallel Old的强力组合)
CMS
特点:以获取最短回收停顿时间为目标,并发收集、低停顿,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作,以给用户带来较好的体验。是基于标记-清除算法实现的。
适用场景:常见WEB、B/S系统的服务器上的应用。
运行过程:
1.初始标记(仅标记一下GC Roots能直接关联到的对象,不耗时,需STW)
2.并发标记(进行GC Roots Tracing的过程,耗时,无需STW)
3.重新标记(修正并发标记期间因程序运作而产生的标记变动,采用多线程提升效率,不耗时,需STW)
4.并发清除(回收所有的垃圾对象,耗时,无需STW)
初始标记、重新标记这两个步骤仍然需要“STW”,但由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。(另概念澄清:并行收集是指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;并发收集是指垃圾收集线程与用户线程同时执行)
缺点:
1.对CPU资源非常敏感(占用不少CPU)
2.无法处理浮动垃圾(并发清理时产生的垃圾对象)
3.产生大量空间碎片
整堆收集器G1
G1核心设计思想
通过把内存拆分成大量小Region,以及追踪每个Region可以回收的对象大小和预估时间,最后保证在垃圾回收的时候对系统的影响控制在你指定的范围内,同时在有限时间内回收尽可能多的垃圾对象。即最大回收价值(最少回收时间和最多回收对象)。
对象分配规则和之前类似,对象优先在Eden对应的Region,YGC后根据对象年龄或者动态年龄判定机制,部分对象进入老年代。大对象进入Humongous。
当年轻代的Eden区用尽时开始YGC过程(其实是很灵活的,根据你设定的gc停顿时间分配一些Region,到一定程度出发YGC,并控制时间在指定范围内);当老年代占比达到一定值(默认45%)时,开始老年代并发标记过程;标记完成马上开始混合回收过程。当正常的GC不满足需求的时候就会触发Full GC,极耗性能。
G1特点
1.并行与并发(垃圾收集线程与用户线程同时执行)
2.分代收集(能独立管理整个GC堆(新生代和老年代),采用不同方式处理不同时期的对象。将整个堆划分为多个大小相等的独立区域(Region,一般2048个),包括Eden、Survivor、Old、Humongous,同一Region可能这次属于新生代,下次就属于老年代,没有新生代多少内存老年代多少内存一说了。虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了。【由于是分代收集,判断对象存活时是否要扫描整个Java堆?JVM都是使用Remembered Set来避免全局扫描,每个Region都有一个对应的Remembered Set,每次引用类型数据写操作时都会判断“将要写入的引用指向的对象是否和该引用类型数据在不同的Region”从而更新Remembered Set,垃圾收集时在GC根节点的枚举范围加入Remembered Set,就可以保证不进行全局扫描,也不会有遗漏。】)
3.空间整合,不产生碎片(从整体看,是基于标记-整理算法;从局部(两个Region间)看,是基于复制算法)
4.可预测的停顿(建立可预测的停顿时间模型,低停顿的同时实现高吞吐量。【如何建立可预测的停顿时间模型?G1跟踪各个Region获得其回收价值大小,在后台维护一个优先列表;每次根据指定的收集时间,优先回收价值最大的Region(名称Garbage-First的由来),这就保证了在有限的时间内可以获取尽可能高的收集效率】)
与CMS相比优势主要是3、4两点。
G1适用场景
面向服务端应用,针对具有大内存(因为可以只对某些Region进行回收)、多处理器的机器;需要低GC延迟的应用。
G1垃圾收集活动汇总
G1的活动周期包括了YoungGC—并发收集—混合收集。当应用运行开始时,堆内存可用空间还比较大,只会在年轻代满时,触发年轻代收集;随着老年代内存增长,当到达IHOP阈值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默认45%)时,G1开始着手准备收集老年代空间。首先经历并发标记周期,识别出高收益的老年代分区,前文已述。但随后G1并不会马上开始一次混合收集,而是让应用线程先运行一段时间,等待触发一次YoungGC。在这次STW中,G1将保准整理混合回收周期。接着再次让应用线程运行,当接下来的几次年轻代收集时,将会有老年代分区加入到CSet中,即触发混合回收,这些连续多次的混合收集称为混合收集。
G1的YoungGC
当新生代达到设定的占据堆内存的最大大小60%(-XX:G1MaxNewSizePercent),且Eden空间耗尽时,G1会启动一次YoungGC。大体就是把Eden对应的Region放到S1对应的Region中,区别是设定系统停顿时间。具体回收过程如下:
1.根扫描,跟CMS类似,Stop the world,扫描GC Roots对象。
2.处理Dirty card,更新RSet.
3.扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。
4.拷贝扫描出的存活的对象到survivor2/old区
5.处理引用队列,软引用,弱引用,虚引用
G1的并发收集
老年代占整堆比默认45%时开始着手并发收集和混合回收。
1. 初始标记阶段(STW): 标记从GC Roots直接可达的对象。会触发一次YoungGC。
2. 根区域扫描:扫描并标记survivor区直接可达的老年代区域对象。
3. 并发标记:GC Roots追踪,标记所有存活对象。
4. 重新标记(Remark,STW):由于应用程序持续进行,需要修正上一次的标记结果。G1中采用了比CMS更快的初始快照法:snapshot-at-the-beginning(SATB)。
5. 清除(Cleanup,STW):(1)统计所有区域的存活对象,并将待回收区域按回收价值排序(垃圾占比),识别可以混合回收的区域;(2)识别并清理完全空闲的区域,不需要等待混合收集。
综述:经上述过程,老年代中百分百为垃圾的内存被回收了,部分为垃圾的内存被标记了(待混合回收)。
G1的混合收集(Mixed GC)
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region。(注意是一部分老年代,而不是全部老年代,混合收集会优先回收垃圾占比多的区域,从而可以对垃圾回收的耗时时间进行控制。)
混合收集具体算法和YoungGC的算法完全一样,只是回收集多了老年代的内存分段。
回收失败的担保机制 Full GC
混合回收时是无论新生代或老年代都是基于复制算法的,一旦发现没有空闲的Region可以承载存活对象,G1会停止应用程序的执行(STW),使用单线程的内存回收算法进行垃圾回收(调用Serial Old GC),性能会非常差,应用程序停顿时间会很长。(一般如果堆内存空间不足以分配新的对象,或者是Metasapce空间使用率达到了设定的阈值,那么就会触发Full GC)
G1参数设置
-XX:MaxGCPauseMills,停顿时间不能设置太小,否则YGC太频繁;也不能设置太大,运行很久才YGC;暂停时间只是一个目标,并不能保证总是得到满足。
G1不需要设置年轻代大小。
G1缺点
G1对内存空间的浪费较高,但通过优先回收垃圾最多的区域,可以及时发现过期对象,从而让内存占用处于合理的水平。
5.实战技巧
YoungGC和FullGC触发条件
YoungGC出发条件:Eden区满
FullGC触发条件:
1.老年代空间不足:
a.YGC前检查,老年代可用连续空间小于历次YGC进入到老年代的平均大小
b.YGC后,有一批对象要放入老年代(这批对象可能是年龄到了也可能是YGC后Survivor区放不下),但空间不足
c.老年代内存使用率超92%(可配置)
2.Metaspace区内存达到阈值
3.System.gc();
对应的FullGC频繁的原因:
1.并发量大导致YGC频繁,存活对象多,频繁进入老年代;
2.一次性加载过多数据进内存,从而产生大对象
3.内存泄漏,短生命周期对象无法回收
4.Metaspace加载过多类
5.System.gc()
G1日志查看
启动参数加入-Xloggc:gc.log和-XX:PrintGCDetails会生成详细的垃圾收集日志到gc.log。分别对应上述G1收集的四个可能周期。
YGC
并发收集
混合收集
FullGC
为什么YGC比FullGC慢很多
YGC只要追踪对象存活(新生代存活对象很少),直接把存活对象放入Survivor,并一次性清理Eden和Survivor即可。
CMS的FullGC需要追踪存活对象(老年代存活对象很多),就会慢;并发清理时不是一次性清理大片内存,而是清理零散在各个地方的对象,慢!;最后还需要整理内存碎片,也慢。
新生代和老年代比例问题调优(作为一个思路,G1不需要关注新生代老年代比例)
项目背景:舆情快照项目,每次下载页面然后在头部加一些声明,内存中进行(处理量每分钟100次处理,每次10M的页面)
问题:原本新生代和老年代分别是1.5GB(1.2G 100M 100M)、1.5GB。发现FullGC频繁。(因为普通业务系统大部分对象都是短生命周期的,不应该频繁进入老年代(老年代中一般是类似@Service、@Controller这样的核心业务逻辑组件,这样的对象一般很少))
解决:利用Arthas发现Eden区每次很快占满,然后每次YGC后老年代占用增加200M,导致老年代很快满了(按道理每次YGC后只有很少的对象进入老年代,老年代应该增长很慢),所以推测是YGC时(Eden区存活对象 S1区存活对象)>S2区大小,这些对象(200M)因空间担保直接进入老年代了,导致老年代很快满了。后改为2GB(1.6G 200M 200M)、1GB。
三、性能调优
1.JVM性能优化总方向
因为内存分配、参数设置不合理导致对象频繁进入老年代,频繁触发Full GC,导致系统卡顿。
2.GC调优总体步骤
1.监控GC的状态:使用各种JVM工具,查看分析应用日志、GC日志、堆快照、线程快照,根据实际的各区域内存划分和GC执行时间,决定是否进行优化。
2.生成堆dump文件
3.分析dump文件
4.分析结果,判断是否需要优化:GC频率不高,GC耗时不高(1-3秒内)。具体YoungGC耗时几十毫秒,几分钟一次;FullGC耗时几百毫秒,几天或几小时一次,则不需要优化。
5.调整内存分配
6.不断的分析和调整:jstat -gcutil 用于查看新生代、老生代及持代垃圾收集的情况
3.调优工具
调优命令
jps
显示指定系统内所有的HotSpot虚拟机进程。
jstat
它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
常用:jstat -gcutil 1262 200 垃圾回收堆的行为统计
jmap
用于生成heap dump文件,还可以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。
常用:jmap -dump:live,format=b,file=dump.hprof 28920 生成堆dump
jmap -heap 28920 查看GC使用的算法,以及堆使用情况
jmap -histo:live 28920 | more 打印堆的对象统计,包括对象数、内存大小(可以查看大对象)等等,使用该命令会先出发gc
jhat
与jmap搭配使用,用来分析jmap生成的dump。
jinfo
实时查看和调整虚拟机运行参数。
jstack
用于生成java虚拟机当前时刻的线程快照。主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。
这里顺便说一下线程堆栈信息(应该属于多线程内容)
线程栈状态:
BLOCKED:线程正在等待获取java对象的监视器(也叫内置锁),即线程正在等待进入由synchronized保护的方法或者代码块。
TIMED_WAITING:意味着线程调用了限时版本的API,如果特定的事件发生或者是时间流逝完毕,都会恢复运行。如果大量线程处于这一状态,可能有问题(一般与第三方交互如数据库、Redis、http请求等)
RUNNABLE:正在运行。但如果大量线程且是同一方法处于这一状态,可能存在性能问题。
WAITING:暂时认为没有问题
其他关键状态(不属于线程状态,伴随上述四大状态出现):
Waiting on condition:如果堆栈信息明确是应用代码,则证明该线程正在等待资源(数据库、第三方组件、http请求)
Waiting on monitor entry和in Object.wait():意味着线程在等待进入一个临界区。当一个线程申请进入临界区时,它就进入了“Entry Set”队列,此时线程状态是“Waiting for monitor entry”;当线程获得了 Monitor,进入了临界区之后,如果发现线程继续运行的条件没有满足,它则调用对象(一般就是被 synchronized 的对象)的 wait() 方法,放弃 Monitor,进入 “Wait Set”队列,线程状态便是“in Object.wait()”。
小技巧:一般搜索带公司包名的内容,如果大量出现,可能是有问题的。
JDK自带可视化工具
jconsole和VisualVM
可分析堆dump、线程dump、CPU、垃圾回收等。
第三方工具
MAT
内存分析工具。可以下载插件版作为Ecplise插件或者安装MAT独立版本。一般用jmap命令生成dump文件(.hprof),导入mat中,查看大对象,找到代码位置。
Arthas(这里详细介绍)
解决的问题
1.是否有一个全局视角来查看系统的运行状况?
2.有什么办法可以监控到 JVM 的实时运行状态?
3.为什么 CPU 又升高了,到底是哪里占用了 CPU ?
4.运行的多线程有死锁吗?有阻塞吗?
5.程序运行耗时很长,是哪里耗时比较长呢?如何监测呢?
6.这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
7.我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
8.遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
安装和运行
wget https://arthas.gitee.io/arthas-boot.jar
java -jar arthas-boot.jar
示例代码
import java.util.HashSet;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import lombok.extern.slf4j.Slf4j;/** * <p> * Arthas Demo * 公众号:未读代码 * * @Author niujinpeng */@Slf4jpublic class Arthas { private static HashSet hashSet = new HashSet(); /** 线程池,大小1*/ private static ExecutorService executorService = Executors.newFixedThreadPool(1); public static void main(String[] args) { // 模拟 CPU 过高,这里注释掉了,测试时可以打开 cpu(); // 模拟线程阻塞 thread(); // 模拟线程死锁 deadThread(); // 模拟内存溢出 addHashSetThread(); } /** * 不断的向 hashSet 集合添加数据 */ public static void addHashSetThread() { // 初始化常量 new Thread(() -> { int count = 0; while (true) { try { hashSet.add(new Object()); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } public static void cpu() { cpuHigh(); cpuNormal(); } /** * 极度消耗CPU的线程 */ private static void cpuHigh() { Thread thread = new Thread(() -> { while (true) { log.info("cpu start 100"); } }); // 添加到线程 executorService.submit(thread); } /** * 普通消耗CPU的线程 */ private static void cpuNormal() { for (int i = 0; i < 10; i ) { new Thread(() -> { while (true) { log.info("cpu start"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } } /** * 模拟线程阻塞,向已经满了的线程池提交线程 */ private static void thread() { Thread thread = new Thread(() -> { while (true) { log.debug("thread start"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }); // 添加到线程 executorService.submit(thread); } /** * 死锁 */ private static void deadThread() { /** 创建资源 */ Object resourceA = new Object(); Object resourceB = new Object(); // 创建线程 Thread threadA = new Thread(() -> { synchronized (resourceA) { log.info(Thread.currentThread() " get ResourceA"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } log.info(Thread.currentThread() "waiting get resourceB"); synchronized (resourceB) { log.info(Thread.currentThread() " get resourceB"); } } }); Thread threadB = new Thread(() -> { synchronized (resourceB) { log.info(Thread.currentThread() " get ResourceB"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } log.info(Thread.currentThread() "waiting get resourceA"); synchronized (resourceA) { log.info(Thread.currentThread() " get resourceA"); } } }); threadA.start(); threadB.start(); }}
常用命令
dashboard(全局监控):可以概览程序的 线程、内存、GC、运行环境信息。
thread(所有线程堆栈):查看所有线程信息,同时会列出每个线程的 CPU
使用率;使用命令 thread 12 查看 CPU 消耗较高的 12 号线程信息
使用 thread | grep pool 命令查看线程池里线程信息。
thread -b 查看直接定位到死锁信息。
jad(反编译):jad com.Arthas
trace(跟踪统计方法耗时):trace com.UserController getUser
stack:查看方法的调用路径,stack com.UserServiceImpl mysql
watch:查看输入输出参数以及异常等信息,watch com.Arthas addHashSet '{params[0],returnObj.toString()}'
4.实战技巧
记一次微服务运行一段时间后CPU过高的问题
1.定位是FullGC频繁占用太多CPU,如何定位的:
方法一:top, top -H -p pid, printf "%x\n" tid, jstack pid |grep tid ("G1 Concurrent Refinement Thread#0" os_prio=0 tid=0x00007fda5803f800 nid=0x2dab runnable)
方法二:一般CPU占用过高,就是多线程并发运行或者GC频繁导致,用jstat -gcutil 3000 100查看gc情况
方法三:直接通过arthas定位到FullGC频繁
2.jmap -histo 12538查看大对象,发现有个静态list(是要存入db的),结果存db时异常了,没有进行list.clear(),导致
a.这个list越来越大-----FullGC
b.插入数据库时用foreach拼接的,因为list越来越大,导致insert语句越来长,赋值耗CPU,此外会产生大量小对象-----FullGC
四、类加载
1.什么是类加载
类加载定义
类的加载机制在整个java程序运行期间处于一个什么环节,下面使用一张图来表示。类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
什么时候加载一个类
首先加载包含main方法的主类,接着是运行你写的代码的时候,遇到你用了什么类,再加载什么类。
一个类只会被加载一次,再次用到时从JVM的class实例缓存中获取。
此外JVM有预加载机制,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果有错,用到时才报错。
类加载时机:
1.反射(如 Class.forName(“”))
2.JVM启动时被标明为启动类的类,虚拟机会先初始化这个主类
3.通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。
从哪里加载.class文件
1.本地磁盘
2.网上加载.class文件(Applet)
3.从数据库中
4.压缩文件中(ZAR,jar等)
5.从其他文件生成的(JSP应用)
2.类加载的过程
类的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
加载
加载阶段,虚拟机主要完成三件事:
1.通过一个类的全限定名来获取其定义的二进制字节流
2.将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
3.在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。
验证
1.文件格式的验证
2.元数据验证
3.字节码验证
4.符号引用验证
准备
该阶段主要为类变量分配内存并设置初始值。
static int a=1;//该阶段a=0
static final int a=1;//该阶段a=1
解析
符号引用转为直接引用。包括类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用。
初始化
在这个阶段,java程序代码才开始真正执行。一句话描述这个阶段就是执行类构造器< clinit >()方法的过程。主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
类初始化时机:
1.创建类的实例,也就是new的方式
2.访问某个类或接口的静态变量,或者对该静态变量赋值
3.调用类的静态方法
4.反射(如 Class.forName(“”))
5.初始化某个类的子类,则其父类也会被初始化
6.JVM启动时被标明为启动类的类(包含main的主类),虚拟机会先初始化这个主类
3.类加载器
双亲委派模型
定义:双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。父类找不到,子加载器才会尝试自己去加载。
作用:
1.避免重复加载,父类已经加载了,子类就不需要再次加载;
2.更加安全,避免用户可以随意定义类加载器来加载核心api。
Java语言系统自带有三个类加载器:
启动类加载器是用 C 实现的,是虚拟机自身的一部分,如果获取它的对象,将会返回 null。主要加载核心类库:%JRE_HOME%\lib下的rt.jar、resources.jar等;
扩展类加载器是独立于虚拟机外部,为 Java 语言实现的,继承自抽象类 java.lang.ClassLoader ,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
应用类加载器是独立于虚拟机外部,为 Java 语言实现的,继承自抽象类 java.lang.ClassLoader ,加载当前应用的classpath的所有类库。
自定义类加载器
主要有两种方式(选第一种):
1.遵守双亲委派模型:继承ClassLoader,重写findClass()方法。
2.破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。
线程上下文类加载器
为解决基础类无法调用类加载器加载用户提供代码的问题,Java 引入了线程上下文类加载器。这个类加载器默认就是应用类加载器。(一般是应用类加载器,也有用自定义类加载器作为线程上下文类加载器如Tomcat)
Tomcat违背双亲委派模型
Tomcat类加载方式:前面3个类加载和默认的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/*
、/server/*
、/shared/*
(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*
中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。
Tomcat类加载违背双亲委派模型:其每个 Web 应用都有一个对应的类加载器实例,首先尝试去加载某个类,如果找不到再代理给父类加载器,这与一般类加载器的顺序是相反的(违背双亲委派)。
这样设计的原因:
1.tomcat中的需要支持不同web应用依赖同一个第三方类库的不同版本,jar类库需要保证相互隔离;
2.同一个第三方类库的相同版本在不同web应用可以共享
3.tomcat自身依赖的类库需要与应用依赖的类库隔离
4.jsp需要支持修改后不用重启tomcat即可生效,为了上面类加载隔离和类更新不用重启,定制开发各种的类加载器
五、编译优化
1.Java编译器种类
Java 语言的编译器大致可以分为三种:
前端编译器,把.java 文件编译成class 文件—— .java -> .class
后端运行期编译器 JIT 编译器(即时编译器),把字节码转变为机器码的过程—— .class -> 机器码
静态提前编译器 AOT ,直接把 .java 文件编译成机器码——.java -> 机器码
2.前端编译器
前端编译过程可分为三步:
1.解析与填充符号表(词法分析、语法分析、填充符号表)
2.插入式注解处理器的注解处理过程
3.分析与字节码生成过程(标注检查、数据及控制流分析、解语法糖、字节码生成)
3.运行期优化
解释器和编译器
解释器:当程序需要迅速启动和执行的时候,解释器可以省去编译时间,立即执行,解释执行可以节约内存。
编译器:在程序运行时,随着时间的推移,编译器逐渐发挥作用根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。
即时编译对象
热点代码,有两类:
1.被多次调用的方法,编译时以整个方法做为编译对象,是标准的JIT 编译方式
2.被多次执行的循环体,以循环体所在方法为编译对象。
判断是否为热点代码也有两种方式:
1.基于采样的热点探测:虚拟机会周期性的检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,那这个方法就是热点方法。
2.基于计数器的热点探测:虚拟机为每个方法建立计数器,如果执行次数超过一定打阈值就认为是热点方法。
两种即时编译器
HotSpot中内置了两个即时编译器,分别称为 Client Compiler和 Server Compiler ,或者简称为 C1编译器和 C2编译器。目前的 HotSpot编译器默认的是解释器和其中一个即时编译器配合的方式工作,具体是哪一个编译器, HotSpot虚拟机会根据自身版本与计算机的硬件性能自动选择运行模式。
分层编译:Java7默认开启分层编译(tiered compilation)策略,由C1编译器和C2编译器相互协作共同来执行编译任务。C1编译器会对字节码进行简单和可靠的优化,以达到更快的编译速度;C2编译器会启动一些编译耗时更长的优化,以获取更好的编译质量。
运行期优化总结
1.解释器 编译器共同协作的架构
2.分层编译技术
3.具体编译优化技术:公共子表达式消除、数组边界检查消除、方法内联、逃逸分析
参考鸣谢:
https://blog.csdn.net/u013735734/article/details/102930307
https://baijiahao.baidu.com/s?id=1636309817155065432&wfr=spider&for=pc