跳至主要內容

JVM

酷风大约 31 分钟

JVM

内存区域

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域

JDK 1.7

  1. 运行时数据区域
    1. 线程共享
      1. 堆 Heap
        1. 分代垃圾收集算法
          1. 新生代
          2. 老年代
          3. 永久代
        2. 字符串常量池 String Constant Pool
      2. 方法区 Method Area
        1. 运行时常量池 Runtime Constant Pool
    2. 线程私有
      1. 线程1
        1. 虚拟机栈 VM Stack
          1. 虚拟机执行 Java 方法 (也就是字节码)服务
        2. 本地方法栈 Native Method Stack
          1. Native 方法
        3. 程序计数器 Program Counter Register
      2. 线程n...
  2. 本地内存
    1. 直接内存 Direct Memory

JDK1.8

  1. 运行时数据区域
    1. 线程共享
      1. 堆 Heap
        1. 分代垃圾收集算法
          1. 新生代
          2. 老年代
          3. 永久代(元空间)
        2. 字符串常量池 String Constant Pool
    2. 线程私有
      1. 线程1
        1. 虚拟机栈 VM Stack
        2. 本地方法栈 Native Method Stack
        3. 程序计数器 Program Counter Register
      2. 线程n...
  2. 本地内存
    1. 元空间 Mate Space
      1. 运行时常量池 Runtime Constant Pool
    2. 直接内存 Direct Memory

知识点

类文件

  • JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件)

Class 文件结构

  • package cn.hutool.json;
  • public class JSONObject extends MapWrapper

  1. 魔数(Magic Number)
    1. 每个 Class 文件的头 4 个字节称为魔数(Magic Number)
    2. Java 规范规定魔数为固定值:0xCAFEBABE。
  2. Class 文件版本号(Minor&Major Version)
  3. 常量池(Constant Pool)
  4. 访问标志(Access Flags)
    1. 类Or接口,abstract 或 public
  5. 当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合
  6. 字段表集合(Fields)
    1. 用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
  7. 方法表集合(Methods)
  8. 属性表集合(Attributes)

类加载过程

类的生命周期
  1. 类从被加载到虚拟机内存中开始到卸载出内存为止
    1. 简单概括为 7 个阶段
    2. 验证、准备和解析这三个阶段可以统称为连接
  • 1、加载(Loading)
  • 连接(Linking)
    • 2、验证(Verification)
    • 3、准备(Preparation)
    • 4、解析(Resolution)
  • 5、初始化(Initialization)
  • 6、使用(Using)
  • 7、卸载(Unloading)
类加载过程
  • 系统加载 Class 类型的文件主要三步:加载->连接->初始化。
  • 连接过程又可分为三步:验证->准备->解析。

  1. 加载( 主要通过类加载器完成)
    1. 通过全类名获取定义此类的二进制字节流。
    2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
    3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。
  2. 连接
    1. 验证(确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。)
      1. 文件格式验证(Class 文件格式检查)
      2. 元数据验证(字节码语义检查)
      3. 字节码验证(程序语义检查)
      4. 符号引用验证(类的正确性检查)
    2. 准备,准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
    3. 解析,是虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
  3. 初始化
    1. 初始化阶段是执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的Java 程序代码(字节码)。
    2. 说明:<clinit> ()方法是编译之后自动生成的。
类卸载
  • 卸载类即该类的 Class 对象被 GC。

类加载器

  • 类加载器的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。

  • 加载规则

    1. JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载
    2. 对内存更加友好
    3. 对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。
  • JVM 中内置加载器

    1. BootstrapClassLoader(启动类加载器),%JAVA_HOME%/lib,主要用来加载 JDK 内部的核心类库
    2. ExtensionClassLoader(扩展类加载器),%JRE_HOME%/lib/ext,
    3. AppClassLoader(应用程序类加载器)
    4. 自定义的类加载器来进行拓展
      1. 继承 ClassLoader抽象类
        1. findClass
        2. loadClass
    5. 除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader 抽象类。
    6. 每个 ClassLoader 可以通过getParent()获取其父 ClassLoader,如果获取到 ClassLoader 为null的话,那么该类是通过 BootstrapClassLoader 加载的。因为BootstrapClassLoader 由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。
  • 双亲委派模型

  1. 类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载呢?这就需要提到双亲委派模型了。
    1. ClassLoader 类使用委托模型来搜索类和资源
    2. 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
    3. ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
  • 打破双亲委派模型方法
  1. 重写 loadClass() 方法
  2. 双亲委派模型,加载类时委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)

