JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器、Java内存模型

目录

一、JVM简介

二、运行时数据区

 1、堆(heap)

2、方法区

3、Java虚拟机栈

4、本地方法栈

5、程序计数器

6、内存布局中的异常问题

三、类加载

1、类加载过程

(1)加载

(2)验证

(3)准备

(4)解析

(5)初始化

2、双亲委派模型

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

(2)双亲委派模型的优点

(3)破坏双亲委派模型

四、垃圾回收(GC)

1、死亡对象判断算法

(1)引用计数算法

(2)可达性分析算法

2、垃圾回收算法

(1)标记清除算法(老年代回收算法)

(2)复制算法(新生代算法)

(3)标记整理算法(老年代算法)

(4)分代算法

3、垃圾收集器

(1)CMS收集器(Concurr Mark  Sweep)

(2)G1收集器(唯一一个全区域的垃圾回收器)

五、Java内存模型

1、主内存和工作内存

2、内存间交互操作

(1)八大原子性的字节码指令

(2)Java内存模型的三大特性


一、JVM简介

JVM意为Java虚拟机。虚拟机是指通过软件模拟的具有完整的硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。

常见的虚拟机:JVM、WMware、Virtual Box。

JVM和其他两个虚拟机的区别:

  • VMware等是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
  • JVM则是通过软件模拟Java字节码的指令集,JVM只是保留了PC寄存器,其他的寄存器都进行了裁剪。

JDK、JRE、JVM的关联关系:

JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器、Java内存模型_第1张图片

JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器、Java内存模型_第2张图片 JDK和JRE的目录下都存在java.exe,通过其来运行class字节码文件,启动的时候就会创建一个JVM。JDK默认的JVM是HotSpot VM。

二、运行时数据区

JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器、Java内存模型_第3张图片

 1、堆(heap)

程序中所有创建的对象(数组也是对象)都存放在堆中

  • 对象指的是等号右边的;
  • 变量指的是等号左边的。

注:jdk1.8,常量池(字符串常量池)也在堆中。

JVM参数设置:

  • -Xms最小堆内存;
  • -Xmx最大堆内存

堆里面分为两个区域(逻辑上):新生代老生代。新生代放新建的对象,经过一定GC次数之后仍然存活的对象以及创建的大对象会放入老生代。

JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器、Java内存模型_第4张图片

 垃圾回收的时候会将Eden中存活的对象放在一个未使用的Survivor中,并把当前正在使用的Eden和Survivor清理掉。

2、方法区

用来存放被虚拟机加载的类信息(class字节码,进行类加载,把类加载到方法区,保存类的代码数据,同时堆中会生成一个类对象)、常量、静态变量、即时编译器编译后的代码等

  • 永久代(JDK1.7):属于Java内存进程;
  • 元空间(JDK1.8):内存属于本地内存,不再受JVM最大内存的影响,与本地内存的大小有关。
  • javac:编译器(编译Java代码为字节码文件);
  • java:包含解释器(Java程序运行的时候,把class字节码文件翻译为机器码);
  • 即时编译器:运行时,将热点代码翻译为机器码,之后就不用再进行翻译。

3、Java虚拟机栈

  • Java虚拟机栈的生命周期和线程相同,
  • 每个方法执行的时候都会创建一个栈帧,进行入栈操作,方法返回时,出栈;包含局部变量表、操作栈、动态链表、方法返回地址。

JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器、Java内存模型_第5张图片

 Java虚拟机栈:

  • 局部变量表:存放八大基本数据类型、变量(等号左边)。局部变量表所需的空间在编译时分配。进入一个方法时,方法在帧内分配的局部变量空间是确定的,执行期间局部变量表的大小不会发生改变。
  • 操作栈:每个方法会生成一个先进后出的操作栈;
  • 动态链表:指向运行时常量池的方法引用;
  • 方法返回地址:PC寄存器的地址。

4、本地方法栈

调用Java方法,就创建栈帧,放在线程的Java虚拟机栈中;如果调用其他的函数,就是用本地方法栈。

5、程序计数器

记录当前线程执行的行号。

6、内存布局中的异常问题

  1. 内存溢出(OOM):指存放数据的大小超出该区域的内存大小。运行时数据区域中,除了程序计数器,都可能发生内存溢出。内存溢出会导致整个进程都挂掉;
  2. 内存泄漏:线程生命周期太长,始终引用一些不使用的数据(没法进行gc垃圾回收),随着使用时间越来越长,不适用的垃圾就越来越多,可用空间越来越少——可能导致OOM。(解决方法:直接重启)
  3. StackOverflow:如果栈中的栈帧数量超过jvm规定,就会出现该异常。(递归使用不恰当)

