JVM问题排查和垃圾回收机制
JVM线程共享区
JVM内存区主要分为5个区域:
- 程序计数器:每条线程都有一个程序计数器,它的作用是记住下一条 JVM 要执行的指令的地址。
- 虚拟机栈:每条线程都有一个私有的虚拟机栈,它的作用是存储局部变量、操作数栈、动态链接、方法出口等信息。
- 本地方法栈:与虚拟机栈相似,用于Native方法。
- 方法区:用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 堆:用于存储对象实例,数组等,是垃圾回收器管理的主要区域。
其中,方法区和堆是所有线程共享的,是实现线程通信和协作的关键。
哪些可以作为GC Roots
GC Roots是垃圾回收器的起点,直接或间接地与GC Roots相关的对象就不会被回收。主要的GC Roots有:
- 本地变量表中引用的对象
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
通过以上GC Roots,垃圾回收器可以遍历整个JVM里所有的存活对象,实现垃圾回收。
如何排查JVM问题
当出现JVM问题如内存溢出时,可以通过以下步骤进行排查:
- 查看GC日志,分析GC的次数、时间以及回收的内存大小,判断是否是内存泄漏导致的溢出。
- 使用JDK自带的JVM监控工具jvisualvm查看内存情况,判断增长最快的区域以及对象。
- 使用Arthas等诊断工具,通过oom命令分析内存溢出时的堆栈信息。
- 通过btrace或者Arthas追踪某个对象的引用链,判断是否有循环引用导致无法回收。
- 使用jmap命令dump出内存镜像,通过MAT、Eclipse Memory Analyzer等工具分析对象关系,找到内存泄漏的对象。
- 采样命令如numastat、async-profiler等分析线程状态,判断是否有线程阻塞导致的内存占用过高。
- 检查项目的资源释放情况,如数据库连接、文件句柄等,确保及时关闭可以释放的资源。
JVM参数调优
JVM的参数调优是提高JVM性能的重要手段,常见的优化参数有:
- -Xms和-Xmx:设置JVM堆的初始大小和最大大小,避免每次GC的时候重新分配内存。
- -XX:NewRatio:设置年轻代和老年代的大小比例,默认为2,可选范围1-n。
- -XX:SurvivorRatio:设置年轻代中Eden区与Survivor区的大小比例,默认为8,可选范围1-n。
- -XX:MaxTenuringThreshold:设置对象进入老年代的年龄,可选范围0-15,默认为15。
- -XX: UseG1GC:使用G1垃圾回收器,它可以并行、并发地回收年轻代和老年代内存。
- -XX:ParallelGCThreads:设置垃圾收集器使用的CPU数量,一般设置为CPU核数。
- -XX: DisableExplicitGC:关闭System.gc(),避免人为触发GC。
- -XX: PrintGCDetails:输出详细的GC日志,用于分析GC情况。
这些参数的设置会根据具体项目的内存情况和需求进行调整,需要结合GC日志和监控工具综合分析,达到以下目标:
- 减少GC次数:通过加大堆空间和提高对象进入老年代的年龄,尽量减少Minor GC的次数。
- 平滑GC曲线:避免突发的长时间Full GC,通过并行GC和分代GC实现渐进式GC。
- 最短GC停顿时间:通过并行GC、G1等新一代GC算法和调整线程数量实现最短GC停顿。
- 达到60%的内存利用率:通过监控内存使用率,确定Xms和Xmx的设置,避免 overtunning。
JVM与垃圾回收器
JVM的主要内存区有方法区、堆、栈、程序计数器等,堆和方法区是所有线程共享的,其中堆主要用于存储对象实例,是一个显著的可回收内存来源。垃圾回收器的主要工作就是规律的遍历堆区,释放那些不再被使用的对象所占用的内存,使其成为可用内存。
常见的垃圾回收器有:
- Serial:单线程回收器,会暂停所有的用户线程进行回收,回收完成后才会重新启动用户线程,可能导致较长的GC停顿时间,但简单高效。
- Parallel:多线程回收器,同时使用多个CPU核心进行垃圾回收以减少停顿时间,在堆内存大于100MB时会自动触发Parallel回收器。
- CMS:并发标记清除,用户线程与回收线程同时执行,但会产生大量的内存碎片,且当老年代空间用完时会变成Serial回收,会导致较长的停顿时间。
- G1:G1垃圾回收器将堆内存划分为多个大小相等的Region,并在回收时不需要停顿整个JVM,可以实现最短回收停顿时间和配合指定的内存占用量来达到可控的GC时间。它是Java 9默认的垃圾回收器。
对于系统吞吐量和响应时间都比较高的服务,推荐使用G1或CMS垃圾回收器,通过「-XX: UseG1GC」或「-XX: UseConcMarkSweepGC」设置。并且可以设置以下参数:
- -XX:ParallelGCThreads:设置CMS线程数量,一般与CPU核数相等。
- -XX:MaxGCPauseMillis:设置GC的最大停顿时间,CMS会尽量不超过这个值,默认是200ms。
- -XX:InitiatingHeapOccupancyPercent:设置CMS触发的堆内存占用百分比,默认是80%。
联合GC日志分析,tuning的GC参数,选择合适的垃圾回收器是运维一个高性能JAVA应用的关键所在。
如何选择垃圾回收器:
- 如果应用对吞吐量要求较高,对响应时间较不敏感,可以选择Serial或Parallel回收器。
- 如果应用是CPU密集型应用,同时对吞吐量和响应时间都有一定要求,可以选择CMS回收器,并适当调高线程数。
- 如果应用对响应时间比较敏感,而且有较大的堆内存(超过4GB),应选择G1垃圾回收器。
- 如果应用带有宿主,则必须使用CMS回收器。
- 如果应用要求实现最短的GC停顿时间并且GC过程可控,应选择G1垃圾回收器,并指定MaxGCPauseMillis时间。
所以,选择合适的垃圾回收器需要综合考虑应用的类型以及对各项性能指标如吞吐量、响应时间、内存占用的需求,并根据具体应用场景选择最优的收集器配置。这也是构建一个高性能Java应用的关键所在。
Java垃圾回收机制
Java垃圾回收主要是利用垃圾回收器对堆内存进行回收,回收那些不再被使用的对象所占用的内存,以实现内存的再利用。
Java垃圾回收机制主要包括:
- Reachability Roots:这些对象直接或间接地被虚拟机栈或本地方法栈中的引用变量引用,它们是回收的起始点。
- 可达性分析:从GC Roots开始对堆内存进行搜索,搜索过程遇到的对象被标记为可达对象。
- 写屏障:写入引用时插入一个屏障,这个屏障会记录此时的引用指向对象的标记状态,并在标记阶段使用。
- 标记-清除:标记出可达对象,之后清除未标记的对象,这会产生大量内存碎片。
- 标记-复制:将内存分为两块,每次只用其中一块。标记可达对象后,将存活对象复制到另一块内存,然后清除上一块内存。
- 标记-整理:标记可达对象后,对未标记对象进行清理,并对存活对象进行整理,使它们在一端连续。
其中,[标记-清除]算法会产生内存碎片较严重,而[标记-复制]算法需要双倍的内存空间,所以目前主流的商业虚拟机一般采用[标记-整理]算法。
虽然我们主要关注标记-清除与标记-整理算法,但其他算法的理解也有助于我们深入理解Java的垃圾回收机制。垃圾回收是Java虚拟机实现自动内存管理的关键,必须理解透彻。
标记-清除与标记-整理算法
标记-清除算法
标记-清除算法主要分为两个阶段:
- 标记阶段:从GC Roots开始对堆内存进行可达性分析,标记出所有可达对象。
- 清除阶段:清除未标记的对象,释放内存。
该算法的实现过程如下:
- GC Roots向下搜索,标记可达对象。
- 重复1,直到没有新的可达对象产生。
- 清除未标记的对象,实现内存回收。
- 可用内存变为未标记对象占用的内存空间。
该算法的主要缺点是会产生大量内存碎片,降低内存利用效率。
标记-整理算法
标记-整理算法通过对象迁移的方式解决了标记-清除算法的内存碎片问题,其实现过程如下:
- 标记阶段:与标记-清除算法相同,标记出所有可达对象。
- 整理阶段:将所有存活对象向一端移动,然后清除端边界以外的内存。
- 设置一个指针,指向未清理的内存起始地址,作为下次分配内存时使用。
- 可用内存变为未标记对象占用的内存空间,没有产生内存碎片。
该算法通过对象迁移的方式,实现了内存的整理和压缩,节约了内存空间并消除了内存碎片。其实现过程也相对复杂一些,效率略低于标记-清除算法。
所以,标记-清除算法实现简单但会产生较多内存碎片,标记-整理算法实现相对复杂但可以有效解决内存碎片问题。理解两种算法的实现原理,有助于我们选择和调优合适的垃圾回收器,构建高性能的Java应用。