一、类加载
【加载】 - 【链接】-【初始化】
1.1 加载(Loading)
加载阶段是类加载过程的第一步,它的主要任务是通过类的全限定名(Fully Qualified Class Name)来获取类的二进制字节流(binary data)。这些字节流通常来自于本地文件系统、网络等。
链接
1.2 验证(Verification)
在验证阶段,Java虚拟机会对加载的类进行验证,以确保其字节流符合JVM规范,防止被恶意代码攻击。验证过程包括四个步骤:文件格式验证、元数据验证、字节码验证和符号引用验证。
- 文件格式验证:确保字节流符合Class文件格式规范。
- 元数据验证:检查类的元数据信息是否正确。
- 字节码验证:确保字节码操作的正确性和安全性。
- 符号引用验证:确保解析动作能正确执行。
1.3 准备(Preparation)
准备阶段是为类的静态变量(static variable)分配内存空间,并设置默认初始值。这些变量在方法区(Method Area)中进行分配。
1.4 解析(Resolution)
解析阶段是将类、接口、字段和方法的符号引用转换为直接引用的过程。符号引用是一组符号来描述所引用的目标,而直接引用可以是指向内存位置的指针、相对偏移量或者是一个能间接定位到目标的句柄。
1.5 初始化(Initialization)
初始化阶段是类加载过程的最后一步,也是真正执行类构造器方法(<clinit>方法)的阶段。在初始化阶段,JVM会按照程序员指定的初始化方式来初始化类的静态变量和执行静态代码块。
类加载的特性
3.1 延迟加载(Lazy Loading)
JVM在类加载时采取了延迟加载策略,即只有在需要使用这个类时才会进行加载操作。
3.2 双亲委派模型(Delegation Model)
类加载器采用了双亲委派模型,即除了启动类加载器外,每个类加载器都有一个父类加载器。当一个类加载器收到类加载请求时,它会先将加载请求委派给父类加载器,直至委派到启动类加载器。只有当父类加载器无法加载时,子类加载器才会尝试自己加载。
JVM的类加载过程是Java程序运行的基础,通过加载、验证、准备、解析和初始化等阶段,保证了类的安全性和正确性。类加载器负责动态加载类的字节码,并创建Class对象,其中双亲委派模型和延迟加载是其重要的特性。
向上委托,向下加载
!!!!安全!!!!
1、防止类重复加载
2、防止开发者用自己写的类替代篡改java核心class库
二、内存结构
年轻代与老年代
在年轻代中,大部分对象朝生夕死,因此,年轻代的设计目标是尽可能地减少对象的存活时间,以便更快地回收内存。
默认情况下,年轻代和老年代的比例为1:2,即:年轻代占整个堆空间的1/3,老年代占整个堆空间的2/3。
-
可以通过参数-XX:NewRatio=<n>来调整年轻代和老年代的比例。其中<n>表示老年代和年轻代的比例,默认值为2。如:设置-XX:NewRatio=2,表示老年代占整个堆空间的2/3,年轻代占整个堆空间的1/3。
-
可以通过参数-XX:SurvivorRatio=<n>来调整Eden区和Survivor区的比例大小。其中<n>表示Eden区和一个Survivor区的比例,默认值为8。如:设置-XX:SurvivorRatio=8,表示Eden区与Survivor区的比例为8:1:1
对象分配过程
对象进入老年代的触发条件:
-
1)对象的年龄达到15岁时。默认的情况下,对象经过15次Minor GC后会被转移到老年代中。对象进入老年代的Minor GC次数可以通过JVM参数:-XX:MaxTenuringThreshold进行设置,默认为15次。
-
2)动态年龄判断。当一批存活对象的总大小超过Survivor区内存大小的50%时,按照年龄的大小(年龄大的存活对象优先转移)将部分存活对象转移到老年代中。
-
3)大对象直接进入老年代 。当需要创建一个大于年轻代剩余空间的对象(如:一个超大数组)时,该对象会被直接存放到老年代中,可以通过参数-XX:PretenureSizeThreshold(默认值是0,即:任何对象都会先在年轻代分配内存)进行设置。
-
4)Minor GC后的存活对象太多无法放入Survivor区时, 会将这些对象直接转移到老年代中。
线程本地分配缓冲区(TLAB)
TLAB(Thread Local Allocation Buffer)是Java虚拟机中的一个优化技术,主要用于提高对象的分配效率。每个线程都有自己的TLAB,用于分配对象。当一个线程需要分配对象时,它会先在自己的TLAB中分配,如果TLAB中的空间不足,则会向堆中申请空间。
TLAB相关参数:
-
参数-XX:UseTLAB:设置TLAB启动或关闭,默认开启。
-
参数-XX:TLABWasteTargetPercent:设置TLAB所占用Eden区空间的百分比。
使用TLAB的原因:
-
1)保证创建对象时线程安全。堆(Heap)是线程共享区域,在并发环境下,对象在堆中分配内存时存在线程安全问题。通过使用TLAB(无锁方式)解决多个线程同时操作同一地址带来的线程安全问题。
-
2)提高对象的内存分配效率。在堆(Heap)中创建对象非常频繁,在并发环境下,通过加载机制为对象在堆中分配内存时会影响内存分配速度。通过使用TLAB(无锁方式)提高对象的内存分配效率。
堆内存常用参数
栈与栈帧
每一个方法从被调用到执行完毕的过程,对应着一个栈帧在虚拟机栈中从入栈到出栈的过程(注:对栈不了解的同学请移步个人主页查阅->「一文讲清楚」数据结构与算法之栈)。栈顶的栈帧是当前执行方法,当这个方法调用其他方法时会创建一个新的栈帧,这个新的栈帧会被放到虚拟机栈的栈顶,变为当前活动栈帧,该栈帧所有指令都完成后,会将其从栈中移除,之前的栈帧变为当前活动栈帧,前面移除栈帧的返回值变为当前活动栈帧的一个操作数。
动态链接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。
在将Java文件编译成Class文件时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在Class文件的常量池中。比如:描述一个方法调用其他方法就是通过常量池中指向该方法的符号引用来表示,动态链接的作用就是将这些符号引用转换为调用方法的直接引用。
元空间相关参数:
-
MetaspaceSize :初始化元空间大小,控制发生GC阈值,默认值为20MB。
-
MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存,默认值为-1(表示无限制)。
三、内存模型【JMM】
现代CPU通过MESI(Modified, Exclusive, Shared, Invalid)缓存一致性协议和总线锁(Bus Locking)来解决多CPU并行操作数据时可能出现的一致性问题。让我们分析一下它们是如何工作的:
MESI缓存一致性协议
MESI是一种缓存一致性协议,用于确保多个CPU或处理器核心在并行访问共享内存时,能够维持数据的一致性。这种协议主要通过以下几种状态来管理缓存行:
-
Modified(M):缓存行已被修改,且该缓存是唯一拥有该数据的缓存。如果其他CPU需要这个数据,那么首先需要将该数据写回到主内存。
-
Exclusive(E):缓存行仅存在于当前CPU的缓存中,且没有被修改。其他CPU可以通过读取内存来访问该数据,不需要同步操作。
-
Shared(S):缓存行可以被多个CPU同时拥有,且没有被修改。如果有其他CPU修改了该数据,则会使当前CPU的缓存失效。
-
Invalid(I):缓存行无效,即当前缓存中没有该数据或者数据已经过期,需要从主内存重新加载数据。
MESI协议通过这些状态来保证在多CPU环境下的数据一致性:
- 当一个CPU修改数据时(状态变为M),其他CPU的相同缓存行状态会变为I,强制它们读取最新的数据。
- 当一个CPU需要读取数据时,它会根据缓存行状态决定是否需要从其他CPU或主内存中获取最新数据。
总线锁
总线锁是一种硬件级别的同步机制,通常通过在总线上发出锁信号来阻止其他CPU访问内存或某些共享资源。在多CPU并行操作数据时,总线锁可以用于实现临界区的互斥访问,从而保证数据操作的原子性和一致性。具体来说:
- 当一个CPU需要进入临界区时,它会尝试获取总线锁。
- 获取总线锁后,其他CPU尝试获取锁时会被阻塞或等待锁释放。
- 在临界区内,CPU可以安全地修改共享数据,而其他CPU则无法同时访问相同的数据,从而避免了数据竞争和不一致性问题。
解决一致性问题的分析
结合MESI协议和总线锁,可以分析如何解决多CPU并行操作数据的一致性问题:
-
保证缓存一致性:MESI协议确保了在多个CPU缓存中的数据一致性,通过缓存行状态的变化来实现数据的同步和更新。
-
确保原子性操作:总线锁可以用于实现临界区的互斥访问,防止多个CPU同时修改同一块数据,从而保证了操作的原子性和数据的一致性。
-
减少内存访问次数:MESI协议通过在本地缓存中处理数据状态,尽可能减少了对主内存的访问,提高了多CPU系统的性能和效率。
-
硬件级别的支持:MESI和总线锁是在硬件级别实现的机制,不依赖于软件的复杂同步机制,因此能够提供更高效的并发控制和数据一致性保障。
综上所述,MESI缓存一致性协议结合总线锁,有效地解决了多CPU并行操作数据时可能出现的一致性问题,保证了系统的正确性和性能。
内存屏障(Memory Barrier)
一种硬件或者编译器优化中的指令,用来确保指令序列的执行顺序和内存操作的可见性,尤其在多线程环境下非常重要。它的作用主要有两个方面:
-
保证指令顺序的执行:
- 内存屏障可以确保特定指令之前和之后的指令顺序不会被重排序。在多核处理器或者乱序执行的CPU中,为了提高执行效率,会对指令进行优化和重排序。然而,某些情况下程序的正确性依赖于特定的指令执行顺序。内存屏障可以确保关键的操作按照程序员指定的顺序执行,而不会被编译器或者处理器重新排序。
-
保证内存可见性:
- 内存屏障可以确保线程对共享变量的修改对其他线程是可见的。在多线程编程中,由于线程之间的工作内存和主内存的交互,可能会导致一个线程的修改不能立即被其他线程看到,这就会引发数据不一致的问题。内存屏障可以强制刷新缓存,使得共享变量的修改对其他线程是可见的。
在Java中,内存屏障的实现通常通过特定的语义来保证,比如使用 volatile
关键字或者 synchronized
关键字就隐含了内存屏障的语义。此外,编译器和处理器在生成指令序列时也会考虑到内存屏障的需要,以确保程序在多线程环境下的正确性。
读内存屏障(Load Barrier)
读内存屏障确保在执行读操作之前,所有之前的读写操作对于当前线程来说都是可见的。具体作用包括:
-
保证可见性:当一个线程执行读操作时,它通过读内存屏障确保从主内存中读取最新的数据,而不是从线程的工作内存中获取过期的值。
-
禁止指令重排序:读内存屏障可以防止编译器或者处理器在执行时优化读操作的顺序,确保读操作不会被移到屏障之后执行。
在Java中,volatile
变量的读操作就隐含了读内存屏障的语义。使用 volatile
声明的变量,每次读取操作都会从主内存中获取最新的值,而不会使用线程的本地缓存。
写内存屏障(Store Barrier)
写内存屏障确保在执行写操作后,所有之后的读写操作对于其他线程来说都是可见的。具体作用包括:
-
保证可见性:当一个线程执行写操作后,通过写内存屏障确保将修改后的值刷新到主内存,使得其他线程能够看到最新的值。
-
禁止指令重排序:写内存屏障可以防止编译器或者处理器在执行时优化写操作的顺序,确保写操作不会被移到屏障之前执行。
happens-before
-
定义:
happens-before 是一个全局的偏序关系,它规定了在并发程序中操作的执行顺序。如果操作 A happens-before 操作 B,那么我们可以说操作 A 在操作 B 之前发生,并且操作 A 的结果对操作 B 是可见的。
四、垃圾回收算法跟垃圾回收器
什么是垃圾
在计算机科学和软件开发领域,"垃圾"(garbage)通常指的是内存中不再被程序使用的对象或数据。在编程中,当程序创建对象或分配内存时,这些对象需要被管理,以确保内存使用的效率和合理性。如果一个对象不再被程序所引用(即没有任何变量指向它),它就变成了"垃圾"。
可达性分析
可达性分析(Reachability Analysis)是垃圾回收算法中的一项关键技术,用于确定哪些对象在运行时是可达的(reachable),即哪些对象可以通过一系列引用路径从根对象(如栈中的引用或静态字段)访问到。如果一个对象不可达,意味着在程序执行过程中无法再通过任何引用路径访问到它,这种对象就可以被垃圾回收机制安全地回收释放其占用的内存空间。
过程详解
-
根集合(Root Set): 根集合包括所有在程序当前执行点直接可访问到的对象,如:
- 虚拟机栈(栈帧中的本地变量引用的对象)
- 方法区中类静态属性引用的对象
-
可达对象(Reachable Objects): 从根集合开始,通过引用路径可以访问到的对象称为可达对象。垃圾回收器通过对这些对象进行可达性分析,标记它们为活跃对象。
-
不可达对象(Unreachable Objects): 如果一个对象不在根集合中,也无法通过任何引用链从根集合中的对象访问到,则认为这个对象是不可达的。垃圾回收器将不可达对象标记为可回收对象,以便在后续的垃圾回收过程中释放它们占用的内存空间。
算法实现
典型的可达性分析算法通常使用广度优先搜索(Breadth-First Search,BFS)或深度优先搜索(Depth-First Search,DFS)来遍历从根集合出发的对象引用链路,确定哪些对象是可达的。其基本步骤如下:
- 初始化:将根集合中的所有对象标记为活跃对象。
- 遍历:从每个活跃对象出发,沿着对象的引用路径遍历访问所有可达的对象,并标记它们为活跃对象。
- 标记完成:所有能够通过引用链从根集合访问到的对象都被标记为活跃对象。未被标记的对象即为不可达对象。
应用与优化
可达性分析在各种垃圾回收算法中起着核心作用,如标记-清除算法、标记-整理算法、复制算法等都依赖于它来决定哪些对象需要被回收。在实际应用中,为了提高性能,可达性分析通常会被优化,例如使用增量标记、并发标记等技术来减少垃圾回收造成的停顿时间,以及使用分代策略来更精确地管理不同生命周期的对象。
总之,可达性分析作为垃圾回收的基础,通过精确地确定对象的可达性状态,确保了垃圾回收器能够有效地管理和释放内存,从而提升应用程序的性能和可靠性。
三色标记法
三色标记法(Tricolor Marking)是一种用于可达性分析的经典算法,主要用于并发垃圾回收器中,能够有效地处理对象的并发访问和变化。它基于三种不同颜色的标记来管理对象的可达性状态,通常是白色、灰色和黑色。
过程详解
三色标记法的核心思想是通过多次迭代,将对象分为三个不同的集合(颜色),直到所有可达对象都被正确标记为黑色,以便垃圾回收器能够安全地回收不可达对象。
-
初始标记阶段(Initial Marking):
- 目标:标记从根集合(如虚拟机栈、静态变量)直接可达的对象为灰色。
- 过程:从根集合出发,将所有直接可达的对象标记为灰色。这些对象是下一步标记的起点。
-
并发标记阶段(Concurrent Marking):
- 目标:通过并发扫描对象图,从灰色对象的引用继续深入,标记其可达的对象为灰色,直到无法继续发现新的灰色对象为止。
- 过程:并发地遍历灰色对象,并将它们引用的对象标记为灰色。如果一个灰色对象所有的引用对象都已经被标记为黑色或灰色,则将它标记为黑色。如果发现新的灰色对象,继续标记它们的引用。
-
重新标记阶段(Remark):
- 目标:由于在并发标记阶段中对象图可能会发生变化(如对象被新增或引用关系变化),因此需要对整个对象图进行重新扫描,确保所有对象的可达性状态都是正确的。
- 过程:停止应用程序线程,暂时挂起程序执行,全局扫描对象图,处理新添加的对象和变化的引用关系,将所有可达的对象正确标记为黑色。
-
清除阶段(Sweep):
- 目标:清理所有未标记为黑色的对象,这些对象即为不可达对象,可以被安全回收。
- 过程:遍历堆中所有对象,将白色对象(未被任何引用链路访问到的对象)回收释放内存,或者将其放入空闲列表以供后续分配使用。
优点与应用
三色标记法通过灰色标记的方式,使得垃圾回收器可以在标记过程中并发执行,不需要停止整个应用程序的执行。这种并发标记的方法大大减少了垃圾回收对程序执行的影响,提高了应用的响应速度和性能稳定性。
垃圾回收算法
垃圾回收算法定义了在何时以及如何回收不再被程序使用的内存。Java 的主要垃圾回收算法包括:
-
标记-清除算法(Mark and Sweep):
- 标记-清除是最基本的垃圾回收算法。它分为两个阶段:标记阶段(标记所有活跃对象)和清除阶段(删除所有未标记的对象)。缺点是会产生内存碎片。
-
复制算法(Copying):
- 复制算法将内存分为两块,每次只使用其中一块。垃圾回收时,将活跃对象从一块复制到另一块,并清理整块未使用的内存。这种算法简单高效,但是会浪费一部分内存空间。
-
标记-整理算法(Mark and Compact):
- 标记-整理算法结合了标记-清除和内存整理的优点。首先标记所有活跃对象,然后将它们向内存一端移动,最后清理边界外的内存。相比标记-清除,它能减少内存碎片。
-
分代算法:
- 分代算法根据对象存活周期将内存分为不同的代(Generation)。通常是将堆分为新生代(Young Generation)和老年代(Old Generation),每代使用不同的垃圾回收算法。新生代通常使用复制算法,老年代使用标记-清除或标记-整理算法。
垃圾回收器(Garbage Collector)
垃圾回收器是 JVM 中实现垃圾回收算法的具体组件,根据不同的内存分区和对象生命周期选择合适的算法。Java 8 及更早版本中常见的垃圾回收器包括:
-
Serial GC:
- 单线程执行垃圾回收,适合于单核处理器环境和小内存应用。
-
Parallel GC:
- 多线程执行垃圾回收,通过并行处理提高回收效率,适合多核处理器和大内存应用。
-
CMS GC(Concurrent Mark Sweep):
- 并发标记清除垃圾回收器,通过减少停顿时间来提高响应速度,适合对响应时间有要求的应用。
-
G1 GC(Garbage-First):
- G1 是一种分代垃圾回收器,将整个堆划分为多个大小相等的区域,通过优先处理垃圾最多的区域来优化回收效率和响应时间。
-
ZGC 和 Shenandoah GC:
- Java 11 引入了 ZGC 和 Java 12 引入了 Shenandoah GC,它们都是面向大内存应用、低延迟的垃圾回收器,尤其适合处理 TB 级别的内存。
JDK7/8后,HotSpot虚拟机所有收集器及组合(连线),如下图:
其中cms在进行FullGc时,STW过程中会使用到Serial-Old;
推荐使用
1.基于低停顿时间的垃圾收集器
-XX:+UseConcMarkSweepGC(该参数隐式启用-XX:+UseParNewGC)
2.基于吞吐量优先的垃圾收集器
-XX:+UseParallelOldGC(该参数隐式启用-XX:+UseParallelGC)
Serial
采用复制算法,GC时发生stop-the-world,使用单个GC线程。
Parallel Scavenge
采用复制算法,GC时发生stop-the-world,使用多个GC线程。 吞吐量优先收集器,可控制最大垃圾收集停顿时间(-XX:MaxGCPauseMillis)与吞吐量大小(-XX:GCTimeRatio),支持GC自适应的调节策略(GC Ergonomics,对应参数-XX:+UseAdaptiveSizePolicy)。
ParNew
ParNew采用复制算法,GC时发生stop-the-world,使用多个GC线程。
ParNew 与 Parallel Scavenge的一个主要区别是,ParNew可以与CMS进行搭配使用。
"ParNew" is a stop-the-world, copying collector which uses multiple GC threads. It differs from "Parallel Scavenge" in that it has enhancements that make it usable with CMS. For example, "ParNew" does the synchronization needed so that it can run during the concurrent phases of CMS.
特点:
Serial收集器的多线程并行版本(除了同时使用多条线程进行垃圾收集之外, 其余的行为(包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等)都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码)
JDK 7之前在服务端模式下首选的新生代收集器(一个重要原因:除了Serial收集器外, 目前只有它能与 CMS 收集器配合工作)
ParNew收集器是激活CMS后(使用-XX: +UseConcMarkSweepGC选项) 的默认新生代收集器,也可以使用-XX: +/-UseParNewGC选项来强制指定或者禁用它。
ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果
默认开启的收集线程数与处理器核心数量相同(可以使用-XX: ParallelGCThreads参数来限制垃圾收集的线程数)
老年代垃圾回收器
除了CMS,其他的老年代垃圾收集器GC时都是stop-the-world,都会在清理垃圾之后进行压缩整理。
Serial Old
采用标记整理算法,GC时发生stop-the-world,使用单个GC线程。
- Serial收集器的老年代版本
- 单线程收集器
- 使用标记-整理算法
Parallel Old
采用标记整理算法,GC时发生stop-the-world,使用多个GC线程。
- Parallel Scavenge收集器的老年代版本
- 使用标记-整理算法
CMS
CMS采用标记清理算法,是一个以低暂停时间为目标的垃圾收集器。GC时大部分时间并发执行,其中初始化标记和重新标记两个阶段仍然会发生stop-the-world,其余阶段都是并发执行。
CMS整个过程的四个步骤如下,其中初始标记、 重新标记这两个步骤仍然需要"Stop The World"。
缺点:
CMS 收集器对处理器资源非常敏感。事实上,面向并发设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低总吞吐量。
由于CMS收集器无法处理"浮动垃圾", 有可能出现"Con-current Mode Failure"失败进而导致另一次完全"Stop The World"的Full GC的产生。
CMS是一款基于"标记-清除"算法实现的收集器,这意味着收集结束时会有大量空间碎片产生,空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间, 但就是无法找到足够大的连续空间来分配当前对象, 而不得不提前触发一次Full GC的情况。
关于CMS收集器浮动垃圾的说明:
由于在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们, 只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为"浮动垃圾"。
由于在垃圾收集阶段用户线程还需要持续运行, 那就还需要预留足够内存空间提供给用户线程使用, 因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。
G1
G1将整个堆划分为多个大小相等的独立区域(Region),保留新生代和老年代的分代概念(但两者不再是物理隔离的)。
从整体来看是基于标记整理算法,从局部(两个Region之间)来看是基于复制算法。因此,可以避免产生内存空间碎片,防止发生并发模式失败。
使用多个GC线程,每次优先回收价值最大的Region。
支持可预测的停顿时间模型,从而提高收集效率,降低stop-the-world的时间。
Region中还有一类特殊的Humongous区域, 专门用来存储大对象。 G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。 每个Region的大小可以通过参数-XX: G1HeapRegionSize设定, 取值范围为1MB~32MB, 且应为2的N次幂。 而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中, G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待.
G1 收集器的运作过程主要步骤如下:
初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
筛选回收: 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序, 根据用户所期望的停顿时间来制定回收计划, 可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
CMS 跟 Parallel Old 的区别
CMS(Concurrent Mark-Sweep)和Parallel Old是Java虚拟机(JVM)中两种不同的垃圾回收器。它们在处理老年代(Old Generation)的对象回收上有一些显著的区别:
CMS(Concurrent Mark-Sweep)
-
并发标记和清除:
- 标记阶段(Concurrent Marking):CMS会在应用程序运行的同时进行垃圾回收标记操作,这意味着垃圾回收线程与应用线程并发执行。这样可以减少应用程序暂停时间,提高响应性能。
- 清除阶段(Concurrent Sweeping):清除阶段也是并发执行的,不会造成长时间的停顿。
-
低暂停时间:
- CMS致力于减少老年代垃圾回收时的停顿时间。这对于需要保证应用程序响应性和低延迟的场景非常重要。
-
内存碎片化问题:
- CMS在进行老年代的垃圾回收时,可能会产生内存碎片,这可能会导致长期运行时需要更频繁地进行Full GC来解决内存分配问题。
Parallel Old
-
多线程处理:
- Parallel Old使用多线程来进行老年代的垃圾回收操作,与应用程序的其他线程并行运行。这种方式通常会牺牲一些响应性能,但是在后台线程执行的情况下可以获得更高的吞吐量。
-
全停顿:
- Parallel Old在进行垃圾回收时,会导致应用程序的全局暂停,直到垃圾回收操作完成。这种暂停时间通常比CMS长,但可以通过调整参数来优化。
-
适用场景:
- Parallel Old适合那些对吞吐量要求高、可以容忍较短暂停时间的应用场景,比如后台运算、批处理等。
区别总结
- 并发性能:CMS强调并发执行,减少停顿时间,适合对响应性能要求较高的应用。Parallel Old则以吞吐量为重点,采用多线程并行处理,适合吞吐量要求高的应用场景。
- 停顿时间:CMS的停顿时间较短,但可能会有碎片化问题;Parallel Old的停顿时间较长,但更适合需要高吞吐量的场景。
- 内存碎片化:CMS容易产生内存碎片,可能需要更频繁的Full GC来解决;Parallel Old在这方面相对较少。
选择CMS还是Parallel Old通常取决于应用的具体需求和性能特征,以及对应用程序响应性和吞吐量的不同优先级需求。
CMS 在哪个阶段会造成 STW
CMS(Concurrent Mark-Sweep)是 Java 虚拟机中的一种垃圾回收器,它的设计目标是减少老年代的停顿时间(Stop-The-World,STW)。然而,尽管 CMS 被设计为并发的垃圾回收器,仍然有一些阶段可能会引起 STW 停顿。
在 CMS 垃圾回收器的工作过程中,主要会涉及到以下几个阶段,其中一些阶段可能会引起 STW 停顿:
-
初始标记阶段(Initial Marking):
- 这是一次短暂的停顿,用于标记所有的根对象,以及直接被根对象引用的对象。这个阶段需要 STW,因为它需要暂停所有应用线程来确保标记的一致性。
-
并发标记阶段(Concurrent Marking):
- 在这个阶段,CMS 尝试并发标记所有存活的对象。这个阶段应该不会引起可见的 STW 停顿,因为它是并发执行的。
-
重新标记阶段(Remark):
- 在并发标记阶段结束后,应用线程会被暂停,进行一次短暂的重新标记。这个阶段的目的是处理并发标记阶段中发生的引用更新,确保标记的准确性。
-
并发清除阶段(Concurrent Sweep):
- 在重新标记之后,CMS 尝试并发地清理未标记的对象。这个阶段应该不会引起可见的 STW 停顿。
-
并发重置阶段(Concurrent Reset):
- 最后一个阶段,用于清理 CMS 中的一些数据结构,为下一次垃圾回收做准备。这个阶段应该不会引起可见的 STW 停顿。
尽管 CMS 设计为尽量减少 STW 停顿时间,但初始标记阶段和重新标记阶段仍然会引起短暂的 STW 停顿。这些停顿时间通常是较短的,相较于传统的全停顿垃圾回收器,CMS 仍然可以显著减少老年代的停顿时间,但在高度优化的场景下,这些短暂的 STW 停顿也是不可避免的。
可逃逸分析
描述:
- 可逃逸分析是一种优化技术,用于判断对象的引用是否会逃逸出当前方法或线程的范围,即是否可以被其他方法或线程访问到。
- 如果对象的引用不会逃逸,虚拟机可以选择将对象分配在栈上而不是堆上,从而避免堆上的分配和垃圾回收的开销,提升性能。
- 可逃逸分析主要用于优化内存分配和访问效率,并不直接参与垃圾回收的标记阶段。
可逃逸分析过程中判定为不可逃逸,但是栈空间又不足
尽管对象在可逃逸分析中被判定为不会逃逸,可以分配在栈上,但如果栈空间不足,虚拟机通常会将其分配到堆上。可以选择适当增加栈空间大小;
Java 8 hotspot 虚拟机有使用可逃逸分析,栈上分配的优化么?
在 Java 8 的 HotSpot 虚拟机中,确实包含了可逃逸分析和栈上分配的优化策略。这些优化技术旨在提升程序的性能和内存利用效率。具体来说:
-
可逃逸分析(Escape Analysis):
- HotSpot 虚拟机在 Java 8 中已经实现了可逃逸分析。该技术用于分析对象的引用是否会逃逸出当前方法或线程的范围。
- 如果对象的引用不会逃逸,HotSpot 虚拟机可以选择将对象分配在栈上而不是堆上。这种做法可以减少堆上的内存分配和垃圾回收的压力,提升程序的执行效率和内存利用率。
-
栈上分配的优化:
- 栈上分配是指将某些对象分配到当前线程的栈上,而不是堆上。这种优化通常适用于那些被判定为不会逃逸的对象。
- Java 8 的 HotSpot 虚拟机会结合可逃逸分析的结果,对于不会逃逸的对象,尝试在栈上进行分配。这可以显著降低对象分配的成本,因为栈上的对象随着方法的结束而自动释放,无需进行复杂的垃圾回收操作。
总之,Java 8 的 HotSpot 虚拟机通过可逃逸分析和栈上分配的优化策略,旨在提高程序的运行效率和内存使用效率。这些技术对于减少内存分配和垃圾回收的开销都起到了积极作用,尤其是在处理大量短期生存的对象时尤为有效。
五、JVM常用问题及分析手段
线程问题
CPU资源占用高或持续占用的问题。
1、top -Hp <java进程id>; 找出消耗多的线程id;
2、printf "%x\n" <线程id10进制值>;输出16进制值;
3、jstack 进程id |grep '0x<16进制值>' -A 20 -B 2;
jstack
jstack
是 Java 开发中常用的一种工具,用于生成 Java 进程的线程堆栈跟踪信息。通过分析 jstack
输出的结果,可以帮助定位应用程序中的性能问题和线程相关的调试信息。下面是一些常用的 jstack
分析细节方法:
1. 查看线程状态和数量
使用 jstack
可以查看当前 Java 进程中所有线程的状态和数量。这对于检查是否有线程死锁或者线程阻塞等问题非常有帮助。
jstack <pid>
其中 <pid>
是 Java 进程的进程ID。
- RUNNABLE 状态:表示线程正在 Java 虚拟机中执行代码。
- BLOCKED 状态:表示线程被阻塞等待监视器锁。
- WAITING 和 TIMED_WAITING 状态:表示线程在等待另一个线程执行特定操作(如等待 I/O 操作完成或等待锁释放)。
- NEW 和 TERMINATED 状态:分别表示线程刚创建或已经结束。
2. 定位死锁
死锁是一个常见的并发问题,通过 jstack
可以检测和定位死锁情况。
jstack -l <pid>
使用 -l
参数输出关于锁信息的详细信息。在输出中,会显示每个线程当前持有的锁和正在等待的锁。通过分析这些信息,可以识别出哪些线程彼此等待对方持有的锁,从而定位死锁。
3. 分析线程堆栈
jstack
输出的主要部分是每个线程的堆栈跟踪信息。这些信息展示了线程当前执行的方法调用栈。
jstack <pid> > thread_dump.txt
将 jstack
输出保存到文件中,然后可以使用文本编辑器或者分析工具来分析。通常情况下,关注每个线程的调用栈,特别是耗时长的方法或者可能存在性能问题的方法。
4. 监控 CPU 占用高的线程
有时候需要定位导致 CPU 使用率高的线程,jstack
可以帮助找到具体的线程和执行路径。
jstack -l <pid> | grep '"java.lang.Thread.State' [-A 20 -B 2] | sort -r
这条命令将输出当前 Java 进程中所有线程的堆栈信息,并按照 CPU 使用率高低排序。通过分析输出,可以找到消耗 CPU 资源的线程和相关的方法。
5. 分析线程 Dump 的时机
通常在以下情况下分析线程 Dump 是有帮助的:
- 应用程序假死:应用程序响应缓慢或者不响应时。
- 性能问题:CPU 使用率高或者内存占用增长异常时。
- 死锁:检测到应用程序中有死锁发生时。
6. 使用 VisualVM 或者其它分析工具
除了手动分析 jstack
输出外,可以结合使用诸如 VisualVM、JProfiler 等图形化分析工具,这些工具可以更直观地展示线程状态、内存使用情况以及性能瓶颈。
通过这些方法,可以更有效地利用 jstack
工具来分析 Java 应用程序中的线程问题和性能瓶颈,帮助开发人员快速定位和解决问题。
常见问题
-
线程死锁:
- 工具:使用 Java 自带的工具如 jstack 或者 jcmd 可以生成线程转储(thread dump),分析线程状态,检测是否存在死锁。
- 分析:分析线程转储中的线程互相等待资源的情况,确定是否存在环形等待。
-
线程泄漏:
- 工具:使用 jmap 或者 jvisualvm 这样的工具,查看 JVM 中的线程数是否在持续增长。
- 分析:检查创建线程的代码,确保线程在不需要时能够正确地被销毁或者结束。
-
线程饥饿:
- 工具:使用 jconsole、jvisualvm 等监控工具,观察线程的运行状态和线程数的变化。
- 分析:检查是否有某些线程长时间无法获取到所需的资源,导致无法执行。
-
线程池问题:
- 工具:使用 jvisualvm、Java Profiler 等工具监控线程池的使用情况,包括线程池的大小、活跃线程数、任务队列长度等。
- 分析:检查线程池的配置是否合理,线程池是否因为任务过多或者线程超时等原因导致阻塞或性能问题。
内存问题
-
内存泄漏:
- 工具:使用 jmap、jvisualvm 或者 Java Profiler 进行堆转储(heap dump),查看内存中的对象情况。
- 分析:分析堆转储中的对象引用关系,查找存在泄漏的对象,例如长期持有但无法释放的对象。
-
内存溢出:
- 工具:当发生 OutOfMemoryError 时,使用堆转储工具获取内存快照。
- 分析:查看堆转储中的内存使用情况,确定是哪部分内存占用过高导致内存溢出,如是否存在大对象、内存泄漏等。
-
GC 问题:
- 工具:使用 jstat、jconsole、jvisualvm 等工具监控 GC 情况,查看 GC 日志。
- 分析:分析 GC 日志,确认 GC 发生的频率、停顿时间,以及各代的内存使用情况,判断是否需要调整堆大小或者调整 GC 算法的参数。
-
持久代/元空间问题:
- 工具:使用 jstat、jvisualvm 监控持久代或元空间的使用情况。
- 分析:检查持久代或元空间的使用是否超出预期,是否有类加载器泄漏等问题,确认是否需要调整持久代大小或元空间大小。
在 JVM 中配置自动生成堆内存转储文件(heap dump)的机制可以帮助在发生 OutOfMemoryError (OOM) 或者达到特定阈值时,自动捕获内存快照,以便后续分析和调试问题。
dump堆内存
方法一:使用 JVM 参数配置
通过设置 JVM 参数 -XX:+HeapDumpOnOutOfMemoryError
可以在发生 OOM 错误时自动生成堆转储文件。
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump/directory
-XX:+HeapDumpOnOutOfMemoryError
:当发生 OOM 错误时,生成堆转储文件。-XX:HeapDumpPath=/path/to/dump/directory
:指定堆转储文件的存储路径。如果未指定,默认存储在启动 Java 进程的当前工作目录下。
方法二:设置堆大小和阈值
除了在 OOM 错误时生成堆转储文件外,也可以通过设置 -XX:HeapDumpBeforeFullGC
和 -XX:HeapDumpAfterFullGC
参数,在每次 Full GC 之前或之后生成堆转储文件。
-XX:HeapDumpBeforeFullGC
:在进行 Full GC 之前生成堆转储文件。-XX:HeapDumpAfterFullGC
:在进行 Full GC 之后生成堆转储文件。
java -XX:+HeapDumpBeforeFullGC -XX:+HeapDumpAfterFullGC -XX:HeapDumpPath=/path/to/dump/directory
jmap
jmap
是 JDK 自带的一个命令行工具,用于生成 Java 进程的内存映像文件(heap dump)和查看 Java 进程的内存使用情况。下面详细解释 jmap
命令的使用。
1. 生成堆转储文件
生成堆转储文件可以帮助分析 Java 进程的内存使用情况,包括对象的分布、大小、引用关系等信息。
命令格式:
jmap -dump:format=b,file=<文件路径> <进程ID>
-dump:format=b,file=<文件路径>
:指定生成堆转储文件的格式为二进制(b),并且指定文件保存的路径和文件名。<进程ID>
:要生成堆转储文件的 Java 进程的进程ID。
示例:
jmap -dump:format=b,file=/path/to/dump.hprof 12345
这个命令会在指定路径下生成一个名为 dump.hprof
的堆转储文件,包含进程ID为 12345
的 Java 进程的内存信息。
2. 查看堆内存信息
除了生成堆转储文件外,jmap
还可以查看 Java 进程的堆内存使用情况。
命令格式:
jmap -heap <进程ID>
-heap
:显示 Java 堆的详细信息,包括堆的大小、已使用量、GC 策略等。<进程ID>
:要查看堆内存信息的 Java 进程的进程ID。
示例:
jmap -heap 12345
这个命令会输出进程ID为 12345
的 Java 进程的堆内存使用情况的详细信息。
3. 查看对象实例数量统计
jmap
还可以用来统计 Java 进程中各个类的对象实例数量,帮助分析内存中的对象分布情况。
命令格式:
jmap -histo <进程ID>
-histo
:显示堆中所有对象的统计信息,包括每个类的实例数量和占用内存大小。<进程ID>
:要统计对象实例数量的 Java 进程的进程ID。
示例:
jmap -histo 12345
这个命令会列出进程ID为 12345
的 Java 进程中所有类的对象实例数量统计信息。
4. 使用前提
在使用 jmap
之前,请确保以下几点:
- 权限 :具有运行
jmap
命令的操作系统用户需要有足够的权限访问目标进程的内存信息。 - 进程ID:需要知道目标 Java 进程的进程ID。
- Java 版本 :
jmap
命令通常随 JDK 一起安装,因此需要安装有 Java JDK。
注意事项
- 影响性能:生成堆转储文件可能会影响 Java 进程的性能,特别是在堆较大或者对象数量众多的情况下。
- 分析工具:生成的堆转储文件通常需要借助分析工具(如 Eclipse MAT、VisualVM 等)来进行详细分析。
使用 jmap
命令可以帮助开发人员和运维人员有效地诊断和分析 Java 进程的内存使用情况,是 Java 应用程序性能调优和故障排查的有力工具之一。
dump堆文件分析
分析内存转储文件(heap dump)是诊断和解决Java应用程序内存问题的重要步骤。通常,我们使用专门的工具来分析堆转储文件,例如 Eclipse Memory Analyzer Tool (MAT) 或者 Java VisualVM。下面是使用 Eclipse MAT 的基本步骤,来帮助您理解如何分析内存转储文件:
使用 Eclipse Memory Analyzer Tool (MAT) 分析堆转储文件
1. 下载和安装 Eclipse MAT
首先,确保您已经下载和安装了 Eclipse MAT。可以从官网下载最新版本:Eclipse MAT 官网
2. 打开内存转储文件
-
启动 Eclipse MAT:
- 打开 Eclipse MAT 后,选择菜单中的
File
->Open Heap Dump...
。
- 打开 Eclipse MAT 后,选择菜单中的
-
选择堆转储文件:
- 在弹出的对话框中,选择您要分析的堆转储文件(通常是以
.hprof
结尾的文件)。
- 在弹出的对话框中,选择您要分析的堆转储文件(通常是以
-
等待文件加载:
- Eclipse MAT 会加载堆转储文件并解析其内容。这可能需要一些时间,具体时间取决于文件大小和计算机性能。
3. 分析堆转储文件
一旦堆转储文件加载完成,您将可以使用 Eclipse MAT 提供的各种功能来分析内存使用情况。
-
主要视图:
- Histogram(直方图):显示堆中各个类的对象实例数量和占用内存大小。可以帮助您识别哪些类占用了大量内存。
- Dominator Tree(支配树):显示堆中对象的引用关系,帮助找出大的对象及其引用链。
-
常用操作:
- 搜索对象:在直方图视图中搜索特定的对象类名或实例。
- 查看对象详细信息:选择特定对象,查看其详细信息,包括实例变量、引用等。
- 内存泄漏分析:使用内存泄漏分析功能,确定哪些对象持有不再需要的引用而导致内存泄漏。
-
高级功能:
- 路径到 GC Roots:查找对象到 GC Roots 的引用路径,帮助理解为什么某些对象没有被 GC 回收。
- 统计报告:生成详细的内存使用统计报告,帮助总结堆中各种类型对象的分布和占用情况。
4. 解决问题和优化
根据分析的结果,您可以识别和解决内存问题,例如优化代码以减少内存使用、修复内存泄漏问题、调整 JVM 参数等。
注意事项
- 堆转储文件大小:大型堆转储文件可能需要更多的内存和时间来加载和分析。
- 分析工具选择:除了 Eclipse MAT,还有其他工具如 Java VisualVM、YourKit 等,选择适合自己需求和习惯的工具进行分析。
通过以上步骤,您应该能够开始使用 Eclipse MAT 或类似工具来分析 Java 应用程序的堆转储文件,以便深入了解应用程序的内存使用情况,并解决可能的内存问题。
阿尔萨斯JVM分析工具
针对线程问题的分析方法:
使用阿尔萨斯可以有效地分析和定位以下线程问题:
-
查看线程堆栈:
- 命令 :
thread
命令可以列出当前 JVM 中所有的线程及其堆栈信息。 - 用途:可以查看每个线程的状态、执行的方法和调用链,帮助识别是否有线程死锁、线程阻塞等问题。
- 命令 :
-
线程监控:
- 命令 :
thread --state BLOCKED
可以列出所有处于阻塞状态的线程,帮助快速定位可能的死锁问题。 - 用途:能够及时发现线程在等待锁资源时被阻塞的情况,帮助解决死锁或者线程饥饿问题。
- 命令 :
-
线程 Dump:
- 命令 :
thread --tid <线程ID>
可以生成指定线程的堆栈信息。 - 用途:当发生线程死锁或者其他异常时,生成线程转储,通过分析堆栈信息来理解线程之间的互相等待关系。
- 命令 :
针对内存问题的分析方法:
阿尔萨斯也提供了多种命令来帮助分析和定位内存问题:
-
查看堆内存:
- 命令 :
heapdump
可以生成 JVM 的堆转储文件(heap dump)。 - 用途:通过分析堆转储文件,可以查看内存中的对象分布情况、各个对象的引用关系,帮助识别内存泄漏问题。
- 命令 :
-
监控 GC 情况:
- 命令 :
gc
可以实时查看 JVM 的 GC 情况。 - 用途:可以观察 GC 的频率、每次 GC 的停顿时间,帮助判断是否存在频繁的 Full GC 或者内存溢出的风险。
- 命令 :
-
类加载器监控:
- 命令 :
classloader
可以查看 JVM 中的类加载器信息。 - 用途:帮助识别是否存在类加载器泄漏或者过多的动态类加载,导致元空间或持久代溢出的问题。
- 命令 :
六、JVM调优实战
接口超时告警 100ms设定,但是会偶发出现超时现象,吻合 GC时间点的 毛刺;
长时间大量本地缓存的业务,缓存时间长,已经进入老年代。即使缓存失效,移除了缓存数据,yongGc还是无法将其从拉年代回收。出现老年代使用增加,最终fullGc导致偶发接口超时的问题。
在业务低峰期手段发起gc命令,避免在业务高峰期出现fullGc的现象;
从CMS改用G1,解决了fullGc导致服务超时的问题;采用G1回收器后,yongGC几乎不影响应用线程执行。
参考的原文地址:
https://blog.csdn.net/Urbanears/article/details/134793515
Java虚拟机垃圾回收器详解百度安全验证https://baijiahao.baidu.com/s?id=1787539733318430902&wfr=spider&for=pc