【八股】JVM 基础知识

一、JVM的作用

  1. Java 代码的编译和解释执行:Java代码首先被编译成字节码,然后由JVM解释执行字节码。JVM 使用即时编译技术(Just-In-Time Compilation,JIT)将频繁执行的代码编译成本地机器代码,从而提高执行效率。
  2. 内存管理:JVM 提供了垃圾回收机制,自动管理内存分配和释放,避免内存泄漏和空指针异常等问题。
  3. 安全管理:JVM 可以对 Java 代码进行安全性检查,防止恶意代码对系统造成损害。
  4. 多线程支持:JVM 可以处理多个线程的并发执行,保证线程安全。
  5. 跨平台兼容性:JVM 实现了一种平台无关的执行环境,使 Java 代码可以在不同平台上运行。

二、JVM运行时数据区

  • 堆 Heap:存放对象实例、GC垃圾回收的主要区域

  • 方法区 MethodArea:存放类相关的信息(Class对象、静态变量、常量、即时编译器编译后的代码缓存)

    • 永久代:永久代是一段连续的内存空间,并且永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。
    • 元空间:元空间的出现是为了解决永久代的一些缺陷;
    • 方法区中的存储变化:元空间不再与堆连续,并且是存在于本地内存(Native memory)中的
  • 虚拟机栈 Java Virtual Machine Stack:由栈桢组成,一个栈桢就是描述一个Java方法执行的内存模型;也就是说每一个Java方法在执行时都会创建一个栈桢来存储局部变量表、操作数栈、方法的返回地址等信息;

  • 本地方法栈 Native Method Stacks:

  • 程序计数器 Program Counter Register:当前线程所执行的字节码的行号指示器,为了线程切换后能恢复到正确的执行位置

线程共享:堆和方法区,线程私有:虚拟机栈、本地方法栈、程序计数器

三、哪些内存区域可能发生 OOM?

堆、方法区、虚拟机栈、本地方法栈都可能发生OOM,唯独程序计数器是内存中没有OOM情况的区域;

1、堆:如果在创建对象的时候,堆中没有足够的空间给对象分配,并且堆无法再扩展,此时就会OOM;

2、方法区:在JDK8之前,方法区由永久代实现的时候,如果元数据的大小超过了设定的永久代的大小则会OOM;但是JDK8之后,方法区又元空间实现,元空间存在于本地内存,物理受限于本地内存,如果本地内存足够,就不会出现OOM;

3、虚拟机栈/本地方法栈:如果动态扩展虚拟机栈空间,此时如果无法申请到足够的空间则会出现OOM;虚拟机还会出现一种错误,栈溢出(StackOverflowError),如果线程请求的深度大于虚拟机所允许的深度,则可能出现;

四、new一个对象在堆中的历程(对象的创建过程)

加载-〉》链接【验证-〉》准备-〉》解析-〉》】-〉》初始化-〉》

1、类加载检查:首先检查根据该class文件中的常量池表(静态常量池)能否找到该类的符号引用;找到则去方法区中的常量池中查找该符号引用所指向类是否已经完成了类的加载;

  • 如果没有,那就先执行相应的类加载过程
  • 如果有,那么进入下一步,为新生对象分配内存

2、分配内存:就是在堆中为该对象分配内存空间;具体的分配方式有【堆内存是否规整】有两种方式:指针碰撞和线性列表;

3、初始化零值:前提知识,对象在内存中的布局有三部分:对象头、实例数据、对齐填充;初始化零值就是初始化对象中的实例数据;

4、设置对象头:设置标记字指向类元数据的指针

5、执行init方法:就是执行构造函数,构造函数即 Class 文件中的 <init>() 方法;

四、如何判断对象是否可回收?

引用计数器和可达性分析(目前主流JVM都选择此方式)单纯的引用计数就很难解决对象之间相互【循环引用】的问题

可达性分析思想可以理解为一颗多叉树,根节点被称为GCRoots作为可达性分析的起点,凡是从GCRoots可以到达的对象就不是垃圾不可以被回收,反之则需要被回收;

可达性分析分为两个步骤:一是枚举可达性分析的起点GCRoots;二是从GCRoots开始遍历

五、哪些对象可作为GCRoots?

GCRoots 是可以从堆外部访问到的对象

1、在==虚拟机栈和本地方法栈==中引用的对象;

2、在==方法区中的类的静态变量、常量所引用的对象==

