JVM面试基本常问问题(可结合实际项目业务回答)

JVM

JVM的一些参数想·

JVM(Java虚拟机)是运行Java程序的关键组件之一,它提供了一系列的参数用于配置和调优Java应用程序的运行环境。以下是一些常见的JVM参数:

  1. -Xmx:设置JVM堆内存的最大值。例如,-Xmx2G 表示将堆内存最大限制设置为2GB。

  2. -Xms:设置JVM堆内存的初始值。例如,-Xms512m 表示将堆内存初始大小设置为512MB。

  3. -Xss:设置每个线程的栈大小。默认值因操作系统而异。

  4. -XX:PermSize 和 -XX:MaxPermSize:设置永久代(Permanent Generation)的初始大小和最大大小。在Java 8及以后版本中,永久代已被元数据区代替。

  5. -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize:设置元数据区(Metaspace)的初始大小和最大大小。在Java 8及以后版本中取代了永久代。

  6. -Xmn:设置年轻代(Young Generation)的大小。

  7. -XX:SurvivorRatio:设置年轻代中Eden区和Survivor区的比例。默认值为8,即Eden区和每个Survivor区的大小比例为8:1:1。

  8. -XX:NewRatio:设置年轻代和老年代的比例。默认值为2,即年轻代和老年代的比例为1:2。

  9. -XX:MaxTenuringThreshold:设置对象进入老年代前经过的最大Minor GC次数。

  10. -XX:ParallelGCThreads:设置并行垃圾回收的线程数。

  11. -XX:+UseConcMarkSweepGC:启用并行标记和清除(CMS)垃圾回收器。

  12. -XX:+UseG1GC:启用G1垃圾回收器。

  13. -XX:MaxGCPauseMillis:设置GC暂停时间的目标值。

  14. -XX:+HeapDumpOnOutOfMemoryError:发生内存溢出错误时生成堆转储文件。

这只是一小部分常见的JVM参数,JVM还有很多其他参数用于调优和配置,具体使用哪些参数应根据应用程序的性能需求和场景来决定。可以通过查阅相应的文档和资料来了解更多的JVM参数及其用法。

在项目中一般怎么进行jvm调优的

在项目中进行 JVM(Java Virtual Machine)调优是优化 Java 应用程序性能和稳定性的重要步骤。以下是一些常见的 JVM 调优策略和步骤:

  1. 监控和分析

    • 使用监控工具如 JConsole、VisualVM、JMC(Java Mission Control)等,分析应用程序的内存使用、CPU 负载、线程状态等信息。这些工具可以帮助你了解 JVM 的实际运行情况。

  2. 调整堆内存

    • Heap 大小设置:通过 -Xms(初始堆大小)和 -Xmx(最大堆大小)参数设置堆内存大小,合理分配堆内存可以减少 Full GC 的频率和停顿时间。

    • 新生代和老年代比例:通过 -XX:NewRatio 参数调整新生代和老年代的比例,根据应用程序的特性来优化内存分配。

  3. GC(垃圾回收)调优

    • 选择合适的垃圾回收器:根据应用程序的特点选择串行、并行、CMS(Concurrent Mark Sweep)、G1(Garbage First)等垃圾回收器。

    • GC 日志分析:启用 GC 日志(-XX:+PrintGCDetails-XX:+PrintGCDateStamps),通过分析日志来优化 GC 的配置和调整。

  4. 线程栈大小

    • 默认情况下,每个线程的栈大小在不同的 JVM 实现中可能有所不同。可以通过 -Xss 参数调整线程栈大小,但需谨慎设置,避免栈溢出或资源浪费。

  5. 类加载优化

    • 预加载类:通过 -XX:+TraceClassLoading-XX:+TraceClassUnloading 参数跟踪类加载和卸载,优化类加载路径和类加载器的使用。

  6. 代码优化和调整

    • 优化代码以减少内存占用和不必要的资源消耗,如避免频繁的对象创建和销毁,优化算法和数据结构等。

  7. 硬件和操作系统优化

    • 配置服务器硬件,如 CPU 核心数、内存速度和大小,以及操作系统的参数调优(如文件描述符、TCP 连接等)可以影响 JVM 性能。

  8. 持续监控和优化

    • 定期进行性能测试和监控,根据实际情况调整 JVM 参数和应用程序代码,持续优化系统性能和稳定性。

