JVM简历话术

本专栏只总结最重要的八股,简历对应的

简历话术:

JVM:理解JVM底层原理,如内存结构、垃圾回收、类载机制

1.内存结构

线程共享的数据区(随JVM生,随JVM灭):堆(存对象实例),方法区(类结构相关的数据,类信息,字段信息,方法...)

线程私有(随线程生,随线程灭):虚拟机栈(每个方法被执行的时候同步创建栈帧,存放存储方法的参数和方法内部定义的局部变量。这里存储的是基本数据类型的值 和对象实例的引用(相当于C语言的指针)。对象实例本身存储在堆中。),本地方法栈(和虚拟机栈类似,只是执行native方法),程序计数器

执行状态 程序计数器(PC Register)的值 原因
执行 Java 方法 字节码指令地址 需要记录下一条要执行的字节码位置
执行 Native 方法 空(Undefined) 执行的是本地机器指令,不在JVM字节码体系内,无需记录字节码地址。

2.垃圾回收

2.1 判定是否是垃圾的办法

2.1.1 引用计数

每个对象维护一个记录其被引用次数的计数器。当计数器归零时,对象立即被回收。实现简单,但是存在循环引用的严重问题,如果AB相互引用,不被其他任何对象引用,比如A的字段指向B,B的字段指向A,不被其他任何对象引用,但是仍然不能回收。所以在《深入理解Java虚拟机》讨论到的主流Java虚拟机中均未涉及

2.1.2 可达性分析算法

从一组称为 GC Roots (GC Roots 就是一些“必须保留让他活着的对象”,比如当前正在执行的方法里的局部变量、静态变量)的根对象出发,遍历所有被引用的对象,能被遍历到的对象视为“存活”,其余对象判定为“可回收”。

2.2 垃圾回收算法

标记-清除算法(在某些收集器 老年代 有内存碎片)

标记存活的,再回收没标记的

标记:从所有的 GC Roots(如栈帧中的局部变量、静态变量、常量等)开始,遍历整个对象引用图。所有能被这些根对象直接或间接引用到的对象,都被标记为“存活”。 清除:线性地遍历整个堆内存。当它遇到一个没有被标记的对象时,就判定它为垃圾,然后回收其内存空间,并将其添加到空闲内存列表中,以便后续分配使用。 问题:面对大量可回收对象时效率低,因为要进行大量标记和清除

标记-复制算法(适合新生代 没有内存碎片)

内存分成两块,先用其中一块,满了之后,开始把存活的复制到另一块 问题:浪费了一半内存

标记-整理算法(为老年代设计 没有内存碎片)

老年代的对象在GC之后的存活率比较高。 标记存活对象后直接移动到一端 标记

内存初始状态:
[对象A][对象B][对象C][对象D][对象E][对象F]
    ↓
标记存活对象:A、C、E存活,B、D、F死亡
    ↓
标记结果:✓ × ✓ × ✓ ×

整理(移动存活对象)

将所有存活对象"向左对齐":
[对象A][对象C][对象E][空位][空位][空位]
    ↑                    ↑
存活对象紧凑排列       连续的空闲空间

直接清理右侧的空闲连续区域

2.3 分代收集理论

一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域

弱分代假说(新生代)

绝大多数对象都是朝生夕死的

强分代假说(老年代)

熬过越多次垃圾收集过程的对象就越难以消亡。 alt

一个问题: 假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性。这让性能大大降低了。

跨代引用假说

跨代引用相对于同代引用来说仅占极 少数

所以我们不为少量的跨代引用扫描整个老年代,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。 这样从扫描这个给老年代变成了扫描老年代的少部分。

2.4 垃圾收集器

G1(JDK 9开始成为默认,高吞吐量与低延迟的平衡 可预测的停顿时间模型)

Remembered Set(RSet) 作用:每个 Region 都有一个 RSet,记录其他 Region 中哪些引用指向本 Region。这样在回收某个 Region 时,不需要扫描整个堆来找出它的所有引用。 五个要点 具体可点击博客:Region的堆内存 布局 高吞吐量与低延迟的平衡 可预测的停顿时间模型 微观标记复制,整体上标记整理 四个步骤(只有第二步并发标记STW)

