Java虚拟机
目前Oracle官方使用的是HotSpot,是当前最主要的JVM。
JVM组成
以java 8 为例,展示java虚拟机组成
-
Class Files:类文件,通常是
.class
文件,包含了编译后的Java代码,包括类的定义、字段、方法等信息。 -
Class Loader Subsystem:类加载子系统,负责从文件系统或其他来源加载类文件到内存JVM中,并把它们转换成运行时的类表示。
-
Method Area:方法区,是JVM内存的一部分,存储类结构,如运行时常量池、字段、方法数据等。
-
从JDK1.8开始此空间由永久代改名为元空间meta space。
-
-
Heap:堆,是JVM内存中最大的一块区域,用于存储对象实例和数组。
-
如果对象无法在堆中申请到可用内存将抛出OOM异常,这是Java虚拟机(JVM)内存管理的一个机制,用来处理内存不足的情况。在Linux操作系统中,当系统级别的内存资源耗尽,即没有足够的物理内存或交换空间(swap space)可供分配时,操作系统会采取行动来释放内存资源。Linux内核的Out of Memory(OOM)管理器会被触发,它负责决定哪个进程应该被终止以释放内存。Linux的OOM管理器会根据进程的重要性(通常是优先级)和内存使用情况等因素来选择一个进程并发送SIGKILL信号,强制终止该进程。这样做是为了保护系统的整体稳定性,防止系统崩溃。所以日常管理优化应避免出现OOM,以防关键业务程序被kill,导致业务中断。
-
堆内存由垃圾收集器管理,定期进行垃圾回收以释放不再使用的对象。
-
-
Java Program:Java程序,指运行在JVM上的应用程序代码。
-
Native Area:本地方法区,存储JVM使用本地方法接口(JNI)调用的本地(native)方法和库的信息。
-
Threads:线程,代表并发执行的路径。JVM允许多个线程并发执行,每个线程都有自己的执行堆栈。
-
Counter:计数器,可能指的是JVM内部用于监控和统计的各种计数器,如循环计数器、性能计数器等。
-
Internal Registers:内部寄存器,JVM内部使用的临时存储空间,用于存储临时数据和指令状态。
-
Runtime Data Areas:运行时数据区域,JVM在执行Java程序时使用的内存区域,包括堆、方法区、栈、本地方法栈和程序计数器。
-
Execution Engine:执行引擎,负责执行Java字节码,将类文件中的字节码指令转换为机器码执行。
-
JIT (Just-In-Time) Compiler:即时编译器,用于将热点代码(经常执行的代码)编译成本地机器码,以提高执行效率。
-
Garbage Collector:垃圾收集器,负责回收不再使用的对象,管理堆内存,防止内存泄漏。
-
Interface Libraries:接口库,提供与本地系统交互的接口,如网络操作、文件系统访问等。
GC(Garbage Collector)垃圾回收器
在堆内存中,如果创建的对象不再使用,仍占用内存,此时即为垃圾,需要进行垃圾回收,释放内存空间给其它对象使用。
GC主要任务是自动管理内存的分配和回收,确保内存资源的有效利用,并防止内存泄漏。
对于垃圾回收,有三个问题
哪些是垃圾、怎么回收垃圾、什么时候回收垃圾
垃圾确定算法
引用计数算法(Reference Counting)
引用计数算法是通过跟踪每个对象的引用数量来确定对象是否可回收的方法。当对象被创建时,它的引用计数初始化为0,然后每次有新的引用指向该对象时,计数增加;当引用被移除或变为无效时,计数减少。当对象的引用计数降为0时,意味着没有任何引用指向该对象,因此该对象可以被回收。
优点:
- 简单直观,易于实现。
- 可以立即回收无用对象。
缺点:
- 需要维护每个对象的引用计数,增加了额外的内存开销。
- 无法处理循环引用的情况,即两个或多个对象相互引用,即使它们不再被其他对象引用,它们的引用计数也不会为0。
根可达算法(Root Liveness)
根可达算法,也称为追踪式垃圾回收或可达性分析,是通过从一组称为“根”(root)的对象开始,递归地追踪所有可达对象的过程。如同Linux文件系统,从 / 根开始,若一个文件从根开始查找,无法追踪到,即为垃圾。
根通常是虚拟机栈中的局部变量表、方法调用栈中的参数和返回地址等。如果一个对象从根出发无法到达,那么这个对象被认为是垃圾,可以被回收。
优点:
- 能够处理循环引用,因为即使对象之间相互引用,只要它们不可达,也会被识别为垃圾。
- 适用于大多数现代垃圾回收器,如Java中的HotSpot VM。
缺点:
- 需要定期执行完整的垃圾回收过程,这可能会导致应用程序的暂停。
- 算法的复杂性较高,需要维护对象之间的引用关系。
垃圾回收算法
标记-清除(Mark-Sweep)
标记-清除算法是垃圾回收中的一个基本算法,它分为两个阶段:
- 标记阶段:首先,从根对象(root objects)开始,通过引用关系遍历所有的可达对象,对这些对象进行标记。
- 清除阶段:然后,算法会遍历整个内存空间,找出所有未被标记的对象,并将这些未标记的对象占用的内存空间视为垃圾,进行回收。
这种算法的优点是实现相对简单,但它存在的最主要的问题是内存碎片化,因为垃圾回收后的内存空间是不连续的,可能会导致大对象无法找到足够的连续空间。
标记-压缩(Mark-Compact)
或称为压实
为了解决标记-清除算法中的内存碎片化问题,标记-压缩算法在标记和清除的基础上增加了一个压缩阶段:
- 标记阶段:与标记-清除算法相同,从根对象开始遍历所有可达对象并进行标记。
- 压缩阶段:在清除阶段之后,算法会将所有存活的对象向内存的一端移动,从而在内存的另一端形成一个连续的空闲空间。这样,即使大对象需要连续的内存空间,也可以被满足。
标记-压缩算法有效地减少了内存碎片化问题,但压缩过程可能会涉及到大量的对象移动,对性能有一定影响。
复制(Copying)
复制算法将内存空间分为两个相等的区域,称为半区(semispaces):
- 分配阶段:新分配的对象首先存放在一个半区内。
- 复制阶段:当进行垃圾回收时,算法会将所有存活的对象复制到另一个半区内,并可能伴随一些压缩操作,以减少内存碎片。完成复制后,原来的半区会被清空,用于下一轮的对象分配。
复制算法的优点是实现简单,且不会产生内存碎片。然而,它的缺点是会有一半的内存在任何时候都是空闲的,这降低了内存的利用率。
不同场景使用不同算法
- 效率: 复制算法>标记清除算法> 标记压缩算法
- 内存整齐度: 复制算法=标记压缩算法> 标记清除算法
- 内存利用率: 标记压缩算法=标记清除算法>复制算法
分代堆内存GC策略
分代堆内存GC策略是一种在Java虚拟机(JVM)中广泛使用的内存管理技术,它根据对象的生命周期将堆内存分为不同的区域,以提高垃圾回收(GC)的效率。这种策略的核心思想是将对象按照其存活时间的长短进行分类,并在不同的区域中进行垃圾回收,从而减少不必要的内存回收操作,优化GC性能。
新生代(Young Generation)
新生代是新创建的对象的默认分配区域。由于大多数对象的生命周期较短,因此在新生代中进行垃圾回收可以快速回收大量不再使用的对象。新生代通常被进一步划分为三个部分:Eden区(伊甸园区)和两个Survivor区(幸存区)(S0和S1)。新创建的对象首先分配在Eden区,当Eden区满时,会触发Minor GC,将存活的对象复制到一个Survivor区,并将不存活的对象回收。随着时间的推移,存活时间较长的对象会被移动到老年代。
老年代(Old Generation)
老年代用于存放长时间存活的对象。这些对象通常从新生代中经过多次GC后仍然存活,因此被认为有较长的生命周期。
当老年代也满了,会触发Major GC(Full GC),即对所有“代”的内存进行垃圾回收。Minor GC比较频繁,Major GC较少。但一般Major GC时,由于老年代对象也可以引用新生代对象,所以先进行一次Minor GC,然后再Major GC会提高效率。可以认为回收老年代的时候完成了一次Full GC。
老年代的垃圾回收频率较低,但是每次回收的过程可能会更加复杂和耗时。老年代的GC通常使用标记-整理(Mark-Compact)算法。
永久代(Permanent Generation)
在JDK 8之前,永久代是HotSpot虚拟机中的一个特殊区域,用于存储类信息、常量以及静态变量等。从JDK 8开始,永久代被元空间(Metaspace)所取代,元空间使用的是本地内存(off-heap memory),不再受堆大小的限制。
逻辑上属于堆内存,物理上不属于堆内存。如下
[root@centos8 ~]#cat Heap.java
public class Heap {
public static void main(String[] args){
//返回JVM试图使用的最大内存,字节单位
long max = Runtime.getRuntime().maxMemory();
//返回JVM初始化总内存
long total = Runtime.getRuntime().totalMemory();
System.out.println("max="+max+"字节\t"+(max/(double)1024/1024)+"MB");
System.out.println("total="+total+"字节\t"+
(total/(double)1024/1024)+"MB");
}
}
[root@centos8 ~]#javac Heap.java
[root@centos8 ~]#java -classpath . Heap
max=243269632字节 232.0MB
total=16252928字节 15.5MB
#JDK-8的执行结果
[root@centos8 ~]#java -XX:+PrintGCDetails Heap
max=243269632字节 232.0MB
total=16252928字节 15.5MB
Heap
def new generation total 4928K, used 530K [0x00000000f1000000,
0x00000000f1550000, 0x00000000f6000000)
eden space 4416K, 12% used [0x00000000f1000000, 0x00000000f1084a60,
0x00000000f1450000)
from space 512K, 0% used [0x00000000f1450000, 0x00000000f1450000,
0x00000000f14d0000)
to space 512K, 0% used [0x00000000f14d0000, 0x00000000f14d0000,
0x00000000f1550000)
tenured generation total 10944K, used 0K [0x00000000f6000000,
0x00000000f6ab0000, 0x0000000100000000)
the space 10944K, 0% used [0x00000000f6000000, 0x00000000f6000000,
0x00000000f6000200, 0x00000000f6ab0000)
Metaspace used 2525K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 269K, capacity 386K, committed 512K, reserved 1048576K
[root@centos8 ~]#echo "scale=2;(4928+10944)/1024" |bc
15.50
说明年轻代+老年代占用了所有heap空间, Metaspace实际不占heap空间,逻辑上存在于Heap
GC触发条件
Minor GC触发条件:当Eden伊甸区满了。
Full GC触发条件:
- 老年代满了
- 手动调用System.gc(),不推荐。
年轻代存活时长短,适合复制算法。
老年代区域大,存活时间长,适合标记压缩算法。
STW
Stop-The-World,暂停。在STW期间,所有的应用程序线程都会被暂停,以便JVM可以安全地进行垃圾回收操作。这种暂停通常非常短暂,通常在毫秒级别。
Minor GC可能会导致短暂的Stop-The-World(STW)暂停。
Major GC通常会引起比Minor GC更长的STW暂停。这是因为老年代的内存容量通常比年轻代大,因此在收集和整理内存时需要更多的时间。
垃圾收集方式
按工作模式不同:指的是GC线程和工作线程是否一起运行
独占垃圾回收器:只有GC在工作,STW 一直进行到回收完毕,工作线程才能继续执行
并发垃圾回收器:让GC线程垃圾回收某些阶段可以和工作线程一起进行,如:标记阶段并行,回收阶段仍然串行。
按回收线程数:指的是GC线程是否串行或并行执行
串行垃圾回收器:一个GC线程完成回收工作
并行垃圾回收器:多个GC线程同时一起完成回收工作,充分利用CPU资源
调整策略
串行回收(Serial Garbage Collection)
适用于客户端或较小的程序,特别是那些内存使用量不大且对延迟要求不高的场景。
- 特点:串行回收使用单线程进行垃圾回收,因此在进行GC时会暂停所有的应用线程(STW)。
- 优势:由于使用单线程,串行回收的实现简单,资源消耗相对较低。
- 适用场景:适用于单核处理器或者对延迟不敏感的应用程序,以及内存需求较小的桌面应用程序。
并行回收(Parallel Garbage Collection)
适用于服务端大型计算或需要处理大量数据的应用程序。
- 特点:并行回收使用多个线程同时进行垃圾回收,可以充分利用多核处理器的优势,加快GC的速度。
- 优势:并行回收可以显著减少GC的总体时间,提高应用程序的吞吐量。
- 适用场景:适用于多核处理器的服务器端应用程序,尤其是计算密集型任务,如数据分析和批量处理。
并发回收(Concurrent Garbage Collection)
适用于大型Web应用或需要最小化STW时间的应用程序。
- 特点:并发回收尝试减少STW时间,允许垃圾回收的部分阶段与应用线程并发执行。
- 优势:通过减少STW时间,提高应用程序的响应性和用户体验。
- 适用场景:适用于对延迟敏感的Web应用或服务,如在线交易系统、实时数据处理等。
在选择GC策略时,需要考虑以下因素:
- 延迟要求:应用程序对响应时间的要求。对于需要快速响应的应用,应选择并发回收。
- 吞吐量要求:应用程序对处理能力的要求。对于计算密集型任务,可以选择并行回收。
- 资源限制:硬件资源,如CPU核心数和内存大小,也会影响GC策略的选择。
- 内存大小:应用程序的内存需求。内存使用量较小的应用程序可能更适合串行回收。
垃圾回收器
JVM1.8 默认的垃圾回收器:PS + ParallelOld,所以大多数都是针对此进行调优。
新生代
Serial 收集器:这是一个单线程的收集器,进行垃圾回收时必须暂停其他工作线程(Stop The World,STW)。它适用于单CPU环境或者客户端模式下的虚拟机,因为它简单且高效,没有多余的线程交互开销。
ParNew 收集器:ParNew 是 Serial 收集器的多线程版本,除了使用多线程进行垃圾回收之外,其它的行为和 Serial 收集器都是相同的。它适用于多CPU环境,可以提供更好的吞吐量。
Parallel Scavenge 收集器:也称为吞吐量优先收集器,它关注点是吞吐量,即用户线程执行时间占总时间的比例。可以通过设置最大垃圾收集停顿时间来控制吞吐量,适用于需要高吞吐量的服务端应用。
老年代
Serial Old 收集器:Serial Old 是 Serial 收集器的老年代版本,使用单线程和标记-整理算法进行垃圾回收。它可以与Parallel Scavenge收集器搭配使用,或者作为CMS收集器的后备预案。
Parallel Old 收集器:Parallel Old 是一款老年代的多线程收集器,使用标记-整理算法。它关注吞吐量,适用于与Parallel Scavenge收集器搭配使用的场景,以提供更高的吞吐量。
CMS(Concurrent Mark Sweep)收集器:CMS 收集器以获取最短回收停顿时间为目标,适用于交互性高、对响应时间要求高的应用程序。它的主要特点是并发标记和并发清理,减少了STW时间,但可能会产生内存碎片。
通用
G1(Garbage-First)收集器:G1 收集器是为了替代CMS而设计的,它可以提供更可预测的垃圾回收暂停时间,特别适合大堆内存的应用。G1 将堆分割成多个区域,优先回收垃圾比例最高的区域,逐步清理内存,减少GC产生的停顿时间。