在实际项目中,根据具体的应用场景和性能需求,综合考虑以上策略,并通过实验和监控来验证调优效果,以达到最佳的 JVM 性能配置。

在项目中进行JVM调优可以遵循以下几个步骤:

  1. 监控和分析:使用性能监控工具(如JConsole、VisualVM、Grafana等)对应用程序进行监控,收集关键的性能指标和数据,如内存使用、CPU利用率、GC情况等。基于这些数据分析应用程序的性能瓶颈和问题点。

  2. 调整堆内存参数:根据分析的结果,调整堆内存的初始大小(-Xms)和最大大小(-Xmx)来优化内存的使用。合理地分配堆内存大小可以避免频繁的垃圾回收和内存溢出。

  3. 调整垃圾回收参数:根据GC日志和监控数据,选择合适的垃圾回收器,并根据应用程序的特点和性能需求调整相关的参数,如新生代大小(-Xmn)、年轻代和老年代的比例(-XX:NewRatio)、垃圾回收线程数(-XX:ParallelGCThreads)等。

  4. 设置GC日志:启用GC日志(-Xloggc)并设置GC日志文件路径,以便分析垃圾回收的行为和效果。根据GC日志可以了解GC暂停时间、吞吐量等关键指标,并优化相关的参数。

  5. 优化代码和内存使用:根据性能分析的结果,优化代码,避免不必要的对象创建和内存泄漏。使用合适的数据结构和算法,合理地管理对象生命周期,以减少内存使用和GC负担。

  6. 进行压力测试:使用压力测试工具模拟高并发和高负载情况,验证调优后的JVM配置和代码优化是否能够满足性能需求,并进行性能测试和指标监控。

  7. 维持调优周期:JVM调优不是一次性的工作,随着应用程序的变化和用户负载的变化,可能需要周期性地进行性能监控和调优,以确保系统的稳定性和可靠性。

需要注意的是,JVM调优是一个复杂的过程,需要结合具体的应用程序、硬件环境和用户负载来进行合理的配置和调整。建议在进行JVM调优时,结合相关的调优手册、性能测试工具和专业人士的指导,以确保安全和有效地进行调优。

说说JVM调优思路

JVM调优三步骤:

  1. 监控发现问题

  2. 工具分析问题

  3. 性能调优

监控发现问题:看服务器有没有以下情况,有的话需要调优:

  • GC频繁

  • CPU负载过高

  • OOM

  • 内存泄露

  • 死锁

  • 程序响应时间较长

工具分析问题:使用分析工具定位oom、内存泄漏等问题

调优依据:吞吐量提高的代价是停顿时间拉长。如果应用程序跟用户基本不交互,就优先提升吞吐量。如果应用程序和用户频繁交互,就优先缩短停顿时间。 GC日志:使用GCViewer、VisualVM、GCeasy等日志分析工具打印GC日志; JDK自带的命令行调优工具: jps:查看正在运行的 Java 进程。jps -v查看进程启动时的JVM参数;

jstat:查看指定进程的 JVM 统计信息。jstat -gc查看堆各分区大小、YGC,FGC次数和时长。如果服务器没有 GUI 图形界面,只提供了纯文本控制台环境,它是运行期定位虚拟机性能问题的首选工具。

jinfo:实时查看和修改指定进程的 JVM 配置参数。jinfo -flag查看和修改具体参数。 jstack:打印指定进程此刻的线程快照。定位线程长时间停顿的原因,例如死锁、等待资源、阻塞。如果有死锁会打印线程的互相占用资源情况。

JDK自带的可视化监控工具:例如jconsole、Visual VM。Visual VM可以监视应用程序的 CPU、GC、堆、方法区、线程快照,查看JVM进程、JVM 参数、系统属性