堆内存

  • Java 世界中“几乎”所有的对象都在堆中分配
  • 垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)

  • 堆的大小可调节
    • 堆空间
      • -Xms 最小
      • -Xmx 最大
      • 通常设置相同,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
      • 默认:
        • 初始内存大小:物理电脑内存大小/64
        • 最大内存大小:物理电脑内存大小/4
    • -Xmn 新生代堆大小
    • -XX:NewRatio
      • 新生代与老年代在堆结构的占比
        • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
        • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
    • -xx:SurvivorRatio
      • Eden空间和Survivor占比
      • 在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1当然开发人员可以通过选项“-xx:SurvivorRatio”调整这个空间比例
    • -Xx:MaxTenuringThreshold
      • 新生代过渡到老年代的年龄设置,年龄阀值

STW

  • Stop The World
    • 是指在执行垃圾算法时,Java应用程序的其他所有线程都被挂起(除了垃圾回收帮助器之外)
    • Java中一种全局暂停现象,全局停顿;
    • 所有Java代码停止;native代码可以执行,但不能与JVM交互
    • 该现象多半是由于GC引起的;
  • 借助 安全点(Safe Point) 加参数排查;

JNI

  • Java Native Interface; Java本地方法接口;
  • 是Java语言允许Java代码与C、C++代码交互的标准机制;

GC过程

  • 新生代GC -> 老年代GC -> Full GC
  1. 部分收集 (Partial GC):
    1. 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
    2. 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
    3. 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
  2. 整堆收集 (Full GC):收集整个 Java 堆和方法区。
  • JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。

MinorGC

  • YGC/新生代GC
  1. 何时发生Minor GC :当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次Minor GC会清理年轻代的内存。)
  2. Minor GC 特性:频率高,回收速度快。因为大部分对象朝生夕死。
  3. 是否会引发STW 会,暂停其他用户线程。但是比Major GC (老年代GC)引发的STW影响小。
  • Eden --> S0 --> S1 --> Tenured/Old
    • 当Eden区存满时,会触发一个MinorGC操作
    • 年龄 + 1
    • Survivor中 超过 阀值(15),晋升老年代
    • 动态年龄判定/阀值变动

动态对象年龄判定:Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累加, 当累加到某个年龄时,所累加的大小超过了survivor区的一半(默认值是 50%,可以通过 -XX:TargetSurvivorRatio=percent 来设置), 则取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值。

内存分配与回收原则

  1. 对象优先在 Eden 区分配
    1. Minor GC 时,若对象太大无法进入Survivor空间,通过分配担保机制会将对象放入老年代
  2. 大对象直接进入老年代
    1. 需要大量连续内存空间的对象(比如:字符串、数组)
    2. 旨在避免将大对象放入新生代,从而减少新生代的垃圾回收频率和成本
  3. 长期存活的对象将进入老年代
    1. 虚拟机给每个对象一个对象年龄(Age)计数器
  4. 主要进行 gc 的区域
    1. 部分收集 Partial GC
      1. 新生代收集(Minor GC / Young GC)
      2. 老年代收集(Major GC / Old GC)
      3. 混合收集(Mixed GC)
    2. 整堆收集 (Full GC)
  5. 空间分配担保
    1. 空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间

