1.Java虚拟机
Java虚拟机(Java Virtual Machine,JVM)是Java编程语言的关键组成部分之一。它是一种可以执行Java字节码的虚拟计算机,提供了跨平台的能力,使得Java程序可以在不同的操作系统和硬件平台上运行。
JVM是一个功能强大的虚拟机,它提供了许多特性和功能,使得Java程序具有高度的可移植性、安全性和性能。开发人员可以通过调优JVM参数和了解其工作原理来优化和提升Java应用程序的性能。
常见虚拟机 :HotSpot
1.JVM内存模型,哪些区,分别干什么
方法区也被称为永久代,
存储虚拟机加载的类信息、常量、静态变量。常量池也是方法区的一部分。
JDK8以后废弃了永久代改用元空间,不在虚拟机,在本地内存中。
方法区移至 Metaspace,字符串常量移至 Java Heap,常量池移至 Metaspace。
2.JVM运行时数据区
JVM(Java虚拟机)在运行时会将内存划分为不同的数据区域,用于存储不同类型的数据和执行不同的操作。以下是JVM运行时数据区的主要部分:
- 方法区(Method Area):
-
- 方法区用于存储类的结构信息,包括类的字节码、常量池、字段和方法信息等。方法区是所有线程共享的内存区域。
- 堆(Heap):
-
- 堆是用于存储对象实例的区域。在堆中分配的对象由垃圾回收器负责回收。堆也是所有线程共享的内存区域。
- 虚拟机栈(VM Stack):
-
- 虚拟机栈用于存储方法的局部变量、参数、方法返回值以及方法调用的栈帧信息。每个线程都有自己的虚拟机栈,栈中的每个栈帧对应一个方法调用。
- 本地方法栈(Native Method Stack):
-
- 本地方法栈与虚拟机栈类似,但是用于存储本地方法(Native Method)的数据和信息。
- 程序计数器(Program Counter Register):
-
- 程序计数器用于存储当前线程执行的字节码指令的地址。每个线程都有自己的程序计数器,用于指示下一条要执行的指令。
- 直接内存(Direct Memory):
-
- 直接内存是JVM在堆外分配的内存区域,用于存储NIO(New I/O)库中的数据。它是通过调用本地方法库来分配和释放内存。
3.JVM垃圾回收算法有哪些?它们有什么区别?
- 标记 -清除算法(Mark-Sweep)
-
- 分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
- 缺点:标记和清除过程都效率不高,空间碎片化问题。
-
-
- 空间碎片太多,导致需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。
-
- 复制算法 (Copying)
-
- 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
- 解决了碎片化问题,但代价使可用容量缩小一半
- 标记 - 整理算法
-
- 标记-整理 算法是在标记-清除 算法之上,又进行了对象的移动排序整理,
- 因此成本更高,但却解决了内存碎片的问题。
- 分代收集算法(Generational Collection)
-
- 根据对象存活周期的不同将内存分为几块。
- 一般把Java堆分为新生代和老年代,根据各个年代的特点采用最合适的收集算法。
- 新生代中的对象生命周期较短,每次垃圾收集时有大批对象死去,只有少量存活,采用复制算法;
- 老年代中的对象存活率高生命周期较长,采用标记-清除或标记-整理算法。
4.堆的结构?
- 堆分为新生代与老年代
- 新生代分为分为 eden区 与两个survivor区。
5.堆和栈的区别?
堆区:
- 线程共享的区域
- 存储new来的对象实例和数组、字符串常量,非static的变量
栈区
- 线程私有
- 栈中存放局部变量(基本类型的变量)和对象的引用
- 栈的优势是,存取速度比堆要快
(3)异常错误不同
如果栈内存或者堆内存不足都会抛出异常。
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.lang.OutOfMemoryError。
(4)空间大小
栈的空间大小远远小于堆的
6.新生代和老年代的区别
所谓的新生代和老年代是针对于分代收集算法来定义的,新生代又分为Eden和Survivor两个区。加上老年代就这三个区。
- 数据会首先分配到Eden区
-
- 如果是大对象可能会直接放入到老年代(大对象是指需要大量连续内存空间的java对象)
- 当Eden没有足够空间的时候就会触发一次Minor GC
- 如果对象经过一次Minor GC还存活,并且又能被Survivor空间接受
- 那么将被移动到Survivor空 间当中。并将其年龄设为1
- 对象在Survivor每熬过一次Minor GC,年龄就加1
- 当年龄达到一定的程度(默认为15,可通过 -XX:MaxTenuringThreshold 配置 )时,就会被晋升到老年代 中了
- 当然晋升老年代的年龄是可以设置的。如果老年代满了就执行:Full GC 因为不经常执行,因此采用了 Mark-Compact
7.年轻代空间的要点
- 大多数新建的对象都位于Eden区。
- 当Eden区被对象填满时,就会执行Minor GC,并把所有存活下来的对象转移到其中一个survivor区。
- Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区。这样在一段时间内,总会有一个空的survivor区。
- 经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间,通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。
8.对象什么时候到老年代?
- 对象首次创建会被放置在新生代的eden区
对象进入老年代主要有下面三种方式:
- 大对象
-
- 比如很长的字符串、很大的数组等
- 可通过参数-XX:PretenureSizeThreshold=3145728,单位是字节,设置超过这个参数设置的值就直接进入老年代
- 长期存活的对象
-
- 对象头中(Header)包含了 GC 分代年 龄标记信息。如果对象在 eden 区出生,那么它的 GC 分代年龄会初始值为 1
- 每熬过一次 Minor GC 而不被回收,这个值就会增 加 1 岁。当它的年龄到达一定的数值时,就会晋升到老年代中
- 可以通过参数 -XX:MaxTenuringThreshold 设置年龄阀值(默认是 15 岁)
- 动态对象年龄判定
-
- 当 Survivor 空间中相同年龄所有对象的大小总和大于 Survivor 空间的一半。
- 年龄大于或等于该年龄的对象就可以直接进入老年代,而不需要达到默认的分代年龄。
9.如何判断对象是否可以被回收?
- 【引用计数法】
-
- 系统为对象添加一个计数器,当有新的引用时加1,引用失效时减1。但此方法无法解决两个对象循环引用的问题。
- 【可达性分析法】
-
- 通过对象的引用链来判断该对象是否需要被回收。
- 通过一系列的GC Roots的对象作为起始点,从这些起节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,就需要回收此对象。
10.GC ROOT 对象
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(Java Native 方法)引用的对象
- 活动线程(Thread)的引用
- 同步锁(synchronized)持有的对象
- Java 虚拟机内部引用的对象,如基本类型对应的 Class 对象等
11.Minor GC、Major GC和Full GC
Minor GC
Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发stop-the-world,但是它的回收速度很快。
Major GC
Major GC清理Tenured区,用于回收老年代,出现Major GC通常会出现至少一次Minor GC。
Full GC
Full GC是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。
12.什么情况下会触发Full GC?
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
- 调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
- 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
- 空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
- JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
- Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
13.JVM如何排查现场问题,在没有线上排查工具的时候排查过程是怎么样的?
- 查看日志信息:首先需要查看日志信息,了解出错原因,排除一些显而易见的问题。
- 堆栈跟踪:在代码中使用堆栈跟踪来查看异常的发生位置,以便定位问题。
- 打印日志:在代码中添加打印日志的语句,可以帮助我们了解程序执行的过程,更容易定位问题。
- JMap工具:使用JMap工具生成堆快照,可以查看内存使用情况和对象分布情况,以便定位内存泄漏等问题。
- JConsole工具:使用JConsole工具查看程序的运行状态,包括线程数、内存使用等信息。
- JStack工具:使用JStack工具生成线程快照,可以查看线程执行的情况,以便定位死锁等问题。
- 分析代码:最后,根据以上信息对代码进行分析,找到问题所在,进行修复。
以上方法不一定适用于所有情况,具体需要根据具体问题而定。在排查问题时需要有一定的经验和技巧,并且需要快速定位问题并解决。同时,在排查过程中也需要注意保护现场数据,避免因排查过程中对数据的影响造成不必要的损失。
14.发生OOM的原因
1.发生OOM(outOfMemoryError)时,年轻代和老年代都正常,那有可能是什么原因导致的?
- 本地内存不足(操作系统不允许申请更大的内存);
- 堆内存不足;
- 元空间不足;
- JVM执行GC耗时太久;
- 创建的线程到超过限制;
15.线上CPU 100%排查过程
- top命令,查看占用CPU高的进程pid
- top -Hp pid 查看进程中最耗CPU的子线程
- printf %x tid 将线程id转换成16进制
- jstack pid<开始的进程id> | grep 0xtid<0x加上上一步转换出来的线程id>
查看具体出现问题的代码位置
16.为什么使用元空间替换永久代
- 内存管理:永久代的大小是固定的,无法动态调整。如果应用程序加载的类过多或者动态生成的类过多,永久代的空间可能会耗尽,导致OutOfMemoryError。而元空间的大小是受限于系统的可用内存,并且可以根据需要动态分配和释放。
- 永久代回收:永久代是需要进行Full GC才能垃圾回收和老年代一起垃圾回收。而元空间使用的是本地内存(Native Memory),它不受Java堆的大小限制,垃圾回收由操作系统负责,不需要进行Full GC来回收元空间的空间。
- 类的动态加载和卸载:永久代的空间是有限的,一旦加载的类过多,就可能导致永久代溢出。而元空间可以根据需要动态分配内存,允许更多的类被加载,同时可以支持类的动态卸载,减少了内存的占用。
- 性能优化:元空间的内存分配和释放不需要进行复制和压缩操作,相对于永久代来说,具有更高的性能。
- 虚拟机厂商变化,hotspot与jrockit合并jrockit中没有永久代
综上所述,使用元空间替换永久代主要是为了解决永久代固定大小、垃圾回收复杂、动态加载和卸载类的限制等问题,提供更灵活、可扩展和性能优化的内存管理方式。
17.什么情况会造成内存泄漏
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点:
首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;
其次,这些对象是无用的,即程序以后不会再使用这些对象。
如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
18.什么是三色标记?
三色标记(Three-color Marking)是一种用于垃圾回收的标记算法,常用于并发垃圾收集器中。它是基于可达性分析的垃圾回收算法的一部分。
在三色标记算法中,对象被标记为三种不同的颜色:白色、灰色和黑色。
- 白色:表示对象尚未被垃圾收集器访问过,或者已被访问但其引用的对象尚未被访问。
- 灰色:表示对象已被垃圾收集器访问过,但其引用的对象尚未被访问。
- 黑色:表示对象已被垃圾收集器访问过,并且其引用的对象也已被访问。
三色标记算法的基本思想是从一组根对象开始,将它们标记为灰色,并将它们放入待处理的工作队列中。然后,从工作队列中取出一个灰色对象,将其标记为黑色,并遍历其引用的对象,将尚未标记的对象标记为灰色,并加入工作队列。这个过程会一直重复,直到工作队列为空。
在并发垃圾收集中,三色标记算法允许垃圾收集器与应用程序并发执行。在标记过程中,应用程序可以继续运行,并且可以修改对象引用关系。为了确保标记的准确性,三色标记算法采用了一些技术,如写屏障和栅栏等,以保证在并发标记过程中不会遗漏任何对象。
三色标记算法在并发垃圾收集器中广泛应用,例如 Java 的并发标记清除算法(CMS)和并发标记压缩算法(G1)等。它能够高效地进行垃圾回收,并且具有较短的停顿时间,使应用程序的响应性得到保证。
19.简述JVM的内联和逃逸分析
- 内联(Inlining):
-
- 编译过程中,将方法调用处的代码替换为被调用方法的实际代码。
-
-
- 内联的目的是减少方法调用的开销,提高程序的执行效率。通过内联,可以避免方法调用时的栈帧切换和参数传递的开销,将被调用方法的代码直接嵌入到调用处。内联可以在编译器优化过程中进行静态内联,也可以在运行时进行动态内联。内联的优化效果取决于多个因素,如方法的大小、调用频率、上下文等。
-
- 逃逸分析(Escape Analysis)
-
- 编译器分析对象的生命周期和作用域,确定对象是否会逃逸出方法的作用域。
- 比如,当对象赋值给对象类并且是静态static修饰。调用某个类方法的时候为属性赋值,则认为是对象逃逸到了全局变量。
-
-
- 逃逸分析的目的是识别那些在方法外部被访问或引用的对象,以便进行进一步的优化。如果对象不逃逸,即只在方法内部使用并且不被外部引用,编译器可以对其进行一些优化,如栈上分配(Stack Allocation)或标量替换(Scalar Replacement)。这些优化可以减少堆内存的使用,提高对象的创建和访问效率。
-
综上通过内联和逃逸分析均是为了提高程序的性能和效率。
- 内联可以减少方法调用的开销,消除不必要的函数调用,提高代码的执行速度。
- 逃逸分析可以减少堆内存的使用,将对象的生命周期限制在局部作用域内,减少垃圾回收的压力。
20.system.gc()一定会触发gc吗?和full gc有什么关系?
调用System.gc()方法并不一定会立即触发垃圾回收(GC)。
System.gc()只是向JVM发送了一个建议执行垃圾回收的请求,具体是否执行垃圾回收还取决于JVM的实现和配置。
JVM通常会根据当前的内存使用情况、垃圾回收策略以及系统负载等因素来决定是否执行垃圾回收。
一般情况下,当系统认为执行垃圾回收可以带来性能提升时,会主动触发垃圾回收操作。而System.gc()方法的调用可以增加垃圾回收的可能性,但不保证立即触发。
关于Full GC(Full Garbage Collection),它是垃圾回收的一种形式,与增量、部分回收等方式相对应。Full GC会清理整个堆内存,包括新生代和老年代,并且会暂停所有的应用线程,对整个堆进行压缩、整理等操作。Full GC通常会耗费较长的时间,因此在性能敏感的应用中,尽量避免频繁触发Full GC。
System.gc()方法的调用会增加Full GC的可能性,因为Full GC会对整个堆进行清理,而不仅仅是部分区域。然而,具体是否执行Full GC还是由JVM来决定,根据JVM的垃圾回收策略和当前的内存使用情况来判断是否需要执行Full GC。
注意的是,过度使用System.gc()方法可能会导致频繁的垃圾回收操作,影响应用程序的性能。通常情况下,JVM的垃圾回收机制是自动管理的,建议不要过于依赖手动触发垃圾回收,而是通过合理的内存管理和性能调优来确保应用程序的性能和内存使用的平衡。
21.a+b操作数栈过程
当执行a+b操作时,假设a和b是两个整数。操作数栈的过程如下:
-
- 将a和b的值压入操作数栈。
- 从操作数栈中弹出a和b的值。
- 执行相加操作,将结果压入操作数栈。
22.方法返回地址的回收
方法返回地址是存储在栈帧中的,栈帧会在方法执行完成后被销毁。当方法执行结束,即执行了return指令时,会将方法返回地址弹出,将控制流程返回到调用方。在栈帧被销毁后,方法返回地址也会被回收。
23.程序计数器为空的情况
程序计数器存储当前线程正在执行的字节码指令地址。当线程处于非Java方法(如本地方法)或线程刚创建时,程序计数器为空。此外,线程在执行异常、死循环等情况下也可能为空。