• JVM内容


    一、JVM基本结构

    JVM基本结构图

    二、JVM中内存结构

    1、方法区

    • 是一个JVM的规范,所有Java虚拟机必须遵守的。是一个逻辑概念;
    • 是线程共享的,用于存储类信息,如:类字段、方法数据、常量池等;
    • 方法区的大小决定了JVM可以存储和运行多少类;
    • 在Java7之前,称为永久代;
    • Java8之后(包含)称为元空间

    为什么使用元空间替代永久代呢?

    1. 永久代是从系统内存中申请到的一片固定大小的内存空间,若程序包含的类较多,很容易撑爆永久代空间,只能通过指定JVM参数的方式改变永久代空间;Java8之后采用元空间代替永久代,是因为元空间在不指定其大小的前提下以系统内存作为瓶颈,不用担心OOM;
    2. Oracle吸取了JRocket没有永久代的依然保持性能相近的经验,在Java8使用元空间代替之;

    2、Java堆

    • 用于存放所有对象的空间,线程共享
    • 运行时数据区,几乎所有的对象都保存在其中
    • 通过垃圾回收器管理,堆是垃圾收集器进行GC最重要的区域;
    • Java堆可以分为:新生代(Eden区、S0区、S1区)和老年代;
    • 在绝大多数情况下,对象首先被分配在Eden区,在一次YoungGC中,90%的对象都会被清理掉,幸存的对象将会被转移至S0或S1,每经历一次YoungGC,未被清理的对象年龄就会+1,当幸存对象的年龄达到阈值(默认15)或满足其他某些条件后(如大对象),就会被认定为是老年代对象,从而进入老年代;

    3、Java栈和本地方法栈

    • 栈都是线程私有的;
    • 虚拟机栈是服务于Java方法,本地方法栈是服务于native方法的;
    • 每次函数调用的数据都是通过栈进行传递的;
    • 在栈中的保存的主要内容为栈帧。当函数被调用时函数入栈,函数执行完成时出栈。栈顶则是当前正在执行的函数;
    • 每个方法在执行的同时都会创建一个栈帧用于存放局部变量表、操作数栈、帧数据区,等信息;

    如下图为栈帧操作栈帧操作

    4、程序计数器

    • 当前线程所执行的字节码行号指示器,指向虚拟机字节码指令的位置;
    • 空间极小;
    • Native方法与非Native方法的区别:
      针对非Native方法:当前线程的字节码行号指示器;
      针对于Native方法:则为undefined;
    • 每个线程都会有自己独立的程序计数器,所以该区域是线程私有的;
    • 这一块区域是JVM中唯一没有OOM的区域;

    5、执行引擎

    负责执行虚拟加载进来的字节码

    二、类加载子系统

    负责将本地Class文件加载进入JVM。

    1、类加载器

    1.1 类加载器概述

    类加载器负责将Java程序运行过程中将Class文件动态加载至JVM中,
    Java中类加载器有三种:启动类加载器(BootstrapClassLoader)、扩展类加载器(ExtensionClassLoader)、应用/系统类加载器(ApplicationClassLoader)

    • 启动类加载器(rt.jar包下的类):负责加载java.lang.ClassLoader,是所有ClassLoader的父类。启动类加载器并非是由Java程序编写的,所以在Java运行程序中是为null的。
    public class ClassLoaderTest {
    
        public static void main(String[] args) {
            ClassLoaderTest clt = new ClassLoaderTest();
            Class clazz = clt.getClass();
            System.out.println(clazz.getClassLoader());                              //应用/系统类加载器
            System.out.println(clazz.getClassLoader().getParent());                  //扩展类加载器
            System.out.println(clazz.getClassLoader().getParent().getParent());      //启动类加载器,由于不是由Java程序编写的,所以为null
        }
    
    }
    //结果为
    //sun.misc.Launcher$AppClassLoader@dad5dc
    //sun.misc.Launcher$ExtClassLoader@7aac27
    //null
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 扩展类加载器(ext/*.jar包下的类):负责加载Java核心类的扩展类;
    • 应用/系统类加载器:负责将应用程序级别(后端程序员编写的Java’代码)的类加载到JVM中;

    类加载器的运行过程:
    类加载流程

    1.2 双亲委派机制

    上图说明了类加载流程的同时也说明了双亲委派机制:加载类时,每一个类加载器都会将当前类传给上一级类加载器。当启动类加载器无法加载该类时,将类向下传递,由下级类加载器加载,当所有类加载器都无法找到当前类时,抛出异常

    2、类加载流程

    涉及Java类的加载过程:加载、验证、准备、解析、初始化
    在这里插入图片描述

    • 加载:
      • 通过全限定名称获取该Class文件的二进制流;
      • 将静态的存储结构转化为方法区中运行时数据结构;
      • 在内存中生成代表这个类的Class对象;
    • 验证:
      • 文件格式验证:验证字节流是否符合Class文件格式的规范, 验证内容主要有:开头魔数、主次版本号、常量标志等
      • 源数据验证:验证字节码语义,包括继承关系、抽象语法等
      • 字节码验证:验证程序语义是否合法、符合正常逻辑;
      • 符号引用验证:该类是否缺少或被禁止访问其依赖的某些外部类、方法、字段等资源;
    • 准备:
      • 为静态变量分配内存并赋初始值;
    • 解析:
      • 将符号引用替换为直接引用;
    • 初始化:
      • 为静态变量赋予代码中指定的值;
      • 为成员变量赋予调用类构造方法时指定的值。

    三、JVM垃圾回收算法

    1、四种引用级别

    • 强引用
    // object对象就是强引用
    		Object object = new Object();
    
    • 1
    • 2
    • 软引用
      在切断对该对象的引用时,只有当堆内存不足时才会被回收
    		Student stu = new Student();
            stu.setName("张三");
            WeakReference<Student> weakReference = new WeakReference<>(stu);
            stu = null;
            if (weakReference.get() != null){
                System.out.println(weakReference.get());
            }else {
                System.out.println("对象为空");
            }
            //结果为:Student{name='张三'}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 软引用
      在将对象置为空后,会立即被垃圾回收器回收
    		Student stu = new Student();
            stu.setName("张三");
            PhantomReference<Student> phantomReference = new 	PhantomReference<>(stu, new ReferenceQueue<Student>());
            stu = null;
            if (phantomReference.get() != null){
                System.out.println(phantomReference.get());
            }else {
                System.out.println("对象为空");
            }
            //结果为:对象为空
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 虚引用
      任何情况下都获取不到该对象

    2、JVM中的几个重要概念

    • 可触及性,可触及性分为三种状态:
      • 可触及:从根节点开始,可以到达某个对象;
      • 对象引用被释放,但是在被销毁的过程中在finalize()函数中被重新初始化复活;
      • 由于finalize()只能被执行一次,所以错过这一次机会的对象,则为不可触及状态
    public class FinalizeObject {
    
       private static FinalizeObject finalizeObject;
    
       public static void main(String[] args) {
           finalizeObject = new FinalizeObject();
           for (int i = 0; i <= 1; i++) {
               System.out.println(String.format("--------------GC nums=%d--------------", i));
               finalizeObject = null;   //将finalizeObject对象置为“垃圾对象”
               System.gc();//通知JVM运行GC
               try {
                   Thread.sleep(100);//等待gc执行
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               if (finalizeObject == null){
                   System.out.println("finalizeObject is dead");
               }else {
                   System.out.println("finalizeObject is still alive");
               }
           }
       }
    
    	//finalize只会被调用一次,给正在被销毁的对象唯一一次复活的机会
       @Override
       protected void finalize() throws Throwable {
           System.out.println("对象正在被销毁");
           super.finalize();
           finalizeObject = this;
       }
    }
    //--------------GC nums=0--------------
    //对象正在被销毁
    //finalizeObject is still alive
    //--------------GC nums=1--------------
    //finalizeObject is dead
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 槽位复用,默认开启
       /**
        * 槽位复用
        * 对于变量a而言,其作用域仅在代码块之内
        * 离开代码块后,即可判定其不再有用
        * 这种时候,若在代码块外存在另一个变量b,
        * 则变量b会复用变量a的槽位
        */
       public void gc1() {
           {
               int[] a = {1,2,3};
               System.out.println(a);
           }
           int b = 10;
           System.out.println(b);
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    槽位复用

    • 对象分配
      对象分配的过程中,除了可以在堆上分配,也可以在栈上分配或TLAB分配。分配的流程如下:
      • 先尝试栈上分配,若不满足条件,尝试TLAB分配,若仍不满足,则在堆上分配

      • 栈上分配

        • 开启栈上分配JVM参数:
        • 对于小&多的对象,可以试图采用栈上分配。此种场景之下,开启栈上分配能够避免大量的GC,由于GC是存在停顿的,若没有栈上分配会产生大量的时间浪费;
        • 栈是线程私有的,所以满足栈上分配的对象也应该是线程私有的 ——逃逸分析
        • 若一个对象满足栈上分配的话,需要将其打散后放到栈中——标量替换
      • TLAB

        • Thread Local Allocation Buffer :线程本地分配缓冲区;
        • 将对象放置在堆上,线程共享;
        • TLAB空间很小,只占用Eden 1% 的空间
      • 堆上分配
        进行堆上分配时,对于不满足进入老年代的对象,将其放入Eden区

    • 逃逸分析
      • 开启逃逸分析JVM参数:
        由于是线程私有的,则若是逃出改私有线程,则说明逃逸
    	//未逃逸
    	public void hello(){
    		User user = new User();
    		user.setName("Alvis");
    		... ...
    	}
    
    	//逃逸
    	User user;
    	public void hello(){
    		user = new User();
    		user.setName("Alvis");
    		... ...
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 标量替换

      • 开启标量替换JVM参数:
      • 标量:Java中的基本类型;
      • 聚合量:与标量含义相反,可进一步拆解;
      • 标量替换就是将聚合量拆解为标量存储于方法栈中。
    • 桥接方法
      桥接方法是在JVM加载期间对泛型进行类型擦除后,为指定了泛型类型参数的方法生成了一个中转方法;
      如下代码,在Dog继承了Animal并指定泛型为String,实现其中eat(String s)方法,当JVM加载Dog和Animal时,会将Animal中的泛型进行类型擦除变成Object,那么Animal中的eat方法的参数将会变成Object类型,则JVM会在Dog类中使用桥接方法将该方法实现。

    import java.lang.reflect.Method;
    
    public interface Animal<T> {
    
        public void eat(T s);
    	
    	//类型擦除后的方法
    	//public void eat(Object s);
    
    }
    
    class Dog implements Animal<String>{
    
        @Override
        public void eat(String s) {
            System.out.println("dog eat " + s);
        }
    
    	//类型擦除后实现的方法(桥接方法)
    	//@Override
        //public void eat(Object s) {
        //    eat((String) s);
        //}
    
    	//测试用的main方法
        public static void main(String[] args) {
            Dog dog = new Dog();
            Method[] declaredMethods = dog.getClass().getDeclaredMethods();
            for (Method declaredMethod : declaredMethods) {
                if (declaredMethod.getName().equals("eat")){
                    System.out.print(declaredMethod.getName() + "  ");
                    for (Class<?> parameterType : declaredMethod.getParameterTypes()) {
                        System.out.print(parameterType.getName());
                    }
                    System.out.println("\n=================================");
                }
            }
        }
    }
    //输出结果
    //eat  java.lang.String
    //=================================
    //eat  java.lang.Object
    //=================================
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    3、主要的垃圾回收算法

    • 引用计数法(Reference Counting)
      • 对于一个对象A,只要存在任意一个对象引用了A,则A的引用计数器就会加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。
      • 但引用计数器有两个严重问题:
        • 无法处理循环引用的情况
          在这里插入图片描述

        • 引用计数器要求在每次因引用产生和消除的时候,需要伴随一个加减法操作,对系统性能有一定影响。

      • 因此,JVM的并未选择此算法作为垃圾回收算法
    • 标记清除法(Mark-Sweep)
      • 标记清楚算法是现代垃圾回收算法的思想基础;
      • 分为两个极端:标记阶段和清楚阶段;
      • 标记清除算法产生最大的问题就是清除之后会产生大量的空间碎片
        标记清除算法
    • 复制算法
      • 将原有空间划分为两块。每次只使用其中一块内存,如:A内存GC时将存货的对象复制到B内存中,然后清楚A中的所有对象,并开始使用B内存。
      • 复制算法没有内存碎片,并且若垃圾对象很多,这种算法效率很高。唯一的缺点就是只能使用1/2的内存。
      • 由于在老年代中存活的对象较多,若采用复制算法每次需要复制的对象很多,会导致效率低下。相反,在新生代中,由于90%以上的对象都是“即将被回收”的,所以复制对象会相对较少,所以复制算法通常被运用于新生代中复制算法
    • 标记压缩法
      • 标记压缩算法是一种老年代的回收算法,是对标记清除法的一种改进。
      • 它首先标记存活的对象,然后将所有存活的对象都压缩到内存的一端,然后再清理所有存活对象之外的空间。
      • 该算法不会产生内存碎片,也不用将内存一分为二标记压缩法
    • 分区算法
      • 将堆空间划分为若干个连续的小区间每个区间独立使用、独立回收。由于堆空间较大,针对整个堆空间的GC会比较耗时,通过控制回收小区间的个数,可以大幅度减少GC所带来的停顿。
    • 分代算法
      • 将堆空间划分为新生代老年代,根据其不同特点,执行不同的回收算法,提升回收效率;
        分代算法

    五、JVM中主要的垃圾收集器

    1、串行垃圾回收器(SerialGC)

    • 串行垃圾回收器的特点:
      • 使用单线程进行GC
      • 独占式的GC
    • 串行垃圾回收器是JVM Client模式下默认的垃圾回收器
      串行回收器
      2、并行垃圾回收器(ParNew&ParallelGC&ParallelOldGC)
    • 将串行回收器多线程化。
    • 与串行回收器有相同的回收策略、算法、参数
      并行垃圾回收器
      3、并行垃圾回收器——CMS垃圾回收器
    • CMS垃圾回收器是针对老年代的垃圾回收器
    • CMS垃圾回收器回收步骤
      CMS垃圾回收器回收步骤
    • CMS垃圾回收器回收详解
      • 初始标记,初始标记是为了标记GC Roots能够直接关联到的对象,为了防止在标记过程中出现新的根节点,所以需要“STW”;
      • 并发标记,并发标记是为了标记根节点之后的对象节点,可以与应用程序并发执行;
      • 预清理,计算YoungGC的执行时间,在不进行YoungGC的时候进行预清理
      • 重新标记,重新标记是为了补充标记在并发标记过程中,程序运行产生的新对象节点,所以为了防止还有新节点的产生,一样需要“STW”;
      • 并发清理,并行清理之前步骤标记到的节点对象;
      • 并发重置,重置CMS,为下一次G C做准备。

    4、G1垃圾回收器

    • G1全称为Garbage First Collector
    • G1垃圾回收器是针对整个JVM堆的垃圾回收器,G1回收器将JVM堆划分为多个区域,每个区域都将根据需求成为Eden、Survivor、Old、Humongous,每次收集部分区域来减少GC产生的停顿时间;
    • 对象被创建时,若对象不大于指定阈值(-XX:G1HeapRegionSize)则存放于Eden,若大于指定阈值则存放于Humongous区,进行YoungGC后依旧存活的进入Survivor区,Survivor进行GC后进入Old区;
    • G1垃圾回收器回收分为四个阶段,其中第四阶段不是必须的,所以不做重点;三个阶段循环往复,不断运行:
      G1回收过程
      • 第一阶段,新生代GC(YoungGC)
        • 新生代GC将会将Eden中依然存活的对象和Survivor中依然存活但还不足以进入Old区的对象移动至新的区域,并将该区域作为新的Survivor区;
        • 将Survivor中已经满足进入Old区的对象移动至新的区域,并将该区域作为新的Old区;
        • 清除原来的Eden和Survivor;
      • 第二阶段,并发标记周期
        并发标记周期有六个步骤,如下图所示 并发标记周期
        • 初始标记,标记根节点,为确保标记过程中不受到程序运行的影响,此处会“STW”;
        • 根区域扫描,扫描Survivor区中即将进入Old区的对象并进行标记,此时在该区域是不会进行YoungGC的;
        • 并发标记,扫描根节点之后的各个对象,与应用程序并发执行;
        • 重新标记,标记因程序运行产生的新节点,需要进行“STW”;
        • 独占清理,会计算每一个区域的垃圾比例并按照垃圾比例进行排序;
        • 并发清理,会将每一个垃圾占比为100%的区域清理掉;
      • 第三阶段,混合收集
        • 负责将独占清理过程中计算出的垃圾比例较高的区域中的垃圾对象。
      • 第四阶段,FullGC

    七、参考文章

    深入理解Java虚拟机[第三版]
    类加载器

  • 相关阅读:
    【剑指offer系列】43. 从1到n整数中1出现的次数
    Spring Cloud 之OpenFeign
    vue实战入门后台篇四:springboot+mybatis实现网站后台-增删改查功能实现
    网络穿透:TCP 打洞、UDP 打洞与 UPnP
    Hadoop3教程(三十六):(生产调优篇)企业开发场景中的参数调优案例概述
    【无标题】
    潘多拉 IOT 开发板学习(RT-Thread)—— 实验2 RGB LED 实验(学习笔记)
    Elasticsearch:使用你的 RAG 来进行聊天
    WPF XAML介绍
    【Vue面试题二十八】、vue要做权限管理该怎么做?如果控制到按钮级别的权限怎么做?
  • 原文地址:https://blog.csdn.net/qq_41395106/article/details/126768566