1 介绍

1.1 [1]jdk工具

01.分析
    a.Linux
        硬盘使用情况:du
        内存使用且情况:free
        CPU使用情况:top
        网络使用情况:netstat
    b.问题分析
        jps:查看本机java进程信息
        jmap:打印内存映射信息,制作 堆dump文件
        jhat:内存分析工具,用于解析堆dump文件并以适合人阅读的方式展示出来
        jstat:性能监控工具
        jstack:打印线程的栈信息,制作 线程dump文件
        jconsole:简易的JVM可视化工具
        jvisualvm:功能更强大的JVM可视化工具
        javap:查看字节码

02.JDK自带工具
    a.jconsole
        Java 自带的监控工具,提供图形界面的监控 JVM 实例。
        通过 jconsole 可以查看内存使用、线程状况、类加载信息、CPU 使用情况等。
        适合用于简单的调试和监控,不需要额外安装。
    b.jvisualvm
        也是 Java 自带的可视化监控工具,比 jconsole 功能更强大。
        支持查看内存分配、GC 活动、CPU 使用、线程分析等。
        可以安装插件以增强功能,例如线程分析和内存泄漏检测。
        使用方法:在命令行输入 jvisualvm。
    c.jstat
        用于命令行查看 JVM 的性能指标,可以获取 GC 统计数据、类加载情况等。
        jstat -gc <pid> 可以查看 GC 的情况,帮助分析内存使用和回收。
        适合开发和运维人员分析 JVM 的性能情况。
    d.jmap
        用于生成 Java 内存转储(heap dump),可以帮助分析内存使用情况。
        jmap -heap <pid> 可查看堆内存配置、GC 使用情况等。
        适用于内存分析和排查内存泄漏问题。
    e.jstack
        用于查看线程的堆栈信息,可用于分析线程状态和锁竞争。
        jstack <pid> 可以输出指定 JVM 进程的线程栈信息。
        在定位死锁、线程堵塞等问题时非常有用。
    f.Java Mission Control (JMC)
        Oracle 提供的高级监控和分析工具,通常与 Java Flight Recorder (JFR) 配合使用。
        可以监控应用的性能,分析延迟和 GC 情况。
        支持深入的性能分析,适合长时间监控和分析生产环境中的应用。
    g.Java Flight Recorder (JFR)
        高效的 Java 事件记录工具,可捕获 JVM 的性能数据。
        记录线程活动、方法调用、GC 情况等,用于深度分析和性能优化。
        常与 JMC 配合使用,可以记录并回放 JVM 的活动。

03.JDK自带工具-说明
    a.java参数
        其一是标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
        其二是非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;
        其三是非Stable参数(-XX),此类参数各个jvm实现会有所不同,这些都是不稳定的并且不推荐在生产环境中使用。

        # 内存公式
        # 年老代大小 = Xmx - Xmn
        # 整个堆大小 = 年轻代大小 + 年老代大小 + 持久代大小

        # jvm自动分配的heap大小取决于机器配置
        java -XX:+PrintFlagsFinal -version | grep -Ei "maxheapsize|maxram"

        # MaxHeapSize = MaxRAM * 1 / MaxRAMFraction
        # MaxRAMFraction 默认是4,意味着,每个jvm最多使用25%的机器内存
        # JVM内存  = heap 内存 + 线程stack内存 (XSS) * 线程数 + 启动开销(constant overhead)
        # -Xmx 设置heap内存
        java -XX:+PrintFlagsFinal -Xmx1g -version | grep -Ei "maxheapsize|maxram"

        # -XX:+UseContainerSupport允许JVM 从主机读取cgroup限制,例如可用的CPU和RAM,并进行相应的配置。这样当容器超过内存限制时,会抛出OOM异常,而不是杀死容器。
        # 该特性在Java 8u191 +,10及更高版本上可用。
        # 注意,在191版本后,-XX:{Min|Max}RAMFraction 被弃用,引入了-XX:MaxRAMPercentage,其值介于0.0到100.0之间,默认值为25.0。
        java -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0

        # -Xms 与 - Xmx 相同可避免每次垃圾回收完成后 JVM 重新分配内存
        java -Xms512m -Xmx512m

        # 年轻代,持久代一般固定大小为 64m,增大年轻代将会减小年老代大小。此值对系统性能影响较大,Sun 官方推荐配置为整个堆的 3/8
        java -Xmn200m

        # 打印gc日志
        java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/dump.hprof -XX:+PrintGC -XX:+PrintGCDetails -Xloggc:/opt/gc.log


        # 实际使用
        java -Xms2G -Xmx2G -Xmn1G -Xss1M -XX:PermSize=128M -XX:MaxPermSize=128M -Duser.timezone=GMT+08 -Dfile.encoding=UTF-8 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+DisableExplicitGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=4m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:MaxTenuringThreshold=15 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:ParallelGCThreads=3 -Dspring.cloud.nacos.discovery.ip=172.17.0.18 -Dspring.cloud.nacos.discovery.port=8080 -Dserver.port=8080 -Dspring.profiles.active=uat -Dapollo.configService=http://10.223.12.32:8080 -Dspring.cloud.nacos.config.server-addr=10.223.12.40:8848 -Dspring.cloud.sentinel.transport.dashboard=10.223.12.44:8858
    b.jps
        jps

        # 只显示 pid
        jps -q

        # 显示 pid 和 参数
        jps -m

        # 输出 Java 进程的进程 ID 和主类全名
        jps -l
    c.jinfo
        # 查看标志参数
        jinfo -flags pid

        # 查看系统属性
        jinfo -sysprops pid

        # 查看特定标志参数的值
        jinfo -flag UseG1GC pid
    d.jstat
        # 类加载统计
        jstat -class pid

        # 编译统计
        jstat -compiler pid

        # 垃圾回收统计
        jstat -gc pid

        jstat -gcutil pid
    e.jstack
        # 显示线程堆栈
        jstack pid

        # 显示线程堆栈和锁信息
        jstack -l pid

        # 显示线程堆栈和每个线程的本地(本地方法)信息
        jstack -m pid
    f.jmap
        # 显示 Java 堆内存中的对象统计信息
        jmap -histo pid

        # 查看大对象
        jmap -histo pid | less

        # 查看对象数最多的对象,并按降序排序输出
        jmap -histo pid | grep com |sort -k 2 -g -r|less

        # 查看占用内存最多的最象,并按降序排序输出
        jmap -histo pid | grep com |sort -k 3 -g -r|less

        # 导出堆文件,将堆内存转储到文件
        jmap -dump:format=b,file=heapdump.bin pid

        # 强制导出堆文件,强制触发FGC导出堆存储到文件
        jmap -dump:live,format=b,file=heap.bin pid

        # 分析堆文件,使用 MAT 分析工具,如 jhat 命令,eclipse 的 mat 插件
        jhat -port 5000 heap.bin

        # 显示内存使用情况,在用 cms gc 的情况下,有些时候会导致进程变 T,因此强烈建议别执行这个命令
        jmap -heap pid

1.2 [1]jvm、jre、jdk

01.JDK、JRE、JVM关系?
    JDK:开发工具包,包含编写Java程序所必须的编译、运行等开发工具以及JRE
    JRE:运行环境,提供了运行Java应用程序所必须的软件环境,包含有Java虚拟机(JVM)和丰富的系统类库
    JVM:虚拟机,提供了字节码文件(.class)的运行环境支持

1.3 [1]oop,ood,oom

01.OOP
    面向对象编程,Object-Oriented Programming
    OOP是一种编程范式,它使用“对象”来设计软件。对象是数据和行为的封装,通常通过类来定义
    面向对象编程的核心概念包括封装、继承和多态。OOP的目标是提高代码的可重用性、可维护性和可扩展性

02.OOD
    面向对象设计,Object-Oriented Design
    OOD是指在软件开发过程中使用面向对象的原则和模式来设计系统结构
    OOD关注的是如何组织和设计类及对象,以实现系统的功能需求
    它通常涉及设计类的关系、接口、继承结构以及如何使用设计模式来解决常见问题

03.OOM
    面向对象建模,Object-Oriented Modeling
    OOM是使用面向对象的概念来创建系统的模型
    它通常涉及使用UML(统一建模语言)等工具来表示系统的结构和行为
    OOM帮助开发人员和设计人员在开发过程的早期阶段理解和定义系统的需求和设计

1.4 [1]Java程序:一次编写、到处运行

00.总结
    跨平台的是Java程序、而不是JVM
    JVM是用C/C++开发的软件,不同平台下需要安装不同版本的JVM

01.JVM,Java跨平台的关键
    在程序运行前,Java源代码(.java)需要经过编译器编译成字节码(.class)。
    在程序运行时,JVM负责将字节码翻译成特定平台下的机器码并运行,
    也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。
    编译的结果是生成字节码、不是机器码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行;

02.JVM与字节码
    同一份Java源代码在不同的平台上运行,它不需要做任何的改变,并且只需要编译一次。
    而编译好的字节码,是通过JVM这个中间的“桥梁”实现跨平台的,
    JVM是与平台相关的软件,它能将统一的字节码翻译成该平台的机器码。

1.5 [2]分层编译

01.定义
    分层编译是HotSpot虚拟机的一种优化策略,旨在平衡程序启动速度和运行效率
    分层编译将编译过程分为多个层次,每个层次的编译和优化程度不同
    通过分层编译,HotSpot虚拟机可以在程序启动时快速执行代码,同时在运行过程中逐步优化热点代码,以提高整体性能

02.每层编译
    第0层:纯解释执行,不开启性能监控
    第1层:使用C1编译器进行简单优化,不开启性能监控
    第2层:使用C1编译器,开启有限的性能监控
    第3层:使用C1编译器,开启全部性能监控
    第4层:使用C2编译器进行复杂优化,基于性能监控信息进行激进优化

1.6 [2]热点探测

01.即时编译器的目标是编译“热点代码”,即频繁执行的代码。热点探测的方法主要有两种
    a.第1种:基于采样的热点探测(Sample Based Hot Spot Code Detection)
        方法:周期性地检查各个线程的调用栈顶,如果某个方法经常出现在栈顶,则认为它是热点方法
        优点:对程序的性能影响较小
    b.第2种:基于计数的热点探测(Counter Based Hot Spot Code Detection)
        方法:为每个方法或代码块建立计数器,统计其执行次数。如果执行次数超过一定阈值,则认为它是热点方法
        优点:可以精确地识别热点代码

1.7 [2]解释器与编译器

01.在HotSpot虚拟机中,Java程序的执行过程通常是解释执行和即时编译相结合的
    a.解释器(Interpreter)
        功能:逐条解释执行字节码,优点是省去编译时间,程序启动速度快
        使用场景:程序启动初期,所有代码都通过解释器执行
    b.即时编译器(JIT Compiler)
        功能:将频繁执行的字节码编译成本地机器码,并进行优化,以提高执行效率
        使用场景:当虚拟机发现某个方法或代码块运行频繁时,使用JIT编译器进行编译和优化

02.HotSpot内置的即时编译器
    客户端编译器(Client Compiler, C1):进行简单可靠的优化,编译速度快
    服务端编译器(Server Compiler, C2):进行复杂的激进优化,编译质量高,但耗时较长
    Graal编译器:在JDK 10引入,目标是替代C2编译器,提供更高效的编译和优化

1.8 [3]编译器:3类

01.编译器分类
    a.前端编译器(Frontend Compiler)
        功能:将Java源代码(.java文件)编译成字节码(.class文件)
        示例:JDK的Javac编译器、Eclipse JDT中的增量式编译器
    b.即时编译器(Just-In-Time Compiler,JIT)
        功能:在运行时将字节码编译成本地机器码,以提高执行效率
        示例:HotSpot虚拟机中的C1、C2编译器,Graal编译器
    c.提前编译器(Ahead-Of-Time Compiler,AOT)
        功能:在程序运行之前,将Java程序直接编译成目标机器指令集相关的二进制代码
        示例:JDK的jaotc,GNU Compiler for Java(GCJ),Excelsior JET

1.9 [3]JIT编译:热点代码

01.默认
    java默认是解释执行

02.JIT(Just-In-Time)编译
    java默认是解释执行,但是解释执行的效率确实比不上编译执行,因此Java就搞了个JTT(Just-ln-Time)编译器,
    它在Java程序运行的时候,【发现热点代码时,就会将字节码转为机器码】,
    因为这种转换是在程序运行时即时进行的,因此得名"Just-In-Time"

1.10 [3]AOT编译:提前编译

01.默认
    java默认是解释执行

02.AOT(Ahead-Of-Time)编译
    AOT(Ahead-Of-Time) 与 JIT(Just-ln-Time)编译相对
    JIT:【在Java运行时将一些代码编译成机器码】
    AOT:【在代码运行之前就编译成机器吗,也就是提前编译】

1.11 [4]jvm参数:3类

00.JVM参数分类
    a.标准参数(-)
        所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
        例子:-verbose:class,-verbose:gc,-verbose:jni
    b.非标准参数(-X)
        默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;
        例子:Xms20m,-Xmx20m,-Xmn20m,-Xss128k
    c.非Stable参数(-XX)
        此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;
        例子:-XX:+PrintGCDetails,-XX:-UseParallelGC,-XX:+PrintGCTimeStamps

01.堆参数设置
    a.-Xms
        初始堆大小,ms是memory start的简称 ,等价于-XX:InitialHeapSize
    b.-Xmx
        最大堆大小,mx是memory max的简称 ,等价于参数-XX:MaxHeapSize
        注意:在通常情况下,服务器项目在运行过程中,堆空间会不断地收缩与扩张,势必会造成不必要的系统压力
        所以在生产环境中,JVM的Xms和Xmx要设置成大小一样的,能够避免GC在调整堆大小带来的不必要的压力
    c.-XX:NewSize=n
        设置年轻代大小
    d.-XX:NewRatio=n
        设置年轻代和年老代的比值
        如:-XX:NewRatio=3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代和年老代的1/4,默认新生代与老年代的比例=1:2
    e.-XX:SurvivorRatio=n
        年轻代中Eden区与两个Survivor区的比值
        注意Survivor区有两个,默认是8,表示:Eden:S0:S1=8:1:1
        如:-XX:SurvivorRatio=3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

02.元空间参数
    a.-XX:MetaspaceSize
        Metaspace 空间初始大小,如果不设置的话,默认是20.79M,这个初始大小是触发首次 Metaspace Full GC的阈值
        例如:-XX:MetaspaceSize=256M
    b.-XX:MaxMetaspaceSize
        Metaspace 最大值,默认不限制大小,但是线上环境建议设置
        例如:-XX:MaxMetaspaceSize=256M
    c.-XX:MinMetaspaceFreeRatio
        最小空闲比,当 Metaspace 发生 GC 后,会计算 Metaspace 的空闲比,如果空闲比(空闲空间/当前 Metaspace 大小)小于此值,就会触发 Metaspace 扩容。默认值是 40 ,也就是 40%,例如 -XX:MinMetaspaceFreeRatio=40
    d.-XX:MaxMetaspaceFreeRatio
        最大空闲比,当 Metaspace发生 GC 后,会计算 Metaspace 的空闲比,如果空闲比(空闲空间/当前 Metaspace 大小)大于此值,就会触发 Metaspace 释放空间。默认值是 70 ,也就是 70%,例如 -XX:MaxMetaspaceFreeRatio=70
        建议将 MetaspaceSize 和 MaxMetaspaceSize设置为同样大小,避免频繁扩容

03.栈参数设置
    a.-Xss
        栈空间大小,栈是线程独占的,所以是一个线程使用栈空间的大小
        例如:-Xss256K,如果不设置此参数,默认值是1M,一般来讲设置成 256K 就足够了

04.收集器参数设置
    a.Serial垃圾收集器(新生代)
        开启:-XX:+UseSerialGC 关闭:-XX:-UseSerialGC //新生代使用Serial  老年代则使用SerialOld
    b.ParNew垃圾收集器(新生代)
        开启 -XX:+UseParNewGC 关闭 -XX:-UseParNewGC //新生代使用功能ParNew 老年代则使用功能CMS
    c.Parallel Scavenge收集器(新生代)
        开启 -XX:+UseParallelOldGC 关闭 -XX:-UseParallelOldGC //新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
    d.ParallelOl垃圾收集器(老年代)
        开启 -XX:+UseParallelGC 关闭 -XX:-UseParallelGC //新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
    e.CMS垃圾收集器(老年代)
        开启 -XX:+UseConcMarkSweepGC 关闭 -XX:-UseConcMarkSweepGC
    f.G1垃圾收集器
        开启 -XX:+UseG1GC 关闭 -XX:-UseG1GC

05.GC策略参数配置
    a.GC停顿时间
        垃圾收集器会尝试用各种手段达到这个时间,比如减小年轻代
        -XX:MaxGCPauseMillis
    b.堆占用了多少比例的时候触发GC
        就即触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%
        -XX:InitiatingHeapOccupancyPercent=n
    c.新生代可容纳的最大对象
        大于则直接会分配到老年代,0代表没有限制
        -XX:PretenureSizeThreshold=1000000
    d.进入老年代最小的GC年龄
        年轻代对象转换为老年代对象最小年龄值,默认值7
        -XX:InitialTenuringThreshol=7
    e.升级老年代年龄
        最大值15
        -XX:MaxTenuringThreshold
    f.GC并行执行线程数
        -XX:ParallelGCThreads=16
    g.禁用 System.gc()
        由于该方法默认会触发 FGC,并且忽略参数中的 UseG1GC 和 UseConcMarkSweepGC,因此必要时可以禁用该方法
        -XX:-+DisableExplicitGC
    h.设置吞吐量大小
        默认99
        XX:GCTimeRatio
    i.打开自适应策略
        各个区域的比率,晋升老年代的年龄等参数会被自动调整。以达到吞吐量,停顿时间的平衡点。
        XX:UseAdaptiveSizePolicy
    j.设置GC时间占用程序运行时间的百分比
        GCTimeRatio

06.Dump异常快照
    a.-XX:+HeapDumpOnOutOfMemoryError
    b.-XX:HeapDumpPath
        堆内存出现OOM的概率是所有内存耗尽异常中最高的,出错时的堆内信息对解决问题非常有帮助
        所以给JVM设置这个参数(-XX:+HeapDumpOnOutOfMemoryError),让JVM遇到OOM异常时能输出堆内信息,并通过(-XX:+HeapDumpPath)参数设置堆内存溢出快照输出的文件地址
        这对于特别是对相隔数月才出现的OOM异常尤为重要
    c.-XX:OnOutOfMemoryError
        表示发生OOM后,运行jconsole.exe程序
        这里可以不用加“”,因为jconsole.exe路径Program Files含有空格。利用这个参数,我们可以在系统OOM后,自定义一个脚本,可以用来发送邮件告警信息,可以用来重启系统等等

91.8G内存的服务器该如何设置
    a.-Xmx3500m
        设置JVM最大可用内存为3550M
    b.-Xms3500m
        设置JVM初始内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存
    c.-Xmn2g
        设置年轻代大小为2G。
        整个堆大小=年轻代大小 + 年老代大小 + 方法区大小
    d.-Xss128k
        设置每个线程的堆栈大小。
        JDK1.5以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右
    e.-XX:NewRatio=4
        设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
    f.-XX:SurvivorRatio=4
        设置年轻代中Eden区与Survivor区的大小比值
        设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
    g.-XX:MaxPermSize=16m
        设置持久代大小为16m
    h.-XX:MaxTenuringThreshold=0
        设置垃圾最大年龄
        如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概论

92.项目中,GC日志配置
    a.比如,我们启动一个user-service项目
        java
         -XX:+PrintGCDetails -XX:+PrintGCDateStamps
         -XX:+UseGCLogFileRotation
         -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=5
         -XX:GCLogFileSize=20M
         -Xloggc:/opt/user-service-gc-%t.log
         -jar user-service-1.0-SNAPSHOT.jar
    b.参数解释
         -Xloggc:/opt/app/ard-user/user-service-gc-%t.log   设置日志目录和日志名称
         -XX:+UseGCLogFileRotation                          开启滚动生成日志
         -XX:NumberOfGCLogFiles=5                           滚动GC日志文件数,默认0,不滚动
         -XX:GCLogFileSize=20M                              GC文件滚动大小,需开启UseGCLogFileRotation
         -XX:+PrintGCDetails                                开启记录GC日志详细信息(包括GC类型、各个操作使用的时间),并且在程序运行结束打印出JVM的内存占用情况
         -XX:+ PrintGCDateStamps                            记录系统的GC时间
         -XX:+PrintGCCause                                  产生GC的原因(默认开启)

