1、JVM 简介

JVMJava Virtual Machine 的简称,也即:Java 虚拟机;
虚拟机:指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统;
简单来说就是启动一个Java进程,会创建并启动一个Java虚拟机,并加载class字节码,运行时翻译为机器码,让CPU执行
在这里插入图片描述
JVM产品:官方提供 hotspot

2、JVM 、JDK、JRE 区别

简单说:包含关系
在这里插入图片描述

3、JVM 执行流程

整体看Java代码,从编译到运行时的流程

  • 程序在执行之前先要把java代码转换成字节码(class文件);
  • JVM 首先需要把字节码通过一定的方式—>(类加载器),把文件加载到内存中(运行时数据区);
  • Java虚拟机将字节码翻译成底层系统指令再交由CPU去执行;

如下图所示

在这里插入图片描述

通过 java 类名,启动一个java进程;

4、JVM 运行时数据区

JVM 运行时数据区域也叫内存布局,它由以下 5 大部分组成,如图所示:

在这里插入图片描述
方法区:存放 class 相关代码,静态变量等;

(1)堆区(线程共享)

:创建的对象都保存在堆中,属于线程共享的;
JDK 8 中,堆区中包含了字符串常量池 ;

(2)方法区(线程共享)

方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的,属于线程共享的;
从垃圾回收视角看:

  • JDK 7 中,方法区被称为永久代;
  • JDK 8中,方法区被称为元空间;

从所处空间看:

  • JDK 7 时,方法区是在Java进程的内存中;
    - 在JDK 8 时,称为元空间,属于本地内存(不在Java进程内存中)

运行时常量池,属于方法区的一部分,用来存放字面量与符号引用;

  • 字面量 包含:字符串(string),final 修饰的常量以及基本数据类型的值;
  • 符号引用 包含:类和结构的信息,字段的名称和描述符,方法的名称和描述符;

(3)Java虚拟机栈(线程私有)

Java 虚拟机栈描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息;

Java 虚拟机栈中包含了以下 4 部分
在这里插入图片描述

  1. 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用;局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小;简单来说就是存放方法参数和局部变量;
  2. 操作栈:每个方法会生成一个先进后出的操作栈;
  3. 动态链接:指向运行时常量池的方法引用;
  4. 方法返回地址PC 寄存器的地址;

=:

  • Java虚拟机栈和线程生命周期相同,创建线程,就创建该线程的栈,销毁也是相同;
  • 栈帧和线程执行一个方法的生命周期相同,线程开始调用一个方法,就会创建该方法的栈帧;线程执行完方法返回(正常执行、异常抛出),就会销毁当次执行该方法的栈帧;

(4)本地方法栈(线程私有)

本地方法栈:用来保存本地方法执行的一些数据;

Java虚拟机可能调用系统函数(本地方法),Java程序中,也可能调用native方法(本地方法);
本地方法指的就是系统提供的方法入口,调用执行时,需要一定的内存空间;

(5)程序计数器(线程私有)

程序计数器的作用:用来记录当前线程执行的行号的;
是一块较小的内存空间,看作是当前线程所执行的字节码的行号指示器;

简单小结