死亡对象判断方法

  1. 引用计数法(主流的虚拟机,都不使用,很难解决对象之间循环引用)
    1. 每当有一个地方引用它,计数器就加 1;
    2. 当引用失效,计数器就减 1;
    3. 任何时候计数器为 0 的对象就是不可能再被使用的
  2. 可达性分析算法
    1. GC Roots: 当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的
  3. 引用类型总结:判定对象的存活都与“引用”有关
    1. JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。
    2. JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
      1. 强引用:垃圾回收器绝不会回收它
      2. 软引用:内存空间足够,垃圾回收器就不会回收它
      3. 弱引用:不管当前内存空间足够与否,都会回收它的内存
      4. 虚引用:任何时候都可能被垃圾回收
  4. 如何判断一个常量是废弃常量?
    1. 运行时常量池主要回收的是废弃的常量。
    2. 如字符串,无String对象引用该字符串常量时,内存回收时会清理;
  5. 如何判断一个类是无用的类?
    1. 方法区主要回收的是无用的类。要同时满足下面 3 个条件
      1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
      2. 加载该类的 ClassLoader 已经被回收。
      3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    2. 仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

垃圾回收算法

  • 标记-清除算法
  • 复制算法
  • 标记-整理算法
  • 分代收集算法
    • 当前虚拟机的垃圾收集都采用分代收集算法
    • 我们就可以根据各个年代的特点选择合适的垃圾收集算法

垃圾收集器

  • 收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
  • 根据具体应用场景选择适合自己的垃圾收集器
    • JDK 默认垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version 命令查看)
      • JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
      • JDK 9 ~ JDK20: G1
  1. 新生代回收器
    1. Serial
    2. ParNew
    3. parallel
  2. 老年代回收器
    1. Serial Old
    2. CMS
    3. Parallel Old
  3. 新生代和老年代回收器
    1. G1

  1. Serial 收集器
    1. 单线程
    2. 新生代采用标记-复制算法,老年代采用标记-整理算法。
    3. 简单而高效(与其他收集器的单线程相比)
    4. Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择
    5. STW
  2. ParNew 收集器
    1. Serial 收集器的多线程版本
    2. 新生代采用标记-复制算法,老年代采用标记-整理算法。
    3. 运行在 Server 模式下的虚拟机的首要选择
    4. 它能与 CMS 收集器(真正意义上的并发收集器)配合工作
  3. Parallel Scavenge 收集器
    1. 使用标记-复制算法的多线程收集器
    2. Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)
    3. JDK1.8 默认收集器
  4. Serial Old 收集器
    1. Serial 收集器的老年代版本,它同样是一个单线程收集器
  5. Parallel Old 收集器
    1. Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
  6. CMS 收集器
    1. “标记-清除”算法实现的
    2. CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
    3. HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
  7. G1 收集器
    1. G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
    2. JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器
  8. ZGC 收集器
    1. 与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。
    2. 在 ZGC 中出现 Stop The World 的情况会更少!
    3. Java11 的时候 ,ZGC 还在试验阶段。经过多个版本的迭代,不断的完善和修复问题,
    4. ZGC 在 Java 15 已经可以正式使用了!

OutOfMemoryError


java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时
java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置,若没有特别配置,将会使用默认值

  • Java 世界中“几乎”所有的对象都在堆中分配
  • 垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)

  • 堆的大小可调节
    • 堆空间
      • -Xms 最小
      • -Xmx 最大
      • 通常设置相同,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
      • 默认:
        • 初始内存大小:物理电脑内存大小/64
        • 最大内存大小:物理电脑内存大小/4
    • -Xmn 新生代堆大小
    • -XX:NewRatio
      • 新生代与老年代在堆结构的占比
        • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
        • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
    • -xx:SurvivorRatio
      • Eden空间和Survivor占比
      • 在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1当然开发人员可以通过选项“-xx:SurvivorRatio”调整这个空间比例
    • -Xx:MaxTenuringThreshold
      • 新生代过渡到老年代的年龄设置,年龄阀值

STW

  • Stop The World
    • 是指在执行垃圾算法时,Java应用程序的其他所有线程都被挂起(除了垃圾回收帮助器之外)
    • Java中一种全局暂停现象,全局停顿;
    • 所有Java代码停止;native代码可以执行,但不能与JVM交互
    • 该现象多半是由于GC引起的;
  • 借助 安全点(Safe Point) 加参数排查;

