基础
请简单的介绍一下jvm?
-
JVM
- 全称:Java Virtual Machine(Java虚拟机)
- 简介:JVM是一种虚拟机,它使计算机能够运行Java程序以及用其他语言编写并编译为Java字节码的程序。Java的设计理念之一是"一次编写,到处运行",JVM的存在使得Java程序具有跨平台性,不受底层硬件和操作系统的影响。
- JVM规范
- 类加载器:负责加载、链接和初始化Java类。
- 虚拟机架构:定义了特定数据类型、高效的垃圾收集器以及内存结构。
- 字节码指令: JVM有自己的指令集,使得相同的字节码在不同操作系统上都有相同的语义。
- JVM语言:允许其他编程语言通过生成Java字节码来运行在JVM上,例如Groovy、Scala等。
- 字节码验证器:确保字节码是安全的,防止恶意代码的执行。
- 字节码解释器和即时编译器:
- 字节码解释器:将字节码解释为机器码,针对不同的硬件架构适配,使得Java程序可以在不同的硬件和操作系统动态地适应环境。
- 即时编译器:将字节码转换为本地机器码,提高程序的执行效率。
-
参考链接:https://en.wikipedia.org/wiki/Java_virtual_machine
Java有哪些内存区域?
- Java内存区域:一般是指程序运行期间,使用的各种运行时期使用的各种运行时数据区域。
- PC寄存器:线程私有。PC寄存器存储当前正在执行的 Java 虚拟机指令的地址,确保线程在正确的位置执行指令。
- 栈:线程私有,用于存储栈帧。栈帧包含了局部变量表、操作数栈、动态链接、方法出口等信息。栈中存储方法调用和局部变量等信息。
- 堆:线程共享区域。所有类的实例(对象),数组都是在这里分配。由垃圾收集器负责释放对象。
- 方法区:线程共享区域。它存储每个类的结构,时常量池。逻辑上属于堆结构
- 运行时常量池:运行时常量池是方法区的一部分,用于存储class文件中的常量池信息。包括类、接口中的常量,字符串常量等。
- Native方法栈:Native方法栈是用于执行native方法,和栈类似。这个区域不一定在所有的Java虚拟机实现中都存在
大致结构如下所示,根据官网描述,拆分成了共享区域和线程私有区域。
- 官网链接:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5
栈帧都有哪些数据?
- 局部变量表(Local Variable Table): 用于存储方法中定义的局部变量。
- 操作数栈(Operand Stack): 用于执行方法时进行计算的栈结构。操作数栈存储了方法执行过程中的操作数,以及执行过程中的中间结果。
- 动态链接(Dynamic Linking): 用于在运行时解析常量池中的符号引用。
- 方法返回地址(Return Address): 用于存储方法调用结束后返回的地址。
一下JDK1.6、1.7、1.8内存区域的变化?
- 1.6 标准的JVM方法区设计规范
- 1.7 将字符串常量池移入堆
- 1.8 去掉永久代,取而代之的是元空间,并且不在堆中,而是放入本地内存
- 官网链接:
- https://www.oracle.com/java/technologies/javase/jdk7-relnotes.html
- https://openjdk.org/jeps/122
为什么使用元空间替代永久代作为方法区的实现?
主要原因有以下三点
-
JRockit没有永久代,希望可以JRockit和Hotspot融合。
-
永久代启动时固定大小,难以调整。受到
-XX:MaxPermSize
参数的控制,在实际使用的过程中有些不便,容易造成OOM -
字符串常量会收到类回收的影响,类被卸载,与之关联的字符串常量也会被回收。
-
官网链接:https://openjdk.org/jeps/122
类的加载过程是?
- 加载(Loading):编译过后的字节码文件加载进JVM,存储在方法区中。
- 链接(Linking):获取类并将其组合到Java虚拟机的运行时状态以便可以执行的过程。
- 验证(Verification):验证二进制结构正确,此过程可能会导致加载其他类和接口
- 准备(Preparation):为类或接口创建静态字段并将这些字段初始化为其默认值。
- 解析(Resolution):解析是从运行时常量池的符号引用中动态确定具体值的过程。
- 初始化(Initialization):执行类初始化方法,如上面静态变量的常量值,静态代码块之类的,在这一步执行。以下情况会初始化
- 通过JVM指令引发的初始化:new、getstatic、putstatic或invokestatic
- 首次调用
MethodHandle
实例 - 调用class库中的反射方法
- 子类被初始化
- 指定为Java虚拟机启动时的初始类。
- 官网链接:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html
Java的双亲委派机制是什么?
双亲委派(Parents Delegation Model):
当加载一个类的时候,如果类不存在,会先交给父类进行加载(前提是没有破坏双亲委派模型),如果父加载器无法完成,自己才会去加载这个类加载器。
/**
* 1. 调用 findLoadedClass(String) 以检查该类是否已加载。
* 2. 在父类加载器上调用 loadClass 方法。如果父级为空,则使用虚拟机内置的类加载器。
* 3. 调用 findClass(String) 方法来查找该类。
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查类是被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//调用父类loadClass
c = parent.loadClass(name, false);
} else {
//使用虚拟机内置的类加载器(BootStrap)。
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//使用findClass来找到类
long t1 = System.nanoTime();
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
以上就是简略版本的双亲委派模型的答案整理。下面是一些多余的衍生小问题,也许用的上呢
- 双亲委派模型的构成
我能找到的入口,就是Launcher
类,其中AppClassLoader
指定了ExtClassLoader
为父类,ExtClassLoader
并没有指定父类,而是通过上面的loadClass
如果parent为null则交给BootstrapClassLoader
处理,BootstrapClassLoader
也是JVM中创建并管理。
public Launcher() {
ExtClassLoader var1;
var1 = Launcher.ExtClassLoader.getExtClassLoader();
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
Thread.currentThread().setContextClassLoader(this.loader);
}
BootstrapClassLoader
,ExtClassLoader
,AppClassLoader
各自管理的部分
-
BootstrapClassLoader
:System.getProperty("sun.boot.class.path")
-
ExtClassLoader
:System.getProperty("java.ext.dirs")
对应如下包 -
AppClassLoader
:System.getProperty("java.class.path")
- 双亲委派核心方法
- loadClass:加载类,双亲委派实现的关键
- findClass:从URL查找并加载指定的类,具体加载实现为
defineClass
为什么需要双亲委派机制?
双亲委派模型保证了,无论什么类加载器,获取一个类,都先由父加载器先加载,因此,访问这个类,返回的也都是同一个。
如何破坏双亲委派机制?
如果想打破,直接重写loadClass即可
如果不想打破,重写findClass
对象创建的过程是?
- 检查类是否被加载:会去运行时常量池中查找该引用所指向的类有没有被JVM加载,如果没有被加载,那么会走上面类加载的过程
- 空间分配:为对象在栈中和堆中分配一定的空间。
- 初始化:JVM会将对象的字段设置为默认值。
- 信息标记:虚拟机对对象进行必要的设置,包括该对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些关键信息存放在对象的对象头中。
- 执行构造方法:执行对象的构造方法。
- 参考链接:https://www.cnblogs.com/xfeiyun/p/15600513.html
能说一下类的生命周期吗?
和类加载机制相比多了使用了卸载
分配内存对象中的指针碰撞和空闲列表说下?
- 指针碰撞(Bump the pointer)
- Java堆中的内存是规整的(标记整理),所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。例如:Serial、ParNew等收集器没有产生内存碎片,所以采用的是"指针碰撞"。
- 空闲列表(Free List)
- Java堆中的内存不是规整的(标记清除),已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。例如:CMS这种基于"标记-清除"的收集器,会产生内存碎片,空闲内存跟已经分配的内存相互交错,所以采用的是"空闲列表"。
- 参考链接:https://www.cnblogs.com/xfeiyun/p/15600513.html
内存溢出和内存泄漏是什么意思
- 内存溢出:我想要申请空间,但是空间不够了,地主家实在是没有余粮了,GC后也不够,伴随着这个报错OutOfMemoryError
- 内存泄漏:这个对象没有用了,但是还是存活,拜拜浪费内存空间,如果放任不管最终会导致内存溢出
强引用、软引用、弱引用、虚引用是什么?
这个我之前写过博客,感兴趣可以看下链接
这里还是总结一下
- 强引用:平时创建对象的方式就是强引用。GC绝对不会回收,内存不够报OOM,也不会回收
Object obj =new Object();
- 软引用:只有在OOM发生前才会对他进行回收。
list[i]=new SoftReference<Element>(new Element(i));
- 弱引用:只存在弱引用的对象会在下一次GC时被回收
list[i]=new WeakReference<Element>(new Element(i));
- 虚引用:用于代替
finalize
,在对象被gc的时候执行clear方法
你平时工作中用过的JVM常用基本配置参数有哪些?
堆配置:
- -Xms:设置JVM初始内存。
- -Xmx:设置最大堆大小。
- -Xmn:设置年轻代大小。
- -Xss:设置线程的栈大小。
- -XX:NewRatio:设置年轻代和年老代的比值。
- -XX:SurvivorRatio:年轻代中Eden区与两个Survivor区的比值。
- -XX:MaxPermSize:设置永久代大小(1.7)
- -XX:MaxMetaspaceSize:元空间最大配置
- -XX:MaxTenuringThreshold:晋升到老年代的年龄
gc配置
- -XX:+UseParallelGC:选择垃圾收集器为并行收集器。
- -XX:ParallelGCThreads:配置并行收集器的线程数,即同时多少个线程一起进行垃圾回收。
- -XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。
- -XX:MaxGCPauseMillis:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
- -XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器自动选择年轻代区大小和相应的Survivor区比例
- -XX:+UseConcMarkSweepGC:设置年老代为CMS。
- -XX:+UseParNewGC:设置年轻代为parNew
- -XX:CMSFullGCsBeforeCompaction:设置运行多少次GC以后对内存空间进行压缩、整理。
- -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。
- -XX:ParallelCMSThreads:CMS使用的线程数
- -XX:+CMSScavengeBeforeRemark:重标记前做一次younggc
- -XX:+UseG1GC:设置垃圾收集器为G1
其他:
-
-XX:+PrintGC:用于输出GC日志。
-
-XX:+HeapDumpOnOutOfMemoryError:内存溢出时导出整个堆信息
-
-XX:HeapDumpPath:导出堆的存放路径
-
参考链接:https://www.ibm.com/docs/en/sdk-java-technology/8?topic=options-xxparallelcmsthreads
请你谈谈对OOM的认识
OOM:指的是java.lang.OutOfMemoryError
,它一出现就代表,再堆中没有空间分配对象了,FullGC后也没有空间了
官网中列出了如下几种情况我见过的可能就第一种=。=
-
Java heap space:最常见的情况。他的出现就是FullGC后的堆内存不够分配给对象了,它的出现,要么对象过大,要么内存泄漏引起的
-
GC Overhead limit exceeded:花费过多的时间在GC上并且释放了很少的内存。可以被
-XX:-UseGCOverheadLimit
关闭 -
Requested array size exceeds VM limit:数组大小超过了VM的限制
-
Metaspace:元空间耗尽了,一般呢和
-XX:MaxMetaspaceSize
优点关系 -
request size bytes for reason. Out of swap space?(Native Heap):JVM尝试在本地堆分配内存,但是本地堆已经快耗尽了。
- 看的我比较晕,我的理解是:内存+swap>堆空间>内存,可能其他程序导致了swap空间不够引发的。
-
Compressed class space:跟类指针压缩有关
UseCompressedClassPointers
,如果超过了另一个配置CompressedClassSpaceSize
会报错 -
reason stack_trace_with_native_method:JNI中发生了OOM
-
官网链接:https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/memleaks002.html
如何排查内存溢出?
亲身经历了多次OOM,可以很负责任的告诉你们,就是按照如下几个步骤来
- 发现OOM
1.1. 已经发生了
一般都是有告警,这个挽救已经来不及了,跳到下面看看怎么分析并处理吧
还记得这两个配置吗,这时候已经有文件了。
- -XX:+HeapDumpOnOutOfMemoryError:内存溢出时导出整个堆信息
- -XX:HeapDumpPath:导出堆的存放路径
1.2. 还没发生,快要发生了
这个一般伴随着几个特征
- 频繁Full GC
- Full GC后内存下不去
针对频繁Full GC,可以查看gc log,针对FullGC后内存下不去,这个方法就比较多了,我比较常用的是jstat,主要查看FullGC后的堆的大小。
不管符不符合,都可以dump内存然后重启了
- 分析OOM
虽然官网列了这么多种,但是,基本也就需要处理第一种情况(因为我处理过这么多次都是第一种=。=),就是堆内存不够的情况。
JProfile中就可以进行分析,看看Biggest Object就行。然后查看调用栈。还需要稍微关注下,List中的数量,也就是内存泄漏的重灾区。
OOM会造成程序退出吗
这个要分成多个问题来看
- JVM退出的场景
jvm退出场景是由于以下场景之一
- 正常退出场景:
- 当应用程序的所有非守护线程执行完成(没有非守护线程)
- 调用
java.lang.System.exit
方法
- 异常退出场景:
- JVM被迫中断(ctrl+C 或者 SIGINT指令)
- JVM被终止(SIGTERM指令)
- 线程退出的场景
通过上面看到JVM退出的场景之一就是所有的非守护线程都不存在,所以下面要列一下非守护线程不存在的几个场景
- 正常场景
- run方法执行完成
- 调用
java.lang.System.exit
方法 - 它是守护线程,所有非守护线程死亡
- 非正常场景
- Exception或者Error抛出或未处理
当线程申请内存的时候会抛出OOM,但它本身并不会导致JVM退出。
所以,OOM和造成JVM退出之间并没有什么关联关系。在单个线程中发生OOM错误通常只会影响到该线程。其他线程仍然可以正常执行,因此,OOM错误与JVM退出之间的直接没有什么关系。
下面是验证子线程,出现OOM的场景
只会导致当前线程退出,对于其他线程不受影响。
public static void main(String[] args) {
new Thread(()->{
List<Object> list=new ArrayList<>();
while (true){
list.add(new Object());
}
}).start();
new Thread(()->{
while (true){
System.out.println(1111);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}
- 参考链接:
- https://stackoverflow.com/questions/32315589/what-happens-when-the-jvm-is-terminated
- https://stackoverflow.com/questions/1269931/when-does-a-java-thread-reach-the-die-state
Java堆为什么要分代回收?
Java堆组成结构如下,划分为年轻代和老年代,年轻代又分成Eden区和Survivor区。这么分配的目的,是为了针对不同存活时间的对象采用更加高效的算法。年轻代主要特点就是朝生夕灭,这样用标记复制算法就特别高效,老年代存活时间长(经历年轻代层层筛选),需要的垃圾清理频次相对而言要少的多,这样标记整理或者标记清除更合适。
- 年轻代
- Eden区
- Survivor区
- From Survivor区
- To Survivor区
- 老年代
-XX:Newratio
配置年轻代和老年代-XX:SurvivorRatio
配置eden区和survivor的区域
JIT是什么?
JIT(Just-in-time compilation):在程序运行时编译代码。
- 传统的解释器在首次运行前将所有字节码解释成机器语言。JIT则是在运行时也可以编译
- JIT编译器可以访问动态运行时信息,而传统的解释器器不能。因此,JIT编译器可以进行更好的优化,将热点代码编译成机器码,从而进行优化。
- 参考链接:https://stackoverflow.com/questions/95635/what-does-a-just-in-time-jit-compiler-do/95679#95679
有没有了解逃逸分析技术?对象一定分配在堆中吗?
- 逃逸分析:简单的说,排查一下这个对象在哪被引用到了。对于逃逸的程度定义了三类。
typedef enum {
NoEscape = 1,
ArgEscape = 2,
GlobalEscape = 3
}
NoEscape
: 没有发生逃逸,可以被替换成标量,可以不分配在堆中ArgEscape
:被当做参数从一个方法传递给另一个方法,但是没有发生逃逸GlobalEscape
:发生了逃逸
对于第一种NoEscape
,对象可以替代成一个变量,那么分配在堆中也没有必要了,直接在栈中分配,无需GC管理。官网给了这个例子,如果全部在堆中,则要分配1亿个对象,如果标量替换,一旦对象返回,则立马释放。
public class Rect {
private int w;
private int h;
public Rect(int w, int h) {
this.w = w;
this.h = h;
}
public int area() {
return w * h;
}
public boolean sameArea(Rect other) {
return this.area() == other.area();
}
public static void main(final String[] args) {
java.util.Random rand = new java.util.Random();
int sameArea = 0;
for (int i = 0; i < 100_000_000; i++) {
Rect r1 = new Rect(rand.nextInt(5), rand.nextInt(5));
Rect r2 = new Rect(rand.nextInt(5), rand.nextInt(5));
if (r1.sameArea(r2)) { sameArea++; }
}
System.out.println("Same area: " + sameArea);
}
}
- 官网链接:https://blogs.oracle.com/javamagazine/post/escape-analysis-in-the-hotspot-jit-compiler
JIT中除了缓存热点代码外还有没有其他优化?
- 逃逸分析:对象的引用进行分析
- 锁消除:如果对象没有被其他线程访问,则
synchronized
会被消除 - 标量替换:对象替换成变量
- 栈中分配:在中分配对象
GC
Java中可作为GC Roots的对象有哪几种?
以下是Java中可作为GC Roots的对象的几种主要类型。GC Roots是一组引用,通过这组引用,可以追踪到所有存活的对象,确保不会对活跃对象进行垃圾回收。
-
Class:由系统类加载器加载的类;还包含对静态变量的引用。
-
本地栈:本地栈中存储的变量和方法参数。如果一个对象被本地栈引用,说明它是一个活跃对象,不能被回收。
-
活跃的Java线程:所有的活跃的Java线程
-
JNI References:JNI调用的创建的Java对象,包含本地变量,JNI方法参数和全局JNI引用
-
锁对象:重量级锁。因为锁对象通常会被多个线程共享,需要确保在使用锁的时候锁对象不会被垃圾回收。
-
由JVM实现定义的特定对象: 这包括一些由Java虚拟机实现定义的特殊对象,例如重要的异常类、系统类加载器或自定义类加载器。这些对象可能被视为GC Roots,以确保系统的正常运行。
-
参考链接:https://www.baeldung.com/java-gc-roots#types-of-gc-roots
JVM垃圾回收时候如何确定垃圾?
主要是两种方法,引用计数法和可达性分析算法
- 引用计数法:
- 原理:每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
- 缺点: 无法解决循环引用的问题。如果存在循环引用,引用计数法会导致计数器值永远不为零,即使这些对象实际上已经不再被程序所使用。
- 可达性分析算法:
- 原理:通过上面的GC Roots为根节点,向下搜索,可达的(可以搜索到的)都是活跃的,不能被回收,而无法到达的对象即为垃圾。
- 缺点:有STW时间,要避免新对象对引用链的影响,所以,会有一定的STW时间,这也是最常见的问题。
GC垃圾回收算法了解吗?与垃圾收集器的关系?
-
标记清除算法:标记和清除两大部分,先标记存活对象,再清除未被标记的对象
- 优点:简单,快(STW时间少)
- 缺点:
- 容易产生碎片
- 为对象分配空间速度慢
- 效率不稳定,需要回收的垃圾越多就越慢
- 垃圾收集器:CMS
-
标记复制算法:标记同上,不过不再是直接清除,而是复制到另一块上面,再将原来的清除。
- 优点:
- 如果存活对象少,这个速度会很快
- 缺点:
- 浪费的内存多
- 垃圾收集器:Serial、ParNew、Parallel Scavenge
- 优点:
- 标记整理算法:和上面类似标记和整理两大部分,先标记存活对象,然后让所有存活的对象都向内存空间一端移动,最后直接清理掉边界以外的内存
- 优点:
- 解决了标记清理算法存在的内存碎片问题
- 提高了吞吐量(不再需要找到合适的内存空间)
- 缺点:
- STW时间不能确定,收到复制量的影响
- 垃圾收集器:Serial Old、Parallell Old
- 优点:
Java对象从年轻代晋升到老年代的过程?
- 首先,对象分配在Eden中
- 当eden区满了就会触发Minor GC,对象复制到Survivor From(S0)区域,然后清除Eden区对象
3. 当Survivor区也有数据了,在Minor GC时,它们会被复制到Survivor To(S1)区域,连同Eden区存活的对象,并且年龄+1.
- 后续就不断重复第三部,但是Survivor From(S0)区域和Survivor To(S1)区域会切换
- 当达到设置的晋升(-XX:MaxTenuringThreshold)年龄,将从Survivor移动到老年代
- 随着次要 GC 的不断发生,对象将继续被提升到老年代。并最终触发老年代GC
-
-XX :PretenureSizeThreshold配置,直接晋升老年代
这个可以配置阈值,当对象超过这个字节大小,将直接在老年代创建该对象。 -
动态年龄相关
survivor区大小、survivor区目标使用率(-XX:TargetSurvivorRatio)、晋升年龄阈值(-XX:MaxTenuringThreshold),JVM会动态的计算tenuring threshold的值。一旦对象年龄达到了tenuring threshold就会晋升到老年代。
- 官网链接:
- https://www.oracle.com/webfolder/technetwork/Tutorials/obe/java/gc01/index.html
- https://www.oracle.com/technical-resources/articles/javame/garbagecollection2.html
CMS垃圾回收的过程
2024-02-06T17:00:36.024+0800: 112904.916: [GC (Allocation Failure) 2024-02-06T17:00:36.025+0800: 112904.917: [ParNew: 1549196K->15440K(1731712K), 0.0153275 secs] 3004250K->1471195K(3655808K), 0.0158411 secs] [Times: user=0.08 sys=0.00, real=0.01 secs]
--
2024-02-06T08:28:06.223+0800: 82155.115: [GC (CMS Initial Mark) [1 CMS-initial-mark: 1770925K(1924096K)] 1796139K(3655808K), 0.0091685 secs] [Times: user=0.04 sys=0.01, real=0.01 secs]
2024-02-06T08:28:06.232+0800: 82155.124: [CMS-concurrent-mark-start]
2024-02-06T08:28:06.431+0800: 82155.323: [CMS-concurrent-mark: 0.199/0.199 secs] [Times: user=0.52 sys=0.01, real=0.20 secs]
2024-02-06T08:28:06.431+0800: 82155.323: [CMS-concurrent-preclean-start]
2024-02-06T08:28:06.441+0800: 82155.333: [CMS-concurrent-preclean: 0.009/0.009 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2024-02-06T08:28:06.441+0800: 82155.333: [CMS-concurrent-abortable-preclean-start]
2024-02-06T08:28:09.672+0800: 82158.564: [CMS-concurrent-abortable-preclean: 3.023/3.231 secs] [Times: user=4.27 sys=0.12, real=3.23 secs]
2024-02-06T08:28:09.676+0800: 82158.568: [GC (CMS Final Remark) [YG occupancy: 790387 K (1731712 K)]2024-02-06T08:28:09.676+0800: 82158.568: [GC (CMS Final Remark) 2024-02-06T08:28:09.677+0800: 82158.569: [ParNew: 790387K->8346K(1731712K), 0.0106311 secs] 2561312K->1779760K(3655808K), 0.0111525 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
2024-02-06T08:28:09.688+0800: 82158.580: [Rescan (parallel) , 0.0061328 secs]2024-02-06T08:28:09.694+0800: 82158.586: [weak refs processing, 0.0272328 secs]2024-02-06T08:28:09.721+0800: 82158.613: [class unloading, 0.0732422 secs]2024-02-06T08:28:09.795+0800: 82158.686: [scrub symbol table, 0.0244528 secs]2024-02-06T08:28:09.819+0800: 82158.711: [scrub string table, 0.0027189 secs][1 CMS-remark: 1771413K(1924096K)] 1779760K(3655808K), 0.1717571 secs] [Times: user=0.25 sys=0.00, real=0.17 secs]
2024-02-06T08:28:09.849+0800: 82158.741: [CMS-concurrent-sweep-start]
2024-02-06T08:28:10.477+0800: 82159.369: [CMS-concurrent-sweep: 0.625/0.629 secs] [Times: user=0.90 sys=0.05, real=0.63 secs]
2024-02-06T08:28:10.478+0800: 82159.370: [CMS-concurrent-reset-start]
2024-02-06T08:28:10.480+0800: 82159.372: [CMS-concurrent-reset: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
-
初始标记(CMS Initial Mark):会STW,所有跟可达的对象都会标记
- [1 CMS-initial-mark: 1770925K(1924096K)]:1924096K老年代容量,1770925K时触发GC
- [1 CMS-initial-mark: 1770925K(1924096K)]:1924096K老年代容量,1770925K时触发GC
-
并发标记(CMS-concurrent-mark):初始标记阶段标记的对象,在这一阶段中标记这些对象可达的对象
- [CMS-concurrent-mark: 0.199/0.199 secs]:前0.199总时间,后0.199挂起时间
- 并发预清理(CMS-concurrent-preclean):减轻重标记的负担。扫描变动的,新晋升的,上一个阶段发生变动的对象,JVM会标记堆的这个区域为Dirty Card
- [CMS-concurrent-preclean: 0.009/0.009 secs]:前0.009运行时间,后0.009总挂起时间
-
可中断并发预清理(CMS-concurrent-abortable-preclean):和上面一致,但是这个阶段是可以中断的。
-
重新标记(CMS Final Remark):会STW,重新扫描 CMS 堆中任何残留的更新对象,从根开始回溯,并处理引用对象。
- [GC (CMS Final Remark) [YG occupancy: 790387 K (1731712 K)]2024-02-06T08:28:09.676+0800: 82158.568: [GC (CMS Final Remark) 2024-02-06T08:28:09.677+0800: 82158.569: [ParNew: 790387K->8346K(1731712K), 0.0106311 secs] 2561312K->1779760K(3655808K), 0.0111525 secs] [Times: user=0.06 sys=0.00, real=0.01 secs][Rescan (parallel) , 0.0061328 secs]2024-02-06T08:28:09.694+0800: 82158.586: [weak refs processing, 0.0272328 secs]2024-02-06T08:28:09.721+0800: 82158.613: [class unloading, 0.0732422 secs]2024-02-06T08:28:09.795+0800: 82158.686: [scrub symbol table, 0.0244528 secs]2024-02-06T08:28:09.819+0800: 82158.711: [scrub string table, 0.0027189 secs][1 CMS-remark: 1771413K(1924096K)] 1779760K(3655808K), 0.1717571 secs] [Times: user=0.25 sys=0.00, real=0.17 secs]
2024-02-06T08:28:09.849+0800: 82158.741: [CMS-concurrent-sweep-start]- YG occupancy: 790387 K (1731712 K):年轻代当前占用790387 K,年轻代总占用1731712 K
- [ParNew: 790387K->8346K(1731712K), 0.0106311 secs],parNew清除后8346K
- [Rescan (parallel) , 0.0061328 secs]:整堆重新扫描
- [weak refs processing, 0.0272328 secs]:处理弱引用
- [class unloading, 0.0732422 secs]:卸载未使用的类
- [scrub symbol table, 0.0244528 secs]:清理符号表和字符串表,这些表存储着类级元数据和内部化字符串。
- [GC (CMS Final Remark) [YG occupancy: 790387 K (1731712 K)]2024-02-06T08:28:09.676+0800: 82158.568: [GC (CMS Final Remark) 2024-02-06T08:28:09.677+0800: 82158.569: [ParNew: 790387K->8346K(1731712K), 0.0106311 secs] 2561312K->1779760K(3655808K), 0.0111525 secs] [Times: user=0.06 sys=0.00, real=0.01 secs][Rescan (parallel) , 0.0061328 secs]2024-02-06T08:28:09.694+0800: 82158.586: [weak refs processing, 0.0272328 secs]2024-02-06T08:28:09.721+0800: 82158.613: [class unloading, 0.0732422 secs]2024-02-06T08:28:09.795+0800: 82158.686: [scrub symbol table, 0.0244528 secs]2024-02-06T08:28:09.819+0800: 82158.711: [scrub string table, 0.0027189 secs][1 CMS-remark: 1771413K(1924096K)] 1779760K(3655808K), 0.1717571 secs] [Times: user=0.25 sys=0.00, real=0.17 secs]
- 并发清除(CMS-concurrent-sweep):开始并发清除
-
重置阶段(CMS-concurrent-reset):重置cms数据,以便开启新的一次
-
参考博客:
- https://poonamparhar.github.io/understanding_cms_gc/
- https://files.mdnice.com/user/10673/6ff75eb4-1e36-47ad-8b19-c80c1dbe488a.png
什么是Stop The World ? 什么是 OopMap ?什么是安全点?
- Stop The World:是指JVM在执行垃圾回收时,会暂停应用程序的所有线程,直到垃圾回收完成为止。 在这个过程中,应用程序无法响应任何请求,所有的线程都会被挂起。
- OopMap:是指对象指针映射表,在Java虚拟机中用于存储对象引用的位置和类型信息。
- SafePoint:是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,线程可以暂停。
GC日志的real、user、sys是什么意思?
-
real:程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待 I/O 完成)。
-
user:进程执行用户态代码(核心之外)所使用的时间。这是执行此进程所使用的实际 CPU 时间,其他进程和此进程阻塞的时间并不包括在内。在垃圾收集的情况下,表示 GC 线程执行所使用的 CPU 总时间。
-
sys:进程在内核态消耗的 CPU 时间,即在内核执行系统调用或等待系统事件所使用的 CPU 时间。
-
参考博客:
- https://cloud.tencent.com/developer/article/1491229
- https://stackoverflow.com/questions/556405/what-do-real-user-and-sys-mean-in-the-output-of-time1?lq=1
生产问题
怎么看死锁的线程?
- 首先,先看最近提交记录,上过哪些需求,来确定范围。一般很好排查
- 如果不行,现在我比较习惯用arthas
- thread -b 可以查看当前阻塞的线程信息
- thread {id} 可以答应堆栈信息
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
new Thread(() -> {
synchronized (lock1) {
sleep(2000);
synchronized (lock2) {
System.out.println("myThread1 over");
}
}
},"myThread1").start();
new Thread(() -> {
synchronized (lock2) {
sleep(2000);
synchronized (lock1) {
System.out.println("myThread1 over");
}
}
},"myThread2").start();
}
private static void sleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 参考链接:https://arthas.aliyun.com/doc/thread.html
对于JDK自带的监控和性能分析工具用过哪些?
之前写过,这里就不重复了链接
频繁Minor GC怎么办?
出现这个问题主要是年轻代设置的不大合理,设置的过于小了。
这样会导致Eden区和Survivor区很快被填满,动态年龄导致过早晋升。
可以通过jstat查看年轻态使用情况来判断
频繁Full GC怎么办?
这个最常见的就是以下两种状况
- 过早晋升导致的
- 内存泄漏
过早晋升可以结合频繁MinorGC一起判断
如果是内存泄漏,频繁FullGC之外,还伴随一个现象,FullGC时间过长,这时候需要把内存dump下来,放入分析工具(我常用JProfile)来进行分析
我之前的博客《Hutool:WeakCache导致的内存泄漏》就是这个场景
假如生产环境CPU占用过高,请谈谈你的分析思路和定位。
根据我定位的众多生产问题经验,尤其是cpu飙高问题,
- 首先先看是否持续,不持续多半是物理机出了点问题。
- 查看是否发版本后,基本上就是代码问题,回滚定位代码,本地模拟即可
- 查看是否有异常请求,Ddos攻击等等。
- 通过top命令是否是java进程。
- 如果cpu100%,且持续,判断是否可以回滚,然后进行重启,其次就是模拟这种场景,通过arthas查看cpu占用率高的线程,获取堆栈信息
内存飙高问题怎么排查?
- 分析是哪个进程占用内存
- 如果是java进程分析dump文件即可
有没有处理过内存泄漏问题?是如何定位的?
我之前的博客《Hutool:WeakCache导致的内存泄漏》
有没有处理过内存溢出问题?
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/xx/xx
正常项目都设置了这个参数,直接分析堆栈即可,OOM更容易,分析大对象即可
什么情况会造成元空间溢出?
默认元空间没有限制,只有到达了-XX:MaxMetaspaceSize
或者物理内存不足的了才会造成元空间OOM
什么时候会造成堆外内存溢出?
当年排查swap的时候,阴差阳错分析过堆外内存。所以对这块源码有点影响
存在泄露有2个地方,1个是java自带的api,1个是netty的。
- java自带的api
同时这个也是arthas扫描的direct的来源
内存溢出的源码,-XX:MaxDirectMemorySize
限制了这个,且达到了,会造成溢出
private static boolean tryReserveMemory(long size, int cap) {
// -XX:MaxDirectMemorySize limits the total capacity rather than the
// actual memory usage, which will differ when buffers are page
// aligned.
long totalCap;
while (cap <= maxMemory - (totalCap = totalCapacity.get())) {
if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {
reservedMemory.addAndGet(size);
count.incrementAndGet();
return true;
}
}
return false;
}
- Netty
还有一个是Netty的,当设置了io.netty.maxDirectMemory
也会造成OOM
SWAP会影响性能么?
之前博客分析过:《生产问题复盘!Swap对GC的影响》