导图社区 JVM 相关知识点
这是一篇关于JVM 相关知识点的思维导图,JVM(Java Virtual Machine,Java虚拟机)是Java技术的核心和基础。作为一种用于计算设备的规范,JVM是一个虚构出来的计算机,通过在实际的计算机平台(操作系统)上仿真模拟各种计算机功能来实现其运作。
编辑于2024-11-03 10:22:58JVM 相关知识点
JVM 核心知识点
Java 虚拟机结构
Java 堆
经典分代设计
新生代
eden 区
survival 区
from
to
默认情况下 eden:survial:to = 8:1:1
老年代
永久代
不分代设计
Java 虚拟机栈
用于存储局部变量表、操作数栈、动态连接、方法出口等信息
本地方法栈
本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
方法区 (“非堆”(Non-Heap))
用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的 代码缓存等数据。
运行时常量池
有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
程序计数器 PC
直接内存
与 NIO 相关,默认与 -Xmx 参数的值大小一致
垃圾回收
垃圾回收概念
垃圾回收算法
标记清除
标记复制
标记整理
分代收集
安全点
安全区域
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。
记忆集
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
记忆集精度
字长精度
每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
对象精度
每个记录精确到一个对象,该对象里有字段含有跨代指针
卡精度
每个记录精确到一块内存区域,该区域内有对象含有跨代指针。
卡表
卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。关于卡表与记忆集的关系,读者不妨按照Java语言中HashMap与Map的关系来类比理解。
卡表维护
内存屏障
垃圾收集器
经典垃圾收集器
Serial (新生代)
Serial Old (老年代)
大致工作过程