JNI

  • Java Native Interface; Java本地方法接口;
  • 是Java语言允许Java代码与C、C++代码交互的标准机制;

GC过程

  • 新生代GC -> 老年代GC -> Full GC
  1. 部分收集 (Partial GC):
    1. 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
    2. 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
    3. 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
  2. 整堆收集 (Full GC):收集整个 Java 堆和方法区。
  • JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。

MinorGC

  • YGC/新生代GC
  1. 何时发生Minor GC :当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次Minor GC会清理年轻代的内存。)
  2. Minor GC 特性:频率高,回收速度快。因为大部分对象朝生夕死。
  3. 是否会引发STW 会,暂停其他用户线程。但是比Major GC (老年代GC)引发的STW影响小。
  • Eden --> S0 --> S1 --> Tenured/Old
    • 当Eden区存满时,会触发一个MinorGC操作
    • 年龄 + 1
    • Survivor中 超过 阀值(15),晋升老年代
    • 动态年龄判定/阀值变动

动态对象年龄判定:Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累加, 当累加到某个年龄时,所累加的大小超过了survivor区的一半(默认值是 50%,可以通过 -XX:TargetSurvivorRatio=percent 来设置), 则取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值。

内存分配与回收原则

  1. 对象优先在 Eden 区分配
    1. Minor GC 时,若对象太大无法进入Survivor空间,通过分配担保机制会将对象放入老年代
  2. 大对象直接进入老年代
    1. 需要大量连续内存空间的对象(比如:字符串、数组)
    2. 旨在避免将大对象放入新生代,从而减少新生代的垃圾回收频率和成本
  3. 长期存活的对象将进入老年代
    1. 虚拟机给每个对象一个对象年龄(Age)计数器
  4. 主要进行 gc 的区域
    1. 部分收集 Partial GC
      1. 新生代收集(Minor GC / Young GC)
      2. 老年代收集(Major GC / Old GC)
      3. 混合收集(Mixed GC)
    2. 整堆收集 (Full GC)
  5. 空间分配担保
    1. 空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间

死亡对象判断方法

  1. 引用计数法(主流的虚拟机,都不使用,很难解决对象之间循环引用)
    1. 每当有一个地方引用它,计数器就加 1;
    2. 当引用失效,计数器就减 1;
    3. 任何时候计数器为 0 的对象就是不可能再被使用的
  2. 可达性分析算法
    1. GC Roots: 当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的
  3. 引用类型总结:判定对象的存活都与“引用”有关
    1. JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。
    2. JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
      1. 强引用:垃圾回收器绝不会回收它
      2. 软引用:内存空间足够,垃圾回收器就不会回收它
      3. 弱引用:不管当前内存空间足够与否,都会回收它的内存
      4. 虚引用:任何时候都可能被垃圾回收
  4. 如何判断一个常量是废弃常量?
    1. 运行时常量池主要回收的是废弃的常量。
    2. 如字符串,无String对象引用该字符串常量时,内存回收时会清理;
  5. 如何判断一个类是无用的类?
    1. 方法区主要回收的是无用的类。要同时满足下面 3 个条件
      1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
      2. 加载该类的 ClassLoader 已经被回收。
      3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    2. 仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

垃圾回收算法

  • 标记-清除算法
  • 复制算法
  • 标记-整理算法
  • 分代收集算法
    • 当前虚拟机的垃圾收集都采用分代收集算法
    • 我们就可以根据各个年代的特点选择合适的垃圾收集算法