1.12 [4]jvm调优:2种情况

00.总结
    a.步骤
        1.确定JVM调优原因
        2.分析JVM(目前)运行情况
        3.设置JVM调优参数
        4.压测观测调优后的效果
        5.应用调优后的配置
    b.垃圾回收的调优
        1.调整堆大小:使用-Xms和-Xmx参数设置堆的初始大小和最大大小
        2.选择合适的垃圾回收器:根据应用的特点选择合适的垃圾回收器,如低延迟应用选择CMS或G1
        3.监控和分析:使用工具(如JVisualVM、JConsole)监控垃圾回收的性能,分析GC日志进行调优
    c.无非属于以下两种情况的一种
        目标驱动型的JVM调优:1.为了最短的停顿时间;2.为了最大吞吐量
        问题驱动型的JVM调优,1.生产环境出现了频繁的FullGC,导致程序执行变慢

01.五个步骤
    a.确定JVM调优原因
        先确定是目标驱动型的 JVM 调优,还是问题驱动型的 JVM 调优。
        如果是目标性的 JVM 调优,那么 JVM 调优实现思路就比较简单了,如:
        以最短停顿时间为目标的调优,只需要将垃圾收集器设置成以最短停顿时间的为目标的垃圾收集器即可,如 CMS 收集器或 G1 收集器。
        以吞吐量为目标的调优,只需要将垃圾收集器设置为 Parallel Scavenge 和 Parallel Old 这种以吞吐量为主要目标的垃圾回收器即可。
        如果是以问题驱动的 JVM 调优,那就要先分析问题是什么,然后再进行下一步的调优了。
    b.分析JVM运行情况
        我们可以借助于目前主流的监控工具 Prometheus + Grafana 和 JDK 自带的命令行工具,如 jps、jstat、jinfo、jstack 等进行 JVM 运行情况的分析。
        主要分析的点是 Young GC 和 Full GC 的频率,以及垃圾回收的执行时间。
    c.设置JVM调优参数
        常见的 JVM 调优参数有以下几个:
        调整堆内存大小:通过设置 -Xms(初始堆大小)和 -Xmx(最大堆大小)参数来调整堆内存大小,避免频繁的垃圾回收。
        选择合适的垃圾回收器:根据应用程序的性能需求和特点,选择合适的垃圾回收器,如 Serial GC、Parallel GC、CMS GC、G1 GC 等。
        调整新生代和老年代比:通过设置 -XX:NewRatio 参数来调整新生代和老年代的比例,优化内存分配。
        设置合适的堆中的各个区域比例:通过设置 -XX:SurvivorRatio 参数和 -XX:MaxTenuringThreshold 参数来调整 Eden 区、Survivor 区和老年代的比例,避免过早晋升和过多频繁的垃圾回收。
        设置对象从年轻代进入老年代的年龄值:-XX:InitialTenuringThreshold=7 表示 7 次年轻代存活的对象就会进入老年代。
        设置元空间大小:在 JDK 1.8 版本中,元空间的默认大小会根据操作系统有所不同。
        具体来说,在 Windows 上,元空间的默认大小为 21MB;而在 Linux 上,其默认大小为 24MB。
        然而如果元空间不足也有可能触发 Full GC 从而导致程序执行变慢,
        因此我们可以通过 -XX:MaxMetaspaceSize=设置元空间的最大容量。
    d.压测观测调优后的效果
        JVM 参数调整之后,我们要通过压力测试来观察 JVM 参数调整前和调整后的差别,以确认调整后的效果。
    e.应用调优后的配置
        在确认了 JVM 参数调整后的效果满足需求之后,就可以将 JVM 的参数配置应用与生产环境了。

2 内存区域

2.1 五种

01.五个部分
    a.堆(Heap)
        存储所有的对象实例和数组,是Java内存管理的主要区域
    b.栈(Stack)
        每个线程都有一个私有的栈,存储局部变量、操作数栈、动态链接、方法出口等信息
    c.程序计数器(Program Counter Register)
        每个线程都有一个独立的程序计数器,用于指示当前线程执行到的字节码指令地址
    d.本地方法栈(Native Method Stack)
        为JVM调用的本地方法(native方法)服务
    e.方法区(Method Area),也叫做静态区
        存储类的元数据、常量、静态变量、即时编译器编译后的代码等

02.总结
    常量池是方法区的一部分,用于存储字面量和符号引用
    方法区用于存储类的元数据和静态信息,是线程共享的内存区域

2.2 [1]堆,共享,3代

00.堆栈
    a.帧为单位
        堆栈以【帧为单位】保存线程的状态,都属于Java内存的一种
        JVM对堆栈只进行两种操作:以帧为单位的【压栈和出栈】操作
        系统都会自动去回收堆栈,但是对于堆内存一般开发人员会断开引用让系统回收它
    b.区别1:线程
        堆内存  共享 线程                              dui          底层:队列
        栈内存  私有 线程                              zhan         底层:栈
    c.区别2:存放
        堆内存  对象实例                               dui          底层:队列
        栈内存  各种基本数据类型、对象的引用            zhan         底层:栈
    d.区别3:GC
        堆内存  存储new的Jva对象实例与数组              dui          底层:队列   GC自动回收/手动触发器GC
        栈内存  存储局部变量、操作数栈、方法返回地址     zhan         底层:栈     GC自动回收,
    e.区别4:顺序
        堆内存  先进先出,后进后出                      dui          底层:队列
        栈内存  后进先出,先进后出                      zhan         底层:栈

01.堆内存(队列)
    a.堆的结构
        a.新生代
            主要是用来【存放新生】的对象。
            一般占据堆空间的1/3,新生代通常分为Eden区和两个Survivor区(S0和S1)
        b.老年代
            用于【存储生命周期较长】的对象,所以MajorGC不会频繁执行
        c.永久代/元空间
            用于【存储类的元数据】,在Java8之前是永久代,Java8及之后是元空间
    b.堆的说明
        a.3个概念
            Java堆:是JVM内存模型的一部分,用于存储对象实例,不是特定的数据结构。
            数据结构中的堆:是一种树形数据结构,用于实现优先队列。
            队列:是一种线性数据结构,遵循FIFO原则。
        b.Java堆(Heap)
            a.定义:
                Java堆是Java虚拟机(JVM)内存模型的一部分,用于存储对象实例和数组。
                它是一个运行时数据区,所有对象实例和数组都在堆上分配。
            b.用途
                Java堆用于动态分配内存给对象,生命周期由垃圾回收器管理。
                堆是线程共享的,所有线程可以访问堆中的对象。
            c.特点
                Java堆不是一种数据结构,而是一个内存区域。
                它的管理涉及复杂的内存分配和垃圾回收机制。
        c.数据结构中的堆(Heap)
            a.定义
                在数据结构中,堆是一种特殊的树形结构,通常用于实现优先队列。
                堆分为最大堆和最小堆:
                最大堆:每个节点的值都大于或等于其子节点的值。
                最小堆:每个节点的值都小于或等于其子节点的值。
            b.用途
                堆常用于实现优先队列,支持快速的最大值或最小值查找。
                堆排序(Heap Sort)是一种基于堆的数据结构的排序算法。
            c.特点
                堆是一种具体的数据结构,通常用数组实现。
                它支持高效的插入、删除和查找操作。
        d.队列(Queue)
            a.定义
                队列是一种线性数据结构,遵循先进先出(FIFO)的原则。
                元素在队列的一端(队尾)插入,从另一端(队头)删除。
            b.用途
                队列用于任务调度、缓冲区管理等场景。
                常见的队列实现包括链表和数组。
            c.特点
                队列是一种线性结构,与堆的树形结构不同。
                它支持顺序的插入和删除操作。

02.栈内存(栈)
    a.栈
        a.定义
            后进先出,最新添加的元素最先被移除
        b.常见操作
            push:将元素压入栈顶
            pop:从栈顶弹出元素
            peek:查看栈顶的元素但不移除它
        c.使用场景
            用于回溯算法,如深度优先搜索(DFS)
            表达式求值中的操作符优先级处理
            递归函数调用的系统栈
    b.栈的结构
        每个线程都有自己的栈,用于存储方法调用的信息。
        栈帧(Stack Frame):每个方法调用对应一个栈帧,包含局部变量表、操作数栈、动态链接、方法返回地址等信息。
    c.栈的生命周期
        栈随着线程的创建而创建,随着线程的结束而销毁。
        栈中的数据是线程私有的,不能被其他线程访问。

2.3 [2]栈,私有

参考【堆】

2.4 [3]程序计数器,私有

01.基本概念
    a.定义
        程序计数器是一个小型的内存区域,用于指示当前线程所执行的字节码指令的地址。
        在JVM中,每个线程都有自己的程序计数器,是线程私有的。
    b.作用
        指令地址:程序计数器记录当前线程正在执行的字节码指令的地址。
        线程切换:在多线程环境中,程序计数器用于记录线程的执行位置,以便线程切换后能恢复到正确的执行位置。
        分支、循环、跳转:在执行分支、循环和跳转等控制流操作时,程序计数器帮助JVM确定下一条要执行的指令。
    c.线程私有
        每个线程都有独立的程序计数器,互不影响。
        由于程序计数器是线程私有的,因此不需要考虑线程安全问题。

02.特点
    a.内存消耗小
        程序计数器是一个非常小的内存区域,因为它只需要存储当前指令的地址
    b.无垃圾回收
        程序计数器是线程私有的,生命周期与线程相同,因此不涉及垃圾回收
    c.唯一的非OutOfMemoryError区域
        在Java虚拟机规范中,程序计数器是唯一一个没有规定任何OutOfMemoryError情况的内存区域

03.常见问题
    a.程序计数器的作用是什么?
        主要用于记录当前线程执行的字节码指令的地址,支持线程切换和控制流操作。
    b.为什么程序计数器是线程私有的?
        因为每个线程需要独立记录自己的执行位置,以便在多线程环境中正确恢复执行。
    c.程序计数器会导致内存溢出吗?
        不会。程序计数器是一个非常小的内存区域,不会导致OutOfMemoryError。
    d.程序计数器与其他内存区域的区别是什么?
        程序计数器是线程私有的,而堆和方法区是线程共享的。
        程序计数器用于记录指令地址,而堆用于存储对象实例,方法区用于存储类的元数据。

2.5 [4]本地方法栈,私有

01.基本概念
    a.定义
        本地方法栈用于存储每个线程调用本地方法时所需的状态信息和数据。
        它与Java栈类似,但专门用于处理本地方法调用。
    b.作用
        支持Java程序通过Java Native Interface(JNI)调用本地方法。
        管理本地方法调用的状态,包括本地变量、输入参数和返回值。
    c.线程私有
        本地方法栈是线程私有的,每个线程都有自己的本地方法栈。
        当线程调用本地方法时,本地方法栈为该调用分配栈帧。

02.特点
    a.内存管理
        本地方法栈的内存分配和释放与Java栈类似,随着线程的创建和销毁而创建和销毁。
        栈的大小可以通过JVM参数(如-Xss)进行配置。
    b.异常处理
        如果本地方法栈的内存不足,JVM会抛出StackOverflowError。
        如果无法分配足够的内存来创建本地方法栈,JVM会抛出OutOfMemoryError。
    c.与Java栈的区别
        Java栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
        Java栈存储Java方法的局部变量和操作数栈,而本地方法栈存储本地方法的状态信息。

03.常见问题
    a.本地方法栈的作用是什么?
        支持Java程序调用本地方法,管理本地方法调用的状态。
    b.本地方法栈与Java栈有什么区别?
        Java栈用于Java方法调用,本地方法栈用于本地方法调用。
        Java栈存储Java方法的局部变量和操作数栈,本地方法栈存储本地方法的状态信息。
    c.本地方法栈会导致内存溢出吗?
        是的,如果本地方法栈的内存不足,可能会导致StackOverflowError或OutOfMemoryError。
    d.如何配置本地方法栈的大小?
        可以通过JVM参数(如-Xss)配置栈的大小。

2.6 [5]方法区(静态区)、常量池,共享

00.总结
    a.回答
        常量池:【不属于堆或栈】
        方法区(静态区):【不属于堆或栈】
    b.说明
        常量池:位于方法区中(在Java 8及之后,方法区是元空间的一部分)
        方法区(静态区):独立于堆和栈,是Java内存模型的一部分,用于存储类的元数据和静态信息

01.常量池,方法区的一部分
    a.定义
        常量池是用于存储编译期生成的各种字面量(如字符串常量)以及符号引用(如类和方法的引用)的内存区域。
        常量池存在于每个类文件中,并在类加载时被加载到方法区中。
    b.分类
        a.字符串常量池
            专门用于存储字符串字面量
            Java中的字符串常量池是一个特殊的区域,用于存储字符串对象,以便在需要时重用相同的字符串对象。
        b.运行时常量池
            每个类或接口的常量池在类加载后会被放入方法区中,称为运行时常量池。它包含类的字面量和符号引用。
    c.特点
        常量池的主要目的是为了提高性能和节省内存,通过重用相同的常量。
        字符串常量池的管理是自动的,Java会在编译时和运行时自动处理字符串的存储和重用。
    d.位置
        常量池是方法区的一部分。
        在Java 8及之后,方法区的实现是元空间(Metaspace),而在Java 8之前,方法区的实现是永久代(PermGen)。
        字符串常量池在Java 7及之后被移到了堆中,而在此之前,它是方法区的一部分。
    e.作用
        常量池用于存储编译期生成的字面量(如字符串常量)和符号引用(如类和方法的引用)。
        运行时常量池是每个类或接口的常量池在类加载后被放入方法区中。

02.方法区(静态区)
    a.定义
        方法区是Java内存模型的一部分,用于存储类的结构信息(如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容)以及静态变量和类加载器信息。
        在Java 8之前,方法区的实现是永久代(PermGen),而在Java 8及之后,方法区的实现是元空间(Metaspace)。
    b.特点
        方法区是线程共享的区域,所有线程都可以访问其中的数据。
        方法区的大小可以通过JVM参数进行配置,尤其是在Java 8之前的永久代中,容易出现内存溢出(OutOfMemoryError)。
        元空间在Java 8中引入,使用本地内存来存储类的元数据,解决了永久代的内存限制问题。
    c.存储内容
        类的字节码、常量池、字段和方法数据。
        静态变量:类的静态变量存储在方法区中。
        类的加载信息:包括类的加载器、类的初始化状态等。
    d.位置
        方法区是Java内存模型的一部分,独立于堆和栈。
        在Java 8之前,方法区的实现是永久代(PermGen),
        而在Java 8及之后,方法区的实现是元空间(Metaspace),使用本地内存。
    e.作用
        方法区用于存储类的结构信息(如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容)以及静态变量和类加载器信息。
        方法区是线程共享的区域,所有线程都可以访问其中的数据。

3 内存模型,JMM

3.1 [1]指令重排序:3种

01.定义
    指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致,所以在多线程下,指令重排序可能会导致一些问题
    指令重排(Instruction Reordering)是指为了优化性能,CPU 或编译器可能会对指令的执行顺序进行调整,但这种调整不会影响单线程程序的最终结果
    为了优化性能,CPU 或编译器可能调整指令执行顺序,在多线程环境中可能导致可见性问题,通过 volatile、synchronized 或内存屏障避免

02.为什么要指令重排序
    为了提高程序执行的效率,CPU或者编译器就将执行命令重排序
    原因是因为内存访问的速度比CPU运行速度慢很多,因此需要编排一下执行的顺序,防止因为访问内存的比较慢的指令而使得CPU闲置着,
    其实和典型的烧水喝茶洗杯子的排序类似
    ---------------------------------------------------------------------------------------------------------
    CPU执行有个指令流水线的概念,还有分支预测等
    总之为了提高效率就会有指令重排的情况,导致指令乱序执行的情况发生,不过会保证结果肯定是与单线程执行结果一致的,这叫as-if-serial
    不过多线程就无法保证了,在Java中的volatile关键字可以禁止修饰变量前后的指令重排

03.指令重排一般分为以下三种
    a.编译器优化重排
        编译器在不改变单线程程序语义的前提下,重新安排语句的执行顺序
    b.指令并行重排
        现代处理器采用了指令级并行技术来将多条指令重叠执行
        如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),
        处理器可以改变语句对应的机器指令的执行顺序
    c.内存系统重排
        由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行
        因为三级缓存的存在,导致内存与缓存的数据同步存在时间差

3.2 [1]指令重排序:volatile

01.volatile
    a.定义
        1.【保证变量可见性】
        2.【防止局部重排序】
    b.特性
        不保证原子性,不会引起线程阻塞
    c.使用场景
        适用于状态标志等简单的读写操作,不适用于复合操作(如自增、自减)
    d.性能
        开销较小,因为不会引起线程上下文切换
    e.示例
        public class VolatileExample {
            private volatile boolean flag = true;
            public void setFlag(boolean flag) {
                this.flag = flag;
            }
            public boolean getFlag() {
                return flag;
            }
        }

02.内存屏障(Memory Barrier)
    a.定义
        当一个变量被声明为volatile时,Java内存模型会在对该变量的读写操作前后插入特定的内存屏障,以防止指令重排序
        在Java中,内存屏障通常由JVM和硬件自动处理,开发者不需要手动插入内存屏障
        然而,在某些低级别编程语言(如 C/C++)中,可以使用内存屏障指令来防止指令重排
    b.内存屏障种类
        1.LoadLoad Barrier:确保在屏障之前的所有加载操作(读操作)在屏障之后的加载操作之前完成
        2.StoreStore Barrier:确保在屏障之前的所有存储操作(写操作)在屏障之后的存储操作之前完成
        3.LoadStore Barrier:确保在屏障之前的所有加载操作在屏障之后的存储操作之前完成
        4.StoreLoad Barrier:确保在屏障之前的所有存储操作在屏障之后的加载操作之前完成
    c.内存屏障规则
        a.写volatile变量时
            在写操作之前插入一个StoreStore Barrier,确保在写volatile变量之前的所有普通写操作都已经完成
            在写操作之后插入一个StoreLoad Barrier,确保在写volatile变量之后的所有普通读操作都不会被重排序到写操作之前
        b.读volatile变量时
            在读操作之前插入一个LoadLoad Barrier,确保在读volatile变量之前的所有普通读操作都已经完成
            在读操作之后插入一个LoadStore Barrier,确保在读volatile变量之后的所有普通写操作都不会被重排序到读操作之前

03.代码示例
    a.使用volatile关键字防止指令重排序
        public class VolatileExample {
            private volatile boolean flag = false;
            private int counter = 0;

            public void writer() {
                counter = 42;       // 普通写操作
                flag = true;        // volatile 写操作
            }

            public void reader() {
                if (flag) {         // volatile 读操作
                    System.out.println(counter); // 普通读操作
                }
            }
        }
    b.说明
        在 writer 方法中,counter = 42 是一个普通写操作,而 flag = true 是一个 volatile 写操作
        根据内存屏障规则,在写 flag 之前,会插入一个 StoreStore Barrier,确保 counter = 42 先于 flag = true 执行
        在写 flag 之后,会插入一个 StoreLoad Barrier,确保后续的读操作不会被重排序到 flag = true 之前
        -----------------------------------------------------------------------------------------------------
        在 reader 方法中,if (flag) 是一个 volatile 读操作,而 System.out.println(counter) 是一个普通读操作
        根据内存屏障规则,在读 flag 之前,会插入一个 LoadLoad Barrier,确保之前的读操作已经完成
        在读 flag 之后,会插入一个 LoadStore Barrier,确保后续的写操作不会被重排序到 flag 之前

3.3 [1]指令重排序:提高性能

00.汇总
    1.提高指令级并行性
    2.隐藏内存访问延迟
    3.减少数据相关性
    4.优化缓存利用率
    5.减少分支预测失败的影响

01.提高指令级并行性(Instruction-Level Parallelism, ILP)
    a.定义
        现代处理器通常具有多个执行单元,可以同时执行多条指令
        指令重排序可以将原本顺序执行的指令重新排列,使得更多的指令可以并行执行,从而提高处理器的利用率和执行效率
    b.示例
        // 原始代码
        int a = 1;
        int b = 2;
        int c = a + b;
        int d = c * 2;
        // 重排序后
        int a = 1;
        int b = 2;
        int c = a + b;
        int d = (a + b) * 2; // 这行可以与前面的指令并行执行