性能调优:

排查大对象和内存泄漏:使用MAT分析堆转储日志中的大对象,看是否合理。大对象会直接进入老年代,导致Full GC频繁。具体排查步骤看下面OOM。 调整JVM参数:主要关注停顿时间和吞吐量,两者不可兼得,提高吞吐量会拉长停顿时间。 减少停顿时间:垃圾收集器做垃圾回收中断应用执行的时间。 可以通过-XX:MaxGCPauseMillis参数进行设置,以毫秒为单位,至少大于1 提高吞吐量:吞吐量=运行时长/(运行时长+GC时长)。通过-XX:GCTimeRatio=n参数进行设置,99的话代表吞吐量为99%, 一般吞吐量不能低于95%。吞吐量太高会拉长停顿时间,造成用户体验下降。 调整堆内存大小:根据程序运行时老年代存活对象大小(记为x)进行调整,整个堆内存大小设置为X的3~4倍。年轻代占堆内存的3/8。 -Xms:初始堆内存大小。默认:物理内存小于192MB时,默认为物理内存的1/2;物理内存大192MB且小于128GB时,默认为物理内存的1/4;物理内存大于等于128GB时,都为32GB。 -Xmx:最大堆内存大小,建议保持和初始堆内存大小一样。因为从初始堆到最大堆的过程会有一定的性能开销,而且现在内存不是稀缺资源。 -Xmn:年轻代大小。JDK官方建议年轻代占整个堆大小空间的3/8左右。 调整堆内存比例:调整伊甸园区和幸存区比例、新生代和老年代比例。Young GC频繁时,我们提高新生代比例和伊甸园区比例。默认情况,伊甸园区:S0:S1=8:1:1,新生代:老年代=1:2。 调整升老年代年龄:JDK8时Young GC默认把15岁的对象移动到老年代。JDK9默认值改为7。当Full GC频繁时,我们提高升老年龄,让年轻代的对象多在年轻代待一会,从而降低Full GC频率。JDK8默认Young GC时将15岁的对象移动到老年代。 调整大对象阈值:Young GC时大对象会不顾年龄直接移动到老年代。当Full GC频繁时,我们关闭或提高大对象阈值,让老年代更迟填满。默认是0,即大对象不会直接在YGC时移到老年代。 调整GC的触发条件: CMS调整老年代触发回收比例:CMS的并发标记和并发清除阶段是用户线程和回收线程并发执行,如果老年代满了再回收会导致用户线程被强制暂停。所以我们修改回收条件为老年代的60%,保证回收时预留足够空间放新对象。CMS默认是老年代68%时触发回收机制。 G1调整存活阈值:超过存活阈值的Region,其内对象会被混合回收到老年代。G1回收时也要预留空间给新对象。存活阈值默认85%,即当一个内存区块中存活对象所占比例超过 85% 时,这些对象就会通过 Mixed GC 内存整理并晋升至老年代内存区域。 选择合适的垃圾回收器:最有效的方式是升级,根据CPU核数,升级当前版本支持的最新回收器。 CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。 CPU多核,关注吞吐量 ,那么选择Parallel Scavenge+Parallel Old组合(JDK8默认)。 CPU多核,关注用户停顿时间,JDK版本1.6或者1.7,那么选择ParNew+CMS,吞吐量降低但是低停顿。 CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。 优化业务代码:绝大部分问题都出自代码。要尽量减少非必要对象的创建,防止死循环创建对象,防止内存泄漏,有些情景下需要以时间换空间,控制内存使用 增加机器:增加机器,分散节点压力 调整线程池参数:合理设置线程池线程数量 缓存、MQ等中间件优化:使用中间件提高程序效率,比如缓存、消息队列等

项目中有没有实际的JVM调优经验?

7.1 CPU飙升 原因:CPU利用率过高,大量线程并发执行任务导致CPU飙升。例如锁等待(例如CAS不断自旋)、多线程都陷入死循环、Redis被攻击、网站被攻击、文件IO、网络IO。