垃圾收集器

  • 收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
  • 根据具体应用场景选择适合自己的垃圾收集器
    • JDK 默认垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version 命令查看)
      • JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
      • JDK 9 ~ JDK20: G1
  1. 新生代回收器
    1. Serial
    2. ParNew
    3. parallel
  2. 老年代回收器
    1. Serial Old
    2. CMS
    3. Parallel Old
  3. 新生代和老年代回收器
    1. G1

  1. Serial 收集器
    1. 单线程
    2. 新生代采用标记-复制算法,老年代采用标记-整理算法。
    3. 简单而高效(与其他收集器的单线程相比)
    4. Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择
    5. STW
  2. ParNew 收集器
    1. Serial 收集器的多线程版本
    2. 新生代采用标记-复制算法,老年代采用标记-整理算法。
    3. 运行在 Server 模式下的虚拟机的首要选择
    4. 它能与 CMS 收集器(真正意义上的并发收集器)配合工作
  3. Parallel Scavenge 收集器
    1. 使用标记-复制算法的多线程收集器
    2. Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)
    3. JDK1.8 默认收集器
  4. Serial Old 收集器
    1. Serial 收集器的老年代版本,它同样是一个单线程收集器
  5. Parallel Old 收集器
    1. Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
  6. CMS 收集器
    1. “标记-清除”算法实现的
    2. CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
    3. HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
  7. G1 收集器
    1. G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
    2. JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器
  8. ZGC 收集器
    1. 与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。
    2. 在 ZGC 中出现 Stop The World 的情况会更少!
    3. Java11 的时候 ,ZGC 还在试验阶段。经过多个版本的迭代,不断的完善和修复问题,
    4. ZGC 在 Java 15 已经可以正式使用了!

OutOfMemoryError


java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时
java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置,若没有特别配置,将会使用默认值

方法区

  • 方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。

  • 当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

  • 永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。

  • Why MateSpace ?

  1. 整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是本地内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
  2. 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了;
  3. 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了;
  • 方法区常用参数
  1. JDK 1.8 之前
    1. -XX:PermSize=N //方法区 (永久代) 初始大小
    2. -XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
  2. JDK 1.8 永久代被移除,JDK 1.7已经开始了
    1. -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。
    2. -XX:MetaspaceSize 调整标志定义元空间的初始大小,如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
  3. 永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。

运行时常量池

  1. 常量池(Constant Pool)
    1. 常量池(Class文件常量池):.java经过编译后生成的.class文件,是Class文件的资源仓库。
    2. 常量池中主要存放俩大常量:
      1. 字面量(文本字符串,final常量)
        1. 字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量。
      2. 符号引用(类和接口的全局定名,字段的名称和描述,方法的名称和描述)
        1. 常见的符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号。
  • 运行时常量池是方法区的一部分
  • 常量池表会在类加载后存放到方法区的运行时常量池中。
  • 运行时常量池的功能类似于传统编程语言的符号表,尽管它包含了比典型符号表更广泛的数据。
  • 受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误

字符串常量池

字符串常量池,在堆区开一段内存存放字符串, 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建;

直接内存

  • 直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的

  • 通过 JNI 的方式在本地内存上分配的。

  • 直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

  • 直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

JVM-内存参数

  • 堆内存
    • –Xms 最小
    • -Xmx 最大
  • 显式新生代内存,默认情况下,YG 的最小大小为 1310 MB,最大大小为无限制。
    • -XX:NewSize 最小
    • -XX:MaxNewSize 最大
    • -Xmn:此设置 NewSize 与 MaxNewSize 设为一致的
  • -XX:NewRatio=<int> 来设置老年代与新生代内存的比值
  • 显式指定永久代/元空间的大小
    • jdk1.8之前
      • -XX:PermSize=N #方法区 (永久代) 初始大小
      • -XX:MaxPermSize=N #方法区 (永久代) 最大大小
    • 从 Java 8 开始,如果我们没有指定 Metaspace 的大小,随着更多类的创建,虚拟机会耗尽所有可用的系统内存(永久代并不会出现这种情况)。
      • -XX:MetaspaceSize=N #设置 Metaspace 的初始大小(Metaspace 的初始容量并不是 -XX:MetaspaceSize 设置,无论 -XX:MetaspaceSize 配置什么值,对于 64 位 JVM 来说,Metaspace 的初始容量都是 21807104(约 20.8m))
      • -XX:MaxMetaspaceSize=N #设置 Metaspace 的最大大小
  • 垃圾回收器
    • -XX:+UseSerialGC
    • -XX:+UseParallelGC
    • -XX:+UseParNewGC
    • -XX:+UseG1GC
  • 处理 OOM
    • 这些参数将堆内存转储到一个物理文件中,以后可以用来查找泄漏
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./java_pid<pid>.hprof
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >"
-XX:+UseGCOverheadLimit

  • GC 日志记录