Parallel Scavenge(新生代)
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而ParallelScavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值 
Parallel Old(老年代)
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
ParNew(新生代)
ParNew收集器实质上是Serial收集器的多线程并行版本  ParNew/Serial Old收集器运行示意图
CMS
从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括: 1)初始标记(CMS initial mark) 2)并发标记(CMS concurrent mark) 3)重新标记(CMS remark) 4)并发清除(CMS concurrent sweep) 
G1
G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然 G1 也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1 不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。 Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。 
G1的工作过程
如果我们不去计算用户线程运行过程中的动作(如使用写屏障维护记忆集的操作),G1收集器的运作过程大致可划分为以下四个步骤: ·初始标记 (Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。 ·并发标记 (Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。 ·最终标记 (Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。 ·筛选回收 (Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
组和使用的关系

低延迟垃圾收集器
Shenandoah
虽然Shenandoah也是使用基于Region的堆内存布局,同样有着用于存放大对象的Humongous Region,默认的回收策略也同样是优先处理回收价值最大的Region……但在管理堆内存方面,它与G1至少有三个明显的不同之处,最重要的当然是支持并发的整理算 法,G1的回收阶段是可以多线程并行的,但却不能与用户线程并发 其次,Shenandoah(目前)是默认不使用分代收集的,换言之,不会有专门的新生代Region或者老年代Region的存在,没有实现分代,并不是说分代对Shenandoah没有价值,这更多是出于性价比的权衡,基于工作量上的考虑而将其放到优先级较低的位置 上。 最后,Shenandoah摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率。连接矩阵可以简单理解为一张二维表格,如果Region N有对象指向Region M,就在表格的N行M列中打上一个标记,如果Region 5中的对象Baz引用了Region 3的Foo,Foo又引用了Region 1的Bar,那连接矩阵中的5行3列、3行1列就应该被打上标记。在回收时通过这张表格就可以得出哪些Region之间产生了跨代引用。
Shenandoah 的工作流程
Shenandoah收集器的工作过程大致可以划分为以下九个阶段 ·初始标记 (Initial Marking):与G1一样,首先标记与GC Roots直接关联的对象,这个阶段仍是“Stop The World”的,但停顿时间与堆大小无关,只与GC Roots的数量相关。 ·并发标记 (Concurrent Marking):与G1一样,遍历对象图,标记出全部可达的对象,这个阶段是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。 ·最终标记 (Final Marking):与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集(Collection Set)。最终标记阶段也会有一小段短暂的停顿。 ·并发清理 (Concurrent Cleanup):这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate GarbageRegion)。 ·并发回收 (Concurrent Evacuation):并发回收阶段是Shenandoah与之前HotSpot中其他收集器的核心差异。在这个阶段,Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中。复制对象这件事情如果将用户线程冻结起来再做那是相当简单的,但如果两者必须要同时并发进行的话,就变得复杂起来了。其困难点是在移动对象的同时,用户线程仍然可能不停对被移动的对象进行读写访问,移动对象是一次性的行为,但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址,这是很难一瞬间全部改变过来的。对于并发回收阶段遇到的这些困难,Shenandoah将会通过读屏障 和被称为“Brooks Pointers”的转发指针来解决(讲解完Shenandoah整个工作过程之后笔者还要再回头介绍它)。并发回收阶段运行的时间长短取决于回收集的大小。 ·初始引用更新 (Initial Update Reference):并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。引用更新的初始化阶段实际上并未做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短,会产生一个非常短暂的停顿。 ·并发引用更新 (Concurrent Update Reference):真正开始进行引用更新操作,这个阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,它不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值即可。 ·最终引用更新 (Final Update Reference):解决了堆中的引用更新后,还要修正存在于GC Roots中的引用。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。 ·并发清理 (Concurrent Cleanup):经过并发回收和引用更新之后,整个回收集中所有的Region已再无存活对象,这些Region都变成Immediate Garbage Regions了,最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用
ZGC
首先从ZGC的内存布局说起。与Shenandoah和G1一样,ZGC也采用基于Region的堆内存布局,但与它们不同的是,ZGC的Region(在一些官方资料中将它称为Page或者ZPage,本章为行文一致继续称为Region)具有动态性——动态创建和销毁,以及动态的区域容量大小。在x64硬件平台下,ZGC的Region可以具有如图所示的大、中、小三类容量: ·小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。 ·中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。 ·大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作“大型Region”,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段,稍后会介绍到)的,因为复制一个大对象的代价非常高昂。 
染色指针
染色指针是一种直接将少量额外的信息存储在指针上的技术  ·染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。 ·染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。
ZGC 的工作流程
接下来,我们来学习ZGC收集器是如何工作的。ZGC的运作过程大致可划分为以下四个大的阶段。全部四个阶段都是可以并发执行的,仅是两个阶段中间会存在短暂的停顿小阶段,这些小阶段,譬如初始化GC Root直接关联对象的Mark Start,与之前G1和Shenandoah 的 Initial Mark阶段并没有什么差异,笔者就不再单独解释了。ZGC的运作过程具体如所示。  ZGC运作过程 ·并发标记 (Concurrent Mark):与G1、Shenandoah一样,并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记(尽管ZGC中的名字不叫这些)的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与G1、Shenandoah不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志位。 ·并发预备重分配 (Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。重分配集与G1收集器的回收集(Collection Set)还是有区别的,ZGC划分Region的目的并非为了像G1那样做收益优先的增量回收。相反,ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。因此,ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里面的Region会被释放,而并不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对全堆的。此外,在JDK 12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段中完成的。 ·并发重分配 (Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。 这样做的好处是只有第一次访问旧对象会陷入转发,也就是只慢一次,对比Shenandoah的Brooks转发指针,那是每次对象访问都必须付出的固定开销,简单地说就是每次都慢,因此ZGC对用户程序的运行时负载要比Shenandoah来得更低一些。还有另外一个直接的好处是由于染色指针的存在,一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配(但是转发表还得留着不能释放掉),哪怕堆中还有很多指向这个对象的未更新指针也没有关系,这些旧指针一旦被使用,它们都是可以自愈的。 ·并发重映射 (Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,这一点从目标角度看是与Shenandoah并发引用更新阶段一样的,但是ZGC的并发重映射并不是一个必须要“迫切”去完成的任务,因为前面说过,即使是旧引用,它也是可以自愈的,最多只是第一次使用时多一次转发和修正操作。重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束后可以释放转发表这样的附带收益),所以说这并不是很“迫切”。因此,ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了。
对象的跟踪
引用的类型
强引用
软引用 SoftReference<T>
软引用会在系统堆内存不足时被回收
弱引用 WeakReference<T>
在GC之后弱引用对象立即被清除
可用于作为Java 的内部缓存
虚引用 PhantomReference<T>
一个对象是否关联到虚引用,完全不会影响该对象的生命周期,也无法通过虚引用来获取一个对象的实例。
可用于逃逸对象的跟踪
并发可达性
三色标记法
该方法依据是否访问过对象来对对象进行标记: 白色:表示对象未被垃圾收集器访问过 黑色:表示该对象已被垃圾收集器访问过且该对象内所有的引用均被扫描过 灰色:该对象被垃圾收集器访问过,且该对象内的所有引用至少有一个没有被扫描过
处理并行标记时对象消失问题问题
增量更新
当黑色对象插入新的指向白色对象的引用关系时,就要将这个新插入的引用记录下来,待并发扫描结束后,再将这些记录过的,引用关系为黑色对象的根,重新扫描一遍。也可以说是将这些黑色对象变为灰色对象重新扫描。
原始快照
当灰色节点指向白色对象的引用被删除时,将被删除的引用记录下来,在并发扫描结束后,从该引用开始,重新进行扫描。相当于将被删除的白色节点记录下来,待扫描结束后,将白色节点作为根,将白色节点变成灰色节点,开始扫描。
对象逃逸
内存分配&GC 规则
栈上分配
TLAB (Eden区,通过 CAS 方式分配 TLAB 的内存)
堆上分配
大对象直接进入老年代
大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组,本节例子中的byte[]数组就是典型的大对象。大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,我们写程序的时候应注意避免。在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复制对象时,大对象就意味着高额的内存复制开销。HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。
长期存活的对象将进入老年代
HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中(详见第2章)。对象通常在Eden区里诞生,如果经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
动态对象年龄判定
为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。
空间分配担保
在发生MinorGC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次MinorGC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(HandlePromotionFailure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次MinorGC,尽管这次MinorGC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次FullGC。
类加载
类加载的过程
加载
加载是整个 “类加载” 过程中的一个阶段。在加载阶段,Java 虚拟机需要完成以下三件事情。 1. 通过一个类的全限定名来获取定义此类的二进制流 2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问接口
验证
验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流包含的信息符合 《Java 虚拟机规范》 的全部约束需求, 保证这些信息被当做代码运行后不会危害虚拟机自身安全。 验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证
文件格式验证
文件格式验证 该阶段主要验证字节流是否符合 Class 文件规范,并且能够被当前版本的虚拟机进行处理。 该阶段的验证主要包含: 魔术开头 主次版本号是否在当前 Java 虚拟机的接受范围之内 常量池中是否有不被支持的常量类型 指向常量池中的各种索引值是否有指向不存在的常量或不符合了下的常量 COUNST_utf8_info 型的常量中是否有不符合 utf8 编码的数据 Class 文件中各个部分及文本文件是否有被删除或附加的其他信息
元数据验证
元数据验证 该阶段是对字节码的描述进行语义分析,以保证其描述信息符合 《Java 语言规范要求》 该阶段会验证如下内容: 这个类是否有父类 这个类的父类是否继承了不允许被继承的父类 类中的字段,方法是否与父类产生矛盾
字节码验证
字节码验证 该阶段主要通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的。该阶段会对方法体进行校验,保证被校验类的方法在运行时不会作出伤害虚拟机的行动 该阶段会验证如下内容: 任何指令都不会跳转到方法体以外的字节码指令上 保证方法体中类型转换总是有效的。
符号引用验证
符号引用验证 该阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将连接的第三阶段 —— 解析阶段中发生。符号引用验证可以看做是对类自身以外的各类信息进行匹配性校验,通俗来说就是该类是否缺少或者被禁止访问他依赖的某些外部类、方法、字段等资源。 该阶段会验证如下内容: 符号引用中通过字符串描述的全限定名是否能找到对应的类 在指定类中是否存在符合方法的字段描述符及简单名称所描述的字段和方法 符号引用中的类、字段、方法的可访问属性(private、 protected、public、<package>) 是否可被当前类访问。
准备
准备阶段是正式为类中的变量(即静态变量、被 Static 修饰的变量)分配内存并设置类变量初始值的阶段。 从概念上说所使用的内存都应当在方法区中进行分配,但必须到方法区本身是一个逻辑上的区域,在 JDK 1.7 及之前,HotSpot 使用永久代来实现方法区时,实现是完全符合这种逻辑的。在 JDK 1.8 之后,类变量则会随着class 对象一起存放在 Java 堆中。 在准备阶段,进行内存分配的仅包括类变量,而不包括实例变量。实例变量将会随着对象一起分配在 Java 堆中。实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
解析
解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程 符号引用 符号引用以一组符号来描述所引述的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。 直接引用 直接引用可以是直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄
类或接口的解析
字段解析
方法解析
接口方法解析
初始化
初始化阶段就是执行类构造器 <cinit>() 方法的过程。在初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。在初始化阶段,虚拟机会根据程序员通过程序编码指定的主观计划去初始化类变量和其他资源。 <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问 <clinit>()方法与类的构造函数(即在虚拟机视角中的实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此在Java虚拟机中第一个被执行的<clinit>()方法的类型肯定是java.lang.Object。 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作 <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。 Java虚拟机必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行完毕<clinit>()方法。如果在一个类的 <clinit>()方法中有耗时很长的操作,那就可能造成多个进程阻塞[2],在实际应用中这种阻塞往往是很隐蔽的。
使用
卸载
类加载器
常见的类加载器
引导类/启动类加载器
扩展类加载器
应用类/系统类加载器
自定义类加载器
双亲委派
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。 使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是 Java 中的类随着它的类加载器一起具备了一种带有优先级的层次关系。
打破双亲委派
OSGI 热部署
Tomact
热替换都是通过打破双亲委派实现
JVM 线上问题排查
性能监控
JDK 自带工具
jps 查看Java进程
jps -l -v
jstat 查看虚拟机运行时信息
jstat -gc PID
jstat -gcutil
jmap 导出到堆文件
jmap -histo PID > ../s.txt 生成Java程序的对象统计信息
jmap -dump:format=b,file=../heap.hprof 得到Java程序的当前堆快照
jhat 堆分析
jhat ../heap.hprof 对 jmap 导出的文件进行分析
在浏览器中访问 http://127.0.0.1:7000 产看分析结果
jstack 查看线程堆栈
jstack -l PID 查看程序中所有锁的信息
jstatd 远程主机信息收集
jstatd -J -Djava.security.policy=../jstatd.all.policy
客户端命令形式
jps IP:port
jstat -gcutil PID@IP:port
图形化监控工具
jconsole
visual VM
内存分析工具
MAT
第三方工具
Arthas
java -jar arthas-boot.jar 启动 arthas
dashboard 可以查看整个进程的运行情况,线程、内存、GC、运行环境信息
thread 查看线程详细情况
thread threadId 查看线程堆栈,那么我们直接定位到导致CPU使用率比较高的代码
thread -b 查看线程死锁
trace 追踪子方法中那个比较慢
trace xxx.xxx.<pacegename>.<classname> <functionname >
monitor 监控方法调用次数
monitor -c 5 org.apache.dubbo.demo.provider.DemoServiceImpl sayHello
profiler 开启 web 监控视图
profiler start
profiler getSamples
profiler status
profiler stop --format flamegraph 生成火焰图
默认情况下,arthas 使用 3658 端口,则可以打开: http://localhost:3658/arthas-output/ 查看到arthas-output目录下面的 profiler 结果:
profiler dump
内存溢出
堆内存溢出
错误表现
错误信息中注明了“java heap space”
解决方法
使用-Xmx参数指定一个更大的堆空间
通过MAT或者Visual VM等工具,找到大量占用堆空间的对象并在应用程序上做出合理的优化
直接内存溢出
错误表现
直接抛出 OOM , 提示 nio 的 DirectByteBuffer 的分配异常
原因分析
32 位的操作系统进程的系统空间位 2GB ,用户空间位 2GB,当 Java 所有进程内存之和大于 2GB 时,就会出现。
nio 的流对象未关闭,或者管道未关闭
解决方案
显示设置 -XX:MaxDirectMemorySize 参数,设定最大直接内存空间
过多线程
错误表现
“unable to create new native thread”
解决方案
减少堆空间 -Xmx
减少每一个线程所占的内存空间 -Xss
注意,线程栈大小设置不能太小,否则会栈溢出
永久区溢出
解决方案
增大MaxPermSize的值。
减少系统需要的类的数量。
使用ClassLoader合理地装载各个类,并定期进行回收。
GC 效率低下