定位步骤:

定位进程ID:通过top命令查看当前服务CPU使用最高的进程,获取到对应的pid(进程ID) 定位线程ID:使用top -Hp pid,显示指定进程下面的线程信息,找到消耗CPU最高的线程id 线程ID转十六进制:转十六进制是因为下一步jstack打印的线程快照(线程正在执行方法的堆栈集合)里线程id是十六进制。 定位代码:使用jstack pid | grep tid(十六进制),打印线程快照,找到线程执行的代码。一般如果有死锁的话就会显示线程互相占用情况。 解决问题:优化代码、增加系统资源(增多服务器、增大内存)。

说说你了解的JVM内存模

JVM由三部分组成:类加载子系统、运行时数据区、执行引擎

类加载子系统:通过类加载机制加载类的class文件,如果该类是第一次加载,会执行加载、验证、解析。只负责class文件的加载,至于是否可运行,则由执行引擎决定。

类加载过程是在类加载子系统完成的:加载 --> 链接(验证 --> 准备 --> 解析) --> 初始化

运行时数据区

在程序运行时,存储程序的内容(例如字节码、对象、参数、返回值等)。运行时数据区包括本地方法栈、虚拟机栈、方法区、堆、程序计数器。

本地方法栈:存放本地方法调用过程中的栈帧。用于管理本地方法的调用,本地方法是C语言写的。不是所有虚拟机都支持本地方法栈,例如Hotspot虚拟机就是将本地方法栈和虚拟机栈合二为一。栈解决程序的运行问题,即程序如何执行、如何处理数据

栈帧:栈帧是栈的元素,由三部分组成,即局部变量表(存方法参数和局部变量)、操作数栈(存方法执行过程中的中间结果,或者其他暂存数据)和帧数据区(存方法返回地址、线程引用等附加信息)。

虚拟机栈:存放Java方法调用过程中的栈帧。用于管理Java方法的调用,Java方法是开发时写的Java方法。

方法区:可以看作是一块独立于Java堆的内存空间,方法区是各线程共享的内存区域。

常量池:就是一张表,JVM根据这张常量表找到要执行的类信息和方法信息

堆:存放对象实例、实例变量、数组,包括新生代(伊甸园区、幸存区S0和S1)和老年代。堆是垃圾收集器管理的内存区域。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。堆实际内存空间可以不连续,大小可以选择固定大小或可扩展,堆是各线程共享的内存区域。

堆:存放对象实例、实例变量、数组,包括新生代(伊甸园区、幸存区S0和S1)和老年代。堆是垃圾收集器管理的内存区域。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。堆实际内存空间可以不连续,大小可以选择固定大小或可扩展,堆是各线程共享的内存区域。

只有方法区和堆是各线程共享的进程内存区域,其他运行区都是每个线程可以独立拥有的

执行引擎:将字节码指令解释/编译为对应平台上的本地机器指令。充当了将高级语言翻译为机器语言的译者。执行引擎在执行过程中需要执行什么样的字节码指令依赖于PC寄存器。每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。

字节码指令(JVM指令):字节码文件中的指令,内部只包含一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息,不能够直接运行在操作系统之上。 本地机器指令:可以直接运行在操作系统之上。

汇总回答

JVM由三部分组成:类加载子系统、执行引擎、运行时数据区。

  1. 类加载子系统,可以根据指定的全限定名来载入类或接口。

  2. 运行时数据区。当程序运行时,JVM需要内存来存储许多内容,例如:字节码、对象、参数、返回值、局部变量、运算的中间结果,等等,JVM会把这些东西都存储到运行时数据区中,以便于管理。而运行时数据区又可以分为方法区、堆、虚拟机栈、本地方法栈、程序计数器。

  3. 执行引擎,负责执行那些包含在被载入类的方法中的指令。

加分回答-运行时数据区