# 必选
# 打印基本 GC 信息
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
# 打印对象分布
-XX:+PrintTenuringDistribution
# 打印堆数据
-XX:+PrintHeapAtGC
# 打印Reference处理信息
# 强引用/弱引用/软引用/虚引用/finalize 相关的方法
-XX:+PrintReferenceGC
# 打印STW时间
-XX:+PrintGCApplicationStoppedTime

# 可选
# 打印safepoint信息,进入 STW 阶段之前,需要要找到一个合适的 safepoint
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1

# GC日志输出的文件路径
-Xloggc:/path/to/gc-%t.log
# 开启日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割几个文件,超过之后从头文件开始写
-XX:NumberOfGCLogFiles=14
# 每个文件上限大小,超过就触发分割
-XX:GCLogFileSize=50M

  • 其他
-server : 启用“ Server Hotspot VM”; 此参数默认用于 64 位 JVM
-XX:+UseStringDeduplication : Java 8u20 引入了这个 JVM 参数,通过创建太多相同 String 的实例来减少不必要的内存使用; 这通过将重复 String 值减少为单个全局 char [] 数组来优化堆内存。
-XX:+UseLWPSynchronization: 设置基于 LWP (轻量级进程)的同步策略,而不是基于线程的同步。
-XX:LargePageSizeInBytes: 设置用于 Java 堆的较大页面大小; 它采用 GB/MB/KB 的参数; 页面大小越大,我们可以更好地利用虚拟内存硬件资源; 然而,这可能会导致 PermGen 的空间大小更大,这反过来又会迫使 Java 堆空间的大小减小。
-XX:MaxHeapFreeRatio : 设置 GC 后, 堆空闲的最大百分比,以避免收缩。
-XX:SurvivorRatio : eden/survivor 空间的比例, 例如-XX:SurvivorRatio=6 设置每个 survivor 和 eden 之间的比例为 1:6。
-XX:+UseLargePages : 如果系统支持,则使用大页面内存; 请注意,如果使用这个 JVM 参数,OpenJDK 7 可能会崩溃。
-XX:+UseStringCache : 启用 String 池中可用的常用分配字符串的缓存。
-XX:+UseCompressedStrings : 对 String 对象使用 byte [] 类型,该类型可以用纯 ASCII 格式表示。
-XX:+OptimizeStringConcat : 它尽可能优化字符串串联操作。

JVM-工具

  • HeapDump 堆转储快照 .hprof
    • Java启动程序配置
    • jmap 工具生成
    • jhat 分析HeapDump工具,分析完成后提供http查看服务

jstack

  • 生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合.
    • 长时间停顿
    • 线程调用堆栈

对象

对象的创建

  1. 类加载检查
    1. new 指令
    2. 常量池中定位类的符号引用,检查符号引用所代表的类是否 加载,解析,初始化
    3. 反之需执行类加载过程
  2. 分配内存
    1. 虚拟机将为新生对象分配内存
    2. 对象所需的内存大小在类加载完成后便可确定
    3. 为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来
    4. 内存分配方式
      1. 指针碰撞
        1. 堆内存规整(即没有内存碎片)的情况
      2. 空闲列表
        1. 堆内存不规整的情况下
    5. 内存分配并发问题
      1. 虚拟机采用两种方式来保证线程安全
  3. 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)
  4. 设置对象头
    1. 初始化零值完成之后,虚拟机要对对象进行必要的设置
    2. 例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中
  5. 执行 init 方法
    1. 执行 new 指令之后会接着执行 <init> 方法

对象的内存布局

  • 在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。

对象的访问定位

  • 建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有:使用句柄、直接指针。

参考网站

上次编辑于:
贡献者: hihcoder