02.隐藏内存访问延迟
    a.定义
        内存访问通常比CPU执行指令要慢得多
        指令重排序可以将不依赖于内存访问结果的指令提前执行,从而隐藏内存访问的延迟,减少CPU等待时间
    b.示例
        // 原始代码
        int a = array[0];
        int b = array[1];
        int c = a + b;
        // 重排序后
        int a = array[0];
        int b = array[1];
        int d = someOtherOperation(); // 不依赖于a和b的结果
        int c = a + b;

03.减少数据相关性(Data Dependency)
    a.定义
        指令重排序可以减少数据相关性,即减少指令之间的依赖关系,从而使得更多的指令可以并行执行
    b.示例
        // 原始代码
        int a = 1;
        int b = a + 2;
        int c = b + 3;
        // 重排序后
        int a = 1;
        int c = (a + 2) + 3; // 这行可以与前面的指令并行执行
        int b = a + 2;

04.优化缓存利用率
    a.定义
        指令重排序可以优化数据的缓存利用率,减少缓存未命中(cache miss)的次数,从而提高程序的执行效率
    b.示例
        // 原始代码
        int a = array1[0];
        int b = array2[0];
        int c = array1[1];
        int d = array2[1];
        // 重排序后
        int a = array1[0];
        int c = array1[1];
        int b = array2[0];
        int d = array2[1]; // 这样可以更好地利用缓存

05.减少分支预测失败的影响
    a.定义
        指令重排序可以减少分支预测失败带来的性能损失
        通过将不依赖于分支结果的指令提前执行,可以减少分支预测失败时的性能开销
    b.示例
        // 原始代码
        if (condition) {
            int a = 1;
        }
        int b = 2;
        // 重排序后
        int b = 2;
        if (condition) {
            int a = 1;
        } // 这样即使分支预测失败,也不会影响b的赋值

3.4 [1]指令重排序:防止手段,3个

00.汇总
    volatile:通过插入内存屏障,保证变量的可见性和防止指令重排
    synchronized:通过锁机制,保证代码块的原子性和内存可见性,同时禁止指令重排
    内存屏障:在硬件层面,通过插入内存屏障来限制指令的执行顺序

01.volatile关键字
    a.说明
        volatile关键字不仅保证变量的可见性,还禁止指令重排
        它通过在读写volatile变量时插入内存屏障来实现这一点
    b.详细设计
        可见性:对volatile变量的写操作会立即被刷新到主内存中,从而确保其他线程能够立即看到最新的值
        内存屏障:在读写volatile变量时,Java内存模型会插入特定的内存屏障,防止编译器和CPU对其进行重排序
    c.示例代码
        public class VolatileExample {
            private volatile boolean flag = false;

            public void setFlag(boolean flag) {
                this.flag = flag; // 写操作,插入 StoreStore 和 StoreLoad 屏障
            }

            public boolean getFlag() {
                return flag; // 读操作,插入 LoadLoad 和 LoadStore 屏障
            }
        }

02.synchronized关键字
    a.说明
        synchronized关键字通过锁机制来保证代码块的原子性和内存可见性,同时禁止指令重排
    b.详细设计
        原子性:synchronized确保同一时刻只有一个线程可以执行被同步的代码块
        可见性:进入synchronized块时,线程会从主内存中读取最新的变量值;退出synchronized块时,线程会将变量的修改刷新到主内存中
        内存屏障:synchronized隐式地插入内存屏障,防止指令重排
    c.示例代码
        public class SynchronizedExample {
            private int counter = 0;

            public synchronized void increment() {
                counter++; // 进入和退出 synchronized 块时插入内存屏障
            }

            public synchronized int getCounter() {
                return counter; // 进入和退出 synchronized 块时插入内存屏障
            }
        }

03.内存屏障(Memory Barrier)
    a.说明
        在硬件层面,通过插入内存屏障来限制指令的执行顺序。内存屏障是一种低级别的同步机制,用于防止指令重排
        在Java中,内存屏障通常由JVM和硬件自动处理,开发者不需要手动插入内存屏障
        然而,在某些低级别编程语言(如 C/C++)中,可以使用内存屏障指令来防止指令重排
    b.详细设计
        LoadLoad Barrier:确保在屏障之前的所有加载操作(读操作)在屏障之后的加载操作之前完成
        StoreStore Barrier:确保在屏障之前的所有存储操作(写操作)在屏障之后的存储操作之前完成
        LoadStore Barrier:确保在屏障之前的所有加载操作在屏障之后的存储操作之前完成
        StoreLoad Barrier:确保在屏障之前的所有存储操作在屏障之后的加载操作之前完成
    c.示例代码
        #include <stdatomic.h>
        void memory_barrier_example() {
            atomic_thread_fence(memory_order_seq_cst); // 全局内存屏障,防止指令重排
        }

3.5 [2]原子性、可见性、有序性

01.原子性、可见性、有序性
    a.原子性
        保证操作不可分割,即使在多线程环境下也不会被中断
        可以通过 synchronized 关键字或 java.util.concurrent.atomic 包中的原子类来实现
    b.可见性
        保证一个线程对共享变量的修改能够及时被其他线程看到
        可以通过 volatile 关键字或 synchronized 关键字来实现
    c.有序性
        保证程序执行的顺序按照代码的先后顺序执行
        可以通过 volatile 关键字或 synchronized 关键字来实现

01.原子性(Atomicity)
    a.定义
        原子性是指一个操作或一系列操作要么全部执行成功,要么全部不执行
        原子操作是不可分割的,即使在多线程环境下也不会被中断
    b.示例
        public class AtomicExample {
            private int count = 0;

            public void increment() {
                count++; // 不是原子操作
            }
        }
        -----------------------------------------------------------------------------------------------------
        在上面的代码中,count++ 不是一个原子操作,它实际上包含了三个步骤:读取 count 的值、增加 count 的值、将新值写回 count。在多线程环境下,这些步骤可能会被其他线程打断,导致数据不一致。
    c.解决方案
        a.使用 synchronized 关键字
            public class AtomicExample {
                private int count = 0;

                public synchronized void increment() {
                    count++;
                }
            }
        b.使用 java.util.concurrent.atomic 包中的原子类
            import java.util.concurrent.atomic.AtomicInteger;

            public class AtomicExample {
                private AtomicInteger count = new AtomicInteger(0);

                public void increment() {
                    count.incrementAndGet();
                }
            }

02.可见性(Visibility)
    a.定义
        可见性是指一个线程对共享变量的修改,能够及时被其他线程看到
        由于 Java 内存模型的原因,线程对变量的修改可能不会立即被刷新到主内存中,其他线程也可能从自己的工作内存中读取过期的值
    b.示例
        public class VisibilityExample {
            private boolean flag = false;

            public void writer() {
                flag = true;
            }

            public void reader() {
                if (flag) {
                    // 可能看不到 flag 的修改
                }
            }
        }
        -----------------------------------------------------------------------------------------------------
        在上面的代码中,reader 方法可能看不到 writer 方法对 flag 的修改
        因为 flag 的值可能没有及时刷新到主内存中
    c.解决方案
        a.使用 volatile 关键字
            public class VisibilityExample {
                private volatile boolean flag = false;

                public void writer() {
                    flag = true;
                }

                public void reader() {
                    if (flag) {
                        // 一定能看到 flag 的修改
                    }
                }
            }
        b.使用 synchronized 关键字
            public class VisibilityExample {
                private boolean flag = false;

                public synchronized void writer() {
                    flag = true;
                }

                public synchronized void reader() {
                    if (flag) {
                        // 一定能看到 flag 的修改
                    }
                }
            }

03.有序性(Ordering)
    a.定义
        有序性是指程序执行的顺序按照代码的先后顺序执行
        由于编译器和处理器的优化,指令可能会被重排序,从而导致程序的执行顺序与代码的顺序不一致
    b.示例
        public class OrderingExample {
            private int a = 0;
            private boolean flag = false;

            public void writer() {
                a = 1; // 语句1
                flag = true; // 语句2
            }

            public void reader() {
                if (flag) {
                    int i = a; // 语句3
                    // 可能看到 i 为 0
                }
            }
        }
        -----------------------------------------------------------------------------------------------------
        在上面的代码中,reader 方法可能会看到 i 为 0,
        因为编译器或处理器可能会对 writer 方法中的语句1和语句2进行重排序
    c.解决方案
        a.使用 volatile 关键字
            public class OrderingExample {
                private volatile int a = 0;
                private volatile boolean flag = false;

                public void writer() {
                    a = 1; // 语句1
                    flag = true; // 语句2
                }

                public void reader() {
                    if (flag) {
                        int i = a; // 语句3
                        // 一定能看到 i 为 1
                    }
                }
            }
        b.使用 synchronized 关键字
            public class OrderingExample {
                private int a = 0;
                private boolean flag = false;

                public synchronized void writer() {
                    a = 1; // 语句1
                    flag = true; // 语句2
                }

                public synchronized void reader() {
                    if (flag) {
                        int i = a; // 语句3
                        // 一定能看到 i 为 1
                    }
                }
            }

3.6 [2]final不可以保证变量的可见性

01.final关键字是否能保证变量的可见性?
    不可以

02.说明
    final关键字可以在特定情况下保证变量的可见性:当对象构造完成后,final字段的值对所有线程都是可见的
    final关键字的主要作用不是解决可见性问题:它主要用于确保变量一旦被赋值后就不能再被修改

3.7 [2]volatile可以保证变量的可见性

01.volatile可以保证变量的可见性?
    可以

02.说明
    对volatile变量的写操作会立即被刷新到主内存中,从而确保其他线程能够立即看到最新的值

3.8 [2]volatile不可以保证变量的原子性

01.volatile不可以保证变量的原子性?
    不可以

02.说明
    volatile关键字不能保证变量的原子性:
    虽然volatile可以确保变量的可见性,但它不能保证对变量的复合操作(如 i++)的原子性
    如果需要保证原子性,可以使用 synchronized 或 Lock。

3.9 [3]内存可见性

01.共享变量和主存
    所有的共享变量都存储在主存(Java堆)中
    主存是所有线程共享的存储区域

02.线程的本地内存
    每个线程都有自己的本地内存(工作内存),它是线程私有的
    本地内存保存了该线程使用的共享变量的副本
    本地内存是一个抽象概念,涵盖了缓存、寄存器等

03.线程间通信
    线程之间的通信必须通过主存进行
    线程A更新共享变量时,会将更新后的值从本地内存刷新到主存
    线程B读取共享变量时,会从主存中获取最新的值,并更新到自己的本地内存

04.内存可见性问题
    由于线程对共享变量的操作是在本地内存中进行的,可能会导致一个线程对共享变量的更新对另一个线程不可见
    这种情况发生是因为线程A更新了共享变量,但没有及时刷新到主存,而线程B读取的是过期的副本

05.Java内存模型(JMM)
    JMM定义了线程和主存之间的抽象关系,规定了线程对共享变量的所有操作都必须在本地内存中进行
    JMM确保了线程间通信的有序性和一致性

3.10 [3]内存可见性:解决

01.解决内存可见性问题的方法
    a.使用volatile关键字
        确保变量的更新操作对所有线程立即可见
    b.使用同步机制(如synchronized关键字或显式锁Lock)
        确保在临界区内的操作是原子的,并且在进入和退出临界区时会刷新本地内存和主存之间的值

3.11 [4]顺序一致性

01.顺序一致性
    a.定义
        顺序一致性模型是一个理想化的内存模型,它为并发程序提供了极强的内存可见性保证
        它确保一个线程中的所有操作按照程序的顺序执行,并且所有线程看到的操作顺序一致
        尽管顺序一致性模型提供了简单且强有力的保证
    b.实际使用JMM
        但在实际应用中,由于性能和优化的原因,现代处理器和编译器通常不会严格遵守这个模型,
        而是采用更宽松的内存模型,如Java内存模型(JMM)
    c.两个主要特性
        a.程序顺序性
            一个线程中的所有操作必须按照程序的顺序(即代码的顺序)来执行
            这意味着在单个线程内,操作的执行顺序与代码中出现的顺序一致
        b.全局顺序性
            不管程序是否同步,所有线程都只能看到一个单一的操作执行顺序
            即在顺序一致性模型中,每个操作必须是原子性的,并且立刻对所有线程可见
            这意味着所有线程都能看到相同的操作执行顺序

02.理解顺序一致性模型
    a.定义
        为了更好地理解顺序一致性模型,我们可以通过一个具体的例子来说明
        假设有两个线程A和B,并发执行,线程A有三个操作A1、A2、A3,线程B也有三个操作B1、B2、B3
    b.正确同步的情况
        假设线程A的三个操作执行后释放锁,线程B获取同一个锁,然后执行它的三个操作
        在顺序一致性模型中,这些操作的执行顺序可能如下所示:
        A1 -> A2 -> A3 -> B1 -> B2 -> B3
        在这种情况下,操作的执行顺序是全局一致的,并且每个线程都能看到这个顺序
    c.未同步的情况
        即使没有同步,顺序一致性模型也保证所有线程看到的操作顺序是一致的。例如,可能的执行顺序如下:
        A1 -> B1 -> A2 -> B2 -> A3 -> B3
        或者:
        B1 -> A1 -> B2 -> A2 -> B3 -> A3
        无论具体的执行顺序如何,所有线程看到的顺序都是一致的

03.顺序一致性模型
    a.图示
        为了更直观地理解顺序一致性模型,我们可以用图示来表示。假设线程A和线程B的操作按如下顺序执行:
        A1 -> A2 -> A3
        B1 -> B2 -> B3
        -----------------------------------------------------------------------------------------------------
        在顺序一致性模型中,可能的执行顺序图示如下:
        时间轴
        | A1 -> A2 -> A3
        | B1 -> B2 -> B3
        或者:
        时间轴
        | A1 -> B1 -> A2 -> B2 -> A3 -> B3
    b.优点
        简单性:顺序一致性模型提供了一个简单且直观的并发编程模型,程序员可以很容易地推断出程序的行为
        强保证:它提供了强有力的内存可见性保证,确保所有线程看到的操作顺序一致
    c.缺点
        性能开销:为了实现顺序一致性模型,可能需要大量的同步操作,这会带来性能开销。现代处理器和编译器通常会进行指令重排序和优化,以提高性能,这可能违反顺序一致性模型的要求
        不现实性:在实际的硬件和编译器中,严格遵守顺序一致性模型的要求可能不现实,因为这会限制优化技术的应用

3.12 [4]顺序一致性:与JMM差异

01.JMM和顺序一致性模型的差异
    a.单线程内的操作顺序
        顺序一致性模型保证单线程内的操作会按程序的顺序执行
        JMM不保证单线程内的操作会按程序的顺序执行,因为JMM允许指令重排序,但JMM保证单线程下的重排序不影响执行结果
    b.全局操作顺序
        顺序一致性模型保证所有线程只能看到一致的操作执行顺序
        JMM不保证所有线程能看到一致的操作执行顺序,因为JMM不保证所有操作立即可见
    c.原子性
        顺序一致性模型保证对所有的内存读写操作都具有原子性
        JMM不保证对64位的long型和double型变量的写操作具有原子性

3.13 [4]顺序一致性:JMM为什么不能保证

01.为什么JMM不保证顺序一致性
    a.性能优化的需要
        顺序一致性模型(Sequential Consistency Model)提供了极强的内存可见性保证
        但这种保证是以牺牲性能为代价的。为了实现顺序一致性,处理器和编译器必须严格按照程序的顺序执行指令
        并且每个操作必须立即对所有线程可见。这种严格的要求会限制很多优化技术的应用,从而影响程序的执行性能
        -----------------------------------------------------------------------------------------------------
        JMM的设计目标是:在不改变正确同步的程序执行结果的前提下,尽量为编译期和处理器的优化打开方便之门
        通过允许一定程度的指令重排序和延迟写入主存,JMM可以显著提高程序的执行效率
    b.指令重排序
        指令重排序是编译器和处理器优化的一种常见技术
        通过重排序,编译器和处理器可以更好地利用CPU的执行单元,
        提高指令级并行性,隐藏内存访问延迟,从而提高程序的执行效率
        -----------------------------------------------------------------------------------------------------
        在JMM中,指令重排序是允许的,
        只要这种重排序不会改变单线程程序的语义,并且不会破坏正确同步的多线程程序的执行结果
    c.缓存和本地内存
        现代处理器通常具有多级缓存系统,以减少内存访问的延迟
        线程在执行过程中,会将共享变量的副本缓存在本地内存(如CPU缓存)中
        只有在必要时,线程才会将本地内存中的数据刷新到主存
        -----------------------------------------------------------------------------------------------------
        这种缓存机制提高了内存访问的效率,但也带来了内存可见性问题
        在JMM中,线程对共享变量的写操作并不一定会立即对其他线程可见,
        只有当数据被刷新到主存后,其他线程才能看到最新的值
    d.最小安全性保证
        JMM为未同步的多线程程序提供了最小的安全性保证:
        线程读取到的值,要么是之前某个线程写入的值,要么是默认值,不会无中生有
        为了实现这个安全性,JVM在堆上分配对象时,首先会对内存空间清零,然后才会在上面分配对象(这两个操作是同步的)

3.14 [5]内存模型、内存区域

01.内存模型(JMM)
    a.定义
        JMM是Java虚拟机规范的一部分,
        它定义了Java程序中各种变量(包括实例字段、静态字段和构成数组对象的元素)的访问规则,
        以及在多线程环境下如何保证变量的可见性、原子性和有序性
    b.关键点
        可见性:确保一个线程对共享变量的修改能够及时被其他线程看到
        原子性:确保操作的不可分割性,即操作要么全部执行,要么全部不执行
        有序性:确保程序执行的顺序符合预期,避免指令重排序带来的问题
    c.作用
        JMM主要解决多线程环境下的内存可见性问题,确保线程间通信的正确性和一致性

02.Java运行时内存区域
    a.定义
        Java运行时内存区域是JVM在运行Java程序时对内存的具体划分
        它描述了JVM如何管理和分配内存,以支持程序的执行
    b.内存区域
        方法区:存储类的元数据、常量、静态变量、即时编译器编译后的代码等
        堆:存储所有的对象实例和数组,是Java内存管理的主要区域
        栈:每个线程都有一个私有的栈,存储局部变量、操作数栈、动态链接、方法出口等信息
        本地方法栈:为JVM调用的本地方法(native方法)服务
        程序计数器:每个线程都有一个独立的程序计数器,用于指示当前线程执行到的字节码指令地址
    c.作用
        Java运行时内存区域描述了JVM在运行时如何划分和管理内存,以支持程序的执行和内存分配

03.区别、联系
    a.区别
        a.概念层次
            JMM是一个抽象的概念,描述了一组规则和规范,用于控制变量的访问方式,主要关注多线程环境下的内存可见性、原子性和有序性
            Java运行时内存区域是具体的内存划分,描述了JVM在运行时如何管理和分配内存
        b.关注点
            JMM主要关注多线程环境下的内存一致性问题
            Java运行时内存区域主要关注内存的分配和管理
    b.联系
        a.私有数据区域和共享数据区域
            JMM中的主存属于共享数据区域,包含了堆和方法区
            JMM中的本地内存属于私有数据区域,包含了程序计数器、本地方法栈、虚拟机栈
        b.内存访问
            JMM定义了线程如何从主存中读取和写入变量,以及如何在本地内存中缓存变量的副本
            Java运行时内存区域描述了JVM如何在不同的内存区域中存储和管理数据
    c.总结
        Java内存模型(JMM):抽象的规则和规范,主要解决多线程环境下的内存可见性、原子性和有序性问题
        Java运行时内存区域:具体的内存划分,描述JVM在运行时如何管理和分配内存,包括方法区、堆、栈、本地方法栈和程序计数器

3.15 [5]happens-before关系有哪些?

