JVM 组成

image.png

JNI:Java Native Interface(本地方法) 用于将 Java 与本地已编译语言交互,可能会丧失平台可移植性,但有其使用场景:沿用旧库、提高性能等。 声明一个 JNI 的方法如下:

  1. public class JNI {
  2. // native 关键字修饰
  3. public native void callNative();
  4. //载入本地库
  5. static {
  6. System.loadLibrary("libName");
  7. }
  8. public static void main(String[] args) {
  9. // 直接调用 JNI 提供的方法
  10. new JNI().callNative();
  11. }
  12. }

线程私有

  • 程序计数器:存储线程执行的字节码行号,不存在 OOM;
  • :在栈帧中存储局部变量表、操作数栈、动态链接、方法出口,为 Java 方法服务;
  • 本地方法区:也是个栈,为 Native 方法服务。

    线程共享

  • :创建的对象、产生的数据都在堆中,是垃圾回收器主要的内存区域;

  • 方法区:存储常量、静态变量、类信息、JIT 产生的机器码、运行时常量池。

    JVM 类加载

    类加载阶段

    • 符号引用:以一组符号来描述所引用的目标,引用的目标不一定被加载到内存,因此与 JVM 内存布局无关,在 class 文件中以 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等类型的常量出现(代码层面的引用);
    • 直接引用:指向目标的指针,是被加载到内存的引用,因此与 JVM 内存布局有关(内存层面的引用)。

    • client 方法:编译器根据类中被 static 修饰的赋值和语句块生成的方法,若不存在 static 则不生成。

  • 加载 Loading:JVM 读取 class 文件到方法区内,并在堆中创建 java.lang.Class 对象;

  • 验证 Verification:确保 class 文件符合 JVM 要求;
  • 准备 Preparation:在方法区中分配内存空间与被 final 修饰的变量初始值;
  • 解析 Resolution:将常量池中的符号引用替换为直接引用;
  • 初始化 Initialization:运行 client 方法(先执行父类的 client 方法成功后才会执行子类的 client 方法),以下几种情况不执行该流程:
    • final 修饰的直接进入常量池,不会触发该类的初始化;
    • 子类只使用父类被 static 修饰的字段时不触发子类初始化;
    • 定义对象数组不触发初始化;
    • 使用类名获取 Class 对象时不触发初始化;
    • 使用 Class.加载特定类时,使用 initialize 参数设置是否需要对该类初始化;
    • 使用 ClassLoader 默认的 loadClass 方法加载类时,不触发初始化。
  • 使用 Using
  • 卸载 Unloading:类不再被需要时从 JVM 中卸载。

    类加载的双亲委派机制

    在类加载阶段使用双亲委派机制,保障类的唯一性和安全性:将类加载请求向上委派给父加载器完成。
    需要注意的是,加载器的父与子关系不是类的“继承”关系,而是父加载器作为子加载器类内变量的方式组合而成。
    image.png

    双亲委派机制如何破坏?

    自定义一个类加载器,重写它的 loadClass 方法,在其中不涉及 parent 即可。

    除了 loadClass 之外还有其他几个方法:

    • findClass:根据名称/位置加载 .class 字节码。
    • defineClass:字节码转化为 Class。

为什么需要双亲委派机制?

类加载器之间有层次关系(优先级),使用双亲委派机制有以下好处:

  • 避免类的重复加载:父加载器加载过一个类之后,子加载器不需要重新加载这个类。
  • 保证安全性:Bootstrap ClassLoader 只加载 JAVA_HOME 中的类,不会被随意替换,保证 JDK 中核心 API 的安全。

    JVM 内存

    JVM 内存分区

    GC:Garbage Collection,垃圾回收。

