Java基础学习笔记:JVM

JVM

1. 类加载过程

1.1 编译

作用:将java源码文件编译成class文件

操作内容:对源代码程序做语法分析、语义分析、注解处理(Lombok)、泛型擦除等

过程

  • 词法分析:一个个字节地读取源代码,识别语言关键字,得到Token流
  • 语法分析:检查Token流中关键字组合是否符合Java语法规范,形成抽象语法树(结构化的语法表达形式)
  • 语义分析:将复杂语法转化为简单语法,如注解处理、泛型擦除、注释等,得到注解抽象语法树
  • 字节码生成:根据注解抽象语法树生成符合Java虚拟机规范的字节码

1.2 加载

作用:将class文件加载到JVM中

步骤

  1. 装载

    • 时机:有需要时才进行装载(new或反射),节省内存开销

    • 操作:通过类加载器装载,防止内存中出现多份同样的字节码,使用双亲委派机制

      不会自己尝试加载这个类,而是把请求委托给父加载器,依次向上

    • 规则:本地方法由根加载器(BootstrapLoader)装载,内部实现的扩展类由扩展加载器(ExtClassLoader)装载,程序中的类由系统加载器(AppClassLoader)装载

    • 内存角度:查找并加载类的二进制数据,在堆中创建一个java.lang.Class对象,类相关信息存储在方法区中

  2. 连接:对Class对象信息进行验证、为类变量分配内存空间并赋默认值

    • 步骤:

      1. 验证:验证类是否符合Java规范和JVM规范

      2. 准备:为静态变量分配内存,初始化为默认值

      3. 解析:将符号引用转为直接引用

        符号引用:可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,与虚拟机布局无关

        直接引用:与虚拟机布局相关,已经被加载到内存中,避免不同虚拟机对于相同符号引用翻译出的直接引用不同

  3. 初始化

    • 为静态变量赋上正确的初始值

    • 收集类中的静态变量、静态代码块、静态方法、构造方法,从上往下开始执行

1.3 解释

作用:将class字节码文件转换为操作系统识别的指令

字节码解释器

  • 非热点代码:直接解释
  • 热点代码:编译,交给即时编译器
  • 热点探测:方法调用计数器、回边计数器。达到阈值后,触发JIT编译

即时编译器JIT

  • 保存热点代码的指令码,下次执行就无需重复解释,直接执行缓存的机器语言

1.4 执行

操作系统执行指令码

1.5 方法执行顺序

  1. 父类静态代码块
  2. 子类静态代码块
  3. 父类代码块
  4. 父类空参构造器
  5. 子类代码块
  6. 子类调用构造器

总结:静态 > 继承关系 > 代码块 > 构造器

默认子类构造器都会调用父类空参构造,如果父类无空参,子类必须说明使用的构造器

代码块早于构造器:提取各个构造器中公用部分,减少冗余

2. 双亲委派机制

2.1 类加载器

BootstrapClassLoader(启动类加载器)

  • 最顶层的加载类,由C++实现
  • 负责加载%JAVA_HOME%lib目录下的jar包和类,或者被-Xbootclasspath参数指定的路径中的所有类

ExtensionClassLoader(扩展类加载器)

  • 由Java实现,继承自java.lang.ClassLoader
  • 负责加载%JAVA_HOME%/lib/ext目录下的jar包和类,或者被java.ext.dirs系统变量所指定的路径下的jar包

AppClassLoader(应用程序类加载器)

  • 由Java实现,继承自java.lang.ClassLoader
  • 面向用户的加载器,负责加载当前应用classpath下的所有jar包和类

2.2 双亲委派模型

  • 在类加载时,系统首先判断当前类是否被加载过
  • 已经被加载过的类会直接返回
  • 否则尝试自己加载
    • 加载时,首先将该请求委派给父类加载器的loadClass()处理,即所有请求最终都会传送到顶层的BootstrapClassLoader
    • 当父类加载器无法处理时,才自己加载
    • 当父类加载器为NULL时,使用BootstrapClassLoader作为父类加载器

3. 内存结构(运行时数据区)

3.1 线程共享

3.1.1 堆

目的

  • 存放对象实例,几乎所有对象实例和数组都在此分配内存(逃逸分析)
  • 分代便于垃圾回收