运行时数据区是开发者重点要关注的部分,因为程序的运行与它密不可分,很多错误的排查也需要基于对运行时数据区的理解。在运行时数据区所包含的几块内存空间中,方法区和堆是线程之间共享的内存区域,而虚拟机栈、本地方法栈、程序计数器则是线程私有的区域,就是说每个线程都有自己的这个区域。

简单说下你对JVM的了解

得分点

Java跨平台、HotSpot、热点代码探测技术、内存模型、垃圾回收算法、垃圾回收器

跨平台

Java跨平台,JVM不跨平台。JVM是Java语言跨平台的关键,一个程序员只要了解了必要的Java类库、Java语法,学习适当的第三方开发框架,就已经基本满足日常开发的需要了,JVM会在用户不知不觉中完成对硬件平台的兼容及对内存等资源的管理工作。

内存模型

JVM由三部分组成:类加载子系统、运行时数据区、执行引擎。

类加载子系统:根据指定的全限定名来载入类或接口。

运行时数据区:在程序运行时,存储程序的内容,例如:字节码、对象、参数、返回值等。而运行时数据区又可以分为方法区、堆、虚拟机栈、本地方法栈、程序计数器。

执行引擎:负责执行那些包含在被载入类的方法中的指令。

说说类加载机制

加载、验证、准备、解析、初始化

类加载过程:加载、链接(验证、准备、解析)、初始化。这个过程是在类加载子系统完成的。

加载:生成类的Class对象。

1、通过类的全限定名获取该类的二进制字节流 类的全限定名:即"包名.类名",例如Object类的全限定名是java.lang.Object 。包名的各个部分之间,包名和类名之间, 使用点号分割。 类的二进制字节流:即类的字节码文件,是一组以8个字节(64位)为基础单位的二进制流,各个单位内部及之间都排列紧凑,中间没有添加任何分隔符和空隙,这使得整个Class文件中存储的内容几乎都是程序运行的必要数据。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8个字节进行存储,保证每个基础单位只有8个字节。

2、将这个字节流的静态存储结构,转化为方法区的运行时数据结构。包括创建运行时常量池,将类常量池的部分符号引用放入运行时常量池。 静态存储结构:二进制文件,存储内容,存储内容包括魔数、版本号、常量池、访问标识、类索引、字段表、方发表、属性表 运行时数据结构:存储在内存中的JVM内存模型中的运行时数据区-方法区,存储内容是类常量池、运行时常量池、字符串常量池,存储形式是永久代(JDK7及之前)和元空间(JDK8及之后)。

3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类各种数据的访问入口。注意类的class对象是运行时生成的、存在内存中的对象,类的class字节码文件是编译时生成的、存在磁盘中的文件。

链接:将类的二进制数据合并到JRE中。该过程分为以下3个阶段:

验证:确保代码符合JAVA虚拟机规范和安全约束。包括文件格式验证、元数据验证、字节码验证、符号引用验证。

准备:为类变量(即static变量)分配内存并赋零值。

解析:将方法区-运行时常量池内的符号引用(类的名字、成员名、标识符)转为直接引用(实际内存地址,不包含任何抽象信息,因此可以直接使用)。

初始化:类变量赋初值、执行静态语句块。

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用、卸载七个阶段,其中验证、准备、解析三个部分统称为连接,而前五个阶段则是类加载的完整过程。

  1. 在加载阶段JVM需要在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。

  2. 验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

  3. 准备阶段是正式为类中定义变量(静态变量)分配到内存并设置类变量初始值的阶段,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域。

  4. 解析阶段是Java虚拟机将常量池内的符号替换为直接引用的过程,符号引用以一组符号来描述所引用的目标,直接引用是可以直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。

  5. 类的初始化阶段是类加载过程的最后一个步骤,直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。本质上,初始化阶段就是执行类构造器的过程。并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物。

说说对象的实例化过程

得分点

类加载、分配内存(内存规整和不规整)、处理并发安全问题、设置对象头、成员变量赋初值、执行构造方法

对象的实例化过程