三、类加载

1、类加载过程

(1)加载

加载class字节码到Java进程的内存中,在堆中创建类对象。

(2)验证

验证class字节码的规范(是否符合jvm规范)。

(3)准备

正式为类中的变量(静态变量)分配内存并对类变量进行初始化:

  • static常量:赋值为初始值;
  • static变量:赋值为缺省值;(Integer缺省值为null,int为0)

(4)解析

把常量池中的符号引用替换为直接引用:

  • 符号引用:class文件(字节码)private static int x=123;此时进程还没有启动,无法表示变量x指向123(本质是指向内存地址),此时使用符号引用来表示这个指向关系;
  • 直接引用:进程运行起来,x直接指向123(内存地址);
  • 替换:把class文件常量池中的符号引用,替换为进程运行起来的运行时常量池中的直接引用。

(5)初始化

是类的初始化,不是对象初始化。初始化阶段,Java虚拟机真正开始执行类中编写的Java程序代码。初始化阶段就是执行类构造器方法的过程。

类对象的初始化:执行静态变量和静态代码块。

2、双亲委派模型

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

类加载的机制:双亲委派模型(jdk默认的类加载机制),其它机制(破坏双亲委派模型的其它机制)。

JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器、Java内存模型_第6张图片

 类加载器:包含四种,从上而下(上下关系,不是以extends继承来实现的,是逻辑上的上下关系):

  • BootStrap ClassLoader   启动类加载器(主要负责加载Java核心类库,即%JRE_HOME%\lib目录)
  • ExtClassLoader   扩展类加载器(主要负责加载目录%JRE_HOME%\lib\ext目录下的类)
  • AppClassLoader  系统/应用类加载器(加载当前应用的classpath目录下的类)
  • 自定义加载器

JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器、Java内存模型_第7张图片

启动/扩展/应用这三类类加载器,只是加载jar目录下不同的jar包。

什么是双亲委派模型?

基于四种类加载器,按照从上到下的顺序,来加载类。类加载器收到类加载请求时,不会立即去自己加载,而是将这个请求向上传递,每一层都是如此,因此所有的加载请求最终应该在最顶层的启动类加载器中。因为类加载只执行一次,所以,上边找到,下边就不执行加载;上边没有找到,就交给下一级的加载。

(2)双亲委派模型的优点

  • 避免重复加载类:如A类和B类都有一个父类C,当A启动起来的时候就会加载C,则B类进行加载时就不需要重复加载C类。
  • 安全性:确保优先采用启动/扩展/应用类加载器来加载类。
    如果自定义一个类加载,加载自定义的java.long.Object类:
    【1】遵循双亲委派模型,就不会加载到自定义的Object,还是jre中的(安全);
    【2】不遵循双亲委派模型,就可以加载到自定义的Object类(不安全)。

【注】在jdk中,自定义类加载器时,进行了安全校验:加载类,如果类的全限定名以java./javax.开头的包,报错。

(3)破坏双亲委派模型

遵循双亲委派机制的类加载,某些场景下,可能无法实现知道需要加载的类名(jdbc中,jdk是无法知道数据库驱动类的类名)。

解决方案:破坏双亲委派机制。

常用的方案:SPI机制(jdk提供的一个ServiceLoader.load,约定了从jar包下/META-INF/services/文件名  中进行加载)

四、垃圾回收(GC)

  • Java语言,不用自己分配内存,也不用自己回收内存(jvm中,实现了垃圾回收机制,自动回收)。
  • 垃圾回收区域:堆(回收的主要区域)、方法区(保存类信息,静态变量,很少回收);

1、死亡对象判断算法

(1)引用计数算法

给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器-1;任何时刻计数器为零的对象不能再被使用,即对象已死。但jvm中没有选用引用计数器来管理内存:引用计数器无法解决对象的循环引用问题如下:

//此时instance的引用计数器会陷入死循环
Test test1 = new Test();
Test test2 = new Test();
test1.instance = test2;
test2.instance = test1;

(2)可达性分析算法

通过一系列称为“GC Roots”的对象作为起始点,从这些节点往下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots没有引用链相连时,证明是“不可达的”

JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器、Java内存模型_第8张图片

 引用的分类(强度依次递减):

  • 强引用:只要引用还在,就不会被回收;
  • 软引用(SoftReference):系统要发生内存溢出之前,会对其及逆行回收;
  • 弱引用:每次GC发生时都会进行回收;
  • 虚引用:无法使用,只是在对象被回收时发起一个系统通知。

2、垃圾回收算法

