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

  • 扩展类加载器(ext/*.jar包下的类):负责加载Java核心类的扩展类;
  • 应用/系统类加载器:负责将应用程序级别(后端程序员编写的Java’代码)的类加载到JVM中;

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

1.2 双亲委派机制

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

2、类加载流程

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

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

三、JVM垃圾回收算法

1、四种引用级别

  • 强引用
// object对象就是强引用
		Object object = new Object();
  • 软引用
    在切断对该对象的引用时,只有当堆内存不足时才会被回收
		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='张三'}
  • 软引用
    在将对象置为空后,会立即被垃圾回收器回收
		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("对象为空");
        }
        //结果为:对象为空
  • 虚引用
    任何情况下都获取不到该对象

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
  • 槽位复用,默认开启
   /**
    * 槽位复用
    * 对于变量a而言,其作用域仅在代码块之内
    * 离开代码块后,即可判定其不再有用
    * 这种时候,若在代码块外存在另一个变量b,
    * 则变量b会复用变量a的槽位
    */
   public void gc1() {
       {
           int[] a = {1,2,3};
           System.out.println(a);
       }
       int b = 10;
       System.out.println(b);
   }

槽位复用

  • 对象分配
    对象分配的过程中,除了可以在堆上分配,也可以在栈上分配或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");
		... ...
	}
  • 标量替换

    • 开启标量替换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
//=================================

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虚拟机[第三版]
类加载器

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值