3、JVM内部的引用对象,核心的系统类对象,如基本数据类型对应的 Class 对象,一些常驻的异常对象(NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器等。

4、所有被同步锁(synchronized)持有的对象;

简单来说,GC 管理的主要区域是 Java 堆,一般情况下只针对堆进行垃圾回收。方法区、虚拟机栈和本地方法栈等不被 GC 所管理,因而选择这些区域内的对象作为 GC roots,被 GC roots 引用的对象不被 GC 回收。

类的成员变量不可以作为 GC Roots,是因为类的成员变量只有在实例化后才会存在,而 GC Roots 用于标识 Java 虚拟机中存活的对象,需要在 Java 虚拟机启动时就存在,否则无法对堆内存中的对象进行正确的标记和回收

六、对象可回收,就一定会被回收吗?

答案是否定的,当对象不可达(可回收)并发生 GC 时,会先判断对象是否执行了 finalize 方法,如果未执行,则会先执行 finalize 方法!那么在执行该方法的时候可以进行一些操作,挽救回该对象。

将当前对象与 GC Roots 关联,这样执行 finalize 方法之后,GC 会再次判断对象是否可达,如果不可达,则会被回收,如果可达,则不回收!

七、Java 中有几种类型的引用?

强引用StronglyReference、软引用SoftReference、弱引用WeakReference、虚引用PhantomReference

引用的强度越强,那么这个被引用的对象就越不容易被垃圾回收器回收掉;

强引用不会被回收、软引用在内存不足时才被回收、弱引用每次垃圾回收时被回收;

虚引用存在的意义是什么呢?—不是用于获取对象的引用或与对象交互;虚引用的主要作用是用于回收监控和清理资源,以提高程序的健壮性和可维护性。

八、分代收集理论是什么?为什么要分代

分代收集理论是一套符合大多数程序运行实际情况的经验法则,后面具体的垃圾回收算法都是基于分代收集理论的;

1、弱分代假说:大多数对象都是朝生夕灭的,生命很短;

2、强分代假说:活的越久(经历垃圾回收次数最多的)的对象越难以消亡;

针对以上两种假说,垃圾收集器将Java堆划分为不同的区域,新生代和老年代,然后根据对象的年龄分配到不同的区域,之所以分为这两个区域,一是在新生代中每进行一次垃圾回收,都会有大批对象死去,二是每进行一次垃圾回收,存活下的少量对象会晋升到老年代存储;

上述划分存在一个问题:跨代引用

3、跨代引用假说:跨代引用相对于同代引用来说是相对较少的;

九、垃圾回收算法

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

1、标记清除:根据可达性分析先标记出需要回收的对象,然后统一回收掉所有需要被回收的对象;

  • 内存碎片问题,可能会导致当为大对象分配内存的时候没有连续的空间,此时就会触发一次垃圾收集;
  • 执行效率不稳定,受是否存在大量大对象的问题,可能会有大量的标记和清除工作;

2、标记复制:按照可用内存的大小平均分为两部分,每次只使用其中一块,当这一块的内存用完了,那就将继续存活的对象复制到空闲的那一块,然后再把剩余需要回收的对象进行回收;

  • 空间利用率低,可为对象分配内存的空间只有实际空间的一半;
  • 不适用于对象存活率较高的情况;

3、标记整理:就是将所有继续存活的对象移向一端,然后清理掉边界以外的内存;

十、HotSpot 虚拟机新生代为什么用 Mark Copy 算法?

新生代的特点是大多数对象会在第一次GC的时候消亡

1、为什么不使用MarkSweep?

  • 空间问题:MarkSweep会导致内存碎片化;
  • 效率问题:MarkSweep中的Sweep阶段是针对需要被回收的对象;MarkCopy是针对存活的对象;根据新生代的特点,每次存活的对象少,需要回收的对象多,所以MarkCopy的效率会高一些;

2、为什么不使用MarkCompact?

  • 效率问题:Mark Compact 是将存活对象移到内存一侧,涉及到对象的移动,而复制算法只是将对象进行一次 copy,单次 copy 的效率大于单次对象移动(Compact 可能要先计算一次对象的目标地址,然后修正指针,最后再移动对象。copy 则可以把这几件事情合为一体来做,所以可以快一些)

十一、为什么需要 Survivor 区?为什么设置两个 Survivor 区?为什么不设置更多的 Survivor 区?

为什么需要Survivor?—避免Eden区的对象经历一次GC就晋升到老年代,导致老年代由于空间填满频繁触发MajorGC(或FullGC);有了Survivor区,只有经历了16次MinorGC才会晋升到老年代,很大程度上减少了被送到老年代的对象,MajorGC的次数也就少了;

为什么设置两个Survivor区?—防止内存碎片化;永远有一个 Survivor 是空的,另一个非空的 Survivor 无碎片

为什么不设置更多的Survivor区?—如果 Survivor 区再细分下去,每一块的空间就会比较小,很容易导致 Survivor 区满(导致对象提前通过分配担保进入老年代),因此,两块 Survivor 区是经过权衡之后的最佳方案。

十二、内存分配与垃圾回收原则

基本的内存分配和回收原则:

  • 对象优先分配在Eden区:当Eden区空间不足时,触发一次MinorGC;
  • 长期存活的对象晋升到老年代:给对象一个年龄计数器,达到阈值则晋升至老年代;
  • 动态对象年龄判定:为更好适应内存状况,不要求对象必须达到某一阈值才晋升至老年代;如果在 Survivor 空间中【相同年龄】所有对象大小的总和大于 Survivor 空间的【一半】,年龄【大于或等于】该年龄的对象就可以直接进入老年代
  • 大对象直接进入老年代:大对象就是指需要大量连续内存空间的 Java 对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组;
  • 空间分配担保:在发生 Minor GC 之前,虚拟机必须先检查【老年代最大可用的连续空间】是否大于【新生代所有对象总空间】;所谓分配担保就是:新生代使用 MarkCopy 收集算法,因为没有办法百分百保证分配给 To Survivor 的内存空间能够容纳全部的存活对象(有风险),常见的做法就是当 To Survivor 空间不足以容纳一次新生代 GC 之后存活的对象时,这些对象便将直接进入老年代

十三、System.gc() 能保证 GC 一定发生吗?

调用System.gc()或者Runtime.getRuntime().gc()System.gc()的底层方法)方法,会显式触发 Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。