(1)标记清除算法(老年代回收算法)

算法分为两个阶段:

  • 标记:标记出所有需要回收的对象;
  • 清除:标记完成后统一回收所有被标记的对象。

缺陷:

  • 效率低:标记和清除过程效率都不高;
  • 清除后产生大量不连续的内存碎片,导致在之后的运行中需要分配较大对象时,无法找到足够的连续内存不得不提前进行下一次垃圾回收。

    JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器、Java内存模型_第9张图片

(2)复制算法(新生代算法)

复制算法解决了”标记-清理“的效率问题。将可用的内存按容量分为大小相等的两块,每次只使用一块。当内存需要进行垃圾回收时,会将该区域存活的对象复制到另一块上面,然后将已经使用过的内存区域清理掉。

使用场景:大多数对象具有朝生夕死的特性。新生代的对象符合该特性,使用复制算法。

缺陷:空间利用率不高,只有50%。

JVM中,新生代的回收算法,是复制算法的优化版本:

  1. 新生代中98%以上的对象都是“朝生夕死”的,所以不需要按照1:1来划分内存空间,而是将内存划分为一块较大的Eden区和两块较小的Survivor区(8:1:1);
  2. 每次使用Eden和一块Survivor。回收时,将Eden和Survivor中存活的对象一次性复制到另一块Survivor空间,最后清理掉Eden和使用的Survivor空间。
  3. 部分对象会在两个Survivor区域来回复制,如此交换15次(默认15)之后如果还存活,会vu你放到老年代。

(3)标记整理算法(老年代算法)

复制收集算法在对象存活率较高时会及逆行大量的赋值操作,效率比较低。因此来年代一般不使用。标记整理算法过程和标记清除算法一致,不同的是,标记完后,不直接进行清除,而是将存活的对象移动到一端,最后清除掉端边界以外的内存。

JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器、Java内存模型_第10张图片

(4)分代算法

分代算法(没有具体的算法实现)是通过区域划分,实现不同的区域不同的垃圾回收策略,从而更好地实现垃圾回收。JVM垃圾收集都采用的是“分代收集”算法,只是根据对象的存活周期的不同将内存分为几块。一般是分为新生代和老年代。新生代中,每次垃圾回收只有少量的对象存活,使用复制算法;老年代中,对象存活率高,使用标记清除算法或标记整理算法。

哪些对象进入新生代?哪些对象进入老年代?

  • 默认创建的对象(非大对象)都进入新生代;
  • 老年代:
    【1】大对象:对象占据的空间超出jvm规定的阈值;
    【2】新生代中年龄超过15的对象。
    【3】新生代GC时,分配担保失败的对象:新生代GC时把Eden区域和s0中的存活对象复制到s1区域中。(新生代对象的特点是朝生夕死,大多数情况下,存活的对象不足10%,但是也有可能s1区域中放不下,此时就放入老年代中。

什么时候发生垃圾回收?

对象进入哪个区域,如果该区域空间不足,就会触发该区域的GC。

Minor GC和Major GC有什么区别?

  • 新生代GC/Minor GC:采用复制算法,效率比较高;
  • 老年代GC/Major GC:采用标记清除算法/标记整理算法,效率比较差,一般比新生代GC慢十倍以上。

3、垃圾收集器

理解以下几个概念:

  • 并行:指多条垃圾收集线程并行工作,用户线程仍处于等待状态(暂停SWT【Stop the World】);
  • 并发:用户线程和垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程序继续运行,二垃圾收集程序在另外一个CPU上;
  • 吞吐量:CPU用于运行用户代码的时间和CPU总消耗时间的比值;(是一个程序的性能指标)
  • 用户体验:stw单次停顿越长,用户体验就越差,此时用户体验就不好;单词stw停顿时间短一点,整个stw的时间可以长一点(用户体验更好,但吞吐量小(性能差一点))
    适用场景:和用户打交道的程序,比如我们的一个Java网站后端。

jvm中经典的垃圾收集器:

JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器、Java内存模型_第11张图片

  •  Serial收集器:新生代收集器(复制算法);单线程(单个垃圾回收线程)的方案=>目前大多数电脑都是支持多线程,所以这个收集器效率不高;
  • ParNew收集器:新生代收集器;搭配CMS(老年代收集器)的方案;
  • Parallel Scanvenge收集器:新生代收集器;吞吐量优先=>适应性能优先的程序;搭配Parallel Old(老年代收集器,也是吞吐量优先)
  • Serial Old收集器:老年代收集器(标记整理算法);单线程;
  • Parallel Old收集器:老年代收集器(标记整理算法);吞吐量优先。

(1)CMS收集器(Concurr Mark  Sweep)

  • 老年代收集器
  • 标记清除算法
  • 用户体验优先=>整体看是并发(垃圾回收线程和用户线程同时执行)的过程,有局部的stw(少许时间暂停用户线程)
  • CMS搭配新生代的ParNew收集器

四个步骤:

  • 初始标记:标记GC Roots能直接关联到的对象,需要SWT;
  • 并发标记:进行GC Roots引用链追踪的过程(搜索引用路径);
  • 重新标记:修复并发标记阶段用户线程同时执行产生标记变动的记录,需要SWT;(这个阶段停顿时间比初始标记阶段稍长,但远比并发标记时间短)
  • 并发清除:并发清除垃圾。

优缺点:

【1】优点:并发收集、低停顿;

【2】缺点:

  • CPU比较敏感:用户体验优先,意味着吞吐量低(单次停顿时间短,整体停顿时间长)=>CPU利用率下降;
  • 无法处理浮动垃圾(浮动垃圾:并发清除时用户线程执行产生的垃圾):
    (1)需要预留一部分空间:用来保存并发清除阶段用户线程创建的对象;
    (2)并发模式失败(Concurrent  MOdel Failure):并发清除阶段用户线程创建的对象超出预留的空间大小=>再次触发另一次老年代GC(老年代GC比较耗时),采用后备方案Serial Old方案进行回收
  • 内存碎片问题:标记清除算法会带来这样的问题,大对象时,内存足够,但连续内存不足,提前触发垃圾回收。

(2)G1收集器(唯一一个全区域的垃圾回收器)

其内存划分方式不再时新生代和老年代,而是把堆划分为多个大小相同的region区,动态分配为E区、S区、T区(老年代)以及H区(用于存储大对象,大小超过region大一的一半的对象)。

JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器、Java内存模型_第12张图片

  •  老年代收集器;
  • 全堆收集器(most garbage优先回收):整体基于“标记整理算法”,局部基于“复制算法”;
  • 用户体验优先。

回收方式:

(1)新生代垃圾收集:使用复制算法,把多个E区和S区存活的对象复制到空的region区在动态指定为S区)。