1、判断对应类是否加载过:首先JVM检查在方法区Metaspace(元空间)的常量池里能否定位到该类的符号引用,能的话通过符号引用检查该类是否加载链接初始化过;若没有则在双亲委派机制下,当前类加载器调用findClass()方法查找类的.class字节码文件,然后调用loadClass("类全限定名")方法遵循双亲委派机制加载链接初始化类到内存中,并生成类的class对象,作为方法区这个类各种数据的访问入口。

2、创建对象: 分配堆内存空间:如果内存规整:(例如标记整理算法),采用指针碰撞法为新对象分配内存。如果内存不规整:(有内存碎片,例如标记清除算法),在空闲列表里找到合适大小的空闲内存分配给新对象。现在主流虚拟机新生代都是使用标记复制算法,内存都是规整的。 处理并发安全问题:CAS失败重试,区域加锁,每个线程分配一块TLAB内存缓冲区。 设置对象头:将哈希码、GC分代年龄、锁信息、GC标记等存在对象头的Mark Word中; 成员变量赋初值:若指定了初值则赋指定的值。若未指定初值,则基本类型赋0或false、引用类型赋null。

3、执行构造方法:有父类的话,子类构造方法第一行会隐式或手动显式地加super()。

说说JVM的双亲委派模型

得分点

三个默认类加载器、工作过程、作用

双亲委派模型:当一个类加载器接收到加载类的请求时,它首先会将这个请求委派给其父类加载器处理,只有在父类加载器无法完成加载任务时,才会由该类加载器自己去加载类。

JVM三个默认类加载器

启动类加载器BootStrapClassLoader(最顶端)

扩展类加载器ExtClassLoader:

应用程序类加载器AppClassLoader(最低端):

双亲委派模型的工作过程:

工作过程:

检查父类加载器是否已经加载过这个类:JVM 会首先询问父类加载器是否已经加载了该类。如果已经加载过了,直接返回该类的 Class 对象。如果没加载过,则:

委派给父类加载器加载:如果父类加载器没有加载过该类,那么 JVM 将委托给父类加载器进行加载。每一层都是这样继续委派,直到达到最顶层的启动类加载器。

尝试加载类:如果父类加载器无法加载该类(即所有的父类加载器都无法加载),那么 JVM 将尝试使用自己的类加载器来加载类。 双亲委派模型的作用:

避免类的重复加载:无论哪一个类加载器要加载某类,最终都是委派最顶端的启动类加载器。

防止核心API被篡改:如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。

请你说说内存溢出

内存溢出: 申请的内存大于系统能提供的内存。

溢出原因:

本地直接内存溢出:本地直接内存设的太小导致溢出。设置直接内存最大值-XX:MaxDirectMemorySize,若不指定则默认与Java堆最大值一致。 虚拟机栈和本地方法栈溢出:如果虚拟机的栈内存允许动态扩展,并且方法递归层数太深时,导致扩展栈容量时无法申请到足够内存。 方法区溢出:运行时生成大量动态类时会内存溢出。 CGlib动态代理:CGlib动态代理产生大量类填满了整个方法区(方法区存常量池、类信息、方法信息),直到溢出。CGlib动态代理是在内存中构建子类对象实现对目标对象功能扩展,如果enhancer.setUseCache(false);,即关闭用户缓存,那么每次创建代理对象都是一个新的实例,创建过多就会导致方法区溢出。注意JDK动态代理不会导致方法区溢出。 JSP:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)。 堆溢出: 死循环创建过多对象; 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收; 内存中加载的数据量过于庞大,如一次从数据库取出的数据集太大、第三方接口接口传输的大对象、接收的MQ消息太大; Tomcat参数设置不当导致OOM:Tomcat会给每个线程创建两个默认4M大小的缓冲区,高并发情况下会导致缓冲区创建过多,导致OOM。 程序计数器不会内存溢出。

使用JDK自带的命令行调优工具 ,判断是否有OOM:

使用jsp命令查看当前Java进程; 使用jstat命令多次统计GC,比较GC时长占运行时长的比例; 如果比例超过20%,就代表堆压力已经很大了; 如果比例超过98%,说明这段时期内几乎一直在GC,堆里几乎没有可用空间,随时都可能抛出 OOM 异常。