新生代(占堆的1/3)

  • Eden区(占新生代8/10):存放新对象

  • Survivor区(占新生代2/10):存放垃圾回收对象

    • from(占新生代1/10)

    • to(占新生代1/10)

老年代(占堆的2/3)

(进入老年代条件)

对象太大

  • 新创建就很大的对象
  • Survivor区无法存下的对象
  • 需要大量连续内存空间的对象:字符串、数组

对象太老

  • 经过MinorGC15次的对象
    • HotSpot:15
    • CMS:6
  • 动态年龄判断:Survivor区中某个年龄占用内存超过50%时,取这个年龄和原来晋升年龄的较小值

对象头信息

  • 存储对象自身运行时数据:哈希码、GC分代年龄、锁状态标识位、线程持有的锁、偏向锁ID、偏向时间戳
  • 类型指针:JVM通过指针来确定该对象是哪个类的实例

3.1.2 方法区(逻辑上属于堆)

存放信息

类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等

内存结构

  • 类型信息、字段信息、方法表
  • JIT代码缓存
  • 运行时常量池:字面量、符号引用
  • 字符串常量池
  • 静态变量

历史演变

JDK版本 变化
JDK6 都在方法区中
JDK7 静态变量、字符串常量池移到堆中
JDK8 方法区具体实现由永久代变为元空间

实现方式

JDK版本 内存位置 区别
JDK8以前 永久代(虚拟机中) 可能内存溢出,OOM
JDK8以后 元空间(本地内存) 不会内存溢出

为什么要把永久代替换为元空间

实现方式 描述 调整参数
永久代 使用JVM内存,虽然可以调整,但有最大上限,容易溢出 -XX: PermSize=N
-XX: MaxPermSize=N
元空间 1. 使用机器直接内存,可以调整,更难溢出,可以加载更多类的信息
2. 合并HotSpot和JRockit代码时,JRockit没有永久代
-XX: MetaspaceSize=N
-XX: MaxMetaspaceSize=N

3.2 线程私有

3.2.1 虚拟机栈

栈帧(每次方法调用创建)

  • 局部变量表
  • 操作数栈
  • 方法返回值
  • 动态链接

3.2.2 本地方法区

本地方法:非Java方法,通常是C

3.2.3 程序计数器

记录各个线程执行的字节码的地址

分支、循环、跳转、异常、线程恢复

4. 垃圾回收机制

4.1 垃圾

垃圾定义

只要对象不再被使用了,就认为该对象是垃圾,对象所占的空间就可以被回收

常见垃圾

  • 废弃常量:没有任何对象引用该常量

  • 无用类

    • 该类的所有实例对象都被回收

    • 该类的类加载器已被回收

    • 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类

垃圾判断算法

  • 引用计数法

    • 对象被引用则加1,引用失败则减1
    • 计数器为0时,说明对象不再被引用,可以被回收
    • 缺点:无法回收循环依赖对象(A依赖B,B依赖A)
  • 可达性分析法

    • 从GC Roots开始向下搜索
    • 若没有任何引用相连时,说明对象不可用,可以回收
    • GC Roots是一组必须活跃的引用,从此处出发,程序通过直接或间接引用,能找到可能正在被使用的对象
  • GC Roots

    • 虚拟机栈(栈帧的局部变量表)中引用的对象:若此栈帧为栈顶,证明活跃,对象也正被引用,即为GC Roots

    • 本地方法栈中引用的对象

    • 方法区中类静态属性引用的对象

    • 方法区中常量引用的对象

    • 所有被同步锁持有的对象

4.2 垃圾回收算法

标记-清除算法

过程

  1. 标记没有被GC Roots引用的对象
  2. 直接清除没有被标记的对象

特点:最基础算法、存在问题

  • 效率问题:每次需要对整个内存区间进行回收
  • 空间问题:产生大量不连续的内存空间,内存碎片

标记-复制算法

过程

  1. 将内存分为大小相同的两块,每次使用其中一块
  2. 当这块内存使用完成后,就将还存活的对象复制到另一块去,然后再把这块使用的空间一次清理掉