(2)老年代垃圾收集:分为四个阶段

  • 初始阶段:和CMS类似(标记GC Roots关联的对象,SWT),不同的是,可以和新生代1GC同时执行;
  • 并发标记:和CMS类似,同时如果发现那些Tenured(老年代)region中对象的存活率很小或者基本没有对象存活,会在次将其回收,不用等到最后阶段;
  • 最终标记:和CMS的重新标记类似;
  • 筛选回收:挑选对象存活率低的region进行回收,也是和新生代回收同时进行。

五、Java内存模型

Java内存模型(java Memory Model):用来屏蔽各种硬件和操作系统的内存访问差异,实现Java程序在各种平台下都能达到一致的内存访问效果。

1、主内存和工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量的底层细节。此处变量包括(实例字段、静态字段和构成数组对象的元素),但不包括局部变量和方法参数(是线程私有的,不会被线程共享)。

线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接对鞋主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的值的传递均需要通过主内存来完成的。

JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器、Java内存模型_第13张图片

  • 主存:存放线程共享的数据;
  • 工作内存:存放线程私有的,及共享变量的拷贝。

2、内存间交互操作

(1)八大原子性的字节码指令

用于主存和工作内存数据的读取操作:

  • lock(锁定):作用于主内存变量,把一个变量标识为一条线程独占的状态;
  • unlock(解锁):作用于主存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
  • read(读取):作用于主存的变量,将变量从“主存”读取到“中间内存”;
  • load(载入):作用于工作内存的变量,变量从“中间内存”读取到“工作内存”;
  • use(使用):作用于工作内存的变量,使用变量参与运算;
  • assign(赋值):作用于工作内存的变量,把值赋给变量;
  • store(存储):作用于工作内存的变量,变量从“工作内存”写入“中间内存”;
  • write(写入):作用于主内存的变量,变量从“中间内存”写入“主存”。

(2)Java内存模型的三大特性

  • 原子性:Java内存模型的八大操作都是原子性的,即一个操作在执行过程中不会被任何因素打断;
  • 可见性:指如果一个线程修改了共享变量的值,其他线程能够立即得知。(volatile、synchronized、final可以实现可见性)
  • 有序性:在本线程内,观察自己线程,所有操作都是有序的;观察别的线程,所有的操作都是无序的。(线程内表现为串行,后半句指“指令重排序”和“工作内存与主内存同步延迟”现象)。

你可能感兴趣的:(javaee,大数据,java-ee)