JVM之内存结构、垃圾回收和类加载

129 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的8天,点击查看活动详情

介绍

这篇文章简要而精炼地介绍了 JVM 的内存结构垃圾回收类加载部分,是基础部分的介绍,相当于八股,而字节码等相关技术日后会继续更新;

内存结构

程序计数器

作用:记录下一条指令的执行的地址

特点:

  1. 线程私有
  2. 不会存在内存溢出

虚拟机栈 JVM Stacks

定义

作用:每个线程运行时需要的内存空间,有多个栈帧组成;

栈帧:每个方法运行时需要的内存,当前执行的栈帧称为活动栈帧;

注意

  1. 栈内存不是越大越好
  2. 栈内的没有逃离方法作用范围的局部变量是线程私有的,即该参数不是作为参数传入或结果返回(特别是引用类型)

栈内存溢出

  1. 栈帧过多,递归调用
  2. 栈帧过大

jstack + 进程id:查看某个进程中所有线程运行情况

本地方法栈 Native Method Stacks

与虚拟机栈差不多,但调用的都是native方法

定义

通过new关键字,创建对象都会用到堆内存

特点:

  1. 线程共享,要考虑线程安全问题
  2. 有垃圾回收机制

堆内存诊断

  1. jsp:查看当前系统中有哪些Java进程
  2. jmap: 查看堆内存占用情况 ===> jmap + heap + 进程id
  3. jconsole:多功能的监测工具,可以连续监测,图形界面
  4. jvirsualvm:功能与jconsole类似,但比jconsole高级

方法区

定义:所有Java线程共享,存储类的结构,类的字段,成员方法和构造器方法,运行时常量池,在虚拟机启动时创建

结构

二进制字节码:类基本信息,常量池,类方法定义(包含虚拟机指令)

常量池

就是一张表,虚拟-机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息

运行时常量池

当类被加载时,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

常量池和串池的关系

StringTable

  1. 常量池中的字符串只是符号,第一次用到时才会变成对象(字符串延迟加载)
  2. 利用串池的机制,来避免重复创建字符串对象
  3. 字符串变量拼接原理是StringBuilder
  4. 字符串常量拼接原理是编译器优化
  5. 可以使用intern方法,主动将串池中还没有的字符串放入串池
  • 1.8将这个字符串对象尝试放入串池,如果有则不放入(不改变原来字符串的指向,且返回池中的对象),如果没有则放入串池,会把串池中的对象返回。(即该字符串对象指向串表中,而不是堆中)

位置

1.8位于堆中,1.6之前位于方法区中

垃圾回收

stringtable里面的对象是可以被垃圾回收的

调优

  1. 调整 -XX:StringTableSize=桶个数
  2. 考虑将字符串对象入池,存放在stringtable中

直接内存

  1. 常见于 NIO 操作时,用于数据缓冲区
  2. 分配回收成本较高,
  3. 但读写性能高 不受 JVM 内存回收管理

分配和回收原理

  1. 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  2. ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调 用 freeMemory 来释放直接内存

垃圾回收

判断垃圾

  1. 引用计数法
  2. 可达性分析算法
    1. Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
    2. 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以 回收

四种引用

  1. 强引用
  • 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
  1. 软引用
  • 仅有软引用引用该对象时,在垃圾回收内存仍不足时会再次出发垃圾回收,回收软引用 对象
  • 可以配合引用队列来释放软引用自身
public class String1 {
    private final static int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        List<SoftReference<byte[]>> list = new ArrayList<>();

        // 使用引用队列 排除已经垃圾回收的引用,若该对象已经被垃圾回收,弱引用就会添加到该队列中
        ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
        for (int i = 0; i < 10; i++) {
            // 创建SoftReference对象, 实现软引用
            SoftReference<byte[]> softReference = new SoftReference<>(new byte[_4MB], referenceQueue);
            // 添加集合中
            list.add(softReference);
        }

        // 排除为空的信息
        Reference<? extends byte[]> poll = referenceQueue.poll();
        while (poll != null) {
            list.remove(poll);
            poll = referenceQueue.poll();
        }
        System.out.println("===================");
        for (SoftReference<byte[]> softReference : list) {
            System.out.println(softReference.get());
        }