线程私有 线程共享
JVM 运行时数据区域 Java虚拟机栈,本地方法栈(方法调用过深就会报(StackOverFlowError),程序计数器 堆区,方法区(空间不足时,就出现 OOM(内存溢出)

内存溢出:某个运行时的数据区域,创建的数据空间不足,就会出现 OOM

OOM 也可能是内存泄露引起的;

内存泄露产生原因:

  • 不用数据堆积过多,可能造成内存泄露;
  • IO 资源没有及时关闭,可能造成内存泄露;

内存泄露,解决方案

(1)从程序本身解决:

  • 可以定时清理一些不用的数据(定时器);
  • 使用弱引用,软引用来保存数据;

(2)加大内存(虽然不用的数据存在,但加大内存还能保存,就不会出现OOM

(3) 定时重启程序(代价小,也很有效)

5、Java 类加载

5.1 类加载过程

一、类加载的时机

  • (1)java 类名Java程序的入口类,需要先执行类加载,然后再执行main()方法;
  • (2)运行时,执行类的静态方法调用,静态变量操作等;
  • (3)new 对象的时候;
  • (4)通过反射创建一个类对象;

类加载只执行一次!(已经执行类加载,方法区就有了类信息,堆中也有了类对象)

对于一个类来说,它的生命周期如下所示:
在这里插入图片描述
二、类加载过程

类加载的过程有

  • 加载
  • 连接(包含验证、准备、解析)
  • 准备

(1)加载

加载阶段:是加载class字节码数据(二进制数据)到方法区,在堆中生成一个class类对象;

(2)验证

验证阶段:验证class字节码数据(二进制数据)是否安全且是否符合Java虚拟机规范;

(3)准备

准备阶段:静态变量设置为初始值(基础数据类型,就设置为对应的值,如int 就为0;对象的初始值就是nullfinal修饰的常量,会设置为真实的值);

(4)解析

解析阶段Java 虚拟机将常量池内的符号引用替换为直接引用,也就是初始化常量的过程;
符号引用:编译的class文件中,需要有变量/引用到值的对应关系,由于此时还没有加载到内存中,因此就使用符号引用来表示这种关系;
直接引用:执行类加载,把class字节码加载到内存后,内存中体现的变量到值的对应关系,称为直接引用;

(5)初始化

静态变量真正的初始化赋值以及静态代码块初始化;

5.2 双亲委派模型

(1)什么是双亲委派模型?

类加载的机制:双亲委派模型 是 JDK 默认的类加载的机制;
执行类加载,是Java虚拟机通过类加载器来实现加载类的;
类加载器有

  • 引导类加载器(BootStrap Class Loader
  • 拓展类加载器(Extension Class Loader
  • 应用类加载器(Application Class Loader
  • 自定义类加载器

类加载是有层级关系的!

双亲委派模型原理不直接执行当前类加载器的类加载代码,而是先查找当前类加载器的父类加载器,父类加载器也是如此,先查找父类加载器,直到找到最顶级的类加载器,然后开始执行类加载

简单而言就是:从下到上查找类加载器,从上到下执行类加载

如下图所示:

在这里插入图片描述

(2)双亲委派模型优点

双亲委派模型优缺点:

优点

  • 避免重复加载类
  • 安全性

缺点

  • 扩展性降低,灵活性低

6、JVM 垃圾回收机制

对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭;并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了;

而Java堆与方法区这两个区域,回收就不具有确定性
Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经死去;

判断对象是否死亡算法如下:

6.1 死亡对象的判断算法

(1)引用计数算法

  1. 引用计数算法原理:

给对象增加一个引用计数器,当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已经死亡;

  1. 引用计数算法特点:

优点:实现简单,判定效率也比较高;
缺陷:无法解决循环引用问题(Java中没有采用,Python中采用)

(2)可达性分析算法

Java没有采用引用计数法来判断对象是否已"死",而采用"可达性分析"来判断对象是否存活(C#也是采用相同算法来判断);

  1. 可达性分析算法原理

通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象 不可达)时,证明此对象是不可用的;

在这里插入图片描述
对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象

引用的功能:

  • 查找对象
  • 判断对象是否死亡

所以Java 对引用的概念做了扩充,将引用分为:

  • 强引用(Strong Reference):new对象的都是强引用
  • 软引用(Soft Reference):OOM发生时回收
  • 弱引用(Weak Reference):下一次垃圾回收时回收
  • 虚引用(Phantom Reference):垃圾回收发起一个通知

这四种引用的强度依次递减;

整体看垃圾回收:
jvm启动就会创建一些垃圾回收线程,执行垃圾回收工作;使用工具(垃圾回收器),按照一定的方式(垃圾回收算法)来清理垃圾;

6.2 垃圾回收算法

(1)标记-清除算法

"标记-清除"算法是最基础的收集算法;
算法分为"标记"和"清除"两个阶段 :

  • 首先标记出所有需要回收的对象;
  • 标记完成后统一回收所有被标记的对象;

标记-清除 算法缺点:

  • 效率低
  • 内存碎片:存放对象时,即使可用空间足够,但连续空间不足,也会触发另一次GC(垃圾收集)

(2)复制算法

复制"算法是为了解决"标记-清理"的效率问题;
原理:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉;

复制算法优缺点:

  • 优点:算法高效,不会出现内存碎片
  • 缺点:内存利用率低(50%

(3)标记-整理算法

类似于标记清除算法,采取的方案,将存活对象移动到连续的空间,再清空剩余空间;
特点:不会出现内存碎片;

(4)分代收集算法

当前 JVM 垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块;

1. 划分:一般是把Java堆分为新生代和老年代:

  • 新生代

新生代内存分为:一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间;
新生代中98%的对象都是"朝生夕死"的,即很快的创建,又很快的变为不可用的垃圾;
新生代采取的算法:复制算法
默认划分为:E:S:S=8:1:1,空间利用率就是90%
默认每次采取E区和一块S区保存对象,另一块S区留空;GC(垃圾回收)时,将存活对象复制到另一块留空的S区,当存活对象需要的空间,留空S区不够时,就会由老年代进行担保(即就是放入老年代中);

  • 老年代

对象可能长期存活;
老年代采取的算法:标记清除,标记整理算法

2. 对象什么时候进入新生代/老年代?

(1)新创建的对象默认放入新生代;
(2)大对象进入老年代;
(3)新生代GC(垃圾回收)时,存活对象在S区放不下的,也会进入老年代;
(4)新生代的对象,每经历一次GC(垃圾回收),年纪+1,直到年纪>15,就会进入老年代;

3. 新生代GC(Minor GC)与老年代GC(Major GC)/(Full GC)对比

新生代GC :对用户所写程序影响比较小,回收速度快;垃圾回收是在垃圾回收线程中并发执行,可能导致用户线程暂停(也称为Stop the World ,简称:STW);

老年代GC:对程序影响大,回收速度慢;
老年代GC的速度一般会比新生代GC10倍以上!

6.3 垃圾收集器

  • 为什么有这么多垃圾收集器?

原因:主要是针对不同的区域,有不同的特性,需要不同的垃圾收集器;

  • 2、垃圾收集器的作用

垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间;

  • 常见垃圾收集器
    在这里插入图片描述
    之间的连线,代表他们之间可以搭配使用!

首先明确三个概念

  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程序继续运行,而垃圾收集程序在另外一个CPU上;
  • 并行(Paralle):指多条垃圾收集线程并行工作,用户线程仍处于等待状态;
  • 吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间(运行用户代码的时间+垃圾收集时间)的比值;

对于Java程序来说,大体上分为两点

  • (1)用户体验好(实时性要求高):用户要使用的程序或系统,每次/单次的 STW(用户线程暂停)时间少,总的STW时间多;
  • (2)吞吐量优先(批任务处理):用户不使用的,只执行任务的;总的STW时间少;每次/单次的 STW(用户线程暂停)时间多;

(1) Serial 收集器

Serial 收集器:为新生代收集器,串行GC,单线程的收集器;
优点:简单而高效;

(2)ParNew 收集器

ParNew 收集器:新生代收集器,并行GC

(3)Parallel Scavenge 收集器

Parallel Scavenge 收集器:新生代收集器,并行GC,使用复制算法的收集器;

(4) Parallel Old 收集器

Parallel Old 收集器:老年代收集器,采用标记整理算法;

(5) Serial Old 收集器

Serial Old 收集器:老年代收集器,串行GC,使用标记-整理算法;

(6) CMS 收集器

CMS (Concurrent Mark Sweep)收集器:老年代收集器;
特性:
(1)用户体验优先(并发收集、低停顿);
(2)采取标记-清除算法(该算法存在两个缺陷:效率不高,内存碎片,实际上jvm已经进行了优化)

CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤:

  1. 初始标记(CMS initial mark

初始标记阶段:标记GC Roots能直接关联到的对象,速度很快,需要用户线程暂停(Stop The World);

  1. 并发标记(CMS concurrent mark

并发标记阶段:进行GC Roots 引用链搜索

  1. 重新标记(CMS remark

重新标记阶段:是为了修正并发标记期间因用户程序在这里插入代码片继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要用户线程暂停(Stop The World)

  1. 并发清除(CMS concurrent sweep

并发清除阶段:清除对象

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的;

  • 优点:并发收集、低停顿
  • 缺陷:

(1)CMS收集器对CPU资源非常敏感;
(2)CMS收集器无法处理浮动垃圾;(由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉,这一部分垃圾就称为“浮动垃圾”)
(3)CMS收集器会产生大量内存空间碎片

(7) G1 收集器

G1 收集器:全堆收集器,用户体验优先;
内存划分:把堆划分为相等的很多个区域,每个区域根据需要设置为E(Eden), S(Survivor),T(Tenured老年区)
整体基于标记-整理算法,局部基于复制算法

同CMS一样,也经过4个阶段

  1. 初始标记(Initial Mark)阶段

该阶段同CMS垃圾收集器的初始标记阶段一样,G1也需要暂停应用程序的执行,G1的垃圾收集器的初始标记阶段是跟新生代 gc一同发生的,也就是说,在G1中,你不用像在CMS那样,单独暂停应用程序的执行来运行初始标记阶段,而是在G1触发新生代 gc的时候一并将年老代上的初始标记给做了;

  1. 并发标记(Concurrent Mark)阶段

该阶段G1做的事情跟CMS一样,但G1同时还多做了
一件事情,就是如果在并发标记阶段中,发现哪些Tenured region中对象的存活率很小或者基本没有对象存活,那么G1就会在这个阶段将其回收掉,而不用等到后面的筛选回收阶段;

  1. 最终标记(CMS中的Remark)阶段

该阶段G1做的事情跟CMS一样, 但是采用的算法不同,G1采用一种叫做SATB(snapshot-at-the-begining)的算法能够在Remark阶段更快的标记可达对象;

  1. 筛选回收(Clean up/Copy)阶段

该阶段中,G1会挑选出那些对象存活率低的空白内存区域进行回收,这个阶段也是和新生代gc一同发生的;

针对于小对象,会经过这样一生:

在这里插入图片描述

7、JMM —Java内存模型

内存模型作用:不同的硬件及操作系统,对内存的访问操作也不同;Java采取统一的Java内存模型来屏蔽差异;

如何划分

  • 主内存
  • 工作内存

(1)主内存与工作内存

主内存:线程共享,Java进程的内存

工作内存:线程私有的,CPU执行线程指令时,使用寄存器来保存上下文;

(2)内存间交互操作

Java内存模型中定义了如下8种操作来完成:

  1. lock(锁定) : 作用于主内存的变量,它把一个变量标识为一条线程独占的状态;
  2. unlock(解锁) : 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
  3. read(读取) : 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
  4. load(载入) : 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;
  5. use(使用) : 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎;
  6. assign(赋值) : 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量;
  7. store(存储) : 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便后续的write操作使用;
  8. write(写入) : 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;

Java内存模型的三大特性 :

  • 原子性:不可分割
  • 可见性:
  • 有序性:

Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则Thread对象的start()方法先行发生于此线程的每个一个动作;
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

(3)volatile型变量的特殊规则

  • volatile型变量:不保证原子性,保证有序性和可见性(操作本身具有原子性);
  • 使用volatile变量的语义是禁止指令重排序;