CMS(追求低延迟)

基于标记-清除算法 1)初始标记(CMS initial mark) 2)并发标记(CMS concurrent mark) 3)重新标记(CMS remark) 4)并发清除(CMS concurrent sweep) 其中初始标记、重新标记这两个步骤仍然需要“Stop The World”

注意:G1最后一步清除需要STW,因为对象移动地址会变

3.类载机制

3.1 类的加载过程

alt

3.1.1 加载(是整个类加载的一个阶段,不要混.)

在加载阶段,Java虚拟机需要完成以下三件事情: 1)通过一个类的全限定名来获取定义此类的二进制字节流。 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 3)在堆中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

3.1.2 验证

确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全

3.1.3 准备

正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段 注意这里的初始值通通是0,因为比如public static int value = 123,但是这里依旧是0,原因是value赋值为123的putstatic指令存放于类构造器()方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行。

3.1.4 解析

Java虚拟机将常量池内的符号引用替换为直接引用的过程 符号引用:可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。 直接引用:可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。和虚拟机实现的内存布局直接相关的

3.1.5 初始化

初始化阶段就是执行类构造器< clinit>()方法 的过程。 静态变量赋值和静态代码块是同等对待的,按照顺序执行。执行类构造器 () 方法

public class InitializationExample {
    // 静态变量赋值
    static int a = 1;           // 会包含在<clinit>()中
    
    // 静态代码块
    static {
        b = 2;                  // 会包含在<clinit>()中
    }
    
    static int b = 3;           // 会包含在<clinit>()中
    static final int C = 4;     // 常量,不会包含在<clinit>()中
    
    // 最终生成的<clinit>()方法内容:
    // a = 1;
    // b = 2;  // 静态块中的赋值
    // b = 3;  // 静态变量赋值
}

3.2 双亲委派模型

3.2.1 是什么

alt

双亲不是指的是两个父辈,而是英文的英译parent译过来的。

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。

3.2.2 为什么需要打破双亲委派

比如,在JDBC中,核心类(由启动类加载器加载)需要调用第三方厂商的实现类(由应用程序类加载器加载)。根据双亲委派,父加载器(Bootstrap)根本不知道子加载器(AppClassLoader)的存在,更不可能去加载它的类,因此必须打破原有规则。

3.2.3 怎么打破双亲委派

核心手段线程上下文类加载器(Thread Context ClassLoader, TCCL)

具体做法(以 JDBC 为例):

  1. Java 在 Thread 类中增加了一个字段contextClassLoader,每个线程都可以设置自己的类加载器(通常默认从父线程继承,一般就是 AppClassLoader,应用程序类加载器)。
  2. 在 java.sql.DriverManager(由 Bootstrap 加载)的静态初始化代码中:
    • 获取当前线程的上下文类加载器:Thread.currentThread().getContextClassLoader()
    • 用这个类加载器去显式加载第三方数据库驱动类(如 com.mysql.cj.jdbc.Driver)
    • 这样就实现了 父加载器(Bootstrap)主动调用子加载器(AppClassLoader) 来加载类,完全绕过了双亲委派的向上委托链。

本质
打破双亲委派并不需要修改 ClassLoaderloadClass 逻辑,而是在需要的地方,主动获取一个能加载目标类的类加载器(通常是线程上下文类加载器),然后直接调用它的 loadClassClass.forName

其他常见的打破方式(补充)

  • 自定义类加载器:重写 loadClass 方法,不先委托给父类,而是优先自己加载。
  • OSGi 模块化:类加载器形成网络,每个模块有自己的类加载器,按模块依赖关系加载,不再遵循树状的双亲委派。
#简历#
简历技能对应的核心八股 文章被收录于专栏

针对面试,整理简历上写的每行技术点+对应完整话术和扩展点,快速速成

全部评论
牛牛牛
点赞 回复 分享
发布于 昨天 21:48 湖南

相关推荐

03-31 00:39
门头沟学院 C++
南岗痞子:不还有俩没结束吗
点赞 评论 收藏
分享
评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务