01.在Java中,有以下天然的happens-before关系
    a.程序顺序规则
        一个线程中的每一个操作,happens-before 于该线程中的任意后续操作
    b.监视器锁规则
        对一个锁的解锁,happens-before 于随后对这个锁的加锁
    c.volatile变量规则
        对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读
    d.传递性
        如果 A happens-before B,且 B happens-before C,那么 A happens-before C
    e.start规则
        如果线程 A 执行操作 ThreadB.start()启动线程 B,
        那么 A 线程的 ThreadB.start()操作 happens-before 于线程 B 中的任意操作
    f.join规则
        如果线程 A 执行操作 ThreadB.join()并成功返回,
        那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功返回

4 jvm、对象、类加载

4.1 [1]jvm运行:4步

01.总结
    1.JVM的装入环境和配置
    2.装载JVM
    3.初始化JVM,获得本地调用接口
    4.运行Java程序

01.JVM的装入环境和配置
    a.环境变量配置
        配置Java环境变量,如JAVA_HOME和PATH,以便系统能够找到Java的可执行文件和库。
        设置CLASSPATH,指定Java程序运行时需要的类路径。
    b.JVM参数配置
        配置JVM启动参数,如堆大小(-Xms和-Xmx)、垃圾回收器选项、线程栈大小(-Xss)等。
        这些参数可以在启动Java程序时通过命令行指定,以调整JVM的性能和行为。

02.装载JVM
    a.JVM启动
        当用户执行java命令时,JVM启动并开始加载Java应用程序。
        JVM的启动过程包括加载JVM的核心库和初始化JVM的内部结构。
    b.类加载器初始化
        JVM初始化类加载器子系统,准备加载应用程序的类。
        启动类加载器(Bootstrap ClassLoader)加载JVM核心类库(如rt.jar)。

03.初始化JVM,获得本地调用接口
    a.JVM初始化
        JVM初始化运行时数据区,包括堆、栈、方法区、程序计数器和本地方法栈。
        初始化执行引擎,包括解释器和即时编译器(JIT)。
    b.本地调用接口(JNI)
        JVM初始化Java Native Interface(JNI),以便Java程序可以调用本地代码(如C/C++)。
        JNI提供了Java与本地代码之间的桥梁,允许Java程序与操作系统和硬件进行交互。

04.运行Java程序
    a.类加载
        应用程序类加载器(Application ClassLoader)加载用户指定的主类。
        类加载过程包括加载、验证、准备、解析和初始化。
    b.字节码执行
        JVM通过解释器逐行解释字节码,或通过JIT编译器将热点代码编译为本地机器码执行。
        执行引擎负责将字节码转换为机器码,并在底层硬件上执行。
    c.内存管理和垃圾回收
        JVM负责管理程序的内存,包括对象的分配和垃圾回收。
        垃圾回收器自动回收不再使用的对象,释放内存空间。
    d.线程管理
        JVM管理Java线程的生命周期,包括创建、调度和销毁。
        程序的并发执行由JVM的线程调度器管理。

4.2 [1]jvm组成:5种

01.JVM组成部分
    1.类加载器子系统(Class Loader Subsystem):负责加载类文件到内存中,并将其转换为Class对象
    2.运行时数据区(Runtime Data Area):包括堆、栈、方法区、程序计数器和本地方法栈
    3.执行引擎(Execution Engine):包括解释器和即时编译器(JIT),负责执行字节码
    4.本地接口(Native Interface):提供与本地代码(如C/C++)交互的接口
    5.垃圾回收器(Garbage Collector):自动管理内存,回收不再使用的对象

4.3 [1]jvm引用:4种

00.总结
    强引用:默认的引用类型,强引用的对象【不会被垃圾回收】
    软引用:用于实现内存敏感的缓存,【只有在内存不足时才会被回收】
    弱引用:用于实现非强制性缓存,弱引用的对象【在垃圾回收时总会被回收】
    虚引用:用于跟踪对象的回收状态,虚引用的对象【在任何时候都可能被回收】

01.强引用
    a.定义
        默认的引用类型,强引用的对象【不会被垃圾回收】
    b.说明
        这是Java程序中最常见的引用方式,即程序创建一个对象,
        并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象。
        当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收。

02.软引用
    a.定义
        用于实现内存敏感的缓存,【只有在内存不足时才会被回收】
    b.说明
        当一个对象只有软引用时,它有可能被垃圾回收机制回收。
        对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象。
        当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中。

03.弱引用
    a.定义
        用于实现非强制性缓存,弱引用的对象【在垃圾回收时总会被回收】
    b.说明
        弱引用和软引用很像,但弱引用的引用级别更低。
        对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。
        当然,并不是说当一个对象只有弱引用时,它就会立即被回收,正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。

04.虚引用
    a.定义
        用于跟踪对象的回收状态,虚引用的对象【在任何时候都可能被回收】
    b.说明
        虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。
        如果一个对象只有一个虚引用时,那么它和没有引用的效果大致相同。
        虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列联合使用。

4.4 [1]jvm助记:10类

10.加载和存储指令:用于在操作数栈和局部变量表之间传递数据
    a.加载指令
        aload: 将引用类型局部变量加载到操作数栈
        iload: 将int类型局部变量加载到操作数栈
        fload: 将float类型局部变量加载到操作数栈
        dload: 将double类型局部变量加载到操作数栈
        lload: 将long类型局部变量加载到操作数栈
    b.存储指令
        astore: 将操作数栈顶的引用存储到局部变量表
        istore: 将操作数栈顶的int值存储到局部变量表
        fstore: 将操作数栈顶的float值存储到局部变量表
        dstore: 将操作数栈顶的double值存储到局部变量表
        lstore: 将操作数栈顶的long值存储到局部变量表

02.常量指令:用于将常量值推送到操作数栈
    a.iconst_m1: 将整数-1推送到操作数栈
    b.iconst_0: 将整数0推送到操作数栈
    c.iconst_1: 将整数1推送到操作数栈
    d.iconst_2: 将整数2推送到操作数栈
    e.iconst_3: 将整数3推送到操作数栈
    f.iconst_4: 将整数4推送到操作数栈
    g.iconst_5: 将整数5推送到操作数栈
    h.fconst_0: 将浮点数0.0推送到操作数栈
    i.fconst_1: 将浮点数1.0推送到操作数栈
    j.fconst_2: 将浮点数2.0推送到操作数栈
    k.dconst_0: 将双精度浮点数0.0推送到操作数栈
    l.dconst_1: 将双精度浮点数1.0推送到操作数栈
    m.lconst_0: 将长整数0推送到操作数栈
    n.lconst_1: 将长整数1推送到操作数栈
    o.bipush: 将一个字节常量值推送到操作数栈
    p.sipush: 将一个短整型常量值推送到操作数栈
    q.ldc: 将常量池中的常量值推送到操作数栈

03.算术指令:用于执行基本的算术运算
    a.iadd: 将操作数栈顶的两个int值相加并将结果推送回操作数栈
    b.isub: 将操作数栈顶的两个int值相减并将结果推送回操作数栈
    c.imul: 将操作数栈顶的两个int值相乘并将结果推送回操作数栈
    d.idiv: 将操作数栈顶的两个int值相除并将结果推送回操作数栈
    e.irem: 将操作数栈顶的两个int值取模并将结果推送回操作数栈
    f.ineg: 将操作数栈顶的int值取反并将结果推送回操作数栈
    g.iinc: 将指定局部变量表中的int值增加指定的常量值

04.类型转换指令:用于在不同数据类型之间进行转换
    a.i2l: 将int值转换为long值
    b.i2f: 将int值转换为float值
    c.i2d: 将int值转换为double值
    d.l2i: 将long值转换为int值
    e.l2f: 将long值转换为float值
    f.l2d: 将long值转换为double值
    g.f2i: 将float值转换为int值
    h.f2l: 将float值转换为long值
    i.f2d: 将float值转换为double值
    j.d2i: 将double值转换为int值
    k.d2l: 将double值转换为long值
    l.d2f: 将double值转换为float值

05.比较指令:用于比较操作数栈顶的值
    a.if_icmpeq: 如果栈顶两个int值相等,则跳转
    b.if_icmpne: 如果栈顶两个int值不相等,则跳转
    c.if_icmplt: 如果栈顶第一个int值小于第二个int值,则跳转
    d.if_icmpge: 如果栈顶第一个int值大于等于第二个int值,则跳转
    e.if_icmpgt: 如果栈顶第一个int值大于第二个int值,则跳转
    f.if_icmple: 如果栈顶第一个int值小于等于第二个int值,则跳转
    g.ifeq: 如果栈顶int值等于0,则跳转
    h.ifne: 如果栈顶int值不等于0,则跳转
    i.iflt: 如果栈顶int值小于0,则跳转
    j.ifge: 如果栈顶int值大于等于0,则跳转
    k.ifgt: 如果栈顶int值大于0,则跳转
    l.ifle: 如果栈顶int值小于等于0,则跳转

06.控制流指令:用于控制程序的执行流程
    a.goto: 无条件跳转到指定位置
    b.jsr: 跳转到子例程并将返回地址压入栈
    c.ret: 从子例程返回
    d.tableswitch: 用于switch语句的多分支跳转
    e.lookupswitch: 用于switch语句的查找表跳转

07.方法调用和返回指令:用于调用方法和从方法返回
    a.invokestatic: 调用静态方法
    b.invokevirtual: 调用实例方法
    c.invokespecial: 调用实例初始化方法、私有方法或父类方法
    d.invokeinterface: 调用接口方法
    e.invokedynamic: 动态调用方法
    f.ireturn: 从方法中返回int值
    g.lreturn: 从方法中返回long值
    h.freturn: 从方法中返回float值
    i.dreturn: 从方法中返回double值
    j.areturn: 从方法中返回引用类型值
    k.return: 从方法中返回void

08.对象创建和操作指令:用于创建和操作对象
    a.new: 创建一个新的对象
    b.newarray: 创建一个新的基本类型数组
    c.anewarray: 创建一个新的引用类型数组
    d.arraylength: 获取数组的长度
    e.athrow: 抛出一个异常或错误
    f.checkcast: 检查类型转换,抛出ClassCastException
    g.instanceof: 检查对象是否是某个类的实例

09.同步指令:用于同步多线程访问
    a.monitorenter: 获取对象的监视器锁
    b.monitorexit: 释放对象的监视器锁

10.其他指令:用于其他各种操作
    a.nop: 无操作
    b.pop: 弹出操作数栈顶的一个元素
    c.pop2: 弹出操作数栈顶的两个元素
    d.dup: 复制操作数栈顶的一个元素并将复制值压入栈
    e.dup_x1: 复制操作数栈顶的一个元素并将复制值插入到栈顶下两个元素之后
    f.dup_x2: 复制操作数栈顶的一个元素并将复制值插入到栈顶下三个元素之后
    g.dup2: 复制操作数栈顶的两个元素并将复制值压入栈
    h.dup2_x1: 复制操作数栈顶的两个元素并将复制值插入到栈顶下两个元素之后
    i.dup2_x2: 复制操作数栈顶的两个元素并将复制值插入到栈顶下三个元素之后
    j.swap: 交换操作数栈顶的两个元素

4.5 [2]对象创建:5步

01.对象的创建
    a.类加载检查
        当JVM遇到一条new指令时,它首先会检查这个指令的参数是否能在常量池中定位到一个类的符号引用
        并检查这个类是否已经被加载、解析和初始化。如果没有,JVM会先执行相应的类加载过程
    b.分配内存
        a.方式1:指针碰撞(Bump the Pointer)
            如果堆内存是规整的,所有已使用的内存和未使用的内存分别在两边,中间有一个指针作为分界点,
            分配内存时只需将指针向空闲空间方向移动一段与对象大小相等的距离
        b.方式2:空闲列表(Free List)
            如果堆内存不规整,已使用和未使用的内存交错在一起,JVM会维护一个列表,
            记录哪些内存块是空闲的,分配时从列表中找到一个足够大的空间分配给对象
    c.初始化内存
        内存分配完成后,JVM会将分配到的内存空间初始化为零值(不包括对象头),
        确保对象的实例字段在Java代码中可以直接使用
    d.设置对象头
        JVM会对对象进行必要的设置,例如对象头(Header)的设置,包括对象的哈希码、GC分代年龄、锁状态标志等
    e.执行构造方法
        JVM执行<init>方法,初始化对象的实例字段,执行构造方法的代码

4.6 [2]对象内存布局:3个

01.对象的内存布局
    a.对象头(Header)
        Mark Word:存储对象的运行时数据,如哈希码、GC分代年龄、锁状态标志等
        Class Pointer:指向对象的类元数据,JVM通过这个指针确定对象的类型信息
        数组长度(可选):如果对象是数组,还会有一个额外的字段存储数组的长度
    b.实例数据(Instance Data)
        存储对象的实际数据,包括各个实例字段的值。实例数据的布局受字段的声明顺序和数据类型的影响
    c.对齐填充(Padding)
        为了使对象的大小是8字节的整数倍,JVM可能会在对象的末尾添加对齐填充

4.7 [2]对象访问定位:2种

01.对象的访问定位
    a.方式1:句柄访问(Handle Access)
        JVM会在内存中维护一个句柄池,句柄池中的每个句柄包含两个指针:一个指向对象实例数据的指针,另一个指向对象类型数据的指针
        优点:对象的引用和对象的实际地址分离,移动对象时只需更新句柄中的指针,不需要更新所有引用
        缺点:每次访问对象需要两次指针定位,性能稍差
    b.方式2:直接指针访问(Direct Pointer Access)
        对象引用直接指向对象在内存中的地址,对象头中包含指向类型数据的指针
        优点:访问速度快,只需一次指针定位
        缺点:对象移动时需要更新所有引用

4.8 [2]对象分配位置:5种

00.总结
    1.Eden区分配:大部分新创建的对象优先在Eden区分配
    2.大对象:大对象直接进入老年代,避免在Survivor区复制
    3.长期存活对象:对象在Survivor区中存活多次后会晋升到老年代
    4.动态年龄判断:根据对象年龄和Survivor空间的使用情况动态决定对象晋升
    5.空间分配担保:在进行Minor GC之前,JVM会检查老年代空间是否足够,必要时触发Full GC

01.Java对象的分配位置
    a.定义
        Java对象的分配位置主要取决于对象的生命周期和大小
        Java堆被划分为新生代和老年代,新生代又进一步划分为Eden区、From Survivor区和To Survivor区
    b.对象优先在Eden区分配
        Eden区:大部分新创建的对象会优先在Eden区分配。Eden区是新生代的一部分,主要用于存放生命周期较短的对象
        Minor GC:当Eden区满了之后,会触发一次Minor GC(小垃圾回收)。存活的对象会被移动到Survivor区(From Survivor或To Survivor)
    c.大对象直接进入老年代
        大对象:如果一个对象非常大,直接在新生代的Survivor区复制会浪费性能,因此大对象会直接分配到老年代
        参数设置:可以通过-XX:PretenureSizeThreshold参数设置大对象直接进入老年代的阈值。例如,设置为1MB(1024KB):
        -XX:PretenureSizeThreshold=1048576
    d.长期存活的对象将进入老年代
        对象年龄:对象在每次从一个Survivor区转移到另一个Survivor区时,年龄会增加。当对象的年龄达到一定阈值(默认为15),它会被转移到老年代
        参数设置:可以通过-XX:MaxTenuringThreshold参数设置年龄阈值。例如,设置为10:
        -XX:MaxTenuringThreshold=10
    e.动态年龄判断
        JVM会检查每个年龄段的对象大小,并估算它们在Survivor空间中所占的总体积
        JVM会选择一个最小的年龄,使得该年龄及以上的对象可以填满Survivor空间的一部分,然后将这些对象晋升到老年代
    f.空间分配担保
        空间分配担保:在进行Minor GC之前,JVM会检查老年代的最大可用连续空间是否大于新生代所有对象的总空间。如果不满足条件,可能会触发Full GC(全堆回收)
        HandlePromotionFailure:这个参数决定了在老年代空间不足时是否继续进行Minor GC。设置为true时,JVM会尝试继续Minor GC,即使老年代空间不足以容纳所有需要晋升的对象

4.9 [3]类的初始化:4步

01.类的初始化
    a.定义
        类的初始化是指对【类的静态变量】和【静态代码块】进行初始化
    b.初始化过程
        1.首次主动使用:类在首次主动使用时进行初始化,包括创建类的实例、访问类的静态变量、调用类的静态方法等。
        2.子类初始化:当初始化一个类时,如果其父类尚未初始化,则会先初始化父类。
        3.反射调用:通过反射调用类的方法或访问类的字段时。
        4.启动类:当虚拟机启动时,初始化用户指定的主类。

4.10 [3]类的使用方式:2类

01.类的使用方式:JVM只会在“首次主动使用”一个类/接口时,才会初始化它们。
    a.主动使用
        1.new构造类的使用,加载该类的静态方法
        2.访问类/接口的静态成员(属性、方法),加载该类的静态方法
        3.使用Class.forName("init.B")执行反射时使用的类(B类
        4.初始化一个子类时,该子类的父类也会被初始化
        5.动态语言在执行所涉及的类也会被初始化(动态代理)
    b.被动使用
        除了主动以外,其他都是被动使用。

4.11 [3]类的使用方式:主动使用中的静态成员问题

01.主动使用中的静态成员问题
    a.常量产生的时机
        final static称为“常量”,不会被初始化
        常量产生时机:①时机:编译期间
                     ②地点:(调用这个常量的方法所在类(Test2))的常量池,并不是类A的常量池
    b.主动使用中的静态成员问题
        1.类的静态成员(属性、方法),会初始化类的静态资源
        2.不是常量,因此也会被初始化
        3.常量值是一个随机值,则会被初始化 (安全考虑)
        4.常量值是一个随机值,则会被初始化 (安全考虑)
        5.初始化一个子类中,该子类的父类也会被初始化

4.12 [3]类的生命周期/加载时机:5个

00.类的生命周期/加载时机
    a.类的加载
        a.第1步
            查找并加载类的二进制数据(class.文件)
        b.第2步
            硬盘上的class文件加载到jvm内存中
    b.连接:确定类与类之间的关系
        a.验证
            .class正确性校验
        b.准备
            static静态变量分配内存,并赋初始化默认值
            <1>在准备阶段,JVM中只有类,没有对象。
            <2>初始化顺序:static -> 非static -> 构造方法
        c.解析
            把类中符号引用,转为直接引用
    c.初始化
        给static变量赋予正确的值
    d.使用
        对像的初始化、对象的垃圾回收、对象的销毁
    e.卸载
        类的生命周期结束,JVM回收类的Class对象和相关资源

01.加载(Loading)
    在加载阶段,虚拟机需要完成以下三件事:
    1.通过一个类的全限定名来获取定义此类的二进制字节流:虚拟机可以从多种来源获取类的二进制字节流,例如JAR包、WAR包、文件系统、网络等
    2.将这个字节流所代表的静态存储结构转换为运行时数据结构:将字节流解析为虚拟机可以理解的内部数据结构
    3.在内存中生成一个代表这个类的java.lang.Class对象:这个Class对象作为访问该类的各种数据的入口

02.验证(Verification)
    验证阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,从而保证这些信息被当做代码运行后不会危害虚拟机自身的安全。
    验证阶段大致会完成以下四项验证:
    1.文件格式验证:验证字节流是否符合Class文件格式的规范
    2.元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求。例如,除了java.lang.Object外,所有的类都应该有父类
    3.字节码验证:通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的。例如,允许把子类对象赋值给父类数据类型,但不能把父类对象赋值给子类数据类型
    4.符号引用验证:验证类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。如果无法验证通过,则会抛出一个java.lang.IncompatibleClassChangeError的子类异常,如java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

03.准备(Preparation)
    准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
    注意,这里的初始值通常是数据类型的默认值,例如int类型的初始值是0,而不是代码中定义的初始值

04.解析(Resolution)
    解析是Java虚拟机将常量池内的符号引用替换为直接引用的过程:
    1.符号引用:符号引用用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可
    2.直接引用:直接引用是指可以直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄
    解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行解析

05.初始化(Initialization)
    初始化阶段是执行类构造器<clinit>()方法的过程,该方法具有以下特点:
    1.<clinit>()方法由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生:编译器收集的顺序由语句在源文件中出现的顺序决定
    2.<clinit>()方法与类的构造方法(即在虚拟机视角中的实例构造器<init>()方法)不同:它不需要显式地调用父类的构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕
    3.父类的<clinit>()方法先执行:这意味着父类中定义的静态语句块要优先于子类变量的赋值操作
    4.<clinit>()方法对于类或者接口不是必须的:如果一个类中没有静态语句块,也没有对变量进行赋值操作,那么编译器可以不为这个类生成<clinit>()方法
    5.接口中不能使用静态语句块,但仍然有变量初始化的赋值操作:因此接口与类一样都会生成<clinit>()方法
    6.Java虚拟机必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步:如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待

4.13 [4]类的加载器:定义

01.定义
    类加载器是能够通过一个类的全限定名来获取描述该类的二进制字节流的工具
    类加载器负责将类的字节码加载到内存中,并将其转换为Java虚拟机可以执行的类对象

02.类加载器的作用
    1.加载类:通过类的全限定名获取类的字节码,并将其转换为Java虚拟机可以执行的类对象
    2.命名空间:每个类加载器都有一个独立的类名空间,确保类的唯一性
    3.类的唯一性:类的唯一性由类加载器和类本身共同决定。即使两个类的字节码完全相同,如果它们由不同的类加载器加载,它们在Java虚拟机中也是不同的类

4.14 [4]类的加载器:3类

01.启动类加载器(Bootstrap ClassLoader)
    功能:负责加载Java核心库(如rt.jar)中的类
    实现:由C++代码实现,是虚拟机自身的一部分
    特点:启动类加载器没有父加载器

02.扩展类加载器(Extension ClassLoader)
    功能:负责加载Java扩展库(如jre/lib/ext目录下的类)
    实现:由Java代码实现,继承自java.lang.ClassLoader
    特点:扩展类加载器的父加载器是启动类加载器

03.应用程序类加载器(Application ClassLoader)
    功能:加载应用程序的类路径(classpath)下的类,是用户自定义类加载器的默认父加载器。
    实现:由Java代码实现,继承自java.lang.ClassLoader
    特点:应用程序类加载器的父加载器是扩展类加载器

4.15 [4]类的加载器:唯一性

01类加载器和类本身共同决定了类的唯一性
    a.类的唯一性
        在Java虚拟机中,类的唯一性由类加载器和类本身共同决定
        即使两个类的字节码完全相同,如果它们由不同的类加载器加载,它们在Java虚拟机中也是不同的类
    b.类的比较
        要比较两个类是否相等,必须在同一类加载器加载的前提下
        如果两个类的类加载器不同,则它们一定不相等

4.16 [4]类的加载器:工作机制

01.类加载器的工作机制遵循双亲委派模型(Parent Delegation Model)
    a.双亲委派模型
        a.定义
            当一个类加载器加载一个类时,它首先将加载请求委派给父类加载器,
            只有当父类加载器无法完成加载请求时,子类加载器才会尝试加载该类
        b.优点
            确保Java核心库的类不会被自定义的类加载器加载,从而保证Java核心库的安全性和稳定性
    b.加载过程
        a.检查缓存
            类加载器首先检查缓存中是否已经加载过该类,如果已经加载过,则直接返回该类的Class对象
        b.委派父加载器
            如果缓存中没有该类,类加载器将加载请求委派给父加载器
        c.加载类
            如果父加载器无法加载该类,当前类加载器尝试加载该类

4.17 [4]类的加载器:自定义类加载器

01.实现
    Java允许开发者自定义类加载器,以满足特定的需求
    自定义类加载器需要继承java.lang.ClassLoader类,并重写findClass方法

02.代码
    public class MyClassLoader extends ClassLoader {
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            // 获取类的字节码
            byte[] classData = getClassData(name);
            if (classData == null) {
                throw new ClassNotFoundException();
            }
            // 将字节码转换为Class对象
            return defineClass(name, classData, 0, classData.length);
        }

        private byte[] getClassData(String name) {
            // 从文件系统或其他来源获取类的字节码
            // ...
            return null;
        }
    }

