1. 什么是JVM?
答案: Java虚拟机(JVM)是Java平台的一部分,是一个虚拟计算机,负责在运行时执行Java字节码。它提供了Java程序运行的环境,包括内存管理、垃圾回收、即时编译等功能,使得Java程序可以在不同的平台上实现一次编写,到处运行的特性。
- 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
- Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
- 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
- Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
- 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
2. Java内存模型(JMM)是什么?
答案: Java内存模型是一种规范,定义了Java虚拟机如何协调多线程访问共享内存的规则。它确保线程之间的可见性、有序性和一致性。JMM包括主内存和每个线程的工作内存,通过内存屏障和同步操作来协调线程之间的交互。这是为了保证在多线程环境中程序的正确性。
3. 什么是垃圾回收?Java中有哪些垃圾回收算法?
答案: 垃圾回收是指自动识别和释放不再被程序使用的内存的过程。在Java中,垃圾回收器负责管理和清理不再被引用的对象。常见的垃圾回收算法有以下几点:
- 标记-清除算法(Mark and Sweep): 首先标记不再使用的对象,然后清除(删除)它们。
- 复制算法(Copying): 将存活的对象复制到新的内存区域,然后清除旧区域。
- 标记-整理算法(Mark and Compact): 首先标记不再使用的对象,然后将存活的对象移到一侧,并清除未使用的内存。
- 分代算法(Generational): 将内存划分为不同的代,年轻代和老年代,根据对象的生命周期分别采用不同的垃圾回收算法
4. 什么是Java堆和栈?
答案: Java堆是Java虚拟机管理的最大一块内存区域,用于存储对象实例。栈则是每个线程私有的,用于存储局部变量和方法调用信息。主要区别在于,堆是用于存储对象的动态分配内存区域,而栈是用于存储基本数据类型和对象引用的内存区域。堆的内存空间由垃圾回收器负责管理,而栈的内存空间由线程自动管理。
5. 什么是类加载器?Java中有哪些类加载器?
答案: 类加载器是Java虚拟机的一部分,负责将类的字节码加载到内存中,并转换成Java对象。常见的类加载器有启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)和自定义类加载器。这些类加载器形成了层次结构,按照双亲委派模型,父类加载器负责委托给子类加载器进行类加载。
6. 垃圾回收的目的是什么?
答:垃圾回收的主要目的是:
- 释放不再被引用的内存对象,以避免内存泄漏。
- 提高内存资源的有效利用,减少内存碎片化。
- 减少程序员手动管理内存的工作,提高开发效率。
- 增加应用程序的稳定性和可靠性,减少内存相关的错误
7. 队列和栈是什么?有什么区别?
- 队列和栈都是被用来预存储数据的。
- 队列允许先进先出检索元素,但也有例外的情况,Deque 接口允许从两端检索元素。
- 栈和队列很相似,但它运行对元素进行后进先出进行检索
8. Java中都有哪些引用类型?
- 强引用:发生 gc 的时候不会被回收。
- 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
- 弱引用:有用但不是必须的对象,在下一次GC时会被回收。
- 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。
9. Java11的默认垃圾收集器是什么?
答:Java9之后,官方JDK默认使用的垃圾收集器是G1。
10. 常见的垃圾收集器有哪些?
常见的垃圾收集器包括:
- 串行垃圾收集器:‐XX:+UseSerialGC
- 并行垃圾收集器:‐XX:+UseParallelGC
- CMS垃圾收集器:‐XX:+UseConcMarkSweepG
- G1垃圾收集器:‐XX:+UseG1GC
11. 说一下类装载的执行过程?
类装载分为以下 5 个步骤:
- 加载:根据查找路径找到相应的 class 文件然后导入;
- 检查:检查加载的 class 文件的正确性;
- 准备:给类中的静态变量分配内存空间;
- 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
- 初始化:对静态变量和静态代码块执行初始化工作。
12. JVM 调优的工具有哪些?
答:
常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory AnalyzerTool)、GChisto。
- jconsole:Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存, 线程和类等的监控
- jvisualvm:jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
- MAT:Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Javaheap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
- GChisto:一款专业分析gc日志的工具
13. 堆和栈的有什么区别
答:栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;堆是存储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在区域不连续,会有碎片。
- 功能不同
栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变量, 还是类变量,它们指向的对象都存储在堆内存中。 - 共享性不同
栈内存是线程私有的。
堆内存是所有线程共有的。 - 异常错误不同
如果栈内存或者堆内存不足都会抛出异常。
栈空间不足:java.lang.StackOverFlowError。堆空间不足:java.lang.OutOfMemoryError。 - 空间大小
栈的空间大小远远小于堆的
14. JDK常用的调优命令有哪些?
答Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo
- jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
- jstat:JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
- jmap:JVM Memory Map命令用于生成heap dump文件
- jhat:JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
- jstack:用于生成java虚拟机当前时刻的线程快照。
- jinfo:JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数
15. Minor GC与Full GC分别在什么时候发生?
答:新生代内存不够用时候发生MGC也叫YGC,JVM内存不够的时候发生FGC
16. JVM选项-XX:+UseCompressedOops有什么作用? 为什么要使用?
答:当你将你的应用从 32 位的 JVM 迁移到 64 位的 JVM 时,由于对象的指针从 32 位增加到了 64 位,因此堆内存会突然增加,差不多要翻倍。这也会对 CPU 缓存(容量比内存小很多)的数据产生不利的影响。因为,迁移到 64 位的 JVM 主要动机在于可以指定最大堆大小,通过压缩 OOP 可以节省一定的内存。通过 -XX:+UseCompressedOops 选项,JVM 会使用 32 位的 OOP,而不是 64 位 的 OOP。
17. 什么是双亲委派模型?
答:在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。
类加载器分类:
启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
其他类加载器:
扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库;
应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
18. JVM性能调优的目标是什么?
答:JVM性能调优的主要目标包括:
- 提高应用程序的响应性能。
- 最小化垃圾回收的停顿时间。
- 减少内存占用,避免内存泄漏。
- 最大程度地利用硬件资源,提高吞吐量。
- 优化应用程序的整体性能和稳定性。
19. 如何选择适合应用程序的垃圾回收器?
答:选择垃圾回收器应考虑以下因素:
- 应用程序的性能需求:低停顿时间、高吞吐量等。
- 堆内存大小:小堆和大堆需要不同的回收器。
- 硬件资源:多核CPU、内存大小等。
- Java版本和JVM实现:不同版本和实现支持不同的回收器。
- 应用程序的特性:长时间运行、短时间运行、大对象等。
- 通常,可以开始使用默认的垃圾回收器,然后根据性能需求进行调优和选择。
20. 如何调整堆大小以优化性能?
答:可以使用-Xmx和-Xms标志来调整堆大小。其中:
- -Xmx用于设置堆的最大大小,例如-Xmx2g表示将堆的最大大小设置为2GB。
- -Xms用于设置堆的初始大小,例如-Xms512m表示将堆的初始大小设置为512MB。
调整堆大小的目标是确保堆足够大,以容纳应用程序的内存需求,同时避免浪费过多内存。
21. 请描述G1(Garbage-First) GC的工作原理。
答:G1 GC是一种面向大堆内存的垃圾回收器,其工作原理包括以下步骤:
- 首先,它将堆划分为多个区域,包括伊甸园区、幸存者区、老年代等。
- 然后,它会并发地标记不再使用的对象。
- 接着,它会根据各个区域的垃圾量优先回收垃圾。这个过程通常不会导致大的停顿。
- 最后,它会进行一次小的"Stop-the-World"事件,以处理剩余的垃圾。
22. 32 位 JVM 和 64 位 JVM 的最大堆内存分别是多数?
答:理论上说上 32 位的 JVM 堆内存可以到达 2^32, 即 4GB,但实际上会比这个小很多。不同操作系统之间不同,如 Windows 系统大约 1.5GB,Solaris 大约3GB。64 位 JVM 允许指定最大的堆内存,理论上可以达到 2^64,这是一个非常大的数字,实际上你可以指定堆内存大小到 100GB。甚至有的 JVM,如 Azul,堆内存到 1000G 都是可能的。
23. JRE、JDK、JVM 及 JIT 之间有什么不同?
JRE 代表 Java 运行时(Java run-time),是运行 Java 引用所必须的。
JDK 代表 Java 开发工具(Java development kit),是 Java 程序的开发工具,如 Java编译器,它也包含 JRE。
JVM 代表 Java 虚拟机(Java virtual machine),它的责任是运行 Java 应用。
JIT 代表即时编译(Just In Time compilation),当代码执行的次数超过一定的阈值时,会将 Java 字节码转换为本地代码,如,主要的热点代码会被准换为本地代码,这样有利大幅度提高 Java 应用的性能。
24. 常用的JVM启动参数有哪些?
答:截止目前(2020年3月),JVM可配置参数已经达到1000多个,其中GC和内存配置相关的JVM参数就有600多个。但在绝大部分业务场景下,常用的JVM配置参数也就10来个。
例如:
# JVM启动参数不换行
# 设置堆内存
‐Xmx4g ‐Xms4g
# 指定GC算法
‐XX:+UseG1GC ‐XX:MaxGCPauseMillis=50
# 指定GC并行线程数
‐XX:ParallelGCThreads=4
# 打印GC日志
‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps
# 指定GC日志文件
‐Xloggc:gc.log
# 指定Meta区的最大值
‐XX:MaxMetaspaceSize=2g
# 设置单个线程栈的大小
‐Xss1m
# 指定堆内存溢出时自动进行Dump
‐XX:+HeapDumpOnOutOfMemoryError
‐XX:HeapDumpPath=/usr/local/
此外,还有一些常用的属性配置:
# 指定默认的连接超时时间
‐Dsun.net.client.defaultConnectTimeout=2000
‐Dsun.net.client.defaultReadTimeout=2000
# 指定时区
‐Duser.timezone=GMT+08
# 设置默认的文件编码为UTF‐8
‐Dfile.encoding=UTF‐8
# 指定随机数熵源(Entropy Source)
‐Djava.security.egd=file:/dev/./urandom
25. 设置堆内存XMX应该考虑哪些因素?
答:需要根据系统的配置来确定,要给操作系统和JVM本身留下一定的剩余空间。推荐配置系统或容器里可用内存的 7080% 最好。
比如说系统有 8G 物理内存,系统自己可能会用掉一点,大概还有 7.5G 可以用,那么建议配置‐Xmx6g
说明:7.5G*0.8 = 6G,如果知道系统里有明确使用堆外内存的地方,还需要进一步降低这个值。
26. 怎样开启GC日志?
答:一般来说,JDK8及以下版本通过以下参数来开启GC日志:
‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps ‐Xloggc:
如果是在JDK9及以上的版本,则格式略有不同
‐Xlog:gc*=info:file=gc.log:time:filecount=0
27. 请指定使用G1垃圾收集器来启动HelloWorld程序
java ‐XX:+UseG1GC
‐Xms4g
‐Xmx4g
‐Xloggc:gc.log
‐XX:+PrintGCDetails
‐XX:+PrintGCDateStamps HelloWorld
28. 什么是内存溢出错误(OutOfMemoryError)?有哪些常见的内存溢出错误类型?
答:内存溢出错误(OutOfMemoryError)是一种Java程序运行时错误,表示程序试图分配更多内存,但没有足够的内存可供使用。内存溢出错误是由于程序中创建的对象太多,而Java虚拟机(JVM)的堆内存不足以容纳这些对象而引起的。常见的内存溢出错误类型包括:
- java.lang.OutOfMemoryError: Java heap space:表示堆内存溢出。
- java.lang.OutOfMemoryError: PermGen space(在旧版本的JVM中):表示永久代内存溢出。
- java.lang.OutOfMemoryError: Metaspace(在新版本的JVM中):表示元空间内存溢出。
- java.lang.OutOfMemoryError: GC overhead limit exceeded:表示垃圾回收开销过大。
29. 如何分析线程转储(Thread Dump)?
答:分析线程转储(Thread Dump)是诊断多线程应用程序问题的关键步骤之一。以下是一般的线程转储分析步骤:获取线程转储:在应用程序运行时,可以使用工具(如jstack命令,VisualVM等)来生成线程转储。通常,您可以通过以下方式获取线程转储:
使用jstack命令:运行jstack ,其中是目标Java进程的进程ID。
使用监控工具:如果您使用监控工具(如VisualVM、JConsole等),通常可以通过工具界面生成线程转储。
在发生问题时生成线程转储:如果应用程序出现性能问题或死锁,您可以使用操作系统工具来生成线程转储,例如在Unix/Linux系统上可以使用kill -3 命令。查看线程状态:打开生成的线程转储文件(通常是文本文件),查看线程列表。了解每个线程的状态,例如RUNNABLE(运行中)、WAITING(等待中)、BLOCKED(阻塞中)等。
分析堆栈跟踪:对于每个线程,查看其堆栈跟踪以了解线程执行的代码路径。堆栈跟踪通常包含了方法和类名,帮助您确定问题代码的位置。
查找共享资源和锁信息:如果线程转储包含锁信息,查看哪些线程正在等待获取哪些锁,以及哪些线程已经持有锁。这有助于识别死锁或资源争用问题。
识别问题线程:找出占用CPU高或处于异常状态的线程,以及哪些线程可能导致了性能问题或死锁。
使用性能分析工具:如果需要更深入的分析,您可以使用性能分析工具(如VisualVM、JProfiler、YourKit等)来可视化和交互式地分析线程转储数据。这些工具通常提供更强大的分析功能,有助于更轻松地识别问题。
解决问题:根据分析的结果,采取必要的措施来解决线程问题。这可能包括优化代码、解决死锁、减少线程争用等。
监控和预防:定期监控应用程序的线程情况,以及线程转储文件,以便在早期发现和解决问题。采取预防措施,如合理的锁策略、避免死锁、资源池管理等,以减少线程问题的发生。
线程转储分析通常需要一定的经验和技能,特别是在处理复杂的多线程应用程序时。性能分析工具可以大大简化这个过程,并提供更多的可视化信息,有助于更快地定位和解决问题。
30. 什么是CPU占用高(High CPU Usage)问题?如何排查?
答:CPU占用高(High CPU Usage)问题是指应用程序或进程占用了大量的CPU资源,导致系统负载升高、性能下降或系统变得不响应。要排查高CPU占用问题,可以采取以下步骤:
监控CPU使用率:使用系统监控工具(如top、htop、Windows任务管理器)来检查哪个进程或线程占用了大量的CPU资源。
查看线程转储:如果CPU占用问题与多线程应用程序相关,生成线程转储并查看问题线程的堆栈跟踪,以了解问题的代码路径。
分析代码:确定占用CPU的代码部分。查看哪些方法或循环消耗了大量的CPU时间。
检查循环:查看是否存在无限循环或非预期的循环条件,这可能导致CPU占用问题。
检查阻塞操作:查看是否存在长时间的阻塞操作,例如等待网络或文件系统操作,这可能导致CPU资源浪费。
使用性能分析工具:使用性能分析工具(如VisualVM、JProfiler、YourKit)来更详细地分析CPU占用问题。这些工具可以提供更多的可视化数据和分析功能。
优化代码:根据分析结果采取必要的措施来优化代码,减少CPU占用。这可能包括改进算法、减少不必要的计算或循环、并行化任务等。
避免不必要的轮询:如果应用程序中存在轮询机制,尽量避免过于频繁的轮询,采用事件驱动或异步机制。
增加硬件资源:如果必要,增加CPU核心数量或升级硬件来缓解CPU占用问题。
通过以上步骤,您可以确定高CPU占用问题的原因,并采取适当的措施来解决问题,从而提高应用程序的性能。