JVM 调优
冯诺依曼结构计算机体系
程序的栈、栈帧和堆
- 栈
- 每个线程都会维护一个栈(线程栈)
- 栈空间会自动释放
- 栈帧(stack frame)
- 堆叠在栈里
- 一个方法结束就会释放一个栈帧,释放后栈顶指针对下移到下一个栈帧
- 堆
- 程序执行时动态分配内容的空间
- 若不合理使用堆空间,则会导致堆空间被占满
- 因此如何对堆空间进行管理成为主要问题
最难调试的 bug
- 野指针
- 两个指针同一个对象,一个指针释放对象后,另一个指针即为野指针
- 该野指针也许未指向对象,也许指向后来新创建在该野指针指向地址的对象
- 野指针产生的情况
- 指针未初始化:任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,可能会指向任意空间。因此,指针在初始化时要么指向一个合理的地址,要么初始化为NULL。
- 指针所指向的地址空间已经被释放:如果指针指向的内存已经被释放,但是指针本身没有被置为NULL,那么这个指针就会成为一个野指针。在C语言中,使用free()函数释放内存后,应当将指针置为NULL。
- 指针操作超越了作用域:如果指针操作超越了变量的作用范围,那么这个指针就会成为一个野指针。例如,如果一个指针在某个作用域内被定义,但是在这个作用域结束之后仍然被使用,那么这个指针就会成为一个野指针。
- 会出现的异常:NullPointerException
- 两个指针同一个对象,一个指针释放对象后,另一个指针即为野指针
- 并发问题
- 多线程访问同一块内存空间
- 会出现数据不一致等问题
编程语言发展史
- c/c++
- 手工管理内存 malloc free / new delete
- 若不合理释放内存,会导致内存泄漏(memory leak),内存泄漏过多会导致内存溢出(out of memory)
- 在多线程的情况下,甚至会导致数据异常,一个线程释放了另一个线程还在使用的内存空间
- 运行效率高,开发效率低
- Java、python、go
- 引入 GC(garbage collector),GC负责回收内存,方便内存管理
- GC在进行垃圾回收时会占用 CPU,因此会导致执行效率低
- 开发效率高
- rust
GC(garbage collector)
如何找到 garbage
- 引用计数法
- 根可达算法(root searching) (Java)
GC Algorithms(垃圾回收算法)
mark-sweep(标记清除)
- 通过根可达算法将找到的 garbage 进行标记,然后回收
- 会导致内存碎片化严重
copying(拷贝)
- 将内存分为两半,只使用一半的内存,在使用的一半内存中将找到的非 garbage 复制到另一半内存并整理好,再将前一半内存进行回收
- 解决内存碎片化,但是浪费了大部分内存
mark-compact(标记压缩)
- 将找到的 garbage 进行标记,然后回收并对非 garbage 的内存整理成连续的
- 解决内存碎片化,效率低
将以上三种垃圾回收算法综合使用,就诞生了各种各样的垃圾回收器(GC)
JVM 的 GC 历史
从 jdk 1.0 到 jdk 14 一共诞生了 10 中垃圾回收器
GC 的演化
GC 的演化是随着内存的不断增大而演化
几兆——几十兆内存
- Serial 组合: 单线程 stw(stop-the-world)垃圾回收新生代(Serial )、老年代(Serial Old)
几十兆——几百兆内存
- parallel 组合: 并行多线程垃圾回收新生代(parallel scavenge)、老年代(parallel old)—— jdk1.8 默认使用的 GC
几G内存
线程是不是越多越好
线程的数量并不是越多越好,这取决于具体的应用场景和需求。以下是一些考虑因素:
- 性能:在多核CPU系统中,多个线程可以并行处理任务,提高整体性能。但是,如果线程数量过多,会导致上下文切换频繁,反而降低性能。
- 资源:每个线程都需要占用一定的内存和CPU资源。如果线程数量过多,会导致资源过度消耗,反而降低系统的整体性能。
- 复杂性:线程的数量增加,会使得程序的复杂性和调试难度增加。需要更多的考虑线程同步、数据一致性和死锁等问题。
- 实际需求:在某些应用场景中,如Web服务器、任务队列等,可以受益于大量的线程。而在其他场景中,如计算密集型任务或I/O密集型任务,线程的数量可能并不是关键因素。
因此,在选择线程的数量时,需要根据实际需求和应用场景进行权衡和测试。在许多情况下,合理的线程数量是几十到几百个,但具体的数量取决于具体的系统和任务特性。
concurrent GC:CMS (Concurrent Mark Sweep) 效率低
在Java中,CMS(Concurrent Mark Sweep)垃圾收集器是一种用于减少垃圾收集暂停时间的垃圾收集器。它的目标是降低在GC(Garbage Collection,垃圾回收)期间系统停顿的时间,使得应用程序在GC期间能够并发执行。
CMS垃圾收集器的工作流程大致可以分为四个阶段:
- 初始标记(Initial Mark):这个阶段会标记出所有直接可达的对象。这个阶段是STW(Stop-The-World)的,意味着在这个阶段,所有的应用程序线程都会被暂停。
- 并发标记(Concurrent Mark):在这个阶段,垃圾收集器会并发地遍历堆中的对象图,找出并标记出所有可达的对象。这个阶段不会停止应用程序线程。
- 重新标记(Remark):这个阶段会修正并发标记阶段中产生的任何遗漏的标记,因为这个阶段也是STW的,所以在这个阶段,所有的应用程序线程都会被暂停。
- 并发清除(Concurrent Sweep):在这个阶段,垃圾收集器会清除所有未被标记的对象。和并发标记阶段一样,这个阶段也是并发的,不会停止应用程序线程。
尽管CMS垃圾收集器可以在应用程序运行时进行垃圾收集,但是它并不能完全消除GC停顿。在某些情况下,如老年代空间不足时,CMS仍然需要进行Full GC,这时可能会导致长时间的停顿。
为了进一步降低GC停顿的影响,Java 9引入了新的垃圾收集器ZGC(Z Garbage Collector)和Shenandoah。ZGC和Shenandoah都采用了读屏障(Read Barrier)技术,可以在并发运行时追踪对象的引用变化,从而在不需要完全暂停应用程序的情况下完成垃圾收集。
三色标记算法
用在并发标记阶段
三色标记算法是一种图搜索算法,用于在插入排序、朴素和快速排序等时间复杂度算法中替代其他算法。该算法基于模式化技术,可以将一个连通图的所有点标记。
在三色标记算法中,所有对象(节点)被划分为三类,分别用日常中的黑白灰三种颜色来表示每一类型的节点集合。具体来说,节点的状态如下:
- 白色:表示可达性分析初始阶段所有新创建的对象(节点)默认标记成白色状态,表示还没有被GC扫描过。
- 灰色:表示该节点至少还存在一个引用没有被扫描过,或者说是正在进行标记的节点,会被标记成灰色节点,这是一种中间状态,最终会被标记成黑色或者停留在白色状态。
- 黑色:表示已经被GC扫描过的节点标记成黑色节点,此时黑色节点就是存活对象,不能被GC回收,黑色节点此时是安全的。
如果存在黑色对象指向了白色对象或者灰色对象取消了对白色对象的引用,可能会出现漏标问题,即把原来存活的对象当作垃圾回收了。
ParNew:配合 CMS 使用,新生代 GC
内存越来越大
- G1
- ZGC
- Shenandoah
JVM 分代模型(jdk1.8之前使用)
- 堆内存逻辑分区
- 新生代
- GC 频繁,多次未被回收的内存对象将被放入老年代
- 新生代使用 copying 算法进行垃圾回收
- 老年代
- 由于老年代中存放的内存对象都是存活比较久的,因此 GC 不会太频繁
- 老年代使用 mark-sweep算法 或 mark-compact算法 或 mark-sweep 与 mark-compact 组合的算法进行垃圾回收
- 新生代
- 堆内存逻辑分区
垃圾回收机制
- 新生代与老年代(old)内存比例:1:3
- 新生代分为 eden 区 和两块 survivor 区,比例为 8:1:1
- 使用 copying 算法进行垃圾回收
- 找到的在 Eden 区存活的内存对象拷贝到某一个 survivor 区,然后清空 Eden 区
- 找到的在某一个 survivor 区存活的内存对象拷贝到另外一个 survivor 区,然后清空之前的 survivor 区
- 当 survivor 区的内存对象存活达到一定次数时,则拷贝到老年代区
- 当老年代内存占用完后,则会触发 full gc
查看 Java 使用的 GC 版本
java -XX:+PrintCommandLineFlags --version
JVM 调优实战
- 首先确定是的是哪个垃圾回收器