解决方案:

  1. 通过jinfo命令查看并修改JVM参数,直接增加内存。如-Xmx256m

  2. 检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。

  3. 对代码进行走查和分析,找出可能发生内存溢出的位置。

  4. 使用内存查看工具动态查看内存使用情况。

请你说说内存泄漏

内存泄漏: 不再使用的对象仍然被引用,导致GC无法回收;

内存泄露的9种情况:

静态容器里的对象:静态集合类的生命周期与 JVM 程序一致,容器里的对象引用也将一直被引用得不到GC;Java里不准静态方法引用非静态方法也是防止内存泄漏。 单例对象引用的外部对象:单例模式里,如果单例对象如果持有外部对象的引用,因为单例对象不会被回收,那么这个外部对象也不会被回收 外部类跟随内部类被引用:内部类持有外部类,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。 数据库、网络、IO等连接忘记关闭:在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用 close 方法来释放与数据库的连接。如果对 Connection、Statement 或 ResultSet 不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。 变量作用域不合理:例如一个变量只会在某个方法中使用,却声明为成员变量,并且被使用后没有被赋值为null,将会导致这个变量明明已经没用了,生命周期却还跟对象一致。 HashSet中对象改变哈希值:当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。否则对象哈希值改变,找不到对应的value。 缓存引用忘删除:一旦你把对象引用放入到缓存中,他就很容易遗忘,缓存忘了删除,将导致引用一直存在。 逻辑删除而不是真实删除:监听器和其他回调:如果客户端在你实现的 API 中注册回调,却没有显示的取消,那么就会积聚。需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为 软WeakHashMap 中的键。例如出栈只是移动了指针,而没有将出栈的位置赋值null,导致已出栈的位置还存在引用。 线程池时,ThreadLocal忘记remove():使用线程池的时候,ThreadLocal 需要在使用完线程中的线程变量手动 remove(),否则会内存泄漏。因为线程执行完后没有销毁而是被线程池回收,导致ThreadLocal中的对象不能被自动垃圾回收。

JVM中一次完整的GC流程是怎样的

堆分为哪几个区、GC流程、注意大对象和年龄15(JDK8)

1首先,任何新对象都分配到 eden 空间。两个幸存者空间开始时都是空的。 2当 eden 空间填满时,将触发一个Minor GC(年轻代的垃圾回收,也称为Young GC),删除所有未引用的对象,大对象(需要大量连续内存空间的Java对象,如那种很长的字符串)直接进入老年代。 3所有被引用的对象作为存活对象,将移动到第一个幸存者空间S0,并标记年龄为1,即经历过一次Minor GC。之后每经过一次4Minor GC,年龄+1。GC分代年龄存储在对象头的Mark Word里。 当 eden 空间再次被填满时,会执行第二次Minor GC,将Eden和S0区中所有垃圾对象清除,并将存活对象复制到S1并年龄加1,此时S0变为空。 5如此反复在S0和S1之间切换几次之后,还存活的年龄等于15的对象(JDK8默认15,JDK9默认7,-XX:InitialTenuringThreshold=7)在下一次Minor GC时将放到老年代中。 6当老年代满了时会触发Major GC(也称为Full GC),Major GC 清理整个堆 – 包括年轻代和老年代。

说说JVM的垃圾回收机制

得分点

新生代收集、老年代收集、混合收集、整堆收集

圾回收可以分为如下几类:

  1. 新生代收集:目标为新生代的垃圾收集。

  2. 老年代收集:目标为老年代的垃圾收集,目前只有CMS收集器会有这种行为。

  3. 混合收集:目标为整个新生代及部分老年代的垃圾收集,目前只有G1收集器会有这种行为。

  4. 整堆收集:目标为整个堆和方法区的垃圾收集。

加分回答-垃圾收集器

