类实例化加载顺序
-
加载:当程序访问某个类时,JVM会首先检查该类是否已经加载到内存中。如果尚未加载,则会进行加载操作。加载操作将类的字节码文件加载到内存,并为其创建一个Class对象。
-
连接(验证、准备、解析):
-
验证:验证阶段会对类的字节码进行验证,确保它的结构正确且满足Java虚拟机规范。
-
准备:准备阶段会为类的静态变量分配内存,并设置初始值,这些变量会在类初始化时赋予其真正的初始值。
-
解析:解析阶段会将符号引用转换为直接引用,建立虚拟机内部的数据结构,以便后续的访问。
-
-
初始化:初始化阶段将执行类的初始化代码,包括静态变量的赋值和静态代码块的执行。类的初始化是按需进行的,即在首次使用该类或创建该类的实例时进行。
对于类的实例化顺序,可以按照以下规则进行:
-
静态变量和静态代码块按照在类中的顺序依次执行,且仅执行一次。
-
实例变量和实例代码块按照在类中的顺序依次执行,在每次创建实例时都会执行一次。
-
构造方法最后执行,用于初始化实例的状态。
JVM创建对象的过程
-
类加载:当程序首次使用某个类时,JVM会先进行类的加载。类加载是将类的字节码文件加载到内存,并创建一个对应的Class对象。
-
分配内存:在类加载完成后,JVM会根据类的内部结构在堆内存中分配对象所需的内存空间。分配的方式可以是指针碰撞(Bump the Pointer)或空闲列表(Free List)。
-
初始化零值:JVM会将分配的内存空间初始化为零值,包括基本类型的零值和引用类型的null。
-
设置对象头:JVM会设置对象的头部信息,包括存储对象的哈希码、锁状态、GC标记等。
-
执行构造函数:JVM会执行类的构造函数,进行对象的初始化。构造函数负责对对象的属性进行初始化,并可以执行其他必要的操作。
-
对象创建完成:经过以上步骤,JVM成功创建了一个对象,并将对象的引用返回给程序。
JVM的运行机制
JVM(Java Virtual Machine)是用于运行Java字节码的虚拟机,包括一套字节码指令集、一组程序寄存器、一个虚拟机栈、一个虚拟机堆、一个方法区和一个垃圾回收器。JVM运行在操作系统之上,不与硬件设备直接交互。Java源文件在通过编译器之后被编译成相应的.Class文件(字节码文件),.Class文件又被JVM中的解释器编译成机器码在不同的操作系统(Windows、Linux、Mac)上运行。每种操作系统的解释器都是不同的,但基于解释器实现的虚拟机是相同的,这也是Java能够跨平台的原因。在一个Java进程开始运行后,虚拟机就开始实例化了,有多个进程启动就会实例化多个虚拟机实例。进程退出或者关闭,则虚拟机实例消亡,在多个虚拟机实例之间不能共享数据。Java程序的具体运行过程如下。
(1)Java源文件被编译器编译成字节码文件。
(2)JVM将字节码文件编译成相应操作系统的机器码。
(3)机器码调用相应操作系统的本地方法库执行相应的方法
类加载器子系统用于将编译好的.Class文件加载到JVM中;
◎ 运行时数据区用于存储在JVM运行过程中产生的数据,包括程序计数器、方法区、本地方法区、虚拟机栈和虚拟机堆;
◎ 执行引擎包括即时编译器和垃圾回收器,即时编译器用于将Java字节码编译成具体的机器码,垃圾回收器用于回收在运行过程中不再使用的对象;
◎ 本地接口库用于调用操作系统的本地方法库完成具体的指令操作。
直接内存(Direct Memory)
并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,
。
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,
因为避免了在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
JVM后台运行的线程
在JVM后台会运行许多线程,其中一些是JVM自己创建和管理的线程,用于支持Java程序的执行和JVM的运行。以下是一些常见的JVM后台运行的线程:
-
主线程(Main Thread): Java程序的入口点是主线程,程序从main方法开始执行,主线程负责启动和执行Java程序的其他线程。
-
垃圾回收线程(Garbage Collection Threads): 垃圾回收线程是JVM中负责执行垃圾回收的线程。它们扫描堆内存,标记和清理不再使用的对象,从而释放内存空间。
-
编译线程(Compilation Threads): 编译线程负责将Java源代码编译成可执行的字节码。JVM中的即时编译器将字节码编译为机器码以提高运行效率。
-
信号分发线程(Signal Dispatcher Thread): 信号分发线程负责接收和分发操作系统发送的信号,如崩溃信号、用户自定义信号等。
-
定时器线程(Timer Threads): 定时器线程负责调度和执行定时任务,它在后台运行,监视计划执行的任务,并在指定的时间触发相应的操作。
除了上述线程之外,还有一些其他的线程用于执行特定的功能,例如监控线程、虚拟机内部线程等。这些线程都运行在JVM后台,对于应用程序来说是透明的,由JVM负责创建、调度和管理。
JVM 常用参数
标准参数中比较有用的:
verbose -verbose:class 输出jvm载入类的相关信息,当jvm报告说找不到类或者类冲突时可此进行诊断。 -verbose:gc 输出每次GC的相关情况。 -verbose:jni 输出native方法调用的相关情况,一般用于诊断jni调用错误信息。
非标准参数又称为扩展参数,比较有用的是
-Xms512m 设置JVM促使内存为512m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmx512m ,设置JVM最大可用内存为512M。
-Xmn200m:设置年轻代大小为200M。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k:
设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
-Xloggc:file 与-verbose:gc功能类似,只是将每次GC事件的相关情况记录到一个文件中,文件的位置最好在本地,以避免网络的潜在问题。 若与verbose命令同时出现在命令行中,则以-Xloggc为准。 -Xprof
跟踪正运行的程序,并将跟踪数据在标准输出输出;适合于开发环境调试。
非Stable参数
用-XX作为前缀的参数列表在jvm中可能是不健壮的,SUN也不推荐使用,后续可能会在没有通知的情况下就直接取消了;但是由于这些参数中的确有很多是对我们很有用的,比如我们经常会见到的-XX:PermSize、-XX:MaxPermSize等等;
首先来介绍行为参数:
参数及其默认值 | 描述 |
---|---|
-XX:-DisableExplicitGC | 禁止调用System.gc();但jvm的gc仍然有效 |
-XX:+MaxFDLimit | 最大化文件描述符的数量限制 |
-XX:+ScavengeBeforeFullGC | 新生代GC优先于Full GC执行 |
-XX:+UseGCOverheadLimit | 在抛出OOM之前限制jvm耗费在GC上的时间比例 |
-XX:-UseConcMarkSweepGC | 对老生代采用并发标记交换算法进行**GC** |
-XX:-UseParallelGC | 启用并行**GC** |
-XX:-UseParallelOldGC | 对Full GC启用并行,当-XX:-UseParallelGC启用时该项自动启用 |
-XX:-UseSerialGC | 启用串行**GC** |
-XX:+UseThreadPriorities | 启用本地线程优先级 |
上面表格中黑体的三个参数代表着jvm中GC执行的三种方式,即串行、并行、并发; 串行(**SerialGC)是jvm的默认GC方式,一般适用于小型应用和单处理器,算法比较简单,GC效率也较高,但可能会给应用带来停顿; 并行(ParallelGC)是指GC运行时,对应用程序运行没有影响,GC和app两者的线程在并发执行,这样可以最大限度不影响app的运行; 并发(ConcMarkSweepGC)**是指多个线程并发执行GC,一般适用于多处理器系统中,可以提高GC的效率,但算法复杂,系统消耗较大;
性能调优参数列表:
参数及其默认值 | 描述 |
---|---|
-XX:LargePageSizeInBytes=4m | 设置用于Java堆的大页面尺寸 |
-XX:MaxHeapFreeRatio=70 | GC后java堆中空闲量占的最大比例 |
-XX:MaxNewSize=size | 新生成对象能占用内存的最大值 |
-XX:MaxPermSize=64m | 老生代对象能占用内存的最大值 |
-XX:MinHeapFreeRatio=40 | GC后java堆中空闲量占的最小比例 |
-XX:NewRatio=2 | 新生代内存容量与老生代内存容量的比例 |
-XX:NewSize=2.125m | 新生代对象生成时占用内存的默认值 |
-XX:ReservedCodeCacheSize=32m | 保留代码占用的内存容量 |
-XX:ThreadStackSize=512 | 设置线程栈大小,若为0则使用系统默认值 |
-XX:+UseLargePages | 使用大页面内存 |
我们在日常性能调优中基本上都会用到以上黑体的这几个属性;
调试参数列表:
参数及其默认值 | 描述 |
---|---|
-XX:-CITime | 打印消耗在JIT编译的时间 |
-XX:ErrorFile=./hs_err_pid<pid>.log | 保存错误日志或者数据到文件中 |
-XX:-ExtendedDTraceProbes | 开启solaris特有的dtrace探针 |
-XX:HeapDumpPath=./java_pid<pid>.hprof | 指定导出堆信息时的路径或文件名 |
-XX:-HeapDumpOnOutOfMemoryError | 当首次遭遇**OOM时导出此时堆中相关信息** |
-XX:OnError="<cmd args>;<cmd args>" | 出现致命ERROR之后运行自定义命令 |
-XX:OnOutOfMemoryError="<cmd args>;<cmd args>" | 当首次遭遇OOM时执行自定义命令 |
-XX:-PrintClassHistogram | 遇到Ctrl-Break后打印类实例的柱状信息,与jmap -histo功能相同 |
-XX:-PrintConcurrentLocks | 遇到**Ctrl-Break后打印并发锁的相关信息,与jstack -l功能相同** |
-XX:-PrintCommandLineFlags | 打印在命令行中出现过的标记 |
-XX:-PrintCompilation | 当一个方法被编译时打印相关信息 |
-XX:-PrintGC | 每次GC时打印相关信息 |
-XX:-PrintGC Details | 每次GC时打印详细信息 |
-XX:-PrintGCTimeStamps | 打印每次GC的时间戳 |
-XX:-TraceClassLoading | 跟踪类的加载信息 |
-XX:-TraceClassLoadingPreorder | 跟踪被引用到的所有类的加载信息 |
-XX:-TraceClassResolution | 跟踪常量池 |
-XX:-TraceClassUnloading | 跟踪类的卸载信息 |
-XX:-TraceLoaderConstraints | 跟踪类加载器约束的相关信息 |
class初始化过程是什么?
首先类加载的机制过程分为5个部分:加载、验证、准备、解析、初始化
类的初始化过程是指对类进行首次使用之前的准备和初始化操作。类的初始化包括以下几个步骤:
-
加载(Loading):类的加载是指通过类加载器将类的字节码文件加载到内存中,并创建一个对应的Class对象。加载过程包括查找类文件、将类文件字节码读取到内存,并创建对应的Class对象。
-
链接(Linking):
-
验证(Verification):验证过程是对加载的类进行验证,确保类的正确性和安全性,包括验证类的格式、语义等方面。
-
准备(Preparation):准备阶段是为类的静态变量(被
static
修饰的变量)分配内存,并进行默认初始化,即赋予默认值(比如0、null等)。 -
解析(Resolution):解析过程是将符号引用转换为直接引用,使得执行字节码时可以直接找到对应的目标。
-
-
初始化(Initialization):在初始化阶段,执行类的初始化代码,包括静态变量的显式赋值、静态代码块中的代码执行等。初始化阶段在Java虚拟机中是被加锁的,确保只有一个线程对类进行初始化。
类的初始化是按需进行的,只有在以下情况下才会触发类的初始化:
-
创建类的实例对象
-
访问类的静态变量
-
调用类的静态方法
-
使用反射操作该类时
需要注意的是,类的初始化是按照以上步骤依次进行的,一旦开始了类的初始化,就会按顺序依次执行各个步骤,且仅进行一次。此外,子类的初始化会触发父类的初始化过程。
值得一提的是,类的加载和初始化过程是由Java虚拟机控制的,开发人员在代码中无法直接干预类的加载和初始化顺序。
JVM内存模型如何分配的?
JVM的类加载阶段 JVM结构
JVM性能调优的原则有哪些?
-
多数的Java应用不需要在服务器上进行GC优化,虚拟机内部已有很多优化来保证应用的稳定运行,所以不要为了调优而调优,不当的调优可能适得其反
-
在应用上线之前,先考虑将机器的JVM参数设置到最优(适合)
-
在进行GC优化之前,需要确认项目的架构和代码等已经没有优化空间。我们不能指望一个系统架构有缺陷或者代码层次优化没有穷尽的应用,通过GC优化令其性能达到一个质的飞跃
-
GC优化是一个系统而复杂的工作,没有万能的调优策略可以满足所有的性能指标。GC优化必须建立在我们深入理解各种垃圾回收器的基础上,才能有事半功倍的效果
-
处理吞吐量和延迟问题时,垃圾处理器能使用的内存越大,即java堆空间越大垃圾收集效果越好,应用运行也越流畅。这称之为GC内存最大化原则
-
在这三个属性(吞吐量、延迟、内存)中选择其中两个进行jvm调优,称之为GC调优3选2
什么情况下需要JVM调优?
-
Heap内存(老年代)持续上涨达到设置的最大内存值
-
Full GC 次数频繁
-
GC 停顿(Stop World)时间过长(超过1秒,具体值按应用场景而定)
-
应用出现OutOfMemory 等内存异常
-
应用出现OutOfDirectMemoryError等内存异常( failed to allocate 16777216 byte(s) of direct memory (used: 1056964615, max: 1073741824))
-
应用中有使用本地缓存且占用大量内存空间
-
系统吞吐量与响应性能不高或下降
-
应用的CPU占用过高不下或内存占用过高不下
在JVM调优时,你关注哪些指标?
-
吞吐量:用户代码时间 / (用户代码执行时间 + 垃圾回收时间)。是评价垃圾收集器能力的重要指标之一,是不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用程序达到的最高性能指标。吞吐量越高算法越好。
-
低延迟:STW越短,响应时间越好。评价垃圾收集器能力的重要指标,度量标准是缩短由于垃圾收集引起的停顿时间或完全消除因垃圾收集所引起的停顿,避免应用程序运行时发生抖动。暂停时间越短算法越好
-
在设计(或使用)GC 算法时,我们必须确定我们的目标:一个 GC 算法只可能针对两个目标之一(即只专注于最大吞吐量或最小暂停时间),或尝试找到一个二者的折衷
-
MinorGC尽可能多的收集垃圾对象。我们把这个称作MinorGC原则,遵守这一原则可以降低应用程序FullGC 的发生频率。FullGC 较耗时,是应用程序无法达到延迟要求或吞吐量的罪魁祸首
-
堆大小调整的着手点、分析点:
-
统计Minor GC 持续时间
-
统计Minor GC 的次数
-
统计Full GC的最长持续时间
-
统计最差情况下Full GC频率
-
统计GC持续时间和频率对优化堆的大小是主要着手点
-
我们按照业务系统对延迟和吞吐量的需求,在按照这些分析我们可以进行各个区大小的调整
-
-
一般来说吞吐量优先的垃圾回收器:-XX:+UseParallelGC -XX:+UseParallelOldGC,即常规的(PS/PO)
-
响应时间优先的垃圾回收器:CMS、G1
JVM常用参数有哪些?
-
Xms 是指设定程序启动时占用内存大小。一般来讲,大点,程序会启动的快一点,但是也可能会导致机器暂时间变慢
-
Xmx 是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常
-
Xss 是指设定每个线程的堆栈大小。这个就要依据你的程序,看一个线程大约需要占用多少内存,可能会有多少线程同时运行等
-
-Xmn、-XX:NewSize/-XX:MaxNewSize、-XX:NewRatio
-
高优先级:-XX:NewSize/-XX:MaxNewSize
-
中优先级:-Xmn(默认等效 -Xmn=-XX:NewSize=-XX:MaxNewSize=?)
-
低优先级:-XX:NewRatio
-
-
如果想在日志中追踪类加载与类卸载的情况,可以使用启动参数 -XX:TraceClassLoading -XX:TraceClassUnloading
JVM常用性能调优工具有哪些?
-
MAT
-
提示可能的内存泄露的点
-
-
jvisualvm
-
jconsole
-
Arthas
-
show-busy-java-threads
-
https://github.com/oldratlee/useful-scripts/blob/master/docs/java.md#-show-busy-java-threads ####
-
线上排查问题的一般流程是怎么样的?
-
CPU占用过高排查流程
-
利用 top 命令可以查出占 CPU 最高的的进程pid ,如果pid为 9876
-
然后查看该进程下占用最高的线程id【top -Hp 9876】
-
假设占用率最高的线程 ID 为 6900,将其转换为 16 进制形式 (因为 java native 线程以 16 进制形式输出) 【printf '%x\n' 6900】
-
利用 jstack 打印出 java 线程调用栈信息【jstack 9876 | grep '0x1af4' -A 50 --color】,这样就可以更好定位问题
-
-
内存占用过高排查流程
-
查找进程id: 【top -d 2 -c】
-
查看JVM堆内存分配情况:jmap -heap pid
-
查看占用内存比较多的对象 jmap -histo pid | head -n 100
-
查看占用内存比较多的存活对象 jmap -histo:live pid | head -n 100
-
什么情况下,会抛出OOM呢?
-
JVM98%的时间都花费在内存回收
-
每次回收的内存小于2%
满足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以做一些Down之前的操作,比如手动打印Heap Dump。并不是内存被耗空的时候才抛出
系统OOM之前都有哪些现象?
-
每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
-
FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
-
老年代的内存越来越大并且每次FullGC后,老年代只有少量的内存被释放掉
如何进行堆Dump文件分析?
可以通过指定启动参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/app/data/dump/heapdump.hpro 在发生OOM的时候自动导出Dump文件
如何进行GC日志分析?
为了方便分析GC日志信息,可以指定启动参数 【-Xloggc: app-gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps】,方便详细地查看GC日志信息
-
使用 【jinfo pid】查看当前JVM堆的相关参数
-
继续使用 【jstat -gcutil 2315 1s 10】查看10s内当前堆的占用情况
-
也可以使用【jmap -heap pid】查看当前JVM堆的情况
-
我们可以继续使用 【jmap -F -histo pid | head -n 20】,查看前20行打印,即查看当前top20的大对象,一般从这里可以发现一些异常的大对象,如果没有,那么可以继续排名前50的大对象,分析
-
最后使用【jmap -F -dump:file=a.bin pid】,如果dump文件很大,可以压缩一下【tar -czvf a.tar.gz a.bin】
-
再之后,就是对dump文件进行分析了,使用MAT分析内存泄露
-
参考案例: https://www.lagou.com/lgeduarticle/142372.html
线上死锁是如何排查的?
-
jps 查找一个可能有问题的进程id
-
然后执行 【jstack -F 进程id】
-
如果环境允许远程连接JVM,可以使用jconsole或者jvisualvm,图形化界面检测是否存在死锁
线上YGC耗时过长优化方案有哪些?
-
如果生命周期过长的对象越来越多(比如全局变量或者静态变量等),会导致标注和复制过程的耗时增加
-
对存活对象标注时间过长:比如重载了Object类的Finalize方法,导致标注Final Reference耗时过长;或者String.intern方法使用不当,导致YGC扫描StringTable时间过长。可以通过以下参数显示GC处理Reference的耗时-XX:+PrintReferenceGC
-
长周期对象积累过多:比如本地缓存使用不当,积累了太多存活对象;或者锁竞争严重导致线程阻塞,局部变量的生命周期变长
-
案例参考: https://my.oschina.net/lishangzhi/blog/4703942
线上频繁FullGC优化方案有哪些?
-
线上频繁FullGC一般会有这么几个特征:
-
线上多个线程的CPU都超过了100%,通过jstack命令可以看到这些线程主要是垃圾回收线程
-
通过jstat命令监控GC情况,可以看到Full GC次数非常多,并且次数在不断增加
-
-
排查流程:
-
top找到cpu占用最高的一个 进程id
-
然后 【top -Hp 进程id】,找到cpu占用最高的 线程id
-
【printf "%x\n" 线程id 】,假设16进制结果为 a
-
jstack 线程id | grep '0xa' -A 50 --color
-
如果是正常的用户线程, 则通过该线程的堆栈信息查看其具体是在哪处用户代码处运行比较消耗CPU
-
如果该线程是 VMThread,则通过 jstat-gcutil命令监控当前系统的GC状况,然后通过 jmapdump:format=b,file=导出系统当前的内存数据。导出之后将内存情况放到eclipse的mat工具中进行分析即可得出内存中主要是什么对象比较消耗内存,进而可以处理相关代码;正常情况下会发现VM Thread指的就是垃圾回收的线程
-
再执行【jstat -gcutil 进程id】, 看到结果,如果FGC的数量很高,且在不断增长,那么可以定位是由于内存溢出导致FullGC频繁,系统缓慢
-
然后就可以Dump出内存日志,然后使用MAT的工具分析哪些对象占用内存较大,然后找到对象的创建位置,处理即可
-
-
参考案例:面试官:如果你们的系统 CPU 突然飙升且 GC 频繁,如何排查? ####
内存逃逸分析
1, 是JVM优化技术,它不是直接优化手段,而是为其它优化手段提供依据。
2,逃逸分析主要就是分析对象的动态作用域。
3,逃逸有两种:方法逃逸和线程逃逸。
方法逃逸(对象逃出当前方法): 当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其它方法中。 线程逃逸((对象逃出当前线程): 这个对象甚至可能被其它线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量 12345
4,如果不存在逃逸,则可以对这个变量进行优化
4.1. 栈上分配。 在一般应用中,不会逃逸的局部对象占比很大,如果使用栈上分配,那大量对象会随着方法结束而自动销毁,垃圾回收系统压力就小很多。
4.2. 同步消除 线程同步本身比较耗时,如果确定一个变量不会逃逸出线程,无法被其它线程访问到,那这个变量的读写就不会存在竞争,对这个变量的同步措施 可以清除。
4.3. 标量替换。
-
标量就是不可分割的量,java中基本数据类型,reference类型都是标量。相对的一个数据可以继续分解,它就是聚合量(aggregate)。
-
如果把一个对象拆散,将其成员变量恢复到基本类型来访问就叫做标量替换。
-
如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那么程序真正执行的时候将可能不创建这个对象,而改为直接在>栈上创建若干个成员变量。
5,逃逸分析还不成熟。 5.1,不能保证逃逸分析的性能收益必定高于它的消耗。 判断一个对象是否逃逸耗时长,如果分析完发现没有几个不逃逸的对象,那时间就白白浪费了。 5.2,基于逃逸分析的优化手段不成熟,如上面提到的栈上分配,由于hotspot目前的实现方式导致栈上分配实现起来复杂。
6,相关JVM参数 -XX:+DoEscapeAnalysis 开启逃逸分析 -XX:+PrintEscapeAnalysis 开启逃逸分析后,可通过此参数查看分析结果。 -XX:+EliminateAllocations 开启标量替换 -XX:+EliminateLocks 开启同步消除 -XX:+PrintEliminateAllocations 开启标量替换后,查看标量替换情况。
如何进行线上堆外内存泄漏的分析?(Netty尤其居多)
-
JVM的堆外内存泄露的定位一直是个比较棘手的问题
-
对外内存的泄漏分析一般都是先从堆内内存分析的过程中衍生出来的。有可能我们分析堆内内存泄露过程中发现,我们计算出来的JVM堆内存竟然大于了整个JVM的Xmx的大小,那说明多出来的是堆外内存
-
如果使用了 Netty 堆外内存,那么可以自行监控堆外内存的使用情况,不需要借助第三方工具,我们是使用的“反射”拿到的堆外内存的情况
-
逐渐缩小范围,直到 Bug 被找到。当我们确认某个线程的执行带来 Bug 时,可单步执行,可二分执行,定位到某行代码之后,跟到这段代码,然后继续单步执行或者二分的方式来定位最终出 Bug 的代码。这个方法屡试不爽,最后总能找到想要的 Bug
-
熟练掌握 idea 的调试,让我们的“捉虫”速度快如闪电(“闪电侠”就是这么来的)。这里,最常见的调试方式是预执行表达式,以及通过线程调用栈,死盯某个对象,就能够掌握这个对象的定义、赋值之类
-
在使用直接内存的项目中,最好建议配置 -XX:MaxDirectMemorySize,设定一个系统实际可达的最大的直接内存的值,默认的最大直接内存大小等于 -Xmx的值
-
排查堆外泄露,建议指定启动参数: -XX:NativeMemoryTracking=summary - Dio.netty.leakDetection.targetRecords=100-Dio.netty.leakDetection.level=PARANOID,后面两个参数是Netty的相关内存泄露检测的级别与采样级别
-
参考案例: Netty堆外内存泄露排查盛宴 - 美团技术团队
线上元空间内存泄露优化方案有哪些?
-
需要注意的一点是 Java8以及Java8+的JVM已经将永久代废弃了,取而代之的是元空间,且元空间是不是在JVM堆中的,而属于堆外内存,受最大物理内存限制。最佳实践就是我们在启动参数中最好设置上 -XX:MetaspaceSize=1024m -XX:MaxMetaspaceSize=1024m。具体的值根据情况设置。为避免动态申请,可以直接都设置为最大值
-
元空间主要存放的是类元数据,而且metaspace判断类元数据是否可以回收,是根据加载这些类元数据的Classloader是否可以回收来判断的,只要Classloader不能回收,通过其加载的类元数据就不会被回收。所以线上有时候会出现一种问题,由于框架中,往往大量采用类似ASM、javassist等工具进行字节码增强,生成代理类。如果项目中由主线程频繁生成动态代理类,那么就会导致元空间迅速占满,无法回收
-
具体案例可以参见: 一次完整的JVM堆外内存泄漏故障排查记录 - 知乎
java类加载器有哪些?
Bootstrap类加载器
启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,没有父类,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头
Extention 类加载器
扩展类加载器是指Sun公司实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,父类加载器为null,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器 [
](深入理解Java类加载器(ClassLoader)_java classloader-CSDN博客)
Application类加载器
称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。父类加载器为ExtClassLoader,它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
Custom自定义类加载器
应用程序可以自定义类加载器,父类加载器为AppClassLoader
双亲委派机制是什么?
双亲委派机制
双亲委派机制(Parent Delegation Model)是Java类加载器的一种工作机制。它定义了类加载器的层次结构和类加载的行为,保证Java类的安全性和一致性。
在Java中,类加载器(ClassLoader)根据特定的规则来加载类的字节码并将其转换为可执行的Java类。双亲委派机制是指当类加载器在加载某个类时,首先将该请求委派给父类加载器来尝试加载。只有当父类加载器无法加载时,才由当前类加载器来尝试加载。这样的委派关系一直往上追溯,直到达到最顶层的启动类加载器。
双亲委派机制的优势在于保证了类的统一性和安全性。当一个类加载器尝试加载某个类时,它会先检查是否被父类加载器加载过。如果父类加载器已经加载了该类,那么直接返回已加载的类,确保了类的唯一性。同时,通过这种机制可以防止恶意代码通过自定义的类加载器来替换Java核心库中的类,提高了系统的安全性。
总结来说,双亲委派机制采用了一种层次式的类加载器结构,它通过逐级委派的方式保证类加载的一致性和安全性。在使用Java类加载器时,了解双亲委派机制的工作原理对于理解类加载的过程和解决类加载问题非常有帮助。
如何破坏双亲委派模型
(一)双亲委派模型的第一次“被破坏”是重写自定义加载器的loadClass(),jdk不推荐。一般都只是重写findClass(),这样可以保持双亲委派机制.而loadClass方法加载规则由自己定义,就可以随心所欲的加载类
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2发布之前。由于双亲委派模型在JDK 1.2之后才被引入,而类加载器和抽象类java.lang. ClassLoader则在JDK 1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK 1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),在此之前,用户去继承java. lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。
(二)双亲委派模型的第二次“被破坏”是ServiceLoader和Thread.setContextClassLoader()。即线程上下文类加载器(contextClassLoader)。双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,
如果基础类又要调用用户的代码,
那该怎么办呢?线程上下文类加载器就出现了。
-
SPI。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。了有线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
-
线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不同。,所以我们应用该少用getSystemClassLoader()。总之不同的服务使用的可能默认ClassLoader是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免不必要的问题
(三)双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。
GC如何判断对象可以被回收?
-
垃圾收集器(Garbage Collector)会在运行时自动判断对象是否可以被回收。下面是一些常见的对象回收判断策略:
-
引用计数法(Reference Counting):每个对象都有一个引用计数器,当有一个新的引用指向对象时,计数器加1;当引用失效时,计数器减1。当计数器为0时,表示对象不再被引用,可以被回收。但引用计数法无法解决循环引用的情况。
-
可达性分析算法(Reachability Analysis):Java虚拟机使用可达性分析算法来判断对象是否可能被程序引用。从GC Roots(如被活动线程栈引用的对象、静态变量引用的对象等)开始,通过对象之间的引用关系进行遍历,无法到达的对象即为不可达对象,可以被回收。
-
那么GcRoot有哪些?
-
虚拟机栈中引用的对象
-
方法区中静态变量引用的对象。
-
方法区中常量引用的对象
-
本地方法栈中(即一般说的native方法)引用的对象
回收机制是
-
强引用:通过关键字new的对象就是强引用对象,强引用指向的对象任何时候都不会被回收,宁愿OOM也不会回收。
-
软引用:如果一个对象持有软引用,那么当JVM堆空间不足时,会被回收。一个类的软引用可以通过java.lang.ref.SoftReference持有。
-
弱引用:如果一个对象持有弱引用,那么在GC时,只要发现弱引用对象,就会被回收。一个类的弱引用可以通过java.lang.ref.WeakReference持有。
-
虚引用:几乎和没有一样,随时可以被回收。通过PhantomReference持有。
如何回收内存对象,有哪些回收算法?
1.标记-清除(Mark-Sweep)算法
分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
它的主要不足有两个:
-
效率问题,标记和清除两个过程的效率都不高。
-
空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
-
复制算法
为了解决效率问题,一种称为复制(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
复制算法的代价是将内存缩小为了原来的一半,减少了实际可用的内存。现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
-
标记-整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。根据老年代的特点,有人提出了另外一种标记-整理(Mark-Compact)算法,标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
-
分代收集算法
当前商业虚拟机的垃圾收集都采用分代收集(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清理或者标记—整理算法来进行回收。
jvm有哪些垃圾回收器,实际中如何选择?
图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。 新生代收集器(全部的都是复制算法):Serial、ParNew、Parallel Scavenge 老年代收集器:CMS(标记-清理)、Serial Old(标记-整理)、Parallel Old(标记整理) 整堆收集器: G1(一个Region中是标记-清除算法,2个Region之间是复制算法) 同时,先解释几个名词: 1,并行(Parallel):多个垃圾收集线程并行工作,此时用户线程处于等待状态 2,并发(Concurrent):用户线程和垃圾收集线程同时执行 3,吞吐量:运行用户代码时间/(运行用户代码时间+垃圾回收时间) 1.Serial收集器是最基本的、发展历史最悠久的收集器。 特点:单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。 应用场景:适用于Client模式下的虚拟机。 Serial / Serial Old收集器运行示意图
2.ParNew收集器其实就是Serial收集器的多线程版本。 除了使用多线程外其余行为均和Serial收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。 特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。 和Serial收集器一样存在Stop The World问题 应用场景:ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。 ParNew/Serial Old组合收集器运行示意图如下:
3.Parallel Scavenge 收集器与吞吐量关系密切,故也称为吞吐量优先收集器。 特点:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类似)。 该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别) GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。 Parallel Scavenge收集器使用两个参数控制吞吐量:
-
XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
-
XX:GCRatio 直接设置吞吐量的大小。
4.Serial Old是Serial收集器的老年代版本。 特点:同样是单线程收集器,采用标记-整理算法。 应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。 Server模式下主要的两大用途(在后续中详细讲解···):
-
在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用。
-
作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。
Serial / Serial Old收集器工作过程图(Serial收集器图示相同):
5.Parallel Old是Parallel Scavenge收集器的老年代版本。 特点:多线程,采用标记-整理算法。 应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。 Parallel Scavenge/Parallel Old收集器工作过程图:
6.CMS收集器是一种以获取最短回收停顿时间为目标的收集器。
特点:基于标记-清除算法实现。并发收集、低停顿。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
CMS收集器的运行过程分为下列4步:
初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
并发清除:对标记的对象进行清除回收。 CMS收集器的内存回收过程是与用户线程一起并发执行的。 CMS收集器的工作过程图:
CMS收集器的缺点:
-
对CPU资源非常敏感。
-
无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生。
-
因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC。
7.G1收集器一款面向服务端应用的垃圾收集器。
特点如下:
并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
分代收集:G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合:G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。
可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。 G1收集器运行示意图:
关于gc的选择 除非应用程序有非常严格的暂停时间要求,否则请先运行应用程序并允许VM选择收集器(如果没有特别要求。使用VM提供给的默认GC就好)。 如有必要,调整堆大小以提高性能。 如果性能仍然不能满足目标,请使用以下准则作为选择收集器的起点:
-
如果应用程序的数据集较小(最大约100 MB),则选择带有选项-XX:+ UseSerialGC的串行收集器。
-
如果应用程序将在单个处理器上运行,并且没有暂停时间要求,则选择带有选项-XX:+ UseSerialGC的串行收集器。
-
如果(a)峰值应用程序性能是第一要务,并且(b)没有暂停时间要求或可接受一秒或更长时间的暂停,则让VM选择收集器或使用-XX:+ UseParallelGC选择并行收集器 。
-
如果响应时间比整体吞吐量更重要,并且垃圾收集暂停时间必须保持在大约一秒钟以内,则选择具有-XX:+ UseG1GC。(值得注意的是JDK9中CMS已经被Deprecated,不可使用!移除该选项)
-
如果使用的是jdk8,并且堆内存达到了16G,那么推荐使用G1收集器,来控制每次垃圾收集的时间。
-
如果响应时间是高优先级,或使用的堆非常大,请使用-XX:UseZGC选择完全并发的收集器。(值得注意的是JDK11开始可以启动ZGC,但是此时ZGC具有实验性质,在JDK15中[202009发布]才取消实验性质的标签,可以直接显示启用,但是JDK15默认GC仍然是G1)
这些准则仅提供选择收集器的起点,因为性能取决于堆的大小,应用程序维护的实时数据量以及可用处理器的数量和速度。 如果推荐的收集器没有达到所需的性能,则首先尝试调整堆和新生代大小以达到所需的目标。 如果性能仍然不足,尝试使用其他收集器 总体原则:减少STOP THE WORD时间,使用并发收集器(比如CMS+ParNew,G1)来减少暂停时间,加快响应时间,并使用并行收集器来增加多处理器硬件上的总体吞吐量。
JVM8为什么要增加元空间?
原因: 1、字符串存在永久代中,容易出现性能问题和内存溢出。 2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。 3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
JVM8中元空间有哪些特点?
1,每个加载器有专门的存储空间。 2,不会单独回收某个类。 3,元空间里的对象的位置是固定的。 4,如果发现某个加载器不再存货了,会把相关的空间整个回收
如何解决线上gc频繁的问题?
-
查看监控,以了解出现问题的时间点以及当前FGC的频率(可对比正常情况看频率是否正常)
-
了解该时间点之前有没有程序上线、基础组件升级等情况。
-
了解JVM的参数设置,包括:堆空间各个区域的大小设置,新生代和老年代分别采用了哪些垃圾收集器,然后分析JVM参数设置是否合理。
-
再对步骤1中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法比较容易排查。
-
针对大对象或者长生命周期对象导致的FGC,可通过 jmap -histo 命令并结合dump堆内存文件作进一步分析,需要先定位到可疑对象。
-
通过可疑对象定位到具体代码再次分析,这时候要结合GC原理和JVM参数设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论。
内存溢出的原因有哪些,如何排查线上问题?
-
java.lang.OutOfMemoryError: ......java heap space..... 堆栈溢出,代码问题的可能性极大
-
java.lang.OutOfMemoryError: GC over head limit exceeded 系统处于高频的GC状态,而且回收的效果依然不佳的情况,就会开始报这个错误,这种情况一般是产生了很多不可以被释放的对象,有可能是引用使用不当导致,或申请大对象导致,但是java heap space的内存溢出有可能提前不会报这个错误,也就是可能内存就直接不够导致,而不是高频GC.
-
java.lang.OutOfMemoryError: PermGen space jdk1.7之前才会出现的问题 ,原因是系统的代码非常多或引用的第三方包非常多、或代码中使用了大量的常量、或通过intern注入常量、或者通过动态代码加载等方法,导致常量池的膨胀
-
java.lang.OutOfMemoryError: Direct buffer memory 直接内存不足,因为jvm垃圾回收不会回收掉直接内存这部分的内存,所以可能原因是直接或间接使用了ByteBuffer中的allocateDirect方法的时候,而没有做clear
-
java.lang.StackOverflowError - Xss设置的太小了
-
java.lang.OutOfMemoryError: unable to create new native thread 堆外内存不足,无法为线程分配内存区域
-
java.lang.OutOfMemoryError: request {} byte for {}out of swap 地址空间不够