        System.in.read();
    }
}
  1. 弱引用
  • 仅有弱引用引用该对象时,在垃圾回收无论内存是否充足,都会回收弱引用对象
  • 可以配合引用队列来释放弱引用自身
public class String1 {
    private final static int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        List<WeakReference<byte[]>> list = new ArrayList<>();

        // 引用队列
        ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
        // 创建对象
        for (int i = 0; i < 10; i++) {
            WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB], referenceQueue);
            list.add(ref);
        }

        Reference<? extends byte[]> poll = referenceQueue.poll();
        // 排除无效的内容
        while (poll != null) {
            list.remove(poll);
            poll = referenceQueue.poll();
        }

        System.out.println("=============");
        for (WeakReference<byte[]> weakReference : list) {
            System.out.println(weakReference.get());
        }
    }
}
  1. 虚引用
  • 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存
  1. 终结器引用
  • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

垃圾回收算法

  1. 标记清除

速度较块,但会造成内存碎片

  1. 标记整理

速度慢,没有内存碎片

  1. 复制

不会有内存碎片,需要占用双倍内存空间

分代垃圾回收

  1. 对象首先分配在伊甸园区域
  2. 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用copy 复制算法到 to中,存活的对象年龄加1并且交换 from to
  3. minor gc 会引发stop the world ,暂停其他的用户线程,等垃圾回收结束,用户线程才恢复运行
  4. 当对象寿命超过阈值时,会晋升为老年代,最大寿命为15(4bit)
  5. 当老年代空间不足时,先会尝试触发minor gc,如果之后空间仍不足,那么会触发 full gc,STW的时间更长
  • 相关JVM参数

垃圾回收器

  1. 串行
  • 单线程
  • 堆内存较小,适合个人电脑
  1. 吞吐量优先
  • 多线程
  • 堆内存较大,多核CPU
  • 让单位时间内,STW的时间最短
  1. 响应时间优先
  • 多线程
  • 堆内存较大,多核CPU
  • 尽可能让单次 (每次) STW的时间最短

串行

垃圾回收后可能会改变内存空间的地址,所以要设定安全点让进程停下来

吞吐量优先

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC

-XX:+UseAdaptiveSizePolicy

-XX:GCTimeRatio=ratio ,公式 1/(1+ratio) ,表示在运行期间的总时间中进程停止时间占总运行时间的比例

-XX:MaxGCPauseMillis=ms ,表示每次暂停的最大时间

-XX:ParallelGCThreads=n

响应时间优先

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld

-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads :并发线程数/并发GC线程数

-XX:CMSInitiatingOccupancyFraction=percent :占用多大内存时触发垃圾回收

-XX:+CMSScavengeBeforeRemark :新生代的对象引用老年代的对象,重新标记时先对新生代进行垃圾回收

G1垃圾回收器

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
  • 超大堆内存,会将堆划分为多个大小相等的 Region
  • 整体上是 标记+整理 算法,两个区域之间是 复制 算法

三个阶段

Young Collection

会 STW

Young Collection + Concurrent Mark

在 Young GC 时会进行 GC Root 的初始标记

老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定

-XX:InitiatingHeapOccupancyPercent=percent (默认45%)

Mixed Collection

会对 E、S、O 进行全面垃圾回收

  • 最终标记(Remark)会 STW
  • 拷贝存活(Evacuation)会 STW

类加载与字节码技术

字节码指令

类加载阶段

加载

  1. 将类的字节码载入方法区中
  2. 如果这个类还有父类没有加载,先加载父类
  3. 加载和链接可能是交替运行的

链接

  1. 验证

验证类是否符合 JVM规范,安全性检查,检查字节码文件的魔数

  1. 准备

为 static 变量分配空间,设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
  1. 解析

将常量池中的符号引用解析为直接引用

初始化

初始化即调用 ()V ,即构造方法,虚拟机会保证这个类的『构造方法』的线程安全

发生时机

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不导致初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName 的参数 2 为 false 时