HotSpot虚拟机内置了很多垃圾收集器,其中针对新生代的垃圾收集器有Serial、ParNew、Parallel Scavenge,针对老年代的垃圾收集器有CMS、Serial Old、Parallel Old。此外,HotSpot还内置了面向整堆的G1收集器。

在上述收集器中,常见的组合方式有:

  1. Serial + Serial Old,是客户端模式下常用的收集器。

  2. ParNew + CMS,是服务端模式下常用的收集器。

  3. Parallel Scavenge + Parallel Old,适用于后台运算而不需要太多交互的分析任务。

说说JVM的垃圾回收算法

标记清除算法、标记复制算法、标记整理算法

标记清除算法(Mark-Sweep):

标记、清除:当堆中有效内存空间被耗尽时,会STW(stop the world,暂停其他所有工作线程),然后先标记,再清除。 标记:可达性分析法,从GC Roots开始遍历,找到可达对象,并在对象头中进行标记。 清除:堆内存内从头到尾进行线性遍历,“清除”非可达对象。注意清除并不是真的置空,垃圾还在原来的位置。实际是把垃圾对象的地址维护在空闲列表,对象实例化的申请内存阶段会通过空闲列表找到合适大小的空闲内存分配给新对象。 优点:简单 缺点效率不高:需要可达性遍历和线性遍历,效率差。 STW导致用户体验差:GC时需要暂停其他所有工作线程,用户体验差。 有内存碎片,要维护空闲列表:回收垃圾对象后没有整理,导致堆中出现一块块不连续的内存碎片。 适用场景:适合小型应用程序,内存空间不大的情况。应用程序越大越不适用这种回收算法。

标记复制算法(Copying) :

标记、复制、清除:将内存空间分为两块,每次只使用一块。在进行垃圾回收时,先可达性分析法标记可达对象,然后将可达对象复制到没有被使用的那个内存块中,最后再清除当前内存块中的所有对象。后续再按同样的流程来回复制和清除。 优点: 垃圾多时效率高:只需可达性遍历,效率很高。 无内存碎片:因为有移动操作,所以内存规整。 缺点内存利用率低,浪费内存:始终有一半以上的空闲内存。 需要调整引用地址:可达对象移动后,内存地址发生了变化,需要调整所有引用,指向移动后的地址。 垃圾少时效率相对差,但还是比其他算法强:如果可达对象比较多,垃圾对象比较少,那么复制算法的效率就会比较低。只为了一点垃圾而移动所有对象未免有些小题大做。所以垃圾对象多的情况下,复制算法比较适合。 适用场景:适合垃圾对象多,可达对象少的情况,这样复制耗时短。非常适合新生代的垃圾回收,因为新生代要频繁地把可达对象从伊甸园区移动到幸存区,而且是新生代满了适合再Minor GC,垃圾对象占比高,所以回收性价比非常高,一次通常可以回收70-90%的内存空间,现在的商业虚拟机都是用这种GC算法回收新生代。

说说七个垃圾回收器

得分点 Serial、Serial Old、PawNew、CMS、Parallel Scavenge、Parallel Old、G1

各版本默认回收器:JDK8默认回收器是Parallel+Parallel Old。

各区域对应算法:

  • 新生代回收算法:标记复制算法;

  • 老年代回收算法:标记清除/整理算法

  • 整堆回收算法:分区算法。

Serial(串行收集器):

介绍:单线程、单处理器回收新生代,回收时会STW。 STW:Stop The World,暂停其他所有工作线程直到收集结束。 算法:标记复制算法 回收区域:新生代 优点:简单、比其他单线程收集器效率高:单线程,不用线程切换,可以专心进行垃圾回收。 应用场景:适用于内存小的桌面应用,可以在较短时间完成收集。Serial GC是最基础、历史最悠久的收集器,曾是HotSpot虚拟机新生代收集器的唯一选择。 命令:指定新生代用Serial GC,老年代用Serial Old GC:-XX:+UseSerialGC

Serial Old(老年代串行收集器):

  • 介绍:Serial收集器的老年代版本。单线程、单处理器回收老年代,回收时会STW。

  • 算法:标记-整理算法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值