4.18 [4]类的加载机制:双亲委派,4步

01.双亲委派
    a.定义
        双亲委派模型是一种【类加载机制】,确保Java类的加载过程具有一致性和安全性
    b.特点
        双亲委派模型保证了【Java类的唯一性】(同一个类只会被加载一次)

02.工作流程
    1.类加载请求:当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类
    2.委派给父加载器:而是将这个请求委派给父加载器去完成
    3.逐级向上委派:每个类加载器都如此,最终请求会到达根加载器
    4.加载类:如果父加载器能够完成加载任务,就返回结果;否则,子加载器才会尝试自己加载

03.优点
    1.安全性:确保Java核心类库不会被自定义的类加载器加载,从而保证Java核心类库的安全性和稳定性
    2.类的唯一性:保证程序中的类在各种类加载器环境中都是同一个类,避免出现一个程序中存在两个不同的java.lang.Object类的情况

04.类加载器的层次关系
    a.图示
        启动类加载器(Bootstrap ClassLoader)
                ↑
        扩展类加载器(Extension ClassLoader)
                ↑
        应用程序类加载器(Application ClassLoader)
    b.说明
        这种层次关系确保了类加载请求能够逐层向上委派,最终由启动类加载器尝试加载

05.代码
    a.说明
        简单的自定义类加载器示例,展示了如何实现双亲委派模型
    b.代码
        public class MyClassLoader extends ClassLoader {
            public MyClassLoader(ClassLoader parent) {
                super(parent);
            }

            @Override
            protected Class<?> findClass(String name) throws ClassNotFoundException {
                // 获取类的字节码
                byte[] classData = getClassData(name);
                if (classData == null) {
                    throw new ClassNotFoundException();
                }
                // 将字节码转换为Class对象
                return defineClass(name, classData, 0, classData.length);
            }

            private byte[] getClassData(String name) {
                // 从文件系统或其他来源获取类的字节码
                // ...
                return null;
            }

            public static void main(String[] args) throws Exception {
                ClassLoader parentClassLoader = MyClassLoader.class.getClassLoader();
                MyClassLoader myClassLoader = new MyClassLoader(parentClassLoader);
                Class<?> myClass = myClassLoader.loadClass("com.example.MyClass");
                System.out.println(myClass.getName());
            }
        }

5 排查、内存溢出、对象死亡、垃圾回收GC

5.1 [0]100%

00.汇总
    a.jstack
        a.找到 CPU 占用高的 Java 进程的 PID(进程 ID)
            top -H -p <PID>
        b.找到占用 CPU 高的线程 ID
            在 top 的输出中,按 P 键可以按 CPU 使用率排序
            找到使用 CPU 最多的线程
            记下这些线程的 ID(nid),这些 ID 是十进制的
        c.将线程 ID 转换为十六进制
            jstack 输出的线程 ID 是十六进制的
            因此需要将找到的高 CPU 使用率的线程 ID 转换为十六进制。可以使用 printf 命令:
            printf "%x\n" <thread_id>
        d.使用 jstack 生成线程快照
            jstack -l <PID> > thread_dump.txt
        e.分析线程快照
            在生成的 thread_dump.txt 文件中,查找转换后的十六进制线程 ID
            可以使用 grep 命令:grep "0x<hex_thread_id>" thread_dump.txt -A 20
            这将显示包含该线程 ID 的线程栈信息。分析这些线程栈,找到可能导致 CPU 高占用的代码
    b.Arthas
        a.排查1
            # 实时监控GC情况
            dashboard -n 5 -i 2000
            # 追踪可疑方法调用频次
            trace com.example.CacheService addCacheEntry -n 10
            # 动态修改日志级别(无需重启)
            logger --name ROOT --level debug
        b.排查2
            # 系统概况
            dashboard
            # 使用 thread 命令定位高 CPU 线程
            thread -n 3
            # 查看具体线程的堆栈信息
            thread 8
    c.其他
        a.MAT分析三板斧
            Dominator Tree:揪出内存吞噬者
            Path to GC Roots:顺藤摸瓜找凶手
            OQL黑科技:SELECT * FROM java.util.HashMap WHERE size > 10000
                       SELECT toString(msg) FROM java.lang.String WHERE msg.value LIKE "%OOM%"
        b.线上救火命令包
            # 快速查看堆内存分布
            jhsdb jmap --heap --pid <PID>
            # 统计对象数量排行榜
            jmap -histo:live <PID> | head -n 20
            # 强制触发Full GC(慎用!)
            jcmd <PID> GC.run

00.cpu占用很高的3大类型,9大场景
    a.业务类问题
       a.死循环
            例子:while (true) { if (file.isUpdated()) { break; } }
            解决方案:添加退出条件,例如 Thread.sleep(1000)
       b.死锁
            例子:两个线程分别持有锁A和锁B,并且互相等待对方释放锁
            解决方案:避免死锁情况,使用ReentrantLock
       c.不必要的线程切换
            例子:synchronized应该尽量减少不必要的使用
            解决方案:将锁范围缩小
    b.并发类问题
       a.大量计算密集型的任务
            例子:多个计算密集型任务同时在执行
            解决方案:使用线程池来管理线程的生命周期
       b.大量线程竞争
            例子:一个请求导致了多个线程同时竞争
            解决方案:减少竞争资源,优化线程切换
    c.内存类问题
       a.内存不够
            例子:系统中无法分配足够的内存
            解决方案:检查是否存在一次性加载大文件
       b.垃圾回收
            例子:频繁的GC导致性能下降
            解决方案:优化内存的使用,减少对象的创建
       c.内存泄漏
            例子:不再使用的对象仍然被引用
            解决方案:确保及时释放不再使用的对象

01.业务类问题
    a.死循环
        死循环是指程序在特定条件下进入了一个无限循环,无法跳出,导致 CPU 资源被完全占用
        例如:我们有一段代码用来检查文件的更新状态,但由于逻辑错误,条件永远无法满足,结果程序进入了死循环
        -----------------------------------------------------------------------------------------------------
        while (true) {
            if (file.isUpdated()) {
                break;
            }
        }
    b.死锁
        死锁是指两个或多个线程互相等待对方释放资源,导致所有线程都无法继续执行,CPU 资源被消耗殆尽
        发生死锁后,就会存在忙等待或自旋锁等编程问题,从而导致繁忙等待问题,从而导致 CPU 100%
    c.不必要的代码块
        一些冗余、不必要的代码块在运行时占用了大量的 CPU 资源
        例如,不需要的地方使用 synchronized 块
        -----------------------------------------------------------------------------------------------------
        public synchronized void unnecessarySync() {
            // 执行一些不需要同步的操作
        }
        在不需要的地方使用 synchronized 块,会导致线程竞争和上下文切换

02.并发类问题
    a.大量计算密集型的任务
        大量计算密集型任务在同一时间运行,会导致 CPU 资源被完全占用
        例如:在数据分析或科学计算中,多个计算密集型任务同时运行
    b.大量并发线程
        系统中存在大量并发线程,线程切换频繁,导致 CPU 资源被大量消耗在上下文切换上
        例如:Web 服务器同时处理大量请求,每个请求都创建一个新线程
        解决方案:使用线程池来限制并发线程数量
    c.大量的上下文切换
        当系统中存在大量线程时,CPU 在不同线程间频繁切换,导致性能下降
        例如:一个程序中开启了数百个线程,每个线程都在不断进行 I/O 操作
        -----------------------------------------------------------------------------------------------------
        for (int i = 0; i < 1000; i++) {
            new Thread(new IOHandler()).start();
        }
        线程是很宝贵的资源,开启线程一定要合理的控制线程数量