image.png

  • 新生代:JVM 新创建的对象(大对象除外,直接进入老年代),GC 算法为 MinorGC;
    • Eden 区:新创建的对象;
    • SurvivorFrom 区:上一次 GC 幸存的对象。
    • SurvivorTo 区:保留 GC 幸存的对象。
  • 老年代:存放长生命周期对象和大对象,Survivor 中存活轮次(年龄)达到阈值后进入该区,GC 算法为 MajorGC;
  • 永久代:存放 Class 和元数据 Meta 信息,无 GC 流程,JDK 8 后被元数据区取代(元数据区使用 OS 的内存,不受 JVM 限制)

    垃圾回收相关

    垃圾确定算法

    引用计数法

    存在对象的引用,引用计数 +1;删除引用,引用计数 -1;引用计数为 0 时判断为垃圾。
    存在循环引用的问题:A 中有 B,B 中有 A 的情况。

    可达性分析

    定义一些 GC Roots,通过它到对象是否存在可达路径,不可达则判断为垃圾。
    常用 GC Root:

  • 栈中的引用;

  • JNI 的引用;
  • 方法区的静态变量;
  • 常量对象;
  • synchronized 修饰的对象。

    垃圾回收算法

    标记清除算法

  • 流程:标记阶段标记需要被回收的对象;清除阶段进行回收与内存释放。

  • 缺点:容易产生内存碎片。

    复制算法

  • 流程:划分两个区域,新生成的对象在区域 A;A 中对象满了则进行标记并全部复制到区域 B,再将 A 清空。

  • 缺点:只能使用一半内存;复制过程影响效率。

    标记整理算法

  • 流程:标记阶段标记需要被回收的对象;整理阶段将存活对象移到内存的一端后清除另一端对象并释放内存。

  • 缺点:针对不同类型对象(生命周期长短、对象大小)无法统一高效。

    分代收集算法

  • 流程

将内存分为新生代、老年代:新生代存新生成的对象,生命周期短且回收数量多;老年代存大对象,生命周期长数量少;
新生代采用复制算法,将内存分为了 Eden 区、SurvivorFrom 区和 SurvivorTo 区,回收过程将 Eden 和 SurvivorFrom 中存活的对象复制到 SurvivorTo 后清空这两个区;
老年代采用标记清除算法。
image.png

垃圾收集器

垃圾收集器名称 适用分代 使用垃圾回收算法 线程 描述
Serial 新生代 复制算法 单线程 必须暂停全部工作线程,无线程交互开销,是 Client 模式下新生代默认 GC
ParNew 多线程 Serial 的多线程实现,是 Server 模式下新生代默认 GC
Parallel Scavenge 多线程 可以控制停顿时间、吞吐量、自适应调节的多线程 GC,效率更高
Serial Old 老年代 标记整理算法 单线程 Serial 的老年代实现,是 Client 模式下老年代默认 GC
Parallel Old 多线程 优先考虑吞吐量,其次考虑停顿时间的 GC
CMS
(Concurrent Mark Sweep)
标记清除算法 优先考虑停顿时间,由于标记与清除的阶段采用兵并发,因此无需暂停用户线程,只需要在标记 GC Roots 直接关联对象的时候暂停
G1
(Garbage First)
不区分 标记整理算法 避免 Full GC 引起的停顿,将堆分为大小固定的几个独立区域进行 GC,优先回收垃圾最多的区域,可以控制停顿时间

引用类型

强引用

将对象赋给一个引用变量时即为强引用,有引用存在就不会被回收,是内存泄漏的主要原因。

软引用

SoftReference 类实现,在系统内存不足时被回收。

弱引用

WeakReference 类实现,在 GC 过程中一定被回收。

虚引用

PhantomReference 类实现,用于跟踪对象的垃圾回收状态。

引用类型引发的问题

ThreadLocal 内存泄漏问题:ThreadLocalMap的 key 为弱引用(发生 GC 会被回收),value 为强引用(GC 过程不会被回收),有可能造成 key 被 GC,value 没被 GC,ThreadLocalMap 中出现 key 为 nullEntry,产生内存泄漏。
解决方式:调用 setgetremove 方法时,会清理掉 key 为 null 的记录,使用 ThreadLocal 的方法后手动使用 remove 方法。

JVM 调优参数

参数 含义
-Xss 栈大小
-Xmx 最大堆大小
-Xms 初始堆大小
-Xmn 新生代大小
-XX:SurvivorRatio Eden 区中两个 survivor 区大小比例
-XX:NewRatio 老年代 : 新生代大小比例
-XX:OldSize 老年代大小
-XX:NewSize 新生代大小