概念:JVM垃圾回收机制是Java虚拟机管理内存的重要部分,其主要任务是自动检测并释放不再使用的对象所占用的内存空间。
堆内存(Heap)
:这是GC的主要工作区域,所有由new创建的对象都在这里分配内存。堆内存进一步划分为年轻代(Young Generation)和老年代(Old Generation)。
引用计数器法:为每个对象创建一个引用计数,有对象引用时+1,引用被释放时-1,当计数器为0时,就可以回收了。
在使用引用计数器法的系统中,每个Java对象实例都会有一个关联的引用计数器。当一个对象被创建时,其引用计数器初始化为1;每当有新的引用指向该对象时,引用计数器加1;而当某个引用离开作用域或赋值为null,即不再引用该对象时,引用计数器减1。
优点:
缺点:
无法处理循环引用问题。例如,如果两个对象互相引用对方但除此之外没有其他对象引用它们,引用计数器永远不会为0,导致这两个对象无法被正确回收,从而造成内存泄漏。
因为需要频繁更新计数器,在多线程环境下可能会引入同步开销。
对于大量琐碎的小对象,维护引用计数器的成本可能较高。
实际应用: 现代Java虚拟机并没有采用引用计数器算法作为主要的垃圾回收机制,而是采用了更复杂的如可达性分析(根搜索算法)、分代收集、并发标记清除以及压缩等技术来提高垃圾回收效率和准确性,并避免了引用计数器方法存在的问题。尽管如此,某些编程语言和环境中仍然使用引用计数器作为内存管理的一种策略。
早期版本的Python、苹果公司的Objective-C和Swift语言等使用或曾经使用引用计数器,但大多数现代高级语言的内存管理系统往往结合多种垃圾回收算法以解决引用计数器法无法处理循环引用等问题,确保更准确、高效的内存管理。
可达性分析算法:JVM的可达性分析算法是一种用于确定对象是否可被回收的垃圾收集机制。该算法基于“对象引用”的概念,通过一系列称为GC Roots的对象作为起始点,来判断一个对象在程序执行期间是否还可能被访问。
工作原理:
**可达性分析算法结合了分代收集策略和其他优化手段,在实际应用中极大地提高了JVM管理内存的效率和准确性。**随着技术的发展,JVM中的垃圾收集器也在不断演进,如HotSpot JVM就采用了多种不同的垃圾收集器实现,并且在可达性分析的基础上引入了如并发标记、压缩整理等更复杂的技术,以进一步降低停顿时间和提高吞吐量。
最基础的垃圾回收算法,主要应用于Java虚拟机(JVM)中。该算法包括两个阶段:标记和清除。
垃圾回收器:
优缺点分析:
优点:
缺点:
一种垃圾回收策略,主要用于解决内存碎片问题。
垃圾回收器:通常应用于年轻代的垃圾回收,如垃圾回收器Serial、ParNew和Parallel Scavenge。
工作原理: 复制算法将堆内存划分为两个或多个相同大小的区域,一般称为From空间和To空间(或者Eden区与Survivor区)。当对象创建时,首先分配到From空间。
优缺点分析:
优点:
缺点:
为了减少复制带来的额外开销,一些JVM实现会采用 Survivor 空间的设计,在多次新生代GC后仍存活的对象会被晋升到老年代,从而降低频繁复制的成本。同时,老年代通常采用其他的垃圾回收算法,例如标记-压缩、分代收集等。
一种垃圾回收算法,主要用于解决标记-清除算法遗留的内存碎片问题。该算法结合了“标记”和“整理”两个步骤。
垃圾回收器:通常应用于老年代的垃圾回收,如垃圾回收器Parallel Old、CMS和G1。
1. 标记阶段(Mark) 与标记-清除算法类似,首先从一组称为GC Roots的对象开始遍历整个对象图,标记所有可达的对象为“存活”。未被标记的对象即被视为垃圾。
2. 整理阶段(Compact) 不同于标记-清除直接删除未被标记的对象,标记-整理算法在标记完成后,会将所有存活的对象向一端移动,并对不连续的空白区域进行合并。这样可以使得存活对象紧凑地排列在一起,而不再使用中的内存空间则聚集到一起,形成一个或多个大的连续空闲区域。
优缺点分析:
优点:
缺点:
在Java虚拟机中,如CMS收集器的老年代部分采用的就是基于标记-清除算法,而G1垃圾收集器虽然不是严格意义上的标记-整理算法,但在其混合回收阶段也会进行类似于整理的操作,以达到消除内存碎片的目的。此外,ZGC和Shenandoah等新一代垃圾收集器也采用了不同的手段来避免内存碎片并减少STW(Stop-The-World)停顿时间。
分代(Generation-based)垃圾回收策略是Java虚拟机中广泛采用的一种内存管理方式,其核心思想是根据对象的生命周期特性将堆内存划分为不同的区域或“代”进行管理。通常JVM将堆内存分为年轻代(Young Generation)、老年代(Old Generation)和永久代/元空间(PermGen/Metaspace)。
1. 年轻代(Young Generation)
2. 老年代(Old Generation)
3. 永久代/元空间(PermGen/Metaspace)
Region-based收集器(如G1)
总的来说,分代垃圾回收策略利用了大多数对象生命周期较短的特点,通过针对性地在不同代之间执行垃圾回收,优化了内存管理和垃圾回收性能。而Region-based垃圾回收策略在此基础上进一步细化了内存管理单元,提供了更灵活、高效的垃圾回收机制。
在Java虚拟机(JVM)的垃圾回收(Garbage Collection, GC)过程中,三色标记法是一种经典的垃圾检测算法,用于识别哪些对象是可达的(即活跃的),哪些对象是不可达的(即垃圾)。以下是三色标记法在JVM垃圾回收中的基本使用流程:
三色标记法能有效避免对象遗漏和重复标记的问题,通过精确地追踪对象的可达性状态,使得JVM能够高效地回收内存,保持程序运行时有足够的可用内存空间。在具体的垃圾回收器实现中,如CMS、G1等,会对三色标记法进行变种或扩展,以适应各自的特点和优化目标。
Java垃圾回收器(Garbage Collector, GC)是Java虚拟机(JVM)的重要组件,用于自动管理内存资源,减轻程序员手动管理内存的负担。它主要用于回收堆内存中不再被任何引用变量所指向的对象所占用的空间,以便释放这部分空间供后续对象分配使用。
在Java的发展历程中,随着JVM技术的演进,出现了多种不同的垃圾回收器,以适应不同的应用场景和性能需求。以下是一些Java中的关键垃圾回收器:
新生代:老年代 = 1:2
新生代使用复制-算法,本身也分三个区,Eden、To Survivor、From Survivor。默认8:1:1
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄都+1,当年龄达到15(默认的)时,升级为老年代,大的对象直接放入老年代。
老年代这边,当空间占用达到某一个阈值之后,触发Full GC,此时一般使用标记整理算法。
对象优先分配到新生代的Eden区,Eden区空间不够时,进行Minor GC,还不够,则分配到老年代。
Minor GC非常频繁,回收速度也快。大对象(需要大量连续内存空间的对象)直接进入老年代。
适用场景:作为CMS或G1的备用老年代收集器,或者用于Client模式下的JVM。
特性:Serial Old是Serial GC的老年代版本,采用标记-压缩算法,同样采用单线程进行垃圾回收。
适用场景:关注吞吐量并且内存较大的应用。
特性:Parallel Old是Parallel Scavenge的老年代版本,使用多线程进行老年代的垃圾回收,采用标记-压缩算法,目的是提高大堆内存下系统的吞吐量。
适用场景:实时性要求高,对响应时间敏感的服务端应用。
特性:CMS是一种低延迟、并发的垃圾回收器,主要用于老年代的回收。它分为四个阶段:初始标记、并发标记、重新标记和并发清除,尝试在应用不完全停止的情况下完成大部分垃圾回收工作。
CMS 工作流程
适用场景:大规模应用,大堆内存,对停顿时间有一定要求。
特性:G1是一种整体化的垃圾回收器,将整个堆划分为多个大小固定的区域。针对大型堆设计的一种分代收集器,能够进行并行的全局并发标记和局部并发清理。它实现了预测性停顿时间和部分并发收集,目标是简化管理和提供一致的性能表现。
G1 工作流程
适用场景:类似ZGC,追求低延迟,适用于大型应用和云环境。
特性:Shenandoah也致力于降低GC停顿时间,引入了并发的“跨代引用处理”和“并发压缩”技术,可以在不显著增加处理器负载的情况下,实现实质上的并发清理和压缩,从而极大地缩短了STW时间。
Win10 系统
Linux 系统:
这里我们查看一下21448这个进程的信息:jinfo 21448
在VM Flags中我们可以看到JVM的配置信息
使用 jinfo -flags 可以仅查看JVM配置信息
这里我们可以看到 JDK17 默认使用的垃圾回收器是G1
而 JDK8 默认使用的垃圾回收器是Parallel Scavenge+Parallel Old
Linux:
jinfo -flags [进程号] 查看JVM参数
常用参数
jstack 21448
jmap -histo (进程号)
jmap -histo 21448 | head -20 :20表示取前20行
仅Linux系统中可使用head命令,在Windows 10系统中,由于没有内置的head
命令,可以通过PowerShell或组合使用其他Windows命令来模拟查看文件头几行的功能。
先将jmap
的输出重定向到临时文件,然后使用more
或find
命令查看前20行:
jmap -histo:live 21448 > temp.txt
more +1 temp.txt
在more
命令中,+1
是为了跳过标题行(如果有的话),然后按回车键逐页查看,直到看到前20行。
注意:JMAP命令不能再生产环境直接执行,会STW拿出当前进程所有对象信息
Linux:
导出文件之后,可以使用jdk自带的文件分析工具
图形界面工具jvisualvm
top -Hp 进程号 查看当前进程中所有线程的CPU和内存占用情况
top
或htop
命令查看当前系统中CPU占用最高的进程(PID)。ps
、pgrep
或者直接在top
结果中找到与Java相关的进程。top -H -p PID
命令来查看该进程中各个线程的资源占用情况。pidstat
命令监视指定进程及其线程的CPU使用情况。jstack
命令获取Java进程的堆栈跟踪信息:jstack PID > thread_dump.txt
,这会输出所有线程的状态和调用堆栈。VisualVM
、JProfiler
或其他Java性能分析工具进一步深入分析CPU热点和瓶颈。下载地址:https://arthas.aliyun.com/
简介:Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。
直接通过java -jar 启动arthas的jar包文件
选择应用 java 进程:jvm-test 进程是第 1 个,则输入 1,再输入回车/enter
。Arthas 会 attach 到目标进程上,并输出日志:
.java
文件为.class
文件.class
文件,redefine 到 JVM 里.class
文件,retransform 到 JVM 里输入 dashboard,按回车/enter
,会展示当前进程的信息,按ctrl+c
可以中断执行。
可以看到进程里面有哪些线程,每个线程的状态、吃CPU的情况等。
在Memory中我们可以看到内存的占用情况:
thread pid
会打印线程 ID pid 的栈,通常pid 1是 main 函数的线程。
thread -b 寻找死锁
我们在garbage collectors(GC)里面可以看到这里垃圾回收的统计情况
monitor
监控方法的执行情况监控com.example.jvm.controller.TestController
类的 “getStr”方法 ,并且每5S更新一次状态。
monitor com.example.jvm.controller.TestController getStr -c 5
监控的维度说明
监控项 | 说明 |
---|---|
timestamp | 时间戳 |
class | Java类 |
method | 方法(构造方法、普通方法) |
total | 调用次数 |
success | 成功次数 |
fail | 失败次数 |
rt | 平均耗时 |
fail-rate | 失败率 |
watch
:检测函数返回值方法执行数据观测,让你能方便的观察到指定方法的调用情况。
能观察到的范围为:返回值
、抛出异常
、入参
,通过编写OGNL 表达式进行对应变量的查看。
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
express | 观察表达式 |
condition-express | 条件表达式 |
[b] | 在方法调用之前观察before |
[e] | 在方法异常之后观察 exception |
[s] | 在方法返回之后观察 success |
[f] | 在方法结束之后(正常返回和异常返回)观察 finish |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[x:] | 指定输出结果的属性遍历深度,默认为 1 |
这里重点要说明的是观察表达式,观察表达式的构成主要由ognl 表达式组成,所以你可以这样写"{params,returnObj}",只要是一个合法的 ognl 表达式,都能被正常支持。
特别说明
通过watch命令可以查看函数的参数/返回值/异常信息。
watch com.example.jvm.controller.UserController list returnObj
trace
:根据路径追踪,并记录消耗时间对方法内部调用路径进行追踪,并输出方法路径上的每个节点上耗时。
简介:
trace 命令能主动搜索 class-pattern/method-pattern 对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。
观察表达式的构成主要由ognl 表达式组成,所以你可以这样写"{params,returnObj}",只要是一个合法的 ognl 表达式,都能被正常支持。
很多时候我们只想看到某个方法的rt大于某个时间之后的trace结果,现在Arthas可以按照方法执行的耗时来进行过滤了,例如trace *StringUtils isBlank '#cost>100’表示当执行时间超过100ms的时候,才会输出trace的结果。
watch/stack/trace这个三个命令都支持#cost耗时条件过滤。
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达匹配 |
method-pattern | 方法名表达式匹配 |
condition-express | 条件表达式,使用OGNL表达式 |
[E] | 开启正则表达式匹配,默认是通配符匹配 |
[n:] |
设置命令执行次数 |
#cost |
方法执行耗时,单位是毫秒 |
案例:
# trace函数指定类的指定方法
trace com.example.jvm.controller.UserController list
# 在浏览器上进行登录操作,检查最耗时的方法
trace *.DispatcherServlet *
tt
:时间隧道,记录多个请求time-tunnel 时间隧道。
记录下指定方法每次调用的入参和返回信息,并能对这些不同时间下调用的信息进行观测
参数解析:
tt的参数 | 说明 |
---|---|
-t | 记录某个方法在一个时间段中的调用 |
-l | 显示所有已经记录的列表 |
-n 次数 | 只记录多少次 |
-s 表达式 | 搜索表达式 |
-i 索引号 | 查看指定索引号的详细调用信息 |
-p | 重新调用:指定的索引号时间碎片 |
案例:
# 最基本的使用来说,就是记录下当前方法的每次调用环境现场。
tt -t com.example.jvm.controller.UserController list
模拟报错:
@Operation(summary = "业务接口模拟测试")
@Parameters({
@Parameter(name = "str",description = "字符串参数",in = ParameterIn.QUERY),
})
@GetMapping("work")
public ResponseEntity<String> work(@RequestParam("str") String str){
if (str.equals("1")){
throw new RuntimeException("异常");
}
testService.work1();
testService.work2();
testService.work3();
return ResponseEntity.ok().body("success");
}
public void work1() {
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
log.info("work1");
}
public void work2() {
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
log.info("work2");
}
public void work3() {
try {
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
log.info("work3");
}
# 对现有记录进行检索
tt -l
# 需要筛选出 `primeFactors` 方法的调用信息
tt -s 'method.name=="getStr"'
# 查看某条记录详细信息
tt -i 1007
可以在不停止项目的情况下,修改java文件,通过javac 类名.java编译 再通过redefine 定义class上传到远程
我在Linux上放了一个小程序,输出zyw.
# 编辑T.java文件
vim T.java
# 编译T.java生成T.class文件
javac T.java
# 启动arthas 绑定TestMain进程
java -jar arthas-boot.jar
# 重新定义T.class 文件
redefine T.class
如果只是退出当前的连接,可以用quit
或者exit
命令。Attach 到目标进程上的 arthas 还会继续运行,端口会保持开放,下次连接时可以直接连接上。
如果想完全退出 arthas,可以执行stop
命令。