03.内存类问题
    a.内存不足
        当系统内存不足时,就会将磁盘存储作为虚拟内存使用,而虚拟内存的运行速度要慢得多
        例如:直接一次性加载一个非常大的文件到内存中,导致内存不足
        -----------------------------------------------------------------------------------------------------
        byte[] largeData = Files.readAllBytes(Paths.get("largeFile.txt"));
        -----------------------------------------------------------------------------------------------------
        这种过度的分页和交换会导致 CPU 占用率居高不下,因为处理器需要花费更多时间来管理内存访问,而不是高效地执行进程
        解决方案:优化内存使用,采用流式处理避免一次性加载大文件
        -----------------------------------------------------------------------------------------------------
        try (BufferedReader reader = Files.newBufferedReader(Paths.get("largeFile.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                // 处理每一行数据
            }
        }
    b.频繁GC
        频繁的垃圾回收(GC)操作会占用大量 CPU 资源,导致性能下降
        例如:程序中频繁创建和销毁对象,导致 GC 频繁触发
        -----------------------------------------------------------------------------------------------------
        for (int i = 0; i < 1000000; i++) {
            String temp = new String("temp" + i);
        }
        -----------------------------------------------------------------------------------------------------
        解决方案:优化对象创建和销毁,减少临时对象的生成
    c.内存泄漏
        内存泄漏导致可用内存逐渐减少,最终触发频繁的 GC 操作,占用大量 CPU 资源
        例如:某个数据结构中不断添加对象,却从未删除,导致内存泄漏
        -----------------------------------------------------------------------------------------------------
        List<Object> list = new ArrayList<>();
        while (true) {
            list.add(new Object());
        }
        -----------------------------------------------------------------------------------------------------
        解决方案:定期清理不再使用的对象,使用合适的数据结构

5.2 [1]jstack

01.介绍
    用系统工具和 JDK 自带的 jstack 工具
    如果这个线程已经忙的一点转圜的余地都没有了,jstack命令可能会执行失败。

02.jstack工具
    a.第一步,使用 top 找到占用 CPU 最高的 Java 进程
        使用 top命令发现占用 CPU 99.7% 的线程是 Java 进程,进程 PID 为 13731。
    b.第二步,找到占用 CPU 最高的线程
        上一步用 top命令找到了那个 Java 进程。
        那一个进程中有那么多线程,不可能所有线程都一直占着 CPU 不放,这一步要做的就是揪出这个罪魁祸首,当然有可能不止一个。
        -----------------------------------------------------------------------------------------------------
        接下来,还是用 top命令,只不过加一个参数-Hp ,就是下面这样
        top -Hp pid
        H参数表示要显示线程级别的信息,p则表示指定的pid,也就是进程id。代入前面得到的那个Java进程,完整的命令是这样的
        -----------------------------------------------------------------------------------------------------
        top -Hp 13731
        执行之后,这个Java进程中占用线程占用 CPU 的情况就列出来了。
        可以看到占用 CPU 最高的那个线程 PID 为 13756。
    c.第三步,保存线程堆栈信息
        这就要用到 JDK 默认提供的一个工具了,叫做 jstack。
        当你安装了 JDK 之后,在 bin目录下会有一大堆内置的工具,java也是其中之一,还有另外我们可能比较熟悉的 javac
        -----------------------------------------------------------------------------------------------------
        stack 用于生成 Java 进程的线程快照(thread dump)。线程快照是一个关于 Java 进程中所有线程当前状态的快照,包括每个线程的堆栈信息。通过分析线程快照,可以了解 Java 进程中各个线程的运行状态、锁信息等。
        我们用jstack的目的就是将那个占用 CPU 最高的线程的堆栈信息搞下来,然后进一步分析。使用命令 jstack pid > out.log将某个进程的堆栈信息输出到 out.log文件中。
        当前 Java 程序的所有线程信息都可以通过 jstack命令查看,我们用jstack命令将第一步找到的 Java 进程的线程栈保存下来。
        jstack 13731 > thread_stack.log
    d.第四步,在线程栈中查找最贵祸首的线程
        第二步已经找到了这个罪魁祸首的线程 PID 是 13756。
        然后我们将 13756转换为 16 进制的,可以用在线进制转换的网站直接转换,比如 https://tool.oschina.net/hexconvert 这个,转换结果为 0x35bc。
        最后,我们在线程栈中,也就是上一步保存的那个 thread_stack.log 文件,在里面查找这个16进制的线程 id (0x35bc)。
        然后,我么能看到了我们需要的线程名称、线程状态,哪个方法的哪一行代码消耗了最多的 CPU 都很清楚了。

5.3 [1]arthas

01.arthas工具
    a.下载 jar 包
        curl -O https://arthas.aliyun.com/arthas-boot.jar
    b.启动 Arthas 服务
        java -jar arthas-boot.jar
    c.排查
        启动之后,会列出当前这台服务器上的所有 Java 进程,然后你选择你要排查的那个服务即可。
        然后出现 arthas@之后表示已经启动,并成功 attach 到目标进程上 。
        然后可以输入命令 dashboard看一下实时面板,默认 5 秒刷新一次,在这个面板上能够看到线程、内存堆栈、GC和Runtime的基本信息。如果你用过 VisualVM 的话,就和那个基本一样。
        好了,开始用 Arthas 找到导致 CPU 负载过高的问题吧。
    d.找到占用CPU最高的进程
        其实还是用 top命令找到占用 CPU 最高的进程,也就是 Arthas 启动时选择 attach 的那个 Java 进程。
        然后 java -jar arthas-boot.jar启动Arthas,并attach 。
    e.找到占用 CPU 最高的线程
        执行 thread命令,这个命令会显示所有线程的信息,并且把CPU使用率高的线程排在前面。
        这样,一眼就看出来了,第一个线程的 CPU 使用率高达 99% 了,就是它。
    f.查看堆栈信息
        使用 thread ID  获取堆栈信息,其实就是 jstack pid相同的作用。
        通过前一步看到这个线程的 ID 是18,然后执行 thread 18
        然后直接就看出来了出现问题的位置,TestController.java文件的 high方法的第23行
        然后进代码看com.moonkite.wallpapermanage.controller.TestController.high(TestController.java:23)
        这个方法是我故意写的死循环,真实情况当然没有这么明显,还需要针对具体代码认真分析。

03.arthas安装
    a.介绍
        阿里巴巴开源的 Java 诊断工具,适合在线上环境下诊断问题。
        支持查看线程、JVM 配置、类加载、方法耗时、内存情况等。
        使用方法简单,适合在生产环境进行调试。
        async-profiler:Java 应用性能分析工具,开源、火焰图、跨平台
    b.下载
        https://github.com/alibaba/arthas/releases
        arthasidea
    c.启动
        java -jar arthas-boot.jar
        java -Dfile.encoding=UTF-8 -jar arthas-boot.jar
        java -Dfile.encoding=UTF-8 -Dserver.port=8088 -jar arthas-boot.jar
    d.示例
        Microsoft Windows [版本 10.0.19044.1741]
        (c) Microsoft Corporation。保留所有权利。

        C:\software\arthas>java -Dfile.encoding=UTF-8 -jar arthas-boot.jar
        [INFO] JAVA_HOME: C:\software\jdk-1.8.0_281\jre
        [INFO] arthas-boot version: 4.0.2
        [INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
        * [1]: 8628 -- process information unavailable
          [2]: 22852 org.jetbrains.jps.cmdline.Launcher
          [3]: 26732 com.intellij.idea.Main
        2
        [INFO] arthas home: C:\software\arthas
        [INFO] Try to attach process 22852
        [WARN] Current VM java version: 1.8 do not match target VM java version: 11, attach may fail.
        [WARN] Target VM JAVA_HOME is C:\software\jdk-11.0.11, arthas-boot JAVA_HOME is C:\software\jdk-1.8.0_281\jre, try to set the same JAVA_HOME.
        java.io.IOException: Non-numeric value found - int expected
                at sun.tools.attach.HotSpotVirtualMachine.readInt(HotSpotVirtualMachine.java:299)
                at sun.tools.attach.HotSpotVirtualMachine.loadAgentLibrary(HotSpotVirtualMachine.java:63)
                at sun.tools.attach.HotSpotVirtualMachine.loadAgentLibrary(HotSpotVirtualMachine.java:79)
                at sun.tools.attach.HotSpotVirtualMachine.loadAgent(HotSpotVirtualMachine.java:103)
                at com.taobao.arthas.core.Arthas.attachAgent(Arthas.java:122)
                at com.taobao.arthas.core.Arthas.<init>(Arthas.java:27)
                at com.taobao.arthas.core.Arthas.main(Arthas.java:161)
        [WARN] It seems to use the lower version of JDK to attach the higher version of JDK.
        [WARN] This error message can be ignored, the attach may have been successful, and it will still try to connect.
        [INFO] Attach process 22852 success.
        [INFO] arthas-client connect 127.0.0.1 3658
          ,---.  ,------. ,--------.,--.  ,--.  ,---.   ,---.
         /  O  \ |  .--. ''--.  .--'|  '--'  | /  O  \ '   .-'
        |  .-.  ||  '--'.'   |  |   |  .--.  ||  .-.  |`.  `-.
        |  | |  ||  |\  \    |  |   |  |  |  ||  | |  |.-'    |
        `--' `--'`--' '--'   `--'   `--'  `--'`--' `--'`-----'

        wiki       https://arthas.aliyun.com/doc
        tutorials  https://arthas.aliyun.com/doc/arthas-tutorials.html
        version    4.0.2
        main_class
        pid        22852
        time       2024-11-12 12:06:17.566

        [arthas@22852]$ help
         NAME         DESCRIPTION
         help         Display Arthas Help
         auth         Authenticates the current session
         keymap       Display all the available keymap for the specified connection.
         sc           Search all the classes loaded by JVM
         sm           Search the method of classes loaded by JVM
         classloader  Show classloader info
         jad          Decompile class
         getstatic    Show the static field of a class
         monitor      Monitor method execution statistics, e.g. total/success/failure count, average rt, fail rate, etc.
         stack        Display the stack trace for the specified class and method
         thread       Display thread info, thread stack
         trace        Trace the execution time of specified method invocation.
         watch        Display the input/output parameter, return object, and thrown exception of specified method invocation
         tt           Time Tunnel
         jvm          Display the target JVM information
         memory       Display jvm memory info.
         perfcounter  Display the perf counter information.
         ognl         Execute ognl expression.
         mc           Memory compiler, compiles java files into bytecode and class files in memory.
         redefine     Redefine classes. @see Instrumentation#redefineClasses(ClassDefinition...)
         retransform  Retransform classes. @see Instrumentation#retransformClasses(Class...)
         dashboard    Overview of target jvm's thread, memory, gc, vm, tomcat info.
         dump         Dump class byte array from JVM
         heapdump     Heap dump
         options      View and change various Arthas options
         cls          Clear the screen
         reset        Reset all the enhanced classes
         version      Display Arthas version
         session      Display current session information
         sysprop      Display and change the system properties.
         sysenv       Display the system env.
         vmoption     Display, and update the vm diagnostic options.
         logger       Print logger info, and update the logger level
         history      Display command history
         cat          Concatenate and print files
         base64       Encode and decode using Base64 representation
         echo         write arguments to the standard output
         pwd          Return working directory name
         mbean        Display the mbean information
         grep         grep command for pipes.
         tee          tee command for pipes.
         profiler     Async Profiler. https://github.com/jvm-profiling-tools/async-profiler
         vmtool       jvm tool
         stop         Stop/Shutdown Arthas server and exit the console.
         jfr          Java Flight Recorder Command

04.arthas命令
    a.jvm相关
        dashboard - 当前系统的实时数据面板
        getstatic - 查看类的静态属性
        heapdump - dump java heap, 类似 jmap 命令的 heap dump 功能
        jvm - 查看当前 JVM 的信息
        logger - 查看和修改 logger
        mbean - 查看 Mbean 的信息
        memory - 查看 JVM 的内存信息
        ognl - 执行 ognl 表达式
        perfcounter - 查看当前 JVM 的 Perf Counter 信息
        sysenv - 查看 JVM 的环境变量
        sysprop - 查看和修改 JVM 的系统属性
        thread - 查看当前 JVM 的线程堆栈信息
        vmoption - 查看和修改 JVM 里诊断相关的 option
        vmtool - 从 jvm 里查询对象,执行 forceGc
    b.class/classloader 相关
        classloader - 查看 classloader 的继承树,urls,类加载信息,使用 classloader 去 getResource
        dump - dump 已加载类的 byte code 到特定目录
        jad - 反编译指定已加载类的源码
        mc - 内存编译器,内存编译.java文件为.class文件
        redefine - 加载外部的.class文件,redefine 到 JVM 里
        retransform - 加载外部的.class文件,retransform 到 JVM 里
        sc - 查看 JVM 已加载的类信息
        sm - 查看已加载类的方法信息
    c.monitor/watch/trace 相关
        monitor - 方法执行监控
        stack - 输出当前方法被调用的调用路径
        trace - 方法内部调用路径,并输出方法路径上的每个节点上耗时
        tt - 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测
        watch - 方法执行数据观测
    d.profiler/火焰图
        profiler - 使用async-profiler对应用采样,生成火焰图
        jfr - 动态开启关闭 JFR 记录
    e.鉴权
        auth - 鉴权
    f.options
        options - 查看或设置 Arthas 全局开关
    g.管道
        Arthas 支持使用管道对上述命令的结果进行进一步的处理,如sm java.lang.String * | grep 'index'
        grep - 搜索满足条件的结果
        plaintext - 将命令的结果去除 ANSI 颜色
        wc - 按行统计输出结果
    h.后台异步任务
        当线上出现偶发的问题,比如需要 watch 某个条件,而这个条件一天可能才会出现一次时,异步后台任务就派上用场了,详情请参考这里
        使用 > 将结果重写向到日志文件,使用 & 指定命令是后台运行,session 断开不影响任务执行(生命周期默认为 1 天)
        jobs - 列出所有 job
        kill - 强制终止任务
        fg - 将暂停的任务拉到前台执行
        bg - 将暂停的任务放到后台执行
    i.基础命令
        base64 - base64 编码转换,和 linux 里的 base64 命令类似
        cat - 打印文件内容,和 linux 里的 cat 命令类似
        cls - 清空当前屏幕区域
        echo - 打印参数,和 linux 里的 echo 命令类似
        grep - 匹配查找,和 linux 里的 grep 命令类似
        help - 查看命令帮助信息
        history - 打印命令历史
        keymap - Arthas 快捷键列表及自定义快捷键
        pwd - 返回当前的工作目录,和 linux 命令类似
        quit - 退出当前 Arthas 客户端,其他 Arthas 客户端不受影响
        reset - 重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类
        session - 查看当前会话的信息
        stop - 关闭 Arthas 服务端,所有 Arthas 客户端全部退出
        tee - 复制标准输入到标准输出和指定的文件,和 linux 里的 tee 命令类似
        version - 输出当前目标 Java 进程所加载的 Arthas 版本号

05.arthas实战
    a.tt命令:获取Spring Bean
        a.场景
            执行Spring中某个Bean的特定方法
            对某段代码做测试,但当前方法不能直接被接口访问
            Dubbo Service中的代码,不想使用telent invoke调用
            线上观察Bean内存中的数据
        b.释义
            理解为watch命令的变体,watch命令是实时监听,而tt命令可以将观测到的数据记录下来
        c.使用示例:记录任意地址的调用
            tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod -n 1
            -------------------------------------------------------------------------------------------------
            -n指定记录次数,否则可能导致OOM
            直接访问localhost:应用端口即可触发
            监听的是Spring MVC中的处理适配器,所有接口调用都会被触发
            成功后,会退出tt命令的监听,并输出一个记录ID,第一次记录为1000
        d.使用示例:获取Spring容器并调用方法
            tt -i 1000 -w 'target.getApplicationContext().getBean("testBean").test()'
            -------------------------------------------------------------------------------------------------
            使用-w以及OGNL表达式获取记录的目标对象并调用目标对象的getApplicationContext方法
            获取到Spring容器后,调用getBean方法,填入BeanName,后面输入要调用的方法
    b.命令别名
        a.问题
            命令太长记不住
            每次都要打开文档查看命令
        b.解决方案
            给命令取一个别名,输入别名直接替换为命令
            使用AutoHotKey(V2)创建别名
        c.别名示例:att(记录调用)
            ::att::tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod -n 1
        d.别名示例:agb(参数1:BeanName;参数2:tt索引,默认值为1000)
            ::agb::
            {
            args := InputBox("请输入tt -i 参数", "tt -i [number:1000] -w 'target.getApplicationContext().getBean(beanName)'", "w520 h100").value
            tti := "1000"
            SplitArgs := StrSplit(args, " ")
            if (SplitArgs.Length >= 2)
                tti := SplitArgs[2]
            newArgs := 'tt -i ' tti ' -w `'target.getApplicationContext().getBean("' SplitArgs[1] '\`")\`''
            Send newArgs
            }
        e.使用方法
            创建aa.ank文件,将上述脚本内容复制进去,保存退出
            给aa.ank文件创建快捷方式,移动到C:\Users\你的用户名\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
    c.OGNL表达式:调用静态方法
        a.被调用者为静态
            ognl '@java.util.Objects@hashCode(1)'
            ognl '@java.lang.Thread@currentThread()'
            ognl '@java.util.ArrayList@DEFAULT_CAPACITY'
        b.被调用者为实例
            ognl '@[email protected]("hello world")'
            ognl '@java.lang.Runtime@getRuntime().gc()'
        c.规则
            访问类:@全类名
            例:@java.lang.System
            访问类对象:@全类名@class
            例:@java.lang.System@class
            静态字段(方法):@静态字段(方法)
            例:ognl '@java.lang.Thread@currentThread()
            实例字段(方法):.实例字段(方法)
            例:ognl '@java.lang.Thread@currentThread().name
    d.对象参数传递
        a.痛点
            string类型和数字类型可以直接传入作为方法参数。
            但是参数为对象类型不可以直接传参,并且也不能支持JSON写法
        b.场景
            使用tt命令调用方法参数为对象时
            使用OGNL调用静态方法时
        c.解决方案
            使用JSON库将JSON字符串解析为实体对象
            @com.alibaba.fastjson.JSON@parseObject("{\\"name\\":\\"a\\",\\"age\\":1}", @com.maple.spring.Student@class)
            -------------------------------------------------------------------------------------------------
            示例
            tt -i 1000 -w 'target.getApplicationContext().getBean("springBootApplication").test03(@com.alibaba.fastjson.JSON@parseObject("{\\"name\\":\\"name_babb0dc2c3cb\\",\\"age\\":1}", @com.maple.spring.Student@class))'
            -------------------------------------------------------------------------------------------------
            注意:是否有getter和setter方法
    e.其他命令
        a.vmtool:查看内存数据
            通过类型找到内存对象(谨慎使用)
            获取对象:vmtool --action getInstances --className com.maple.spring.SpringBootApplication --limit 1
            执行方法:vmtool --action getInstances --className com.maple.spring.SpringBootApplication --limit 1 --express 'instances[0].test03(null)'
        b.thread:找出忙碌线程
            输出最忙的前3个线程并打印堆栈:thread -n 3

5.4 [1]jmeter

00.介绍
    JMeter 是一个基于 Java 的应用程序,主要用于性能测试和负载测试
    它支持多种协议,包括 HTTP、HTTPS、SOAP、REST、FTP、JDBC 等

01.原理
    a.模拟用户请求
        JMeter 通过模拟多个用户对服务器发起请求,来测试服务器的负载能力
        它可以生成大量的并发请求,并记录响应时间、吞吐量、错误率等性能指标
    b.测试计划组成
        JMeter 的测试计划由线程组、取样器、监听器等组件组成

02.常用API
    a.线程组(Thread Group)
        定义测试中虚拟用户的数量和行为。
    b.取样器(Sampler)
        用于发送请求到服务器,如 HTTP 请求取样器。
    c.监听器(Listener)
        用于收集和显示测试结果,如查看结果树、聚合报告。
    d.定时器(Timer)
        用于设置请求之间的等待时间。
    e.断言(Assertion)
        用于验证响应数据是否符合预期。
    f.配置元件(Config Element)
        用于设置请求的默认值,如 HTTP 请求默认值

03.使用步骤
    a.启动
        C:\software\apache-jmeter-5.6.3\bin\jmeter.bat
    b.创建测试计划
        启动 JMeter,创建一个新的测试计划
    c.添加线程组
        在测试计划中添加线程组,设置线程数、循环次数等
    d.添加取样器
        在线程组中添加取样器,如 HTTP 请求
    e.配置请求
        设置请求的 URL、方法、参数等
    f.添加监听器
        在线程组中添加监听器,查看测试结果
    g.运行测试
        启动测试,观察结果并分析性能指标

04.场景及代码示例
    a.场景1:HTTP 请求性能测试
        a.说明
            a.创建线程组
                设置线程数为 10,循环次数为 5
            b.添加 HTTP 请求
                设置请求 URL 为 http://example.com/api/test
            c.添加监听器
                添加聚合报告查看响应时间和吞吐量
        b.配置
            - Test Plan
              - Thread Group
                - Number of Threads (users): 10
                - Loop Count: 5
                - HTTP Request
                  - Server Name or IP: example.com
                  - Path: /api/test
                - Aggregate Report
    b.场景2:登录接口测试
        a.说明
            a.创建线程组
                设置线程数为 20,循环次数为 10
            b.添加 HTTP 请求
                设置请求 URL 为 http://example.com/login,方法为 POST,添加参数 username 和 password
            c.添加断言
                验证响应中是否包含 "Login Successful"
        b.配置
            - Test Plan
              - Thread Group
                - Number of Threads (users): 20
                - Loop Count: 10
                - HTTP Request
                  - Server Name or IP: example.com
                  - Path: /login
                  - Method: POST
                  - Parameters:
                    - username: testuser
                    - password: testpass
                - Response Assertion
                  - Contains: Login Successful
                - View Results Tree
    c.场景3:数据库查询性能测试
        a.说明
            a.创建线程组
                设置线程数为 5,循环次数为 3
            b.添加 JDBC 请求
                配置数据库连接,设置 SQL 查询语句
            c.添加监听器
                查看结果树查看查询结果
        b.配置
            - Test Plan
              - Thread Group
                - Number of Threads (users): 5
                - Loop Count: 3
                - JDBC Request
                  - Variable Name: mydb
                  - Query Type: Select Statement
                  - SQL Query: SELECT * FROM users WHERE active = 1
                - View Results Tree

5.5 [2]频繁FullGc

00.推荐
    a.使用Aviator缓存方法
        将原本的compile("1+1")方法替换为compile("1+1",true)缓存方法
        避免相同的计算表达式重复计算,导致大量用途相同的AviatorClassLoader和“Script_”类重复产生
    b.修改JVM参数
        提高metaSpace最大内存 将原本MaxMetaspaceSize=256m修改为MaxMetaspaceSize=512m
        因为在未发生OOM时metaSpace空间占比已经达到了65%距离触发Full GC的70%的波动空间较小
        那么为什么不修改MaxMetaspaceFreeRatio这个参数呢? 公司中间件不支持修改这个参数
    c.上线前压测
        这个事故的产生,同时也暴露出了我们组对部分高QPS的接口并没有进行充分了压测
        在后续开发过程中,需要对高QPS接口进行充分的压测,让这类问题扼杀在摇篮之中

01.介绍
    a.全堆回收
        Full GC会对整个堆进行回收,包括年轻代和老年代
        这意味着它会暂停所有应用线程(Stop-The-World),直到回收完成
    b.触发条件
        内存泄漏(代码有问题,对象引用没及时释放,导致对象不能及时回收)
        死循环
        大对象
        尤其是大对象,80%以上的情况就是他
        那么大对象从哪里来的呢
        数据库(包括MySQL和MongoDB等NoSQL数据库),结果集太大
        第三方接口传输的大对象
        消息队列,消息太大
    c.性能影响
        由于Full GC会暂停所有应用线程,因此它通常会导致应用程序的停顿时间较长,影响性能
    d.优化建议
        调整堆内存大小,确保老年代有足够的空间
        优化代码,减少对象的创建和内存占用
        使用合适的垃圾收集器,根据应用程序的特点选择合适的GC策略
        监控和分析GC日志,识别和解决Full GC频繁的问题

02.原理
    a.垃圾回收机制
        Java虚拟机通过垃圾回收机制自动管理内存
        Full GC是其中一种回收方式,涉及整个堆内存的回收,包括年轻代和老年代
    b.Stop-The-World
        Full GC会暂停所有应用线程,进行内存清理和回收
        这种暂停会导致应用程序的响应时间增加

03.常用API
    a.GC日志
        通过JVM参数(如-XX:+PrintGCDetails)启用GC日志,监控垃圾回收活动
    b.JVM监控工具
        使用JVisualVM、JConsole等工具监控内存使用情况和GC活动

04.使用步骤
    a.启用GC日志
        在启动JVM时添加参数:-XX:+PrintGCDetails -XX:+PrintGCDateStamps
        分析GC日志,识别频繁Full GC的原因
    b.监控内存使用
        使用JVisualVM或JConsole监控堆内存使用情况
        识别内存泄漏或大对象分配的来源
    c.优化代码
        检查代码中未释放的资源和不必要的对象引用
        优化数据处理逻辑,避免大对象的频繁创建
    d.调整JVM参数
        根据应用程序需求调整堆内存大小(-Xms和-Xmx)
        选择合适的垃圾收集器(如G1、CMS)和配置参数
    e.定期分析和调优
        定期分析GC日志和内存使用情况
        根据分析结果进行代码和配置的调优

05.每个场景对应的代码示例
    a.启用GC日志示例
        java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -jar YourApplication.jar
    b.监控内存使用示例
        使用JVisualVM连接到运行中的Java应用程序,查看内存使用情况和GC活动。
    c.优化代码示例
        // 优化对象创建,避免不必要的临时对象
        public void processData(List<Data> dataList) {
            for (Data data : dataList) {
                // 处理数据
            }
        }
    d.调整JVM参数示例
        java -Xms512m -Xmx2048m -XX:+UseG1GC -jar YourApplication.jar

5.6 [2]逃逸分析

01.概况
    a.Java对象只能分配到堆上
        在 Java 中,堆(Heap)是 JVM 运行时数据区域之一,用于存放对象实例
        当我们使用 new 关键字创建对象时,JVM 会在堆上为对象分配内存
    b.逃逸分析
        逃逸分析 是 JVM 在 JIT 编译时 对代码进行的一种优化分析,用于判断对象的作用范围
        如果对象没有逃逸出方法或线程,则可以进行 栈上分配、标量替换、同步消除 等优化

02.逃逸分析的优化手段
    a.栈上分配(Stack Allocation)
        a.说明
            如果对象没有逃逸出方法,JVM 可以直接在栈上分配内存,这样 方法执行完后
            对象随栈帧销毁,无需 GC 回收,提升性能
        b.示例
            public void test() {
                User user = new User();  // user 没有逃逸,JVM 可能优化为栈上分配
                user.sayHello();
            }
        c.优化前
            User 对象分配在堆中,由 GC 负责回收
        d.优化后
            User 对象分配在 栈上,随着方法执行完毕自动销毁,无 GC 压力
    b.标量替换(Scalar Replacement)
        a.说明
            如果对象的字段可以 分解为标量(基本数据类型),JVM 可能不会创建完整对象
            而是 拆解存储,减少对象分配和 GC 负担
        b.示例
            public void test() {
                Point p = new Point(3, 4); // 逃逸分析:Point 仅在方法内使用
                int distance = p.x * p.x + p.y * p.y; // JVM 可能直接优化为标量
            }
        c.优化前
            Point 是一个对象,存储在 堆上,需要 GC 管理
        d.优化后(标量替换)
            Point 不会真正创建,JVM 直接用两个 int 变量代替,减少堆分配
    c.同步消除(Synchronization Elimination)
        a.说明
            如果 JVM 发现某个同步代码块中的对象 不会被其他方法访问,可以消除 synchronized 关键字,减少锁的开销
        b.示例
            public void test() {
                Object lock = new Object();  // lock 没有逃逸,JVM 可能优化
                synchronized (lock) {
                    System.out.println("Thread safe operation");
                }
            }
        c.优化前
            synchronized (lock) 需要加锁
        d.优化后(同步消除)
            JVM 发现 lock 仅在当前方法内使用,不会逃逸,直接 去掉 synchronized,提升性能

03.JVM 如何启用逃逸分析?
    a.说明
        JVM 默认会执行逃逸分析,但可以通过 JVM 参数控制
    b.参数
        # 启用逃逸分析(默认开启)
        -XX:+DoEscapeAnalysis

        # 关闭逃逸分析
        -XX:-DoEscapeAnalysis

        # 启用标量替换
        -XX:+EliminateAllocations

        # 启用同步消除
        -XX:+EliminateLocks

04.逃逸分析的适用场景
    a.适用于
        短生命周期的对象
        仅在方法内部使用的对象
        频繁创建的小对象(如局部变量)
        线程安全的对象(避免不必要的同步)
    b.不适用于
        需要跨方法、跨线程共享的对象
        需要长期存活的对象

05.逃逸分析的总结
    逃逸分析 主要用于 优化对象分配和减少 GC 压力
    如果对象没有逃逸,可以进行栈上分配、标量替换、同步消除
    逃逸分析适用于短生命周期的对象,能显著优化性能

5.7 [2]内存溢出:定义

01.OOM是什么?
    JVM OOM(OutOfMemoryError)是指Java虚拟机的内存耗尽,
    垃圾回收器(GC)无法回收足够的内存来分配给新对象,从而抛出错误。
    这类似于汽车油箱耗尽但仍想继续行驶的情况。

02.OOM为啥会发生?OOM的原因主要是【内存不够用】
    a.内存分配不足
        JVM初始化时,堆内存、永久代(或元空间)等区域分配得太小。
    b.大对象申请
        一次性申请的内存太大,超出JVM的承受范围。
    c.内存泄漏
        程序中某些地方申请了内存但未释放。
    d.代码问题
        对象频繁创建但未及时释放。

03.OOM都有哪些变种
    a.Java堆内存溢出
        错误信息:java.lang.OutOfMemoryError: Java heap space
    b.永久代/元空间溢出
        错误信息(JDK 7 及以下):java.lang.OutOfMemoryError: PermGen space
        错误信息(JDK 8 及以上):java.lang.OutOfMemoryError: Metaspace
    c.栈内存溢出
        错误信息:java.lang.StackOverflowError
    d.直接内存溢出
        错误信息:java.lang.OutOfMemoryError: Direct buffer memory

04.排查OOM的杀手锏
    a.启用JVM诊断选项
        使用参数:-XX:+HeapDumpOnOutOfMemoryError, -XX:HeapDumpPath, -Xlog:gc*, -XX:+PrintGCDetails
    b.分析错误日志
        查看应用日志和OOM错误堆栈信息
    c.分析堆转储文件
        使用工具如JVisualVM、Eclipse MAT、JProfiler
    d.检查 GC 日志
        分析垃圾回收的频率、暂停时间和内存使用情况
    e.代码审查和优化
        查找内存泄漏问题,优化代码

05.解决OOM的锦囊妙计
    a.增加内存
        堆内存:-Xmx参数
        永久代/元空间:-XX:MaxPermSize或-XX:MaxMetaspaceSize
        直接内存:-XX:MaxDirectMemorySize
    b.优化代码
        释放对象、避免大对象、使用弱引用/软引用。
    c.调优垃圾回收器
        选择合适的GC算法并调整参数。
    d.管理外部资源
        确保文件句柄、数据库连接等资源正确关闭。
    e.持续监控和预警
        使用JMX、Prometheus、Grafana等工具。

06.main方法的执行过程
    a.示例代码
        public class Application {
            public static void main(String[] args) {
                Person p = new Person("大彬");
                p.getName();
            }
        }

        class Person {
            public String name;

            public Person(String name) {
                this.name = name;
            }

            public String getName() {
                return this.name;
            }
        }
    b.执行过程
        1.编译Application.java后得到Application.class
        2.JVM启动,加载Application类信息到方法区
        3.执行main方法
        4.创建Person对象,加载Person类信息到方法区
        5.在堆中分配内存给Person对象,调用构造函数初始化
        6.执行p.getName(),找到对象并调用getName()方法。

5.8 [2]内存溢出:7种

00.防泄漏军规十二条
    a.所有缓存必须设置双保险:过期时间 + 容量上限
    b.IO操作三重防护:
        try (InputStream is = ...) { // 第一重
            useStream(is);
        } catch (IOException e) {    // 第二重
            log.error("IO异常", e);
        } finally {                  // 第三重
            cleanupTempFiles();
        }
    c.线程池四不原则
        不用无界队列
        不设不合理核心数
        不忽略拒绝策略
        不存放大对象
    d.Spring组件三查
        查事件监听器引用链
        查单例对象中的集合类
        查@Async注解的线程池配置
    e.第三方库两验
        验连接池归还机制
        验缓存默认配置
    f.代码审查重点关注
        所有static修饰的集合
        所有close()/release()调用点
        所有内部类持有外部引用的地方

00.自测题:你能看出这段代码哪里会泄漏吗?
    a.代码
        // 危险代码!请找出三个泄漏点
        public class ModelLoader {
            private static List<Model> loadedModels = new ArrayList<>();

            public void load(String path) {
                Model model = new Model(Files.readAllBytes(Paths.get(path)));
                loadedModels.add(model);
                Executors.newSingleThreadScheduledExecutor()
                         .scheduleAtFixedRate(() -> model.refresh(), 1, 1, HOURS);
            }
        }
    b.说明
        static集合无清理机制
        定时任务线程池未关闭
        匿名内部类持有Model强引用

01.Static集合成永动机
    a.翻车代码(真实项目片段)
        // 缓存用户AI对话历史 → 翻车写法!
        public class ChatHistoryCache {
            private static Map<Long, List<String>> cache = new HashMap<>();
            public static void addMessage(Long userId, String msg) {
                cache.computeIfAbsent(userId, k -> new ArrayList<>()).add(msg);
            }
        }
    b.翻车现场
        用户量暴增时,缓存数据只进不出,48小时撑爆内存
        用Arthas抓现行:vmtool --action getInstances -c 4614556e 看到Map尺寸破千万
        MAT分析:HashMap$Node对象占堆内存82%
    c.正确姿势
        // 改用Guava带过期时间的缓存
        private static Cache<Long, List<String>> cache = CacheBuilder.newBuilder()
                .expireAfterAccess(1, TimeUnit.HOURS)
                .maximumSize(10000)
                .build();

02.Lambda忘记关文件流
    a.致命代码(处理AI模型文件)
        // 加载本地模型文件 → 翻车写法!
        public void loadModels(List<File> files) {
            files.forEach(file -> {
                try {
                    InputStream is = new FileInputStream(file); // 漏了关闭!
                    parseModel(is);
                } catch (IOException e) { /*...*/ }
            });
        }
    b.诡异现象
        服务运行三天后突然报 "Too many open files"
        Linux排查:lsof -p 进程ID | grep 'deleted' 发现大量未释放文件句柄
        JVM监控:jcmd PID VM.native_memory显示文件描述符数量突破1万
    c.抢救方案
        // 正确写法:try-with-resources自动关闭
        files.forEach(file -> {
            try (InputStream is = new FileInputStream(file)) { // 自动关流
                parseModel(is);
            } catch (IOException e) { /*...*/ }
        });

03.Spring事件监听成钉子户
    a.坑爹代码(消息通知模块)
        // 监听AI处理完成事件 → 翻车写法!
        @Component
        public class NotifyService {
            @EventListener
            public void handleAiEvent(AICompleteEvent event) {
                // 错误持有外部服务引用
                externalService.registerCallback(this::sendNotification);
            }
        }
    b.内存曲线
        每次事件触发,监听器对象就被外部服务强引用,永远不释放
        MAT分析:NotifyService实例数随时间线性增长
        GC日志:老年代占用率每周增长5%
    c.避坑绝招
        // 使用弱引用解除绑定
        public void handleAiEvent(AICompleteEvent event) {
            WeakReference<NotifyService> weakRef = new WeakReference<>(this);
            externalService.registerCallback(() -> {
                NotifyService service = weakRef.get();
                if (service != null) service.sendNotification();
            });
        }

04.线程池里的僵尸任务
    a.问题代码(异步处理AI请求)
        // 异步线程池配置 → 翻车写法!
        @Bean
        public Executor asyncExecutor() {
            return new ThreadPoolExecutor(10, 10,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>()); // 无界队列!
        }
    b.灾难现场
        请求突增时队列堆积50万任务,每个任务持有一个AI响应对象
        堆dump显示:byte[]占内存90%,全是待处理的响应数据
        监控指标:queue_size指标持续高位不降
    c.正确配置
        // 设置队列上限+拒绝策略
        new ThreadPoolExecutor(10, 50,
            60L, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(1000),
            new ThreadPoolExecutor.CallerRunsPolicy());

05.MyBatis连接池里的幽灵
    a.致命代码(查询用户对话记录)
        public List<ChatRecord> getHistory(Long userId) {
            SqlSession session = sqlSessionFactory.openSession();
            try {
                return session.selectList("queryHistory", userId);
            } finally {
                // 忘记session.close() → 连接池逐渐枯竭
            }
        }
    b.泄露证据
        Druid监控面板显示活跃连接数达到最大值
        日志报错:Cannot get connection from pool, timeout 30000ms
        堆分析:SqlSession实例数异常增长
    c.正确姿势
        // 使用try-with-resources自动关闭
        try (SqlSession session = sqlSessionFactory.openSession()) {
            return session.selectList("queryHistory", userId);
        }

06.第三方库的温柔陷阱
    a.问题代码(缓存用户偏好设置)
        // 使用Ehcache时的错误配置
        CacheConfiguration<Long, UserPreference> config = new CacheConfiguration<>()
            .setName("user_prefs")
            .setMaxEntriesLocalHeap(10000); // 只设置了数量,没设过期时间!
    b.内存症状
        GC日志显示老年代每周增长3%
        Arthas监控:watch com.example.CacheService getCachedUser返回对象存活时间超7天
        压测时触发OOM,堆中发现大量UserPreference对象
    c.正确配置
        config.setTimeToLiveSeconds(3600) // 1小时过期
              .setDiskExpiryThreadIntervalSeconds(60); // 过期检查间隔

07.ThreadLocal用完不打扫
    a.致命代码(用户上下文传递)
        public class UserContextHolder {
            private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
            public static void set(User user) {
                currentUser.set(user);
            }
            // 缺少remove方法!
        }
    b.内存异常
        线程池复用后,ThreadLocal中旧用户数据堆积
        MAT分析:User对象被ThreadLocalMap强引用无法释放
        监控发现:每个线程持有平均50个过期用户对象
    c.修复方案
        // 使用后必须清理!
        public static void remove() {
            currentUser.remove();
        }
        // 在拦截器中强制清理
        @Around("execution(* com.example..*.*(..))")
        public Object clearContext(ProceedingJoinPoint pjp) throws Throwable {
            try {
                return pjp.proceed();
            } finally {
                UserContextHolder.remove(); // 关键!
            }
        }

5.9 [2]内存溢出:6种

00.总结
    a.堆内存相关的OOM
        a.Java heap space
            描述:在堆内存中,由于对象过多导致内存不足。
            常见原因:在一个死循环中不停地往集合中添加对象,导致堆内存耗尽。
            解决方法:优化代码以减少对象创建,修复内存泄漏,或增加堆大小(通过-Xms和-Xmx参数)。
    b.栈内存相关的OOM
        a.unable to create new native thread
            描述:由于系统资源限制,无法创建新的线程。
            常见原因:业务系统创建了太多的线程,超过了系统或JVM的限制。
            解决方法:减少线程的创建,优化线程池的使用,或增加系统资源。
        b.java.lang.StackOverflowError
            描述:栈内存不足以分配新的栈帧。
            常见原因:递归调用过深或方法调用过多。
            解决方法:优化递归算法,确保递归有正确的终止条件,或增加栈大小(通过-Xss参数)。
    c.直接内存相关的OOM
        a.Direct buffer memory
            描述:直接内存空间不足。
            常见原因:使用NIO的直接缓冲区而未正确释放。
            解决方法:确保直接缓冲区被及时释放,或增加直接内存大小(通过-XX:MaxDirectMemorySize参数)。
    d.垃圾回收相关的OOM
        a.GC overhead limit exceeded:
            描述:JVM在垃圾回收时,花费过多时间而回收的内存很少。
            常见原因:对象过多,导致垃圾回收频繁且效率低。
            解决方法:优化内存使用,减少对象创建,或调整GC策略。
    e.元空间相关的OOM
        a.java.lang.OutOfMemoryError: Metaspace:
            描述:元空间内存不足。
            常见原因:加载到内存中的类太多,或者类的体积太大。
            解决方法:减少动态生成的类数量,或增加元空间大小(通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize参数)。
    f.总结
        1.堆内存00M,Java heap space,在一个死循环中不停往里面添加对象
        2.栈内存00M:unable to create new native thread,业务系统创建了太多的线程,可能会导致栈内存OOM。
        3.栈内存00M:java.lang.StackOverflowError,如果递归的深度超过了JVM允许的最大深度,可能会出现栈内存溢出问题
        4.直接内存00M:Direct buffer memory,直接内存空间不足的异常
        5.GC OOM:GC overhead limit exceeded,GC OOM是由于JVM在GC时,对象过多,导致内存溢出,建议调整GC的策略
        6.元空间00M:java.lang.OutOfMemoryError: Metaspace,由于加载到内存中的类太多,或者类的体积太大导致的。

01.堆内存OOM(最常见的OOM)
    a.报错
        出现堆内存OOM问题的异常信息如下:
        java.lang.OutOfMemoryError: Java heap space
    b.示例
        public class HeapOOMTest {

            public static void main(String[] args) {
                List<HeapOOMTest> list = Lists.newArrayList();
                while (true) {
                    list.add(new HeapOOMTest());
                }
            }
        }
        这里创建了一个list集合,在一个死循环中不停往里面添加对象。
        -----------------------------------------------------------------------------------------------------
        出现了java.lang.OutOfMemoryError: Java heap space的堆内存溢出。
        很多时候,excel一次导出大量的数据,获取在程序中一次性查询的数据太多,都可能会出现这种OOM问题。

02.栈内存00M
    a.原因
        业务系统创建了太多的线程,可能会导致栈内存OOM。
    b.报错
        出现堆内存OOM问题的异常信息如下:
        java.lang.OutOfMemoryError: unable to create new native thread
    c.示例
        public class StackOOMTest {
            public static void main(String[] args) {
                while (true) {
                    new Thread().start();
                }
            }
        }
        使用一个死循环不停创建线程,导致系统产生了大量的线程。
        -----------------------------------------------------------------------------------------------------
        如果实际工作中,出现这个问题,一般是由于创建的线程太多,或者设置的单个线程占用内存空间太大导致的。
        建议在日常工作中,多用线程池,少自己创建线程,防止出现这个OOM。

03.栈内存溢出
    a.原因
        我们在业务代码中可能会经常写一些递归调用,如果递归的深度超过了JVM允许的最大深度,可能会出现栈内存溢出问题。
    b.报错
        出现栈内存溢出问题的异常信息如下:
        java.lang.StackOverflowError
    c.示例
        public class StackFlowTest {
            public static void main(String[] args) {
                doSamething();
            }

            private static void doSamething() {
                doSamething();
            }
        }
        -----------------------------------------------------------------------------------------------------
        出现了java.lang.StackOverflowError栈溢出的错误。
        我们在写递归代码时,一定要考虑递归深度。即使是使用parentId一层层往上找的逻辑,也最好加一个参数控制递归深度。
        防止因为数据问题导致无限递归的情况,比如:id和parentId的值相等。

04.直接内存00M
    a.介绍
        直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
        它来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存,是属于堆外内存,可以直接向系统申请的内存空间。
    b.报错
        出现直接内存OOM问题时异常信息如下:
        java.lang.OutOfMemoryError: Direct buffer memory
    c.示例
        public class DirectOOMTest {
            private static final int BUFFER = 1024 * 1024 * 20;
            public static void main(String[] args) {
                ArrayList<ByteBuffer> list = new ArrayList<>();
                int count = 0;
                try {
                    while (true) {
                        // 使用直接内存
                        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
                        list.add(byteBuffer);
                        count++;
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                } finally {
                    System.out.println(count);
                }
            }
        }
        -----------------------------------------------------------------------------------------------------
        会看到报出来java.lang.OutOfMemoryError: Direct buffer memory直接内存空间不足的异常。

05.GC OOM
    a.原因
        GC OOM是由于JVM在GC时,对象过多,导致内存溢出,建议调整GC的策略。
    b.报错
        出现GC OOM问题时异常信息如下:
        java.lang.OutOfMemoryError: GC overhead limit exceeded
    c.示例
        为了方便测试,我先将idea中的最大和最小堆大小都设置成10M:
        -Xmx10m -Xms10m
        -----------------------------------------------------------------------------------------------------
        public class GCOverheadOOM {
            public static void main(String[] args) {
                ExecutorService executor = Executors.newFixedThreadPool(5);
                for (int i = 0; i < Integer.MAX_VALUE; i++) {
                    executor.execute(() -> {
                        try {
                            Thread.sleep(10000);
                        } catch (InterruptedException e) {
                        }
                    });
                }
            }
        }
        -----------------------------------------------------------------------------------------------------
        出现这个问题是由于JVM在GC的时候,对象太多,就会报这个错误。
        我们需要改变GC的策略。
        在老代80%时就是开始GC,并且将-XX:SurvivorRatio(-XX:SurvivorRatio=8)和-XX:NewRatio(-XX:NewRatio=4)设置的更合理。

06.元空间00M
    a.介绍
        JDK8之后使用Metaspace来代替永久代,Metaspace是方法区在HotSpot中的实现。
        Metaspace不在虚拟机内存中,而是使用本地内存也就是在JDK8中的ClassMetadata,被存储在叫做Metaspace的native memory。
    b.报错
        出现元空间OOM问题时异常信息如下:
        java.lang.OutOfMemoryError: Metaspace
    c.示例
        为了方便测试,我修改一下idea中的JVM参数,增加下面的配置:
        -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
        指定了元空间和最大元空间都是10M。
        -----------------------------------------------------------------------------------------------------
        public class MetaspaceOOMTest {
            static class OOM {
            }

            public static void main(String[] args) {
                int i = 0;
                try {
                    while (true) {
                        i++;
                        Enhancer enhancer = new Enhancer();
                        enhancer.setSuperclass(OOM.class);
                        enhancer.setUseCache(false);
                        enhancer.setCallback(new MethodInterceptor() {
                            @Override
                            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                                return methodProxy.invokeSuper(o, args);
                            }
                        });
                        enhancer.create();
                    }
                } catch (Throwable e) {
                    e.printStackTrace();
                }
            }
        }
        -----------------------------------------------------------------------------------------------------
        程序最后会报java.lang.OutOfMemoryError: Metaspace的元空间OOM。
        这个问题一般是由于加载到内存中的类太多,或者类的体积太大导致的。

5.10 [2]内存溢出:堆、栈

00.总结
    分为2种:堆内存溢出、栈内存溢出
    堆内存溢出:与【对象的分配和垃圾回收】有关,通常由于【对象过多】导致
    栈内存溢出:与【方法调用和递归深度】有关,通常由于【递归过深】或【方法调用过多】导致

01.堆内存溢出
    a.定义
        堆内存溢出发生在Java堆空间不足以分配新的对象时。JVM会抛出java.lang.OutOfMemoryError: Java heap space错误。
    b.常见原因
        对象过多:程序创建了过多的对象,超出了堆的容量
        内存泄漏:对象不再使用,但仍然被引用,导致无法被垃圾回收
        不合理的堆大小配置:堆的初始大小和最大大小配置不合理,无法满足程序的内存需求
    c.解决方法
        优化代码,减少不必要的对象创建
        检查和修复内存泄漏
        增加堆的大小(通过-Xms和-Xmx参数)

02.栈内存溢出
    a.定义
        栈内存溢出发生在Java栈空间不足以分配新的栈帧时,JVM会抛出java.lang.StackOverflowError错误。
    b.常见原因
        递归过深:递归调用没有正确终止,导致栈帧无限增长
        方法调用过多:程序中存在大量的方法调用,超出了栈的容量
    c.解决方法
        优化递归算法,确保递归调用有正确的终止条件
        增加栈的大小(通过-Xss参数)

5.11 [2]内存溢出、内存泄露

00.总结
    a.相同点
        都会导致应用程序运行出现问题,性能下降或挂起
    b.不同点
        1.内存泄露是导致内存溢出的原因之一,内存泄露积累起来将导致内存溢出
        2.内存泄露可以通过完善代码来避免,内存溢出可以通过调整配置来减少发生频率,但无法彻底避免

01.内存溢出,Out of Memory
    a.定义
        程序在申请内存时,没有足够的内存空间供其使用,出现OutOfMemoryError
    b.原因
        1.JVM内存过小。
        2.程序不严密,产生了过多的垃圾
  c.在程序上的体现为
        1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据
        2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收
        3.代码中存在死循环或循环产生过多重复的对象实体
    c.解决方法
        1.优化内存使用:检查代码,优化数据结构和算法,减少不必要的内存分配
        2.增加内存:调整JVM参数(如-Xmx)增加堆内存大小
        3.监控和分析:使用内存分析工具(如VisualVM、MAT)监控内存使用情况,找出内存消耗的热点

02.内存泄漏,Memory Leak
    a.定义
        内存泄漏是指程序在运行过程中分配的内存无法被释放,导致内存逐渐减少,最终可能导致内存溢出
        广义并通俗的说,就是:不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露
    b.原因
        1.未释放的对象引用:对象不再使用,但仍然被引用,导致垃圾回收器无法回收这些对象
        2.静态集合:静态集合(如static变量)持有对象引用,导致这些对象无法被回收
        3.监听器和回调:注册的监听器或回调未被正确移除,导致对象无法被回收
        4.线程问题:线程未正确终止,导致线程持有的对象无法被回收
        5.表现:内存使用量逐渐增加,系统性能下降,最终可能导致内存溢出
    c.解决方法
        1.及时释放资源:确保不再使用的对象引用被及时清除
        2.使用弱引用:使用WeakReference、SoftReference等弱引用类型,允许垃圾回收器回收不再使用的对象
        3.监控和分析:使用内存分析工具(如VisualVM、MAT)检测内存泄漏,找出未被回收的对象及其引用链
        4.良好的编码习惯:遵循良好的编码习惯,避免长时间持有对象引用,及时移除不再需要的监听器和回调

03.关系和顺序
    a.结论1:内存泄漏导致内存溢出
        内存泄漏本身不会立即导致内存溢出,
        但如果内存泄漏持续存在,未释放的内存会逐渐累积,最终可能导致内存溢出
        因此,内存泄漏可以被视为内存溢出的潜在原因之一
    b.结论2:内存溢出不一定由内存泄漏引起
        内存溢出可以由多种原因引起,不仅仅是内存泄漏
        例如,程序中存在无限制的数据增长或一次性分配非常大的对象,都可能导致内存溢出
    c.总结
        内存泄漏:是指程序中存在未释放的对象引用,导致内存逐渐减少。内存泄漏本身不会立即导致内存溢出,但如果持续存在,可能最终导致内存溢出
        内存溢出:是指程序需要分配的内存超过了系统所能提供的最大内存限制,导致程序无法继续正常运行。内存溢出可以由多种原因引起,包括内存泄漏

5.12 [2]内存溢出、内存泄露、频繁FullGc

01.频繁的Full GC
    a.原因
        老年代空间不足:当老年代的内存空间不足以容纳新对象时,会触发Full GC
        内存碎片:频繁的对象分配和回收可能导致内存碎片化,使得大对象无法分配
        不合理的内存配置:堆内存设置过小,导致频繁的垃圾回收
    b.影响
        应用程序停顿:Full GC会暂停所有应用线程,导致响应时间增加
        性能下降:频繁的Full GC会显著降低应用程序的吞吐量

02.内存泄露(Memory Leak)
    a.定义
        内存泄露是指程序中不再使用的对象仍然被引用,导致垃圾回收器无法回收这些对象
        广义并通俗的说,就是:不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露
    b.关系
        内存泄露会导致老年代的对象不断增加,最终导致老年代空间不足,从而触发频繁的Full GC
        随着时间的推移,内存泄露会导致堆内存耗尽,最终可能导致内存溢出

03.内存溢出(OutOfMemoryError)
    a.定义
        内存溢出是指程序尝试分配内存时,堆内存已被耗尽,无法再分配新的对象
    b.关系
        内存泄露是导致内存溢出的常见原因,因为泄露的对象占用内存而不被释放
        频繁的Full GC可能是内存溢出的前兆,因为它表明JVM正在努力回收内存以满足分配需求
        当Full GC无法释放足够的内存来满足新对象的分配时,就会抛出OutOfMemoryError

5.13 [2]一个线程OOM后,其他线程还能运行吗?能

00.回答
    能

01.OOM类型
    a.堆溢出
        java.lang.OutOfMemoryError: Java heap space
    b.永久带溢出
        java.lang.OutOfMemoryError:Permgen space
    c.不能创建线程
        java.lang.OutOfMemoryError:Unable to create new native thread

02.说明
    其实发生OOM的线程一般情况下会死亡,也就是会被终结掉,该线程持有的对象占用的heap都会被gc了,释放内存
    因为发生OOM之前要进行gc,就算其他线程能够正常工作,也会因为频繁gc产生较大的影响

5.14 [3]对象引用:4种

00.总结
    强引用:默认的引用类型,强引用的对象【不会被垃圾回收】
    软引用:用于实现内存敏感的缓存,【只有在内存不足时才会被回收】
    弱引用:用于实现非强制性缓存,弱引用的对象【在垃圾回收时总会被回收】
    虚引用:用于跟踪对象的回收状态,虚引用的对象【在任何时候都可能被回收】

01.可达性分析是基于引用链进行判断的,在JDK 1.2之后,Java将引用关系分为以下四类:
    a.强引用(Strongly Reference)
        最传统的引用,如Object obj = new Object()
        只要强引用关系还存在,垃圾收集器就永远不会回收被引用的对象
    b.软引用(Soft Reference)
        用于描述一些还有用,但非必须的对象
        只被软引用关联的对象,在系统将要发生内存溢出之前,会被列入回收范围内进行第二次回收
        如果这次回收后还没有足够的内存,才会抛出内存溢出异常
    c.弱引用(Weak Reference)
        用于描述那些非必须的对象,强度比软引用弱
        被弱引用关联的对象只能生存到下一次垃圾收集发生时,无论当前内存是否足够,弱引用对象都会被回收
    d.虚引用(Phantom Reference)
        最弱的引用关系。为一个对象设置虚引用关联的唯一目的是为了能在这个对象被回收时收到一个系统通知
        虚引用必须和引用队列(ReferenceQueue)联合使用

5.15 [3]对象真正死亡:2步

01.要真正宣告一个对象死亡,需要经过至少两次标记过程
    a.第一次标记
        如果对象在进行可达性分析后发现GC Roots不可达,将会进行第一次标记
    b.筛选
        筛选的条件是此对象是否有必要执行finalize()方法
        如果对象没有覆盖finalize()方法,或者finalize()已经被虚拟机调用过,这两种情况都会视为没有必要执行
        如果判定结果是有必要执行,此时对象会被放入名为F-Queue的队列,等待Finalizer线程执行其finalize()方法
        在这个过程中,收集器会进行第二次小规模的标记
        如果对象在finalize()方法中重新将自己与引用链上的任何一个对象进行了关联
        如将自己(this关键字)赋值给某个类变量或者对象的成员变量
        此时它就实现了自我拯救,则第二次标记会将其移除“即将回收”的集合,否则该对象就将被真正回收,走向死亡

5.16 [3]对象是否死亡:引用计数、可达性分析

00.总结
    两种方式:【引用计数】、【可达性分析】

01.引用计数法(Reference Counting)
    a.定义
        有循环依赖的问题,但是是可以解决的
    b.说明
        每个对象维护一个引用计数器,记录有多少引用指向该对象。
        当一个新的引用指向对象时,计数器加一;当引用失效时,计数器减一。
        如果计数器为零,则对象可以被回收。
        缺点:无法处理循环引用的问题,因此Java不使用这种方法。
    c.代码
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        System.gc();
        -----------------------------------------------------------------------------------------------------
        在上述代码中,两个对象已经不能再被访问,但它们互相持有对方的引用,如果采用引用计数法,则两个对象都无法被回收

02.可达性分析法(Reachability Analysis):
    a.定义
        从根引用(GCRoots)开始进行引用链遍历扫描,如果可达则对象存活,如果不可达则对象已成为垃圾。
    b.说明
        通过一组称为"GC Roots"的对象作为起点,进行引用链的遍历。
        如果一个对象无法从GC Roots到达,则认为该对象是不可达的,可以被回收。
        GC Roots包括:栈中的引用、静态变量、常量池中的引用、本地方法栈中的引用等。
    c.在Java语言中,固定可作为GC Roots的对象包括以下几种
        在虚拟机栈(栈帧中的本地变量表)中引用的对象
        在方法区(元空间)中类静态变量引用的对象
        在方法区(元空间)中常量引用的对象
        在本地方法栈中的JNI(Native方法)引用的对象
        Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象及系统类加载器
        所有被同步锁(synchronized关键字)持有的对象

5.17 [3]对象是否回收:引用置为null,垃圾收集器是否会立即释放对象占用的内存?

01.如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?
    不会
    对象回收需要一个过程,这个过程中对象还能复活。而且垃圾回收具有不确定性,指不定什么时候开始回收

5.18 [4]垃圾回收:GC

01.定义
    GC就是垃圾回收,释放掉没用的对象占用的空间,保证内存空间不被迅速耗尽

02.垃圾回收的目的
    释放无用对象占用的内存,避免内存泄漏,提升程序的稳定性和性能
    防止内存泄漏,确保程序在长时间运行中不会因为内存耗尽而崩溃
    优化内存使用,提高程序的性能和稳定性

03.垃圾回收的触发
    垃圾回收通常由虚拟机自动触发,程序员不需要手动调用
    当堆内存不足、JVM判断需要回收或系统资源紧张时,垃圾回收器会触发回收

04.垃圾回收的过程
    通过三色标记法标记可回收对象,并通过分代回收策略优化不同生命周期对象的回收

05.垃圾回收器的选择
    根据具体场景选择合适的垃圾回收器,以获得最优的性能

5.19 [4]垃圾回收:触发条件

00.总结
    垃圾回收通常由虚拟机自动触发,程序员不需要手动调用

01.垃圾回收的触发条件通常是以下几种
    a.堆内存空间不足
        当JVM的堆内存区域中的可用内存不足时,垃圾回收就会被触发
        这时,垃圾回收器会尝试回收不再被引用的对象,从而释放内存,尽可能避免系统崩溃或内存溢出
    b.JVM判断需要回收
        现代JVM实现通常会基于内存使用情况、垃圾对象的数量以及当前程序的运行状态来判断是否需要进行垃圾回收
        比如,垃圾回收器会定期监控堆内存中的对象,如果有很多对象变得不可达或没有引用,回收器就会进行回收
    c.系统资源紧张
        除了内存,CPU和磁盘的资源也会影响垃圾回收的触发
        例如,在进行大规模数据处理时,如果系统资源紧张,垃圾回收也可能会被触发

5.20 [4]垃圾回收:三色标记法、分代回收

00.汇总
    a.三色标记法
        白色:表示一个对象是不可达的,也就是说,这个对象不再被引用,可以被回收
        灰色:表示一个对象是可达的,但它的引用对象可能还没有被检查。垃圾回收器会进一步检查这些灰色对象的引用
        黑色:表示一个对象已经被标记为可达,且它的引用已经被完全遍历过
    b.分代回收
        java采用【分代回收】,分为年轻代、老年代、永久代;年轻代又分为E区、S1区、S2区,到jdk8,永久代被元空间取代了
        年轻代:都使用复制算法
        老年代:看具体用什么收集器
        默认是PS收集器,采用标记-整理算法

01.三色标记法
    a.白色
        表示一个对象是不可达的,也就是说,这个对象不再被引用,可以被回收
    b.灰色
        表示一个对象是可达的,但它的引用对象可能还没有被检查。垃圾回收器会进一步检查这些灰色对象的引用
    c.黑色
        表示一个对象已经被标记为可达,且它的引用已经被完全遍历过

02.分代回收
    a.年轻代(Young Generation):复制算法
        a.定义
            年轻代是对象最初分配内存的地方,主要用于存放新创建的对象
        b.Eden区(E区)
            大部分新对象在这里分配内存。当 Eden 区满时,会触发一次 Minor GC(小型垃圾回收),将存活的对象移动到 Survivor 区
        c.Survivor区(S1+S2)
            年轻代中有两个Survivor区,通常称为 S1 和 S2(或 From 和 To)
            在一次 Minor GC 后,Eden 区和一个 Survivor 区中的存活对象会被复制到另一个 Survivor 区两个 Survivor 区是对称的,没有固定的角色,彼此交替使用
    b.老年代(Old Generation):看具体用什么收集器
        老年代用于存放生命周期较长的对象
        经过多次 Minor GC 仍然存活的对象会被移动到老年代
        当老年代满时,会触发 Major GC(或 Full GC),这通常会导致应用程序暂停时间较长
    c.永久代(Permanent Generation)与元空间(Metaspace)
        a.永久代
            永久代用于存放类的元数据(如类信息、方法信息等)
            在 JDK 8 之前,永久代是堆的一部分,存储类的元数据、常量池、方法描述符等
        b.元空间(Metaspace)
            从 JDK 8 开始,永久代被移除了,取而代之的是元空间(Metaspace)
            元空间不再是堆的一部分,而是使用本地内存来存储类的元数据
            这种变化的主要原因是为了更好地管理内存和避免永久代的内存溢出问题
        c.元空间的特点
            动态调整:元空间的大小可以根据应用程序的需要动态调整
            本地内存:使用本地内存而不是堆内存,因此不受堆大小的限制
            配置参数:可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 来设置元空间的初始大小和最大大小

5.21 [4]垃圾的收集器:7种,默认PS,采用标记-整理算法

00.汇总
    1.Serial收集器:单线程垃圾收集器,使用一个线程进行垃圾回收
    2.ParNew收集器:Serial收集器的多线程版本,适用于新生代垃圾回收
    3.Parallel Scavenge收集器:关注吞吐量的垃圾收集器,适用于新生代,【默认PS,采用标记-整理算法】
    4.Serial Old收集器:Serial收集器的老年代版本,单线程执行
    5.Paralled Old收集器:Parallel Scavenge收集器的老年代版本,支持多线程并行垃圾回收
    6.CMS收集器:低延迟垃圾收集器,适用于老年代
    7.G1收集器:面向服务端应用的垃圾收集器,适用于大内存和多处理器环境

01.Serial收集器
    a.特点
        单线程垃圾收集器,使用一个线程进行垃圾回收
        在进行垃圾回收时,会暂停所有应用线程(Stop-The-World, STW)
        适用于单线程环境或小型应用
    b.适用场景
        客户端应用程序,或对暂停时间不敏感的小型应用

02.ParNew收集器
    a.特点
        Serial收集器的多线程版本,适用于新生代垃圾回收
        可以与CMS收集器配合使用
        在多核处理器上表现更好,因为它可以并行执行垃圾回收
    b.适用场景
        需要与CMS收集器配合使用的多线程环境

03.Parallel Scavenge收集器
    a.特点
        关注吞吐量的垃圾收集器,适用于新生代
        通过多线程并行进行垃圾回收,减少垃圾回收的总时间
        提供自适应调节策略,可以自动调整停顿时间和吞吐量之间的平衡
    b.适用场景
        需要高吞吐量的后台应用程序,如批处理任务

04.Serial Old收集器
    a.特点
        Serial收集器的老年代版本,单线程执行
        作为CMS收集器的后备方案,或在Client模式下使用
    b.适用场景
        与Serial收集器配合使用的小型应用

05.Parallel Old收集器
    a.特点
        Parallel Scavenge收集器的老年代版本,支持多线程并行垃圾回收
        提高了老年代的垃圾回收效率。
    b.适用场景
        需要高吞吐量的后台应用程序,与Parallel Scavenge收集器配合使用

06.CMS(Concurrent Mark-Sweep)收集器
    a.特点
        低延迟垃圾收集器,适用于老年代
        通过并发标记和清除阶段,减少应用暂停时间
        可能会产生内存碎片,需要定期进行内存整理
    b.适用场景
        需要快速响应的应用程序,如Web服务器
    c.四个阶段
        初始标记(initial mark)
        并发标记(concurrent mark)
        重新标记(remark)
        并发清除(concurrent sweep)

07.G1(Garbage-First)收集器
    a.特点
        面向服务端应用的垃圾收集器,适用于大内存和多处理器环境
        将堆划分为多个区域(Region),优先回收垃圾最多的区域
        提供可预测的停顿时间,适合需要低延迟的应用
    b.适用场景
        大型应用程序,特别是需要低延迟和高吞吐量的场景

5.22 [4]垃圾回收算法:4种

00.总结
    1.标记-清除:标记活动对象,然后清除未标记的对象
    2.复制:将活动对象复制到新空间,清除旧空间
    3.标记-整理:标记活动对象,然后整理活动对象到一端,清除其他对象
    4.分代收集:将堆分为新生代和老年代,根据对象的生命周期进行不同的垃圾回收策略

01.垃圾回收算法
    a.标记-清除算法(Mark-Sweep)
        标记阶段:遍历对象图,标记所有可达对象
        清除阶段:回收未标记的对象,释放内存
    b.复制算法(Copying)
        将活动对象从一个内存区域复制到另一个区域,清除原区域的所有对象
        常用于新生代垃圾回收
    c.标记-整理算法(Mark-Compact)
        标记阶段:标记所有可达对象
        整理阶段:将活动对象移动到内存的一端,清除其他对象
        常用于老年代垃圾回收
    d.分代收集算法(Generational Collection)
        将堆分为新生代和老年代,根据对象的生命周期进行不同的垃圾回收策略
        新生代对象存活时间短,使用复制算法
        老年代对象存活时间长,使用标记-整理算法

02.不同垃圾回收方法的区别
    a.标记-清除算法(Mark-Sweep)
        过程:首先标记所有可达对象,然后清除未标记的对象
        优点:不需要移动对象,简单直接
        缺点:可能产生内存碎片,因为清除阶段不整理内存
    b.复制算法(Copying)
        过程:将活动对象从一个内存区域复制到另一个区域,清除原区域的所有对象
        优点:没有内存碎片,分配简单
        缺点:需要额外的内存空间,适合新生代对象
    c.标记-整理算法(Mark-Compact)
        过程:首先标记所有可达对象,然后将活动对象移动到内存的一端,清除其他对象
        优点:没有内存碎片,适合老年代对象
        缺点:需要移动对象,开销较大
    d.分代收集算法(Generational Collection)
        过程:将堆分为新生代和老年代,根据对象的生命周期进行不同的垃圾回收策略
        优点:针对不同对象的生命周期优化回收策略,提高效率
        缺点:需要管理不同代之间的对象引用

5.23 [4]垃圾收集机制:标记、清除

01.垃圾回收的基本过程
    a.标记阶段
        标记所有存活的对象,即那些仍然被引用的对象
        常用的标记算法包括引用计数法和可达性分析法
    b.清除阶段
        清除所有未被标记的对象,即那些不再被引用的对象
        清除阶段可以采用标记-清除、标记-复制、标记-整理等算法

5.24 [4]垃圾的收集器:将堆分为老年代和新生代

00.总结
    因为不同对象的生命周期不一样,大部分对象朝生夕死,而少部分一直存在堆中,所以按照存活时间分区管理更加高效
    将堆分为【新生代和老年代】是Java垃圾收集器的一种【优化策略】,旨在提高垃圾回收的效率和性能

01.对象生命周期的观察
    a.大多数对象是短命的
        在Java应用程序中,大多数对象的生命周期都很短。例如,临时变量和中间计算结果通常在方法结束后就不再需要
        新生代专门用于存储这些短命对象,垃圾回收器可以频繁地回收新生代,以快速释放内存
    b.少数对象是长命的
        一些对象在程序的整个生命周期中都存在,例如缓存、会话信息等
        老年代用于存储这些长命对象,垃圾回收在老年代发生的频率较低

02.提高垃圾回收效率
    a.新生代的回收效率高
        新生代通常使用复制算法进行垃圾回收。复制算法将存活的对象从Eden区复制到Survivor区,未存活的对象直接清除
        由于大多数对象在新生代中很快变得不可达,复制算法可以快速回收大量内存
    b.减少老年代的回收频率
        老年代的垃圾回收通常使用标记-整理算法,因为对象存活率高,复制算法的效率不如标记-整理算法
        通过减少老年代的回收频率,可以降低垃圾回收对应用程序的影响

03.分代收集的策略
    a.新生代的分区
        新生代进一步分为Eden区和两个Survivor区(S0和S1)。对象首先分配在Eden区,当Eden区满时,进行Minor GC,将存活的对象复制到Survivor区
        多次Minor GC后仍存活的对象会被晋升到老年代
    b.老年代的管理
        老年代用于存储生命周期较长的对象,垃圾回收的频率较低,但每次回收的时间较长(Major GC或Full GC)

04.优化内存管理
    a.减少内存碎片
        通过分代收集策略,可以有效减少内存碎片,提高内存分配和回收的效率
    b.提高应用性能
        分代收集策略通过优化垃圾回收的频率和时间,减少了垃圾回收对应用程序性能的影响