System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用,也就是说,这个方法只是提醒垃圾收集器进行垃圾回收,具体什么时候会进行垃圾回收就不一定了,此方法不保证一定会回收

十四、有几种类加载器

站在 Java 虚拟机的角度来看,只存在两种不同的类加载器:

  • 一种是==启动类加载器==(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
  • 另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader

站在 Java 开发人员的角度来看,可以把类加载器划分为三层类加载器、双亲委派的类加载架构:

启动类加载器Bootstrap Cla〈-扩展类加载器ExtensionCla〈-应用程序类加载器ApplicationCla

1、启动类加载器:最顶层的加载器,是JVM自身的一部分,由C++语言实现,无法被 Java 程序直接引用,负责加载 %JAVA_HOME%/lib 目录下的 jar 包和类,或者是被 -Xbootclasspath 参数指定的路径中的所有 jar 包和类;

2、扩展类加载器:该类加载器是在类 sun.misc.Launcher$ExtClassLoader 中以 Java 代码的形式实现的,负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或者被 java.ext.dirs 系统变量所指定的路径中所有的 jar 包和类;

3、应用程序类加载器:该类加载器由 sun.misc.Launcher$AppClassLoader 来实现,负责加载用户类路径(ClassPath)上所有的 jar 包和类,开发者同样可以直接在代码中使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器;

4、用户自定义类加载器:如果用户认为有必要,还可以加入自定义的类加载器来进行拓展,加载指定路径的 class 文件;

十五、什么是双亲委派模型?

各种类加载器之间的层次关系被称为类加载器的 “双亲委派模型(Parents Delegation Model)”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器

不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该先传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载

为什么要使用双亲委派模型?—Java 中的类随着它的类加载器一起具备了一种带有优先级的层次关系;类的唯一性需要类和加载它的类加载器来保证在程序的各种类加载器环境中都能够保证是同一个类

双亲委派被破坏的情况—因为双亲委派机制都是在 loadClass 方法中实现的,如果用我们自己的方式重写了 loadClass 方法,双亲委派自然也就被破坏掉了Tomcat 破坏双亲委派原则,提供隔离的机制,每个 Webapp Class Loader 加载自己的目录下的 class 文件,不会传递给父类加载器。

十六、类加载器什么情况下会被回收

类加载器的回收并不是由应用程序代码直接控制的,而是由 JVM 自行管理和决定的,在以下情况下会被回收:

1、不可达性:当一个类加载器及其加载的类没有被任何活动线程引用时,它们可以被判定为不可达,从而成为垃圾对象,在后续的垃圾回收过程中会被回收。

2、类加载器的生命周期结束:当一个类加载器的生命周期结束时,例如当其对应的 JVM 实例关闭或卸载模块时,相关的类加载器会被回收。这通常发生在应用程序退出或模块卸载的情况下。

3、强制回收:在某些情况下,可以通过调用 ClassLoader 对象的 close() 方法或类加载器相关的 API 来显式地释放类加载器所持有的资源,从而触发类加载器的回收。

4、父类加载器回收:如果一个类加载器是由另一个类加载器所加载的,当父类加载器被回收时,其子类加载器也可能会被回收。这是因为类加载器之间存在层次关系,子类加载器依赖于父类加载器来加载类,如果父类加载器不可达,则子类加载器及其加载的类也可能成为垃圾对象。

十七、JVM 的参数

1、标准参数:在所有的 JVM 实现中都是标准的,并且向后兼容。

  • -classpath/-cp:设置类路径
  • -version:打印 JVM 版本信息
  • -help:打印命令行参数的帮助信息

2、X参数:JVM调优常用参数,用于控制 JVM 的行为,但不保证在所有的 JVM 实现中都有效。

  • -Xmx:设置堆的最大内存,默认为物理内存的1/4;
  • -Xms:设置堆的最小内存,默认为物理内存的1/64;
  • -Xmn:设置新生代的大小;
  • -Xss:设置线程栈的大小;
  • -Xnoclassgc:关闭类垃圾回收;
  • -Xverify:开启类验证;

3、XX参数:也是用于控制 JVM 行为的,但它们的作用比 X 参数更加专业和深入。

  • -XX:PermSize/-XX:MaxPermSize:设置永久代大小
  • -XX:+UseParallelGC:开启并行垃圾回收器
  • -XX:+UseConcMarkSweepGC:开启并发标记清除垃圾回收器
  • -XX:+HeapDumpOnOutOfMemoryError:当内存溢出时自动生成堆转储文件
  • -XX:+PrintGCDetails:打印 GC 日志
  • -XX:+DisableExplicitGC:禁止显式垃圾回收

4、其它参数:主要用于开发和调试。

  • -agentlib:加载 Java 代理库,例如用于调试或分析应用程序的代理库
  • -D:设置系统属性
  • -ea:开启断言检查
  • -da:关闭断言检查

十八、JVM 分析的工具

JDK自带的命令诊断工具:

  • jps:打印正在运行的所有进程,用于列出当前正在运行的 Java 进程的进程 ID 以及其主要参数;
  • jstat:监视 JVM 的各种统计信息,例如垃圾回收、类加载、JIT 编译等;
  • jinfo:查看和修改进程的运行时参数(属性、堆大小、垃圾收集器设置等),jinfo命令可用于调试和优化Java应用程序的性能;
  • jmap:生成堆转储快照,在出现内存泄漏或者内存占用过高时,通过分析堆转储快照可以查看具体的对象信息,帮助开发者快速定位问题
    • 堆转储快照:将JVM堆内存中的对象状态快照信息以二进制文件的形式进行存储的操作;
      • 头部信息:包括 JVM 版本、堆转储快照格式、生成时间等基本信息。
      • 类型定义:描述了 Java 中所有的类和对象的结构信息。
      • 对象实例数据:Java 堆中所有对象实例的数据,包括对象的头部信息和字段信息。
      • 线程数据:所有的线程的信息,包括线程的状态、堆栈帧信息等。
      • 类实例数据:所有 Java 类的实例信息,包括类的头部信息和字段信息。
  • jhat:分析堆转储快照,将 jmap 生成的内存转储快照文件加载到一个内嵌的 HTTP 服务器中,以便通过浏览器查看分析结果;
  • jstack:打印线程堆栈信息(线程 ID、线程状态、线程名、线程调用栈),查看每个线程的调用栈,以及被阻塞或者等待的线程,在调试 Java 应用程序时,可以使用 jstack 命令快速诊断线程相关的问题;

十九、OOM 了如何排查?

基本步骤:

1、查看错误日志

2、使用 jps 命令查看 JVM 运行的进程 ID(PID)

3、使用 jstat 命令检查进程的 GC 情况,观察 GC 是否频繁,如果频繁则可能存在内存泄漏。

4、如果程序还没挂,可以使用 jmap 命令生成目标进程的内存转储快照:内存转储快照是指将当前内存中的所有对象信息记录下来,以便后续分析。

5、使用 jhat 命令或者其他工具(Visual VM)分析 jmap 生成的内存转储快照。分析过程主要包括查看堆中的对象信息、分析对象引用链等,以确定内存中存在的对象和可能的泄漏点。

二十、线上 CPU 100% 怎么排查?

1.、找到占用 CPU 最高的进程:

  • 利用 top 命令监控 CPU 运行状态,显示进程运行信息,看看到底是哪些进程占用了大量 CPU

2、找到占用 CPU 最高的线程:

  • 执行 top -Hp pid 命令,pid 就是上面排查出来的进程 PID;

3、打印线程堆栈信息:根据堆栈信息定位代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值