特点

  • 解决效率问题:每次对内存区间的一半进行回收
  • 新的效率问题:对象存活率高时,需要进行较多复制操作,效率降低
  • 如果不想浪费50%的内存空间,就需要提供额外空间进行分配担保
    • 一般情况,复制算法将内存分为相等的两块,交替使用
    • 为提高内存利用率,将内存空间按照8:1:1划分为三块,即堆中的Eden、Survivor区。每当需要垃圾回收,先将存活对象复制到保留的Survivor,然后将Eden和之前使用的Survivor一起清理
    • 由于Survivor内存空间较小,极端情况可能不够保存存活下来的对象,因此需要提供一块额外空间进行分配担保

标记-整理算法

过程

  1. 标记没有被GC Roots引用的对象
  2. 不直接清除没有被标记的对象,而是将所有存活对象向一端移动,然后清理掉端边界以外的内存

分代收集算法

根据对象存活周期不同,将内存分为几块(新生代、老生代),选择合适的垃圾回收算法

  • 新生代:每次收集都有大量对象死去,选择标记-复制算法
  • 老年代:对象存活率较高,且没有其他内存进行分配担保,选择标记-整理算法

4.3 MinorGC

触发时间:Eden区空间不足时

扫描对象(HotSpot虚拟机):年轻代对象

  • 从GCRoots出发,通过地址判断对象分代

  • CardTable避免全局扫描老年代对象

    • 堆中一小块区域形成卡页

    • 一个卡页中存在对象的跨代引用时,将这个页标记为脏页

4.4 垃圾回收器

Serial收集器

特点

  • 单线程:只使用一条垃圾收集线程,且同时暂停其他所有工作线程,直到收集结束
  • 简单高效:没有线程交互的开销
  • 适用于运行在Client模式下的虚拟机

ParNew收集器

特点

  • 多线程并行:使用多条垃圾收集线程,但仍暂停其他工作线程
  • 适用于运行在Server模式下的虚拟机

Parallel Scavenge收集器

特点

  • 多线程并行:与ParNew类似
  • 高吞吐量:提供许多参数供用户找到最合适的停顿时间或最大吞吐量(CPU中运行程序时间与总耗时的比值)
  • JDK1.8默认收集器

CMS收集器

特点

  • 多线程并发:垃圾收集线程和用户线程(基本上)同时工作
  • 低停顿,提高用户体验

步骤:标记-清除算法

  1. 初始标记:暂停所有其他工作线程,CMS线程记录下直接与GC Roots相连的对象。速度很快
  2. 并发标记:同时开启垃圾收集线程和用户线程,用闭包结构标记可达对象(由于用户线程可能会不断更新引用域,所以无法保证可达性分析的实时性。因此也会跟踪记录这些发生引用更新的地方)
  3. 重新标记:暂停所有其他工作线程,CMS线程修正并发标记期间引用更新对象的标记。停顿时间稍大于初始标记,远小于并发标记
  4. 并发清除:同时开启垃圾收集线程和用户线程,垃圾回收线程对未标记的区域做清扫

缺点

  • 对CPU资源敏感
  • 无法处理浮动垃圾
  • 使用标记-清除算法,导致收集结束时会有大量空间碎片产生

G1收集器

特点

  • 并行与并发:充分利用CPU、多核环境下的硬件优势,缩短StopTheWorld停顿时间
  • 分代收集
  • 空间整合:从整体上看,使用标记-整理算法;从局部上看,使用标记-复制算法
  • 可预测的停顿:低停顿,且建立可预测的停顿时间模型,让使用者明确指定在一个长度为M毫秒的时间片段内

步骤

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

5. 调优

全部评论

相关推荐

明天不下雨了:我靠2022了都去字节了还什么读研我教你****:你好,本人985电子科大在读研一,本科西南大学(211)我在字节跳动实习过。对您的岗位很感兴趣,希望获得一次投递机会。
点赞 评论 收藏
分享
菜鸡29号:根据已有信息能初步得出以下几点: 1、硕士排了大本和大专 2、要求会多语言要么是招人很挑剔要么就是干的活杂 3、给出校招薪资范围过于巨大,说明里面的薪资制度(包括涨薪)可能有大坑
点赞 评论 收藏
分享
点赞 评论 收藏
分享
评论
1
2
分享

创作者周榜

更多
牛客网
牛客企业服务