深入理解JVM【内存结构-垃圾回收-类加载&字节码技术-内存模型】

资料下载
内容参考

一、什么是 JVM ?

JVM(Java Virtual Machine)其实就类似于一台小电脑运行在windows或者linux这些操作系统环境下。它直接和操作系统进行交互,与硬件不直接交互,然后操作系统可以帮我们完成和硬件进行交互的工作。

JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。所以,JAVA虚拟机JVM是属于JRE的,而现在我们安装JDK时也附带安装了JRE(当然也可以单独安装JRE)。

JVM的用处比如:自动装箱、自动拆箱是怎么实现的,反射是怎么实现的,垃圾回收机制是怎么回事…
一次编译,处处执行 ,自动的内存管理,垃圾回收机制 , 数组下标越界检查…

Java程序具有跨平台特性主要是指字节码文件可以在任何具有Java虚拟机的计算机或者电子设备上运行,Java虚拟机中的Java解释器负责将字节码文件解释成为特定的机器码进行运行。因此在运行时,Java源程序需要通过编译器编译成为.class文件。众所周知java.exe是java class文件的执行程序,但实际上java.exe程序只是一个执行的外壳,它会装载jvm.dll(windows下,下皆以windows平台为例,linux下和solaris下其实类似,为:libjvm.so),这个动态连接库才是java虚拟机的实际操作处理所在。

下面我们一起走入JVM的世界!

Chapter 2. The Structure of the Java Virtual Machine (oracle.com)

1、HotSpot介绍

HotSpot Virtual Machine Garbage Collection Tuning Guide

Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide,(oracle.com)

**HotSpot 的正式发布名称为" Java HotSpot Performance Engine ",**是 Java虚拟机 的一个实现,包含了服务器版和桌面应用程序版,现时由 Oracle 维护并发布。它利用 JIT 及自适应优化技术(自动查找性能热点并进行动态优化,这也是HotSpot名字的由来)来提高性能。

HotSpot VM,它是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。 其最初并非由Sun公司开发,而是由一家名为“Longview Technologies”的小公司设计的; 甚至这个虚拟机最初并非是为Java语言而开发的,它来源于Strongtalk VM, 而这款虚拟机中相当多的技术又是来源于一款支持Self语言实现“达到C语言50%以上的执行效率”的目标而设计的虚拟机, Sun公司注意到了这款虚拟机在JIT编译上有许多优秀的理念和实际效果,在1997年收购了Longview Technologies公司,从而获得了HotSpot VM。HotSpot VM既继承了Sun之前两款商用虚拟机的优点(如前面提到的准确式内存管理),也有许多自己新的技术优势, 如它名称中的HotSpot指的就是它的热点代码探测技术(其实两个VM基本上是同时期的独立产品,HotSpot还稍早一些,HotSpot一开始就是准确式GC, 而Exact VM之中也有与HotSpot几乎一样的热点探测。 为了Exact VM和HotSpot VM哪个成为Sun主要支持的VM产品,在Sun公司内部还有过争论,HotSpot打败Exact并不能算技术上的胜利)。

HotSpot VM的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知JIT编译器以方法为单位进行编译。 如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准编译和OSR(栈上替换)编译动作。通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,而且无须等待本地代码输出才能执行程序, 即时编译的时间压力也相对减小,这样有助于引入更多的代码优化技术,输出质量更高的本地代码。

在2008年和2009年,Oracle公司分别收购了BEA公司和Sun公司,这样Oracle就同时拥有了两款优秀的Java虚拟机:JRockit VM和HotSpot VM。 Oracle公司宣布(大约应在发布JDK 8的时候)会完成这两款虚拟机的整合工作,使之优势互补。 整合的方式大致上是在HotSpot的基础上,移植JRockit的优秀特性,譬如使用JRockit的垃圾回收器与MissionControl服务, 使用HotSpot的JIT编译器与混合的运行时系统。

2、HosSpot中的概念

2.1解释执行与 JIT

**解释器:**Java 程序在运行的时候,主要就是执行字节码指令,一般这些指令会按照顺序解释执行,这种就是解释执行,解释执行的方式是非常低效的,它需要把字节码先翻译成机器码,才能往下执行。

编译器:字节码是 Java 编译器做的一次初级优化,许多代码可以满足语法分析,其实还有很大的优化空间。
所以,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。完成这个任务的编译器,就称为
即时编译器(Just In Time Compiler),简称 JIT 编译器。

  • **动态编译(dynamic compilation)**指的是“在运行时进行编译”;与之相对的是事前编译(ahead-of-time compilation,简称AOT),也叫*静态编译(static compilation)。

  • JIT编译(just-in-time compilation)**狭义来说是当某段代码即将第一次被执行时进行编译,因而叫“即时编译”。JIT编译是动态编译的一种特例。JIT编译一词后来被泛化,时常与动态编译等价;但要注意广义与狭义的JIT编译所指的区别。

  • **自适应动态编译(adaptive dynamic compilation)**也是一种动态编译,但它通常执行的时机比JIT编译迟,先让程序“以某种式”先运行起来,收集一些信息之后再做动态编译。这样的编译可以更加优化。

2.2热点代码

热点代码,就是那些被频繁调用的代码,比如调用次数很高或者在 for 循环里的那些代码。这些再次编译后的机器码会被缓存起来,以备下次使用,但对于那些执行次数很少的代码来说,JIT这种编译动作就纯属浪费。
JVM 提供了一个参数“-XX:ReservedCodeCacheSize”,用来限制 CodeCache 的大小。也就是说,JIT 编译后的代码都会放在 CodeCache 里。
如果这个空间不足,JIT 就无法继续编译,编译执行会变成解释执行,性能会降低一个数量级。同时,JIT 编译器会一直尝试去优化代码,从而造成了 CPU占用上升。

2.3热点探测

**在 HotSpot 虚拟机中的热点探测是 JIT 优化的条件,热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法” 。**虚拟机为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,会触发 JIT 编译。

  • 方法调用计数器
    用于统计方法被调用的次数,方法调用计数器的默认阈值在 C1 模式下是 1500 次,在 C2 模式下是 10000 次,可通过 -XX: CompileThreshold 来设定;
    而在分层编译的情况下,-XX: CompileThreshold 指定的阈值将失效,此时将会根据当前待编译的方法数以及编译线程数来动态调整。当方法计数器和回边计数器之和超过方法计数器阈值时,就会触发 JIT 编译器。

  • 回边计数器
    用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”

    (Back Edge),**该值用于计算是否触发 C1 编译的阈值,在不开启分层编译的情况下,C1 默认为 13995,**C2 默认为 10700,可通过 -XX: OnStackReplacePercentage=N 来设置;而在分层编译的情况下,

    -XX:OnStackReplacePercentage 指定的阈值同样会失效,此时将根据当前待编译的方法数以及编译线程数来动态调整。**建立回边计数器的主要目的是为了触发 OSR(On StackReplacement)编译,即栈上编译。**在一些循环周期比较长的代码段中,当循环达到回边计数器阈值时,JVM 会认为这段是热点代码,JIT 编译器就会将这段代码编译成机器语言并缓存,在该循环时间段内,会直接将执行代码替换,执行缓存的机器语言。

3、JVM、JRE、JDK 的关系

在这里插入图片描述

4、常见的 JVM

我们主要用的是 HotSpot 版本的虚拟机。

img

5、JAVA运行时环境逻辑图

img

在这里插入图片描述

6、JVM运行原理

  • **ClassLoader:**Java 代码编译成二进制后,会经过类加载器,这样才能加载到 JVM 中运行。
  • **Method Area:**类是放在方法区中。
  • **Heap:**类的实例对象。
  • 当类调用方法时,会用到 JVM Stack、PC Register、本地方法栈。
  • 方法执行时的每行代码是有执行引擎中的解释器逐行执行,
  • 方法中的热点代码频繁调用的方法,由 JIT 编译器优化后执行,
  • GC 会对堆中不用的对象进行回收。
  • 需要和操作系统打交道就需要使用到本地方法接口。

img

img

7、关于JVM的几个问题

7.1几个数据结构的概念

内存空间大致可以用下图表示:

函数在调用的时候都是在栈空间上开辟一段空间以供函数使用,所以下面来详细谈一谈函数的栈帧结构。如图示,栈是由高地址向地地址的方向生长的,而且栈有其栈顶和栈底,在x86系统的CPU中,寄存器ebp保存的是栈底地址,称为帧指针,寄存器esp保存的是栈顶地址,称为栈指针。而且还应该明确一点,栈指针和帧指针一次只能存储一个地址,所以,任何时候,这一对指针指向的是同一个函数的栈帧结构。并且ebp一般由系统改变它的值,而esp会随着数据的入栈和出栈而移动,也就是说esp始终指向栈顶。

内存空间:

【1】堆

堆: 堆是一种常用的树形结构,是一种特殊的完全二叉树,当且仅当满足所有节点的值总是不大于或不小于其父节点的值的完全二叉树被称之为堆。

  • **堆的这一特性称之为堆序性。**因此,在一个堆中,根节点是最大(或最小)节点。如果根节点最小,称之为小顶堆(或小根堆),如果根节点最大,称之为大顶堆(或大根堆)。堆的左右孩子没有大小的顺序。
  • 堆的存储一般都用数组来存储堆,第0个结点左右子结点下标分别为1和2。

这里写图片描述

【2】栈

理解栈帧和栈的运行原理_

栈: 栈是一种运算受限的线性表,FILO先进后出的数据结构。

  • 其限制是指只仅允许在表的一端进行插入和删除操作,这一端被称为栈顶(Top),相对地,把另一端称为栈底(Bottom)。把新元素放到栈顶元素的上面,使之成为新的栈顶元素称作进栈、入栈或压栈(Push);把栈顶元素删除,使其相邻的元素成为新的栈顶元素称作出栈或退栈(Pop)。这种受限的运算使栈拥有“先进后出”的特性(First In Last Out),简称FILO。

  • 栈分顺序栈和链式栈两种。栈是一种线性结构,**所以可以使用数组或链表(单向链表、双向链表或循环链表)作为底层数据结构。使用数组实现的栈叫做顺序栈,使用链表实现的栈叫做链式栈,**二者的区别是顺序栈中的元素地址连续,链式栈中的元素地址不连续。

【3】栈帧

JVM虚拟机-虚拟机栈帧栈结构

**栈帧: 栈帧是指为一个函数调用单独分配的那部分栈空间。**也叫过程[活动记录],是编译器用来实现过程[函数调用]的一种[数据结构]。

  • 运行的程序从当前函数调用另外一个函数时,就会为下一个函数建立一个新的栈帧,并且进入这个栈帧,这个栈帧称为当前帧。而原来的函数也有一个对应的栈帧,被称为调用帧。每一个栈帧里面都会存入当前函数的局部变量
  • 当函数被调用时,就会被加入到调用栈顶部,执行结束之后,就会从调用栈顶部移除该函数。并将程序运行权利(帧指针)交给此时栈顶的栈帧。这种后进先出的结构也就是函数的调用栈。

在这里插入图片描述

栈帧的两个边界分别有FP(R11)和SP(R13)L来限定

img

1.栈帧:虚拟机用来进行方法调用和方法执行的数据结构

2.栈帧的组成 = 局部变量表 + 操作数栈 + 动态链接 + 方法返回地址 + 附加信息

3.局部变量表

(1)存放的内容 = 方法参数列表对应的值 + 方法内定义的局部变量的值

(2)局部变量表 = 变量槽 * n(即多个变量槽构成)

​ 1)一个变量槽存放一个32位以内的数据类型:

​ char,int ,bit,boolean,float,short,reference,returnAddress

​ 2)64位的数据结构就需要2个变量槽:long,double

​ 3)变量槽的访问是根据索引定位来完成的

(3)局部变量表和类变量不同,类变量有一个初始化赋值的过程,局部变量表中的值如果不赋值,那就真的是没值

4.操作数栈

(1)数据结构 = 先入后出的栈结构

(2)操作数栈在编译的过程中最大深度就已经确定好了

(3)操作数栈中的数据类型必须严格遵照字节码指令规定的类型

(4)从概念模型上来看,每一个栈帧是独立的。但是实际上上一个栈帧的局部变量表会和下一个栈帧的操作数栈有一部分重合

(5).java虚拟机的解释执行引擎 = 基于栈的执行引擎

5.动态链接

每一个栈帧都包含一个指向运行时常量池的该栈帧多对应的方法,用于动态链接

6.方法返回地址

(1)方法返回的两种方式 = 执行引擎遇到方法返回的指令 + 遇到错误

(2)不管哪种方法返回,程序都会回到上一层继续执行,那么栈帧中需要保存一些方法返回的信息。最常见的信息就是保存上一层的计数器,好让程序能准确定位到上一层。

7.附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与高度相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。

7.2为什么HotSpot虚拟机要使用解释器与编译器并存的架构?

尽管并不是所有的Java虚拟机都采用解释器与编译器并存的架构,但许多主流的商用虚拟机(如HotSpot),都同时包含解释器和编译器。解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制较大(如部分嵌入式系统中),可以使用解释器执行节约内存,反之可以使用编译执行来提升效率。此外,如果编译后出现“罕见陷阱”,可以通过逆优化退回到解释执行。

img

7.3为何HotSpot虚拟机要实现两个不同的即时编译器?

HotSpot虚拟机中内置了两个即时编译器:Client Complier和Server Complier,简称为C1、C2编译器,分别用在客户端和服务端。目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。程序使用哪个编译器,取决于虚拟机运行的模式。HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式。

用Client Complier获取更高的编译速度,用Server Complier 来获取更好的编译质量。为什么提供多个即时编译器与为什么提供多个垃圾收集器类似,都是为了适应不同的应用场景。

3)哪些程序代码会被编译为本地代码?如何编译为本地代码?

程序中的代码只有是热点代码时,才会编译为本地代码,那么什么是热点代码呢?

运行过程中会被即时编译器编译的“热点代码”有两类:
1、被多次调用的方法。 2、被多次执行的循环体。

两种情况,编译器都是以整个方法作为编译对象。 这种编译方法因为编译发生在方法执行过程之中,因此形象的称之为栈上替换(On Stack Replacement,OSR),即方法栈帧还在栈上,方法就被替换了。

7.4如何判断方法或一段代码或是不是热点代码呢?

要知道方法或一段代码是不是热点代码,是不是需要触发即时编译,需要进行Hot Spot Detection(热点探测)。

目前主要的热点探测方式有以下两种:
(1)基于采样的热点探测
采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这个方法就是“热点方法”。这种探测方法的好处是实现简单高效,还可以很容易地获取方法调用关系(将调用堆栈展开即可),缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
(2)基于计数器的热点探测

采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阀值,就认为它是“热点方法”。这种统计方法实现复杂一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对更加精确严谨。

7.5HotSpot虚拟机中使用的是哪种热点检测方式呢?

在HotSpot虚拟机中使用的是第二种——**基于计数器的热点探测方法,因此它为每个方法准备了两个计数器:方法调用计数器回边计数器。**在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。

二、JVM 的内存结构

1、PC Register程序计数器

1)定义

Program Counter Register **程序计数器(寄存器)**作用:是记录下一条 jvm 指令的执行地址行号。
特点:

  • 是线程私有的(每个线程都有自动的程序计数器)
  • 不会存在内存溢出问题
2)作用

程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。

0: getstatic #20 // PrintStream out = System.out; 
3: astore_1 // -- 
4: aload_1 // out.println(1); 
5: iconst_1 // -- 
6: invokevirtual #26 // -- 
9: aload_1 // out.println(2); 
10: iconst_2 // -- 
11: invokevirtual #26 // -- 
14: aload_1 // out.println(3); 
15: iconst_3 // -- 
16: invokevirtual #26 // -- 
19: aload_1 // out.println(4); 
20: iconst_4 // -- 
21: invokevirtual #26 // -- 
24: aload_1 // out.println(5); 
25: iconst_5 // -- 
26: invokevirtual #26 // -- 
29: return

image-20210720000725703

**解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,**这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
**多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,**以便于接着往下执行。

2、JVM Stacks虚拟机栈

1)定义

每个线程运行需要的内存空间,称为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的方法

image-20210720002051877

package cn.itcast.jvm.t1._01stack;

/**
 * 演示栈帧
 */
public class Demo1_1 {

    public static void main(String[] args) throws InterruptedException {
        method1();
    }

    private static void method1() {
        method2(1, 2);
    }

    private static int method2(int a, int b) {
        int c =  a + b;
        return c;
    }
}
问题辨析:

垃圾回收是否涉及栈内存?

  • 不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。垃圾回收的是堆内存中的无用对象

栈内存分配越大越好吗?

  • 不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
  • if 物理内存=500M 一个线程1M 就可以500个线程 一个2M 250个线程(一般采用系统默认的栈内存大小)

方法呢的局部变量是否线程安全(私有的就不需要考虑,static修饰的公共资源要考虑

  • 如果方法内部的变量(基本变量/引用变量)没有逃离方法的作用访问,它是线程安全的
  • 如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。
  • image-20210722142548972
/**
 * 局部变量的线程安全问题
 */
public class Demo1_2 {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        sb.append(4);
        sb.append(5);
        sb.append(6);
        new Thread(()->{
            m2(sb);
        }).start();
    }

    // 方法内部的变量(基本变量/引用变量)没有逃离方法的作用访问,它是线程安全的
    public static void m1() {  // 不存在线程安全问题
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }

    // 局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。
    public static void m2(StringBuilder sb) {  // 不存在线程安全问题 可以使用StringBuffer
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }

    public static StringBuilder m3() {   // 不存在线程安全问题 可以使用StringBuffer
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        return sb;
    }
}
2)栈内存溢出

**栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError **

【1】-Xss256k

默认栈帧在1M左右使用 -Xss256k 指定栈内存大小!

/**
 * 演示栈内存溢出 java.lang.StackOverflowError
 * -Xss256k
 */
public class Demo1_3 {
    private static int count; // 计数 打印调用栈帧的次数 

    public static void main(String[] args) {
        try {
            method1();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(count);
        }
    }

    // 递归调用
    private static void method1() {
        count++;
        method1();
    }
}

调用38313次栈溢出

image-20210722144343106

设置栈帧大小

image-20210722144259057

再次执行

image-20210722144505191

【2】第三方类库操作

Emp和Dept类相互调用

/**
 * json 数据转换
 */
public class Demo1_19 {

    public static void main(String[] args) throws JsonProcessingException {
        Dept d = new Dept();
        d.setName("Market");

        Emp e1 = new Emp();
        e1.setName("zhang");
        e1.setDept(d);

        Emp e2 = new Emp();
        e2.setName("li");
        e2.setDept(d);

        d.setEmps(Arrays.asList(e1, e2));

        // { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
        ObjectMapper mapper = new ObjectMapper();
        System.out.println(mapper.writeValueAsString(d));
    }
}

class Emp {
    private String name;
    @JsonIgnore  // 作用:遇到部门属性就不转换json 变成单向关联
    private Dept dept;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Dept getDept() {
        return dept;
    }

    public void setDept(Dept dept) {
        this.dept = dept;
    }
}

class Dept {
    private String name;
    private List<Emp> emps;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Emp> getEmps() {
        return emps;
    }

    public void setEmps(List<Emp> emps) {
        this.emps = emps;
    }
}
3)线程运行诊断
【1】案例一:cpu 占用过多
/**
 * 演示 cpu 占用过高
 */
public class Demo1_16 {

    public static void main(String[] args) {
        new Thread(null, () -> {
            System.out.println("1...");
            while(true) {

            }
        }, "thread1").start();


        new Thread(null, () -> {
            System.out.println("2...");
            try {
                Thread.sleep(1000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread2").start();

        new Thread(null, () -> {
            System.out.println("3...");
            try {
                Thread.sleep(1000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread3").start();
    }
}

解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程

nohup java cn.itcast.jvm.t1.Demo1_16 >/dev/null &  运行

top 命令,查看是哪个进程占用 CPU 过高

ps H -eo pid, tid(线程id), %cpu | grep 刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高
例如:ps H -eo pid,tid,%cpu | grep 32665

jstack 进程 id 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,
# 注意 jstack 查找出的线程 id 是 16 进制的,需要转换。
# 会详细定位到出现问题的源码行数

image-20210722161148460

【2】案例二:死锁迟迟得不到结果

通过jstack排查出思索的问题

/**
 * 演示线程死锁
 */
class A{};
class B{};
public class Demo1_3 {
    static A a = new A();
    static B b = new B();


    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (a) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println("我获得了 a 和 b");
                }
            }
        }).start();
        Thread.sleep(1000);
        new Thread(()->{
            synchronized (b) {
                synchronized (a) {
                    System.out.println("我获得了 a 和 b");
                }
            }
        }).start();
    }
}

image-20210722162243974

image-20210722162053334

3、Native Method Stacks本地方法栈

一些带有 native 关键字的方法就是需要 JAVA 去调用本地的C或者C++方法

因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈

服务于带 native 关键字的方法。

这些本地方法运行时所使用的内存就是 本地方法栈

例如:Object基类

image-20210722163353375

4、Heap堆

与前面三个区别:堆是线程共享的区 ,PC Register/JVM Stracks/ Native Method Stacks都是线程私有的区

1)定义

Heap 堆:通过new关键字创建的对象都会被放在堆内存
特点:

它是线程共享,堆内存中的对象都需要考虑线程安全问题,

有垃圾回收机制(堆中不再被引用的对象,被垃圾回收,释放内存)

2)堆内存溢出问题

java.lang.OutofMemoryError :java heap space. 堆内存溢出。

通常我们可以把堆内存设置的小些,提早的排查到堆内存溢出问题

package cn.itcast.jvm.t1._02heap;

import java.util.ArrayList;
import java.util.List;

/**
 * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m
 */
public class Demo1_5 {

    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "hello";
            while (true) {
                list.add(a); // hello, hellohello, hellohellohellohello ...
                a = a + a;  // hellohellohellohello
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

image-20210722164735795

可以使用 -Xmx8m 来指定堆内存大小

image-20210722165502335

3)堆内存诊断
  • jps 工具
    **查看当前系统中有哪些 java 进程:**命令行输入 jps

  • jmap 工具
    **查看堆内存占用情况:**命令行输入 jmap - heap 进程id

  • jconsole 工具
    图形界面的,多功能的监测工具,可以连续监测

  • jvisualvm 工具

    Java 可视化 虚拟机,多功能的监测工具,可以连续监测

package cn.itcast.jvm.t1._02heap;

/**
 * 演示堆内存
 */
public class Demo1_4 {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");
        Thread.sleep(30000);
        byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
        
        System.out.println("2...");
        Thread.sleep(20000);
        array = null;
        
        System.gc();  // 垃圾回收
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}
【1】jps、 jmap 工具

代码运行之命令行输入 jps , jmap - heap 进程id ,观察 Eden Space内存占用情况

Eden :伊甸园的意思 这里 就是新生代

C:\Users\ahcfl\Desktop\JVM\代码\jvm>jps
45712 RemoteMavenServer36
17572 KotlinCompileDaemon
40740 Jps
46756 Demo1_4
47236
36892 Launcher

C:\Users\ahcfl\Desktop\JVM\代码\jvm>jmap -heap 46756

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4269801472 (4072.0MB)
   NewSize                  = 89128960 (85.0MB)
   MaxNewSize               = 1422917632 (1357.0MB)
   OldSize                  = 179306496 (171.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:  
   capacity = 67108864 (64.0MB)
   used     = 6711256 (6.400352478027344MB)   # 新生代初始时使用情况
   free     = 60397608 (57.599647521972656MB)
   10.000550746917725% used
From Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation
   capacity = 179306496 (171.0MB)
   used     = 0 (0.0MB)
   free     = 179306496 (171.0MB)
   0.0% used

3171 interned Strings occupying 281072 bytes.

C:\Users\ahcfl\Desktop\JVM\代码\jvm>jmap -heap 46756
Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4269801472 (4072.0MB)
	.......
	.......
	
Heap Usage:
PS Young Generation
Eden Space:
   capacity = 67108864 (64.0MB)
   used     = 17197032 (16.400367736816406MB)  # 新创建的内存使用情况
   free     = 49911832 (47.599632263183594MB)
   25.625574588775635% used
From Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation
   capacity = 179306496 (171.0MB)
   used     = 0 (0.0MB)
   free     = 179306496 (171.0MB)
   0.0% used

3172 interned Strings occupying 281120 bytes.

C:\Users\ahcfl\Desktop\JVM\代码\jvm>jmap -heap 46756
Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4269801472 (4072.0MB)
   NewSize                  = 89128960 (85.0MB)
   MaxNewSize               = 1422917632 (1357.0MB)
	.......
	.......

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 67108864 (64.0MB)
   used     = 1342200 (1.2800216674804688MB)  # 垃圾回收后的使用情况
   free     = 65766664 (62.71997833251953MB)
   2.0000338554382324% used
From Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation
   capacity = 179306496 (171.0MB)
   used     = 1047360 (0.99884033203125MB)
   free     = 178259136 (170.00115966796875MB)
   0.5841171532346491% used

3158 interned Strings occupying 280128 bytes.
【2】jconsole 工具

image-20210722171620058

【3】jvisualvm 工具
案例:垃圾回收后,内存占用仍然很高
/**
 * 演示查看对象个数 堆转储 dump
 */
public class Demo1_13 {

    public static void main(String[] args) throws InterruptedException {
        List<Student> students = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            students.add(new Student());
//            Student student = new Student();
        }
        Thread.sleep(1000000000L);
    }
}
class Student {
    private byte[] big = new byte[1024*1024];
}

堆转储 dump 查看占用内存前20

image-20210722173420370

定位到ArrayList ,student 对象

image-20210722173344129

5、Method Area方法区

参考:The Structure of the Java Virtual Machine (oracle.com)

javap -v HelloWorld.class 命令反编译查看结果

1)定义

J**ava 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。**方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。**它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化方法区域是在虚拟机启动时创建的。 尽管方法区域在逻辑上是堆的一部分,但简单的实现可能不会选择垃圾收集或压缩它。此规范不强制指定方法区的位置或用于管理已编译代码的策略。**方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的!

2)组成结构

Hotspot 虚拟机 jdk1.6 1.7 1.8 内存结构图

1.6 方法区是个概念 在内存结构 PermGen 永久代实现

1.8 方法区是个概念 在本地内存(OS内存) Metaspace 元空间实现

1.6 --> 1.8 其中的 常量池 StringTable放到了Heap中 实现变为 Metaspace

在这里插入图片描述

3)方法区内存溢出
【1】1.8 之前会导致永久代内存溢出

使用 -XX:MaxPermSize=8m 指定永久代内存大小模拟

import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

/**
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=8m
 */
public class Demo1_8 extends ClassLoader { // ClassLoader可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 参数:版本号, 修饰符public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}

-------------------------------
Error occurred during initialization of VM
MaxMetaspaceSize is too small.
【2】1.8 之后会导致元空间内存溢出

使用 -XX:MaxMetaspaceSize=8m 指定元空间大小模拟

JDK换为1.6版本

import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;
import com.sun.xml.internal.ws.org.objectweb.asm.Opcodes;

/**
 * 演示永久代内存溢出  java.lang.OutOfMemoryError: PermGen space
 * -XX:MaxPermSize=8m
 */
public class Demo1_8 extends ClassLoader {
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 20000; i++, j++) {
                ClassWriter cw = new ClassWriter(0);
                cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                byte[] code = cw.toByteArray();
                test.defineClass("Class" + i, code, 0, code.length);
            }
        } finally {
            System.out.println(j);
        }
    }
}
【3】可能导致溢出场景

spring 中运用到的字节码技术 cglib 运行期间,动态生成字节码,动态类加载 。

sring面向切面编程基于动态代理来实现的

静态代理:也就是自己手动创建的代理对象
动态代理:也就是在程序运行中通过配置参生的

那么Spring的AOP也就是面向切面编程,就是基于动态代理来实现的,通过代理原始类增加额外功能,我们可以将额外功能一次定义然后配合切点达到多次使用的效果,比如 做日志啊 事物啊 时间啊等等…提高了复用性 开发效率.

那么在Spirng当中动态代理有两种
1.JDK自带的动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理.需要指定一个类加载器,然后生成的代理对象实现类的接口或类的类型,接着处理额外功能.

2.Cglib是动态代理利用asm的开源包,对代理对象的Class文件加载进来,通过修改其字节码生成的子类来处理
Cglib是基于继承父类生成的代理类.

在Spirng当中动态代理的使用

1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP

3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

如何强制使用CGLIB实现AOP?
(1)添加CGLIB库,SPRING_HOME/cglib/*.jar
(2)在spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class=“true”/>

JDK动态代理和CGLIB字节码生成的区别?
(1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类
(2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法
因为是继承,所以该类或方法最好不要声明成final

所以spring 运行期间可能产生大量的类加载,会导致永久代的内存溢出

1.8后元空间在系统内存中,内存充裕许多,并且垃圾回收机制,垃圾回收机制也是元空间自行管理。

image-20210722181738724

4)运行时常量池

二进制字节码包含**(类的基本信息,常量池,类方法定义,包含了虚拟机的指令)**
首先看看常量池是什么,编译如下代码:

public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }

}
 javap -v HelloWorld.class 命令反编译查看结果

**常量池:**就是一张地址表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
**运行时常量池:**常量池是 *.class 文件中的,当该类被加载以后, 它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

运行时会把 常量池中的 #1,#2…变成物理地址查找

每条指令都会对应常量池表中一个逻辑地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。

C:\Users\ahcfl\Desktop\JVM\代码\jvm\out\production\jvm\cn\itcast\jvm\t5>javap -v HelloWorld.class
Classfile /C:/Users/ahcfl/Desktop/JVM/代码/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class
  Last modified 2021-7-20; size 567 bytes
  MD5 checksum 8efebdac91aa496515fa1c161184e354
  Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
-----------------------------【常量池】------------------------------------------------
Constant pool:  
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // cn/itcast/jvm/t5/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcn/itcast/jvm/t5/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               hello world
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               cn/itcast/jvm/t5/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
-----------------------------【方法定义】---------------------------------------------
{
  public cn.itcast.jvm.t5.HelloWorld();    // 默认的构造方法
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t5/HelloWorld;
---------------------------------main方法 中的虚拟机指令--------------------------------
  public static void main(java.lang.String[]); 
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field    java/lang/System.out:Ljava/io/PrintStream;   获取静态变量
         3: ldc           #3                  // String hello world 加载参数,找引用地址
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V  虚方法调用
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
5)StringTable特性
  • 常量池中的字符串仅是符号,

  • 只有在被用到时才会转化为对象**,**

  • 利用串池的机制,来避免重复创建字符串对象

  • 字符串变量拼接的原理是StringBuilder(1.8) ,字符串常量拼接的原理是编译器优化

  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中。

package cn.itcast.jvm.t1.stringtable;

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    // ldc #2 会把 a 符号变为 "a" 字符串对象
    // ldc #3 会把 b 符号变为 "b" 字符串对象
    // ldc #4 会把 ab 符号变为 "ab" 字符串对象

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的 遇到 ldc 才加载
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
        String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab
		 String s6 = s2 + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab
        
        System.out.println(s3 == s4); // false
        System.out.println(s3 == s5); // true
   		System.out.println(s3 == s6); // false
    }
}
C:\Users\ahcfl\Desktop\JVM\代码\jvm\out\production\jvm\cn\itcast\jvm\t1\stringtable>javap -v Demo1_22.class
Classfile /C:/Users/ahcfl/Desktop/JVM/代码/jvm/out/production/jvm/cn/itcast/jvm/t1/stringtable/Demo1_22.class
  Last modified 2021-7-22; size 534 bytes
  MD5 checksum d9e9908ec91d554181ff9db0f72419d5
  Compiled from "Demo1_22.java"
public class cn.itcast.jvm.t1.stringtable.Demo1_22
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#24         // java/lang/Object."<init>":()V
   #2 = String             #25            // a
   #3 = String             #26            // b
   #4 = String             #27            // ab
   #5 = Class              #28            // cn/itcast/jvm/t1/stringtable/Demo1_22
   #6 = Class              #29            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcn/itcast/jvm/t1/stringtable/Demo1_22;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               s1
  #19 = Utf8               Ljava/lang/String;
  #20 = Utf8               s2
  #21 = Utf8               s3
  #22 = Utf8               SourceFile
  #23 = Utf8               Demo1_22.java
  #24 = NameAndType        #7:#8          // "<init>":()V
  #25 = Utf8               a
  #26 = Utf8               b
  #27 = Utf8               ab
  #28 = Utf8               cn/itcast/jvm/t1/stringtable/Demo1_22
  #29 = Utf8               java/lang/Object
{
  public cn.itcast.jvm.t1.stringtable.Demo1_22();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t1/stringtable/Demo1_22;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=4, args_size=1
         0: ldc           #2                  // String a
         2: astore_1 // 存储变量
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: return
      LineNumberTable:
        line 11: 0
        line 12: 3
        line 13: 6
        line 21: 9
      LocalVariableTable:  // 局部变量
        Start  Length  Slot  Name   Signature
            0      10     0  args   [Ljava/lang/String;
            3       7     1    s1   Ljava/lang/String;
            6       4     2    s2   Ljava/lang/String;
            9       1     3    s3   Ljava/lang/String;
}
SourceFile: "Demo1_22.java"

image-20210722204104841

package cn.itcast.jvm.t1.stringtable;

/**
 * 演示字符串字面量也是【延迟】成为对象的
 */
public class TestString {
    public static void main(String[] args) {
        int x = args.length;
        System.out.println(); // 字符串个数 2275

        System.out.print("1");
        System.out.print("2");
        System.out.print("3");
        System.out.print("4");
        System.out.print("5");
        System.out.print("6");
        System.out.print("7");
        System.out.print("8");
        System.out.print("9");
        System.out.print("0");
        System.out.print("1"); // 字符串个数 2285
        System.out.print("2");
        System.out.print("3");
        System.out.print("4");
        System.out.print("5");
        System.out.print("6");
        System.out.print("7");
        System.out.print("8");
        System.out.print("9");
        System.out.print("0");
        System.out.print(x); // 字符串个数
    }
}
【1】1.8 intern 方法

调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,则放入成功(直接用同一个地址)
  • 如果有该字符串对象,则不放入
  • 无论放入是否成功,都会返回串池中的字符串对象

注意:此时如果调用 intern 方法成功,堆内存与串池中的字符串对象是同一个对象;

如果失败,则不是同一个对象

public class Main {
	public static void main(String[] args) {
		// "a" "b" 被放入串池中,str 则存在于堆内存之中
		String str = new String("a") + new String("b");
		// 调用 str 的 intern 方法,这时串池中没有 "ab" ,则会将该字符串对象放入到串池中,此时堆内存与串池中的 "ab" 是同一个对象
		String st2 = str.intern();
		// 给 str3 赋值,因为此时串池中已有 "ab" ,则直接将串池中的内容返回
		String str3 = "ab";
		// 因为堆内存与串池中的 "ab" 是同一个对象,所以以下两条语句打印的都为 true
		System.out.println(str == st2);
		System.out.println(str == str3);
	}
}

--------------------------------------------------------------------------
public class Main {
	public static void main(String[] args) {
        // 此处创建字符串对象 "ab" ,因为串池中还没有 "ab" ,所以将其放入串池中
		String str3 = "ab";
        // "a" "b" 被放入串池中,str 则存在于堆内存之中
		String str = new String("a") + new String("b");
        // 此时因为在创建 str3 时,"ab" 已存在与串池中,所以放入失败,但是会返回串池中的 "ab" 
		String str2 = str.intern();
        // false
		System.out.println(str == str2);
        // false
		System.out.println(str == str3);
        // true
		System.out.println(str2 == str3);
	}
}

【2】1.6 intern 方法

调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,会把对象赋值一份(又创建了一个新的对象)放入串池中
  • 如果有该字符串对象,则不会放入
  • 无论放入是否成功,都会返回串池中的字符串对象
6)StringTable 的位置

jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。

永久代的内存回收效率低,Full GC才会触发,老年代,触发的时机晚,占用内存。

heap中 只需要minor GC触发回收 ,减轻字符串对内存中的占用

/**
 * 演示 StringTable 位置
 * 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
 * 在jdk6下设置 -XX:MaxPermSize=10m
 */
public class Demo1_6 {

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        int i = 0;
        try {
            for (int j = 0; j < 260000; j++) {
                list.add(String.valueOf(j).intern());
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

image-20210722213641970

7)StringTable 垃圾回收

-Xmx10m 指定堆内存大小
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc 打印 gc 的次数,耗费时间等信息

StringTable底层 HashTable hash表 数组+链表 哈希桶 就是数组

/**
 * 演示 StringTable 垃圾回收 添加下面参数  运行代码
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 * 10m 内存  当内存不够时  会触发垃圾回收机制 把无用的对象回收(没有引用的)
 */
public class Code_05_StringTableTest {

    public static void main(String[] args) {
        int i = 0;
        try {
            for(int j = 0; j < 10000; j++) {
                // j = 100(会1754+100), 
                // j = 10000 会触发到GC [GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->739K(9728K), 0.0112330 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
                String.valueOf(j).intern();
                i++;
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println(i);
        }
    }

}

image-20210723000704066

在初始化时 类名 方法名 常量名 也是以字符串的形式存在的 所以 开始就又1754个了。

8)StringTable 性能调优
【1】-XX:StringTableSize=桶个数

因为StringTable是由HashTable实现的,适当增加HashTable桶的个数,HashTable桶的个数增多,元素的存放就比较分散,hash碰撞的几率减小,从而减少字符串放入串池所需要的时间,提高查询速度。

反之,hash碰撞的几率曾大,链表的长度越长,查询速度变慢。

-XX:StringTableSize=桶个数(最少设置为 1009 以上)

考虑是否需要将字符串对象入池,可以通过 intern 方法减少重复入池

linux.words模拟读取存放4万个单词的文件

StringTableSize=1009 大概花费12秒, 每个桶下面大约挂400个单词

正常默认StringTableSize=60013 大概0.6秒

package cn.itcast.jvm.t1.stringtable;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * 演示串池大小对性能的影响
 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
 */
public class Demo1_24 {

    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) { //linux.words模拟读取存放4万个单词的文件
            String line = null;
            long start = System.nanoTime();
            while (true) {
                line = reader.readLine();
                if (line == null) {
                    break;
                }
                line.intern();
            }
            System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
        }
    }
}
【2】为什么用StringTable?

推特保存用户地址的时候,使用intern 方法入池,减少重复地址的存储。

/**
 * 演示 intern 减少内存占用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Demo1_25 {

    public static void main(String[] args) throws IOException {

        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i = 0; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    address.add(line.intern()); // 为了不被垃圾回收,放入生命周期更长的list集合
                    address.add(line); // 不加 intern 放在堆内存中
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read(); // 敲一次回车运行
      }
}

6、Direct Memory直接内存[系统内存]

1)定义

Direct Memory常见于 NIO 操作时

用于数据缓冲区分配回收成本较高(因为和系统交互),

但读写性能高,

不受 JVM 内存回收管理

2)直接内存的好处

文件读写流程:

因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。

缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。

在这里插入图片描述

使用 DirectBuffer 文件读取流程

ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb); 分配1Mb的直接内存

直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。

在这里插入图片描述

package cn.itcast.jvm.t1._05direct;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 演示 ByteBuffer 作用
 */
public class Demo1_9 {
    static final String FROM = "E:\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
    static final String TO = "E:\\a.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // io 用时:1535.586957 1766.963399 1359.240226
        directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
    }

    private static void directBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
             FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
            while (true) {
                int len = from.read(bb);
                if (len == -1) {
                    break;
                }
                bb.flip();
                to.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
    }

    private static void io() {
        long start = System.nanoTime();
        try (FileInputStream from = new FileInputStream(FROM);
             FileOutputStream to = new FileOutputStream(TO);
        ) {
            byte[] buf = new byte[_1Mb];
            while (true) {
                int len = from.read(buf);
                if (len == -1) {
                    break;
                }
                to.write(buf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io 用时:" + (end - start) / 1000_000.0);
    }
}
3)直接内存回收原理

直接内存不受 JVM 内存回收管理,但不释放也会造成内存溢出

package cn.itcast.jvm.t1._05direct;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;


/**
 * 演示直接内存溢出
 */
public class Demo1_10 {
    static int _100Mb = 1024 * 1024 * 100;

    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
        }
        // 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
        //                  jdk8 对方法区的实现称为元空间
    }
}

运行在winds任务管理器产看内存状态

public class Code_06_DirectMemoryTest {

    public static int _1GB = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
//        method();
        method1();
    }

    // 演示 直接内存 是被 unsafe 创建与回收
    private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {

        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe)field.get(Unsafe.class);
		// 分配内存
        long base = unsafe.allocateMemory(_1GB);
        unsafe.setMemory(base,_1GB, (byte)0);
        System.in.read();
		// 释放内存
        unsafe.freeMemory(base);
        System.in.read();
    }

    /**
 	 * 禁用显式回收对直接内存的影响   垃圾回收只能释放java的内存,不能自动释放直接内存 需手动调用
 	  -XX:+DisableExplicitGC 显式的
	 */
    // 演示 直接内存被 释放  
    private static void method() throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
        System.out.println("分配完毕");
        System.in.read();
        System.out.println("开始释放");
        byteBuffer = null;
        System.gc(); // 手动 gc   // 显式的垃圾回收,Full GC  
        System.in.read();
    }

}

直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。
第一步:allocateDirect 的实现

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

底层是创建了一个 DirectByteBuffer 对象。
第二步:DirectByteBuffer 类

DirectByteBuffer(int cap) {   // package-private
   
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        base = unsafe.allocateMemory(size); // 申请内存
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); // 通过虚引用,来实现直接内存的释放,this为虚引用的实际对象, 第二个参数是一个回调,实现了 runnable 接口,run 方法中通过 unsafe 释放内存。
    att = null;
}

这里调用了一个 Cleaner 的 create 方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是 DirectByteBuffer )被回收以后,就会调用 Cleaner 的 clean 方法,来清除直接内存中占用的内存。

 public void clean() {
        if (remove(this)) {
            try {
            // 都用函数的 run 方法, 释放内存
                this.thunk.run();
            } catch (final Throwable var2) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null) {
                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                        }

                        System.exit(1);
                        return null;
                    }
                });
            }

        }
    }

可以看到关键的一行代码, this.thunk.run(),thunk 是 Runnable 对象。run 方法就是回调 Deallocator 中的 run 方法,

public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    // 释放内存
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
}

直接内存的回收机制总结

  • 使用了 Unsafe 类来完成直接内存的分配回收,回收需要主动调用freeMemory 方法
  • ByteBuffer 的实现内部使用了 Cleaner(虚引用)来检测 ByteBuffer 。一旦ByteBuffer 被垃圾回收,那么会由 ReferenceHandler(守护线程) 来调用 Cleaner 的 clean 方法调用 freeMemory 来释放内存
    注意:
/**
     * -XX:+DisableExplicitGC 显示的 
     */
    private static void method() throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
        System.out.println("分配完毕");
        System.in.read();
        System.out.println("开始释放");
        byteBuffer = null;
        System.gc(); // 手动 gc 失效
        
        // 直接内存使用较多 使用unsafe释放内存
        unsafe.freeMemory(byteBuffer);
        
        System.in.read();
    }

一般用 jvm 调优时,会加上下面的参数:

System.gc(); // 手动 gc 失效 是影响性能的回收

-XX:+DisableExplicitGC  // 静止显示的 GC

意思就是禁止我们手动的 GC,

比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。

所以我们就通过 unsafe 对象调用 freeMemory 的方式释放内存。

三、JVM中的垃圾回收

1、如何判断对象可以回收

1)引用计数法

**当一个对象被引用时,对当前引用对象的值加一,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。**这个引用计数法听起来不错,但是有一个弊端。

如下图所示,循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。

在这里插入图片描述

2)可达性分析算法
  • JVM 中的垃圾回收器通过可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找到该对象,如果找不到,则表示可以回收
  • 可以作为 GC Root 的对象
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象(也就是局部变量list所引用的对象new ArrayList())
    • 方法区中参数引用的对象
    • 本地方法栈中 JNI(即一般说的Native方法)引用的对象
public static void main(String[] args) throws IOException {

        ArrayList<Object> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add(1);
        System.out.println(1);
        System.in.read();

        list = null;
        System.out.println(2);
        System.in.read();
        System.out.println("end");
    }

Eclipse Memory Analyzer 分析

对于以上代码,可以使用如下命令将堆内存信息转储成一个文件,然后使用
Eclipse Memory Analyzer 工具进行分析。
第一步:

启动代码运行之,使用 jps 命令,查看程序的进程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-50ZyREz7-1628668126793)(jvm01.assets/20210209111015399.png)]

第二步:

# 转储文件命令
jmap -dump:format=b,live,file=1.bin 16104 命令转储文件命令

dump:转储文件
format=b:二进制文件

live:触发一次垃圾回收

file:文件名
16104:进程的id

输入转储文件命令,在2的时候再输入一次

jmap -dump:format=b,live,file=2.bin 16104 命令转储文件命令

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m9xUSPKj-1628668126794)(jvm01.assets/20210209111229838.png)]
第三步:打开 Eclipse Memory Analyzer 对 1.bin 文件进行分析。

image-20210725195828011

系统类(核心类):启动类加载器加载 可以作为 GC Root 的对象(不会被垃圾回收 )

image-20210725195955267

本地类:操作系统方法执行时引用的java类对象

image-20210725200056926

busy Monitor:正在加锁的引用对象,也可以作为 GC Root 的对象

image-20210725200259391

Thread(活动线程):线程引用的对象,包括线帧方法中引用的局部变量(如 上面代码中的 list1),后面所引用的对象(new ArrayList())是存放在堆中的。

image-20210725200515768

可以看到现在ArrayList仍在被引用

在这里插入图片描述

第四步:打开 Eclipse Memory Analyzer 对 2.bin 文件进行分析。

这时候list=nulll分析的 gc root,找到Thread,那么 list 对象的引用不显示,说明被回收了。

jmap -dump:format=b,live,file=2.bin 16104 命令转储文件命令

live:这里的live参数,对ArrayList进行回收

image-20210725201921962

3)JVM中四种引用

实线:强引用 ,虚线:软 弱 终结器引用

在这里插入图片描述

1)当软引用引用的对象被回收时,软引用本身也是对象,软引用就会进入 引入队列,当强引用不引用时,

遍历回收,配合引用队列来释放弱引用自身(软引用,弱引用既可以配合引用队列使用,也可不配合)

2)虚引用和终结器引用必须配合引用队列使用,当虚引用和终结器引用对象被创建时,会关联引用队列

image-20210725204126372

【1】强引用

只有所有 GC Roots 对象都不【强引用】引用该对象,该对象才能被垃圾回收,例如图中 A1对象

【2】软引用(SoftReference)

仅有软引用引用该对象时,当内存不足时,并且没有其他强引用引用该对象时,此对象会被垃圾回收

当软引用引用的对象被回收时,软引用本身也是对象,软引用就会进入 引入队列,当强引用不引用时,

遍历回收,配合引用队列来释放弱引用自身

【3】弱引用(WeakReference)

仅有弱引用引用该对象时,只要发生了垃圾回收,不管内存是否充足,并且没有其他强引用引用该对象时,此对象会被垃圾回收
配合引用队列来释放弱引用自身(同软引用)

【4】虚引用(PhantomReference)

必须配合引用队列使用,主要配合 ByteBuffer 使用,当不被强引用时,被引用对象回收时,会将虚引用入队,
由 Reference Handler 线程定时找引用队列,调用虚引用方法Cleaner() 中的unsafe.freeMemory方法 把直接内存释放

【5】终结器引用(FinalReference)

无需手动编码,内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程(优先级很低)某个时间通过 终结器引用 找到被引用对象并调用它的 finalize 方法,下一次 GC 时才能回收被引用对象。 (所以效率很低,不推荐使用 finalize 方法)

软引用演示

模拟对不重要的资源进行软引用,释放内存

/**
 * 演示 软引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Code_08_SoftReferenceTest {

    public static int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        method2();
    }

    // 设置 -Xmx20m , 演示堆内存不足,
    public static void method1() throws IOException {
        ArrayList<byte[]> list = new ArrayList<>();

        for(int i = 0; i < 5; i++) {
            list.add(new byte[_4MB]);  // 这边就是强引用  
        }
        System.in.read();
    }

    // 演示 软引用
    public static void method2() throws IOException {
        
        // list -->强引用new byte[_4MB]
        // 变为 list -->强引用 SoftReference -->软引用引用 new byte[_4MB]
        List<SoftReference<byte[]>> list = new ArrayList<>();
        for(int i = 0; i < 5; i++) {
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get()); //获得 new byte[_4MB] hash地址值
            list.add(ref);  
            System.out.println(list.size());
        }
        System.out.println("循环结束:" + list.size());
        for(SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());  // 再次循环获取全为null
        }
    }
}

method1 方法解析:
首先会设置一个堆内存的大小为 20m,然后运行 mehtod1 方法,会抛异常,堆内存不足,

因为 mehtod1 中的 list 都是强引用。

在这里插入图片描述

method2 方法解析:
在 list 集合中存放了 软引用对象,当内存不足时,会触发 full gc,将软引用的对象回收。细节如图:

在这里插入图片描述

上面的代码中,当软引用引用的对象被回收了,但是软引用还存在,

所以,一般软引用需要搭配一个引用队列一起使用。
修改 method2 如下:

// 演示 软引用 搭配引用队列
    public static void method3() throws IOException {
        ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
        // 引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        for(int i = 0; i < 5; i++) {
            // 关联了引用队列,当软引用所关联的 byte[] 被回收时,软引用自己会加入到 queue 中去
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }

        // 从队列中获取无用的 软引用对象,并移除  (先进先出)
        Reference<? extends byte[]> poll = queue.poll(); 
        while(poll != null) {
            list.remove(poll);  // list中移除无用的软引用  最后只剩下最后一个byte数组了
            poll = queue.poll();// 获取队列的下一个 软引用对象
        }

        System.out.println("=====================");
        for(SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }

在这里插入图片描述

弱引用演示
public class Code_09_WeakReferenceTest {

    public static void main(String[] args) {
//        method1();
        method2();
    }

    public static int _4MB = 4 * 1024 *1024;

    // 演示 弱引用
    public static void method1() {
        // list --强引用--> WeakReference --弱引用--> new byte[_4MB]
        List<WeakReference <byte[]>> list = new ArrayList<>();
        for(int i = 0; i < 10; i++) {
            WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB]);
            list.add(weakReference);

            for(WeakReference<byte[]> wake : list) {
                System.out.print(wake.get() + ",");
            }
            System.out.println();
        }
    }

    // 演示 弱引用搭配 引用队列
    public static void method2() {
        List<WeakReference<byte[]>> list = new ArrayList<>();
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        for(int i = 0; i < 9; i++) {
            WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB], queue);
            list.add(weakReference);
            for(WeakReference<byte[]> wake : list) {
                System.out.print(wake.get() + ",");
            }
            System.out.println();
        }
        
        
        System.out.println("===========================================");
        // 配合引用队列释放软引用
        Reference<? extends byte[]> poll = queue.poll();
        while (poll != null) {
            list.remove(poll);
            poll = queue.poll();
        }
        for(WeakReference<byte[]> wake : list) {
            System.out.print(wake.get() + ",");
        }
    }

}
[B@7f31245a 
[B@7f31245a [B@6d6f6e28 
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 
[GC (Allocation Failure) [PSYoungGen: 1879K->488K(6144K)] 14167K->13094K(19968K), 0.0037759 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 [B@45ee12a7 
[GC (Allocation Failure) [PSYoungGen: 4696K->488K(6144K)] 17302K->13094K(19968K), 0.0009393 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null [B@330bedb4 
[GC (Allocation Failure) [PSYoungGen: 4809K->496K(6144K)] 17415K->13126K(19968K), 0.0008360 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null null [B@2503dbd3 
[GC (Allocation Failure) [PSYoungGen: 4702K->440K(6144K)] 17332K->13078K(19968K), 0.0006972 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null null null [B@4b67cf4d 
[GC (Allocation Failure) [PSYoungGen: 4646K->440K(6144K)] 17284K->13078K(19968K), 0.0011908 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null null null null [B@7ea987ac 
[GC (Allocation Failure) [PSYoungGen: 4646K->392K(5120K)] 17284K->13030K(18944K), 0.0023634 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null null null null null [B@12a3a380 
[GC (Allocation Failure) [PSYoungGen: 4578K->64K(5632K)] 17216K->13166K(19456K), 0.0008675 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 64K->0K(5632K)] [ParOldGen: 13102K->678K(8704K)] 13166K->678K(14336K), [Metaspace: 3250K->3250K(1056768K)], 0.0091226 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
null null null null null null null null null [B@29453f44 
循环结束:10
Heap
 PSYoungGen      total 5632K, used 4370K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
  eden space 4608K, 94% used [0x00000000ff980000,0x00000000ffdc4b30,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 8704K, used 678K [0x00000000fec00000, 0x00000000ff480000, 0x00000000ff980000)
  object space 8704K, 7% used [0x00000000fec00000,0x00000000feca98f8,0x00000000ff480000)
 Metaspace       used 3257K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

2、垃圾回收算法

1)标记清除

Mark Clean

  • 速度较快
  • 会产生内存碎片(空间不连续)

首先没有引用的对象做一个标记为垃圾

然后清除垃圾对象所占用的空间 (这里并不是把内存每个字节进行清0操作,而是把对象占用没存的起始结束地址,记录下来,放在空闲的地址列表里,当下次在分配对象时,到这个列表中找由于没有足够空间能容纳新对象,如果有,进行内存分配)

image-20210725230952811

2)标记整理

Mark Compact

标记的垃圾清除,可用的对象移动整理

  • 速度慢
  • 没有内存碎片

在这里插入图片描述

3)复制

Copy

From中可用的对象copy到TO中 ,再把From中的垃圾一次清空

  • 不会有内存碎片
  • 需要占用两倍内存空间

在这里插入图片描述

3、分代垃圾回收

1)概念

长时间使用的对象放在老年代中,

用完后就可以丢弃的对象放在新生代中

针对不同对象的不同声明周期,进行不同的垃圾回收策略(垃圾回收算法)。

新生代的gc发生频率较高。老年代执行频率较低,耗时也较长。

比如:

一栋居民楼, 新生代 》小区门口的垃圾场,每天 gc》 环卫工人 都来清理一次。

老年代 ==》用户家中使用陈旧的家具(桌子椅子待回收),当家中空间不足时,叫gc帮忙清理一下,比较耗时间。

2)回收原理

在这里插入图片描述

(1)新创建的对象首先分配在 eden 区当再次放入对象,eden空间不足时(第一次空间满了),会触发Minor gc ,使用可达性分析算法,根据GC root 找次空间的对象是否有引用链,判断是否有为可用或垃圾,进行标记,之后把可用的对象copy到幸存区TO中; 幸存区From 原本为空,幸存区TO做暂时缓存,完成一次Minor gc,From就指向了原本TO的空间,To指向From的空间。对象的初始寿命为0,通过头一次GC后,存活的对象寿命+1,最后把伊甸园剩下的垃圾对象全部回收。那么再次放入的对象又可继续存储到eden 区。

(2)**当第二次eden空间不足时,**再次触发Minor gc ,区别是它同时也会找得到幸存区中的对象进行判断,然后再次先移动到to —> from,存活的对象寿命+1 (此时from中的 -》to -》from 寿命 为 2),其他没在from中的对象全部回收。

(3)后面依次类推,当经过15次Minor gc垃圾回收时,from幸存区存在寿命值为15的对象(这个对象价值比较高),当该对象超过阈值(>15)时,会晋升到老年代,最大的寿命是 15(对象的头 ,寿命占(4bit)所以最大表示15,15 只是最大阈值,当空间紧张时,也可能会不到15就晋升到老年代)。

(4)当新生代和老年代空间都不足时,会先触发 minor gc,如果空间仍然不足,那么就触发 full gc (停止的时间更长SWT)

(5)如果 full gc 对老年代没太大效果,那么就会报 Error:Java.Long.OutOfMemoryError

**注意:**minor gc 会引发 stop the world (暂停其他的用户线程,垃圾回收线程动作完成后,其他用户线程才能回复运行)。因为minor gc时,copy时会改变对象的地址引用,那么多个线程运行时,就会产生混乱,找不到引用。minor gc引发的STW时间较短(因为采用的是复制算法),且频繁。 full fc 引发的STW时间较长(因为采用的是标记清楚、标记整理算法)

3)JVM 参数
**含义**	          	**参数**
堆初始大小			 -Xms
堆最大大小			 -Xmx 或 -XX:MaxHeapSize=size
新生代大小			 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例			 -XX:InitialSurvivorRatio=ratio和-XX:+UseAdaptiveSizePolicy 动态比例
幸存区比例			 -XX:SurvivorRatio=ratio  // ratio默认比例为8,10M:8—>eden,1->from,1->to
晋升阈值			  -XX:MaxTenuringThreshold=threshold // 默认和垃圾回收器有关
晋升详情			  -XX:+PrintTenuringDistribution   // 打印晋升详情
GC详情			   -XX:+PrintGCDetails -verbose:gc  // 打印GC详情
FullGC前MinorGC	    -XX:+ScavengeBeforeFullGC  // FullGC前使用MinorGC
4)GC操作演示

1、给 list 分配内存,来观察 新生代和老年代的情况,什么时候触发 minor gc,什么时候触发 full gc 等情况,

使用前需要设置 jvm 参数。

// -Xms20m -Xmx20m 初始 和 最大 堆空间为20M  
// -Xmn10m  新生代为10M
// -XX:+UseSerialGC 指定垃圾回收器 (jdk8 默认的是动态幸存区比例)
// -XX:+PrintGCDetails -verbose:gc  打印gc详情
public class Code_10_GCTest {

    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;
    
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_6MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_6MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_6MB]);
    }

}
-----------------------------初始化时的信息-----------------------------------------
# 堆
Heap 
 # 新生代  # total 9216K,-Xmn10m, to的1M空着不能用,used 2024K 初始使用2M
 def new generation   total 9216K, used 2024K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
 # 伊甸园  
  eden space 8192K,  24% used [0x00000000fec00000, 0x00000000fedfa238, 0x00000000ff400000)
  # 幸存区from 1M
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  # 幸存区to 1M
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
  # 老年代/晋升代
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
  # 元空间
 Metaspace       used 3187K, capacity 4496K, committed 4864K, reserved 1056768K
  # 类空间
  class space    used 345K, capacity 388K, committed 512K, reserved 1048576K
  
-----------------------------添加字节数组内存不足时的信息------------------------------------
[GC (Allocation Failure) [DefNew: 5216K->894K(9216K), 0.0033020 secs] 13408K->10110K(19456K), 0.0033751 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
# real=0.00 secs SWT四舍五入的时间
Heap  
 def new generation   total 9216K, used 8167K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  88% used [0x00000000fec00000, 0x00000000ff31a5f0, 0x00000000ff400000)
  from space 1024K,  87% used [0x00000000ff500000, 0x00000000ff5df8c8, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 9216K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  90% used [0x00000000ff600000, 0x00000000fff00020, 0x00000000fff00200, 0x0000000100000000)
 Metaspace       used 4646K, capacity 4750K, committed 4992K, reserved 1056768K
  class space    used 520K, capacity 565K, committed 640K, reserved 1048576K

2、大对象

// -Xms20m -Xmx20m 初始 和 最大 堆空间为20M  
// -Xmn10m  新生代为10M
// -XX:+UseSerialGC 指定垃圾回收器 (jdk8 默认的是动态幸存区比例)
// -XX:+PrintGCDetails -verbose:gc  打印gc详情
public class Code_10_GCTest {

    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;
    
    public static void main(String[] args) {
         // 当在主线程内存不足时,程序会结束
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
        list.add(new byte[_8MB]);
        
        // 当在子线程内存不足时,主线程并不会停止,其他程序还可以运行
        ----------------------------
         new Thread(() -> {
            ArrayList<byte[]> list = new ArrayList<>();
            list.add(new byte[_8MB]);
            list.add(new byte[_8MB]);
            //list.add(new byte[_7MB]);
        }).start();
    }
}
添加一个8M字节数组新生代放不下,会直接晋升到老年代
-----------------------------添加两个8M字节数组内存不足时的信息--------------------------------
[GC (Allocation Failure) [DefNew: 4191K->894K(9216K), 0.0031727 secs][Tenured: 8192K->9084K(10240K), 0.0044397 secs] 12383K->9084K(19456K), [Metaspace: 4162K->4162K(1056768K)], 0.0077011 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [Tenured: 9084K->9028K(10240K), 0.0040138 secs] 9084K->9028K(19456K), [Metaspace: 4162K->4162K(1056768K)], 0.0040633 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
	at cn.itcast.jvm.t2.Demo2_1.lambda$main$0(Demo2_1.java:20)
	at cn.itcast.jvm.t2.Demo2_1$$Lambda$1/1023892928.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)
Heap
 def new generation   total 9216K, used 1293K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  15% used [0x00000000fec00000, 0x00000000fed43550, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 9028K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  88% used [0x00000000ff600000, 0x00000000ffed1070, 0x00000000ffed1200, 0x0000000100000000)
 Metaspace       used 4683K, capacity 4748K, committed 4992K, reserved 1056768K
  class space    used 521K, capacity 560K, committed 640K, reserved 1048576K

4、垃圾回收器

  • 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
  • 并发收集:**指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。**用户程序在继续运行,而垃圾收集程序运行在另一个 CPU 上
  • **吞吐量:**即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99% 。
1)串行
  • 单线程
  • 堆内存较少,适合个人电脑
  • -XX:+UseSerialGC=serial + serialOld serial 采用复制算法 serialOld采用标记加整理算法

在这里插入图片描述

  • 安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。
    因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态。

  • Serial 收集器是最基本的、发展历史最悠久的收集器
    **特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。**对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)!

  • ParNew 收集器是 Serial 收集器的多线程版本
    特点:多线程、ParNew 收集器默认开启的收集线程数与CPU的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题

  • Serial Old 收集器是 Serial 收集器的老年代版本
    特点:同样是单线程收集器,采用标记-整理算法

2)吞吐量优先
  • 多线程
  • 堆内存较大,多核 cpu
  • 让单位时间内,STW 的时间最短 ( 假如1h触发2次gc每次0.2s:0.2 0.2 = 0.4)

在这里插入图片描述

-XX:+UseParallelGC ~ -XX:+UsePrallerOldGC  // jdk1.8默认开启的垃圾回收器
-XX:+UseAdaptiveSizePolicy  // 调整自适应GC策略开关
-XX:GCTimeRatio=ratio       // 1/(1+radio)  调整吞吐量的    ratio大 堆变大  
-XX:MaxGCPauseMillis=ms 	// 200ms 最大的垃圾收集停顿时间(
-XX:ParallelGCThreads=n   // 指定垃圾回收运行线程数
  • Parallel Scavenge 收集器与吞吐量关系密切,故也称为吞吐量优先收集器
    特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)

    该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与 ParNew 收集器最重要的一个区别)

  • GC自适应调节策略:Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy 参数。
    当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、
    晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略。

  • Parallel Scavenge 收集器使用两个参数控制吞吐量

    XX:MaxGCPauseMillis=ms 控制最大的垃圾收集停顿时间(默认200ms)
    XX:GCTimeRatio=rario 直接设置吞吐量的大小

  • Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本
    特点:多线程,采用标记-整理算法(老年代没有幸存区)

3)响应时间优先
  • 多线程

  • 堆内存较大,多核 cpu

  • **尽可能让 STW 的单次时间最短 ** (假如1h触发5次gc每次01.s:0.1 +0.1+ 0.1+ 0.1+ 0.1 = 0.5)

在这里插入图片描述

CMS 收集器:Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器
特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。 如 web 程序、b/s 服务

CMS 收集器的运行过程分为下列4步:
**初始标记:**标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。
**并发标记:**进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
**重新标记:**为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题

**并发清除:**对标记的对象进行清除回收,清除的过程中,可能任然会有新的垃圾产生,这些垃圾就叫浮动垃圾。如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,当然这也是很耗费时间的!

CMS 收集器的内存回收过程是与用户线程一起并发执行的,可以搭配 ParNew 收集器(多线程,新生代,复制算法)与 Serial Old 收集器(单线程,老年代,标记-整理算法)使用。

4)G1 垃圾收集器

定义: Garbage First 2004年论文发布 2009JDK6u14 2017Jdk9默认
适用场景:

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

相关参数:
JDK8 并不是默认开启的,所需要参数开启

-XX:+UseG1GC  // 显示启用G1 收集器
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time  // 默认暂停目标200ms
【1】G1 垃圾回收阶段
img

分为下面三个阶段:

Young Collection:对新生代垃圾收集
Young Collection + Concurrent Mark:如果老年代内存到达一定的阈值了,新生代垃圾收集同时会执行一些并发的标记。
**Mixed Collection:**会对新生代 + 老年代 + 幸存区等进行混合收集,然后收集结束,会重新进入新生代收集。

【2】Young Collection

新生代存在 STW:
分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多个小区间,方便控制 GC 产生的停顿时间!
E:eden,S:幸存区([survive](javascript:😉 ),O:老年代
新生代收集会产生 STW !

在这里插入图片描述

【3】Young Collection + CM

在 Young GC 时会进行 GC Root 的初始化标记(新生代)

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

-XX:InitiatingHeapOccupancyPercent=percent (默认45%) 老年代占整个堆内存的45%进行并发标记
在这里插入图片描述

【4】Mixed Collection

Mixed(混合的) Collection会对 E S O 进行全面的回收,最终标记会 STW。拷贝存活会 STW
-XX:MaxGCPauseMills=xxms 用于指定最长的停顿时间!

问:为什么有的老年代被拷贝了,有的没拷贝?
因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)
在这里插入图片描述

【5】Full GC

image-20210726215534584

新生代都相同,老年代CMS和G1与其他两个有所区别:

G1 在老年代内存不足时(老年代所占内存超过阈值)
如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理
**如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC,**然后退化成 serial Old 收集器串行的收集,就会导致停顿的时候长。

【6】Young Collection 跨代引用问题

新生代回收的跨代引用(老年代引用新生代)问题

如果遍历整个老年代去找GCroot ,效率很低,因此采用卡表的技术,在分为一个个card,若card引用了新生代就标记为赃卡。 在脏卡中遍历Gcroot,减少时间。

  • 卡表 与 Remembered Set
    • Remembered Set 存在于E中,用于保存新生代对象对应的脏卡
      • **脏卡:**O 被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡
  • 在引用变更时通过 post-write barried + dirty card queue
  • concurrent refinement threads 更新 Remembered Set

在这里插入图片描述

【7】Remark重新标记阶段

在垃圾回收时,收集器处理对象的过程中

  • 黑色:已被处理,需要保留的对象
  • 灰色:正在处理中的
  • 白色:还未处理的

image-20210726221215225

但是在并发标记过程中,有可能 A 被处理了以后未引用 C ,但该处理过程还未结束,在处理过程结束之前 A 引用了 C ,这时就会用到 remark 。
过程如下

  • **C被引用了在回收前B不引用了, C 未被引用,这时 A 又引用了 C ,就会给 C 加一个写屏障,**写屏障的指令会被执行,将 C 放入一个队列当中,并将 C 变为 处理中状态
  • 在并发标记阶段结束以后,重新标记阶段会 STW ,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它,由灰色变成黑色。

image-20210726221234258

image-20210726221248561

【8】jdk8-9的优化
1. JDK 8u20 字符串去重

u 代表 Update

-XX:+UseStringDeduplication  // 手动开启

image-20210802002135345

  • 节省了大量内存
  • 新生代回收时间略微增加,导致略微多占用 CPU
2. JDK 8u40 并发标记类卸载

在并发标记阶段结束以后,就能知道哪些类不再被使用如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类

-XX:+ClassUnloadingWithConcurrentMark // 默认启用
  • 比如 框架程序 ,自定义的类加载器 当其中所以的类和类的实例 不再使用,会进行类卸载。释放内存空间。
3. JDK 8u60 回收巨型对象

一个对象大于region的一半时,就称为巨型对象

  • G1不会对巨型对象进行拷贝,回收时被优先考虑

  • G1会跟踪老年代所有进来(incoming)的引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

image-20210802003607387

image-20210802003831792

4. JDK 9 并发标记起始时间的调整
  • 并发标记必须在堆空间占满前完成,否则退化为 FulGC
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent(新生代 老年代占比阈值 默认45%)
  • JDK 9 可以动态调整
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 进行数据采样并动态调整阈值
    • 总会添加一个安全的空挡空间(让Heap的空间足够的大,容纳浮动的垃圾)尽可能的避免退化为FullGC
5.JDK 9 更高的回收

250+的增强 和 180bug修复

Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide, Release 12 (oracle.com)

5、GC垃圾回收调优

查看虚拟机参数命令

D:\JavaJDK1.8\bin\java  -XX:+PrintFlagsFinal -version | findstr "GC"

可以根据参数去查询具体的信息

1)调优领域

内存
锁竞争
cpu 占用
io
gc

2)确定目标

低延迟/高吞吐量? 选择合适的GC

CMS G1 ZGC 低延迟
ParallelGC 高吞吐量

其他的虚拟机 Zing JVM

3)最快的 GC

首先排除减少因为自身编写的代码而引发的内存问题

  • 查看 Full GC 前后的内存占用,考虑以下几个问题
    • 数据是不是太多?
      • resultSet = statement.executeQuery(“select * from 大表 limit n”)
    • 数据表示是否太臃肿
      • 对象图
      • 对象大小 16 Integer 24 int 4 能用基本类型的 就不用包装类型
    • 是否存在内存泄漏
      • static Map map … 尽量不要使用java的堆内存做缓存
      • 第三方缓存实现
4)新生代调优
新生代的特点
  • 所有的 new 操作分配内存都是非常廉价的
    • TLAB thread-lcoal allocation buffer 线程局部分配缓冲区 每个线程对自己私有的eden内存,进行对象的内存分配
  • 死亡对象回收零代价 因为是幸存发对象复制到to中
  • 大部分对象用过即死(朝生夕死)
  • Minor GC 所用时间远小于 Full GC
新生代内存越大越好么?

不是

  • 新生代内存太小:频繁触发 Minor GC ,会 STW ,会使得吞吐量下降
  • 新生代内存太大:老年代内存占比有所降低,会更频繁地触发 Full GC。而且触发 Minor GC 时,清理新生代所花费的时间会更长
  • 新生代内存设置为内容纳 [并发量(请求-响应)]* 的数据为宜
  • 幸存区需要能够保存 【当前活跃对象+需要晋升的对象】 太小了会让一些对象提前晋升到old区
  • 晋升阈值配置得当,让长时间存活的对象尽快晋升 释放晋升区(幸存区的)空间,减少复制时间
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistrubution // 显示晋升区的对象详情
5)老年代调优

以 CMS 为例:

CMS 的老年代内存越大越好
先尝试不做调优,如果没有 Full GC ,那么已经内存占用,先尝试调优新生代。
观察发现 Full GC 时老年代内存占用**,将老年代内存预设调大 1/4 ~ 1/3**

 -XX:CMSInitiatingOccupancyFraction=percent  // cms触发老年代的回收占比
6)案例

案例1:Full GC 和 Minor GC 频繁

调大新生代的内存

案例2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)

查看日志 看那个阶段比较耗时 参考响应时间处

案例3:老年代充裕情况下,发生 Full GC(jdk1.7)

1.7 永久代空间不足 也会触发 整个堆的Full GC垃圾回收 1.8 改为元空间

四、类加载与字节码技术

在这里插入图片描述

1、类文件结构【了解】

public class HelloWorld { 
    public static void main(String[] args) {
        System.out.println("hello world"); 
    }
}

执行 javac -parameters -d . HellowWorld.java
通过 javac 类名.java 编译 java 文件后,会生成一个 .class 的文件!

字节码文件:

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e 
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63 
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01 
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63 
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f 
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16 
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13 
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61 
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46 
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e 
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74 
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61 
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61 
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f 
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72 
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76 
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a 
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01 
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00 
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00 
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00 
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00 
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a 
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b 
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00 
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00 
0001120 00 00 02 00 14

根据 JVM 规范,类文件结构如下:

u4 			   magic
u2             minor_version;    
u2             major_version;    
u2             constant_pool_count;    
cp_info        constant_pool[constant_pool_count-1];    
u2             access_flags;    
u2             this_class;    
u2             super_class;   
u2             interfaces_count;    
u2             interfaces[interfaces_count];   
u2             fields_count;    
field_info     fields[fields_count];   
u2             methods_count;    
method_info    methods[methods_count];    
u2             attributes_count;    
attribute_info attributes[attributes_count];
1)魔数

对应字节码文件的 0~3 个字节
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
ca fe ba be :意思是 .class 文件,不同的东西有不同的魔数,比如 jpg、png 图片等!

2)版本

u2 minor_version;
u2 major_version;
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
00 00 00 34:34H(16进制) = 52(10进制),代表JDK8

4~7 字节,表示类的版本 00 34(52) 表示是 Java 8

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

3)常量池

常量池参考文档

CONSTANT_Class 7

CONSTANT_Fieldref 9

CONSTANT_Methodref 10

CONSTANT_InterfaceMethodref 11

CONSTANT_String 8

CONSTANT_Integer 3

CONSTANT_Float 4

CONSTANT_Long 5

CONSTANT_Double 6

CONSTANT_NameAndType 12

CONSTANT_Utf8 1

CONSTANT_MethodHandle 15

CONSTANT_MethodType 16

CONSTANT_InvokeDynamic 18

8~9 字节,表示常量池长度,00 23 (35) 表示常量池有 #1~#34项,注意 #0 项不计入,也没有值

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

第#1项 0a 表示一个 Method 信息,00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 项来获得

这个方法的【所属类】和【方法名】

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

第#2项 09 表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和 # 23 项

来获得这个成员变量的【所属类】和【成员变量名】

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

第#3项 08 表示一个字符串常量名称,00 18(24)表示它引用了常量池中 #24 项

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

第#4项 0a 表示一个 Method 信息,00 19(25) 和 00 1a(26) 表示它引用了常量池中 #25 和 #26项来获得这个方法的【所属类】和【方法名】

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

第#6项 07 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

第#7项 01 表示一个 utf8 串,00 06 表示长度,3c 69 6e 69 74 3e 是【 】

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

第#8项 01 表示一个 utf8 串,00 03 表示长度,28 29 56 是【()V】其实就是表示无参、无返回值

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

4)访问标识与继承信息

21 表示该 class 是一个类,公共的

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

05 表示根据常量池中 #5 找到本类全限定名

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

06 表示根据常量池中 #6 找到父类全限定名

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

表示接口的数量,本类为 0

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

image-20210802181401188

5)Field 信息

表示成员变量数量,本类为 0

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

image-20210802181643842

6)Method 信息

表示方法数量,本类为 2

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成

红色代表访问修饰符(本类中是 public)

蓝色代表引用了常量池 #07 项作为方法名称

绿色代表引用了常量池 #08 项作为方法参数描述

黄色代表方法属性数量,本方法是 1

红色代表方法属性

  • 00 09 表示引用了常量池 #09 项,发现是【Code】属性

  • 00 00 00 2f 表示此属性的长度是 47

  • 00 01 表示【操作数栈】最大深度

  • 00 01 表示【局部变量表】最大槽(slot)数

  • 00 00 00 02 表示方法细节属性数量,本例是 2

  • 00 0a 表示引用了常量池 #10 项,发现是【LineNumberTable】属性

  • 00 00 00 06 表示此属性的总长度,本例是 6

  • 00 01 表示【LineNumberTable】长度

  • 00 00 表示【字节码】行号 00 04 表示【java 源码】行号

  • 00 0b 表示引用了常量池 #11 项,发现是【LocalVariableTable】属性

  • 00 00 00 0c 表示此属性的总长度,本例是 12

  • 00 01 表示【LocalVariableTable】长度

  • 00 00 表示局部变量生命周期开始,相对于字节码的偏移量

  • 00 05 表示局部变量覆盖的范围长度

  • 00 0c 表示局部变量名称,本例引用了常量池 #12 项,是【this】

  • 00 0d 表示局部变量的类型,本例引用了常量池 #13 项,是【Lcn/itcast/jvm/t5/HelloWorld;】

  • 00 00 表示局部变量占有的槽位(slot)编号,本例是 0

7)附加属性
  • 00 01 表示附加属性数量

  • 00 13 表示引用了常量池 #19 项,即【SourceFile】

  • 00 00 00 02 表示此属性的长度

  • 00 14 表示引用了常量池 #20 项,即【HelloWorld.java】

0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00

0001120 00 00 02 00 14

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

2、字节码指令【掌握】

字节码指令

2.1 基础代码指令

1)public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令

  1. 2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数

  2. b7 => invokespecial 预备调用构造方法,哪个方法呢?

  3. 00 01 引用常量池中 #1 项,即【 Method java/lang/Object.""😦)V 】

  4. b1 表示返回

2)public static void main(java.lang.String[]); 主方法的字节码指令

b2 00 02 12 03 b6 00 04 b1

  1. b2 => getstatic 用来加载静态变量,哪个静态变量呢?

  2. 00 02 引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】

  3. 12 => ldc 加载参数,哪个参数呢?

  4. 03 引用常量池中 #3 项,即 【String hello world】

  5. b6 => invokevirtual 预备调用成员方法,哪个方法呢?

  6. 00 04 引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】

  7. b1 表示返回

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5

2.2 javap 工具分析

Oracle 提供了 javap 工具来反编译 class 文件,不用再分析复杂的类文件结构了。

image-20210802205558084

image-20210802210014372

2.3 图解方法执行流程
1)原始java 代码
package cn.itcast.jvm.t3.bytecode;
/*** 演示 字节码指令 和 操作数栈、常量池的关系 */ 
public class Demo3_1 {
    public static void main(String[] args) { 
        int a = 10; 
        int b = Short.MAX_VALUE + 1; 
        int c = a + b; 
        System.out.println(c); 
    } 
}
2)编译后的字节码文件

image-20210802211025805

3)常量池载入运行时常量池
4)方法字节码载入方法区
5)main 线程开始运行,分配栈帧内存

3、4、5图示如下

image-20210802211343146

6)执行引擎开始执行字节码

bipush 10将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有

还有其他类型的数据入栈:

sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)

ldc 将一个 int 压入操作数栈

ldc2_w 将一个 long 压入操作数栈**(分两次压入,因为 long 是 8 个字节)**

这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池

image-20210802212350289

istore_1

将操作数栈顶数据弹出,存入局部变量表的 slot 1

image-20210802212933720

image-20210802212104443

ldc #3

从常量池加载 #3 数据到操作数栈

注意 Short.MAX_VALUE 是 32767,

所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的

image-20210802213647613

istore_2

image-20210802213754332

image-20210802214035741

iload_1

iload_2

image-20210802214349971

iadd

image-20210802214549275

image-20210802214810897

istore_3

image-20210802215020271

getstatic #4

image-20210802215234327

image-20210802215330067

iload_3

image-20210802215639337

invokevirtual #5

找到常量池 #5 项

定位到方法区 java/io/PrintStream.println:(I)V 方法

生成新的栈帧(分配 locals、stack等)

传递参数,执行新栈帧中的字节码

image-20210802215946869

执行完毕,弹出栈帧

清除 main 操作数栈内容

image-20210802220120593

return

完成 main 方法调用,弹出 main 栈帧

程序结束

2.4 练习
/**
 * 从字节码角度分析 a++  相关题目
 */
public class Demo3_2 {
    public static void main(String[] args) {
        int a = 10;      // a++ 使用一次后+1  ++a自身+1后再使用
        int b = a++;   // b=10  a= 11        a=a+1    a++先加载到操作计数栈,再数据槽内+1
        // int b0 = a++ + 1; // b0 = 12 
        int b1 = a++ + ++a;  // b1=11+12+1=24  a=11+1+1=13
        int b2 = a++ + ++a + a--; // b2=13+14+1+15=43  a=13+1+1-1=14
        int c = a++ + a--;
        System.out.println(a);
        System.out.println("b1 = " + b1);  
        System.out.println("b2 = " + b2);
        System.out.println(c);
    }
}

2.5条件判断指令

image-20210803075245090

几点说明:

  • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节

  • goto 用来进行跳转到指定行号的字节码

  • image-20210803075641207

以上比较指令中没有 long,flfloat,double 的比较,那么它们要比较怎么办?

参考 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp

2.6循环控制指令

其实循环控制还是前面的那些指令,例如 while 循环

image-20210803080227232

再看看 for 循环:

image-20210803081139992

注意:比较 while 和 for 的字节码,你发现它们是一模一样的,殊途也能同归

2.7 练习
public class Demo3_6_1 {
    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;
            i++;
            // 每次局部变量槽的值+1 都会被计数栈的0 覆盖掉  
            System.out.println("x = " + x);  
        }
        System.out.println(x); // 所以 x 始终 为0
    }
}
2.8 构造方法
1)类的构造方法<cinit>()V

image-20210803082457148

编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 ()V : i 最终 为30

image-20210803082438987

()V 方法会在类加载的初始化阶段被调用

练习:可以自己调整一下 static 变量和静态代码块的位置,观察字节码的改动

2)类的实例对象构造方法<init>()V

image-20210803104150266

编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,

形成新的构造方法,但【原始构造方法 (与类名相同的方法)】内的代码总是在最后。

image-20210803104415268

2.9 方法调用

构造方法 final -》invokespecial 静态绑定

static -》invokestatic 静态绑定 编译期间就能确定

public -》invokevirtual 编译期间不确定调用哪个对象的方法 可能出现方法重写的情况 动态绑定,运行时才能确定方法的入口地址

public class Demo3_9 {
    public Demo3_9() { }

    private void test1() { }

    private final void test2() { }

    public void test3() { }

    public static void test4() { }

    @Override
    public String toString() {
        return super.toString();
    }

    public static void main(String[] args) {
        Demo3_9 d = new Demo3_9(); // 在堆中创建一个对象,调用完初始化方法后,并把对象地址放到局部变量表中
        d.test1();
        d.test2();
        d.test3();
        d.test4();  // 对象调用静态方法 会产生 20 21两条不必要的虚拟机指令
        Demo3_9.test4();
        d.toString();
    }

}

image-20210803140136397

2.10 多态的原理
import java.io.IOException;

/**
 * 演示多态原理,注意加上下面的 JVM 参数,禁用指针压缩  64位采用了指针压缩技术
 * -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
 */
public class Demo3_10 {

    public static void test(Animal animal) {
        animal.eat();
        System.out.println(animal.toString());
    }

    public static void main(String[] args) throws IOException {
        test(new Cat());
        test(new Dog());
        System.in.read();
    }
}

abstract class Animal {
    public abstract void eat();

    @Override
    public String toString() {
        return "我是" + this.getClass().getSimpleName();
    }
}

class Dog extends Animal {

    @Override
    public void eat() {
        System.out.println("啃骨头");
    }
}

class Cat extends Animal {

    @Override
    public void eat() {
        System.out.println("吃鱼");
    }
}

1)运行代码

停在 System.in.read() 方法上,这时运行 jps 获取进程 id

2)运行HSDB工具

进入 JDK 安装目录,执行

java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

进入图形界面 attach 进程 id,打开 Tools -> Find Object By Query

输入 select d from cn.itcast.jvm.t3.bytecode.Dog d 点击 Execute 执行

4)查看对象内存结构

点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是MarkWord,后 8 字节就是对象的 Class 指针

但目前看不到它的实际地址,可以通过 Windows -> Console 进入命令行模式,执行

image-20210805093111191

mem 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)

mem 0x00000001299b4978 2

类中的方法

image-20210805100748193

初始化object中的方法

1 finalize是在对象回收前做一些清扫工作,以及可清理栈上的内存。

比如调用本地方法可能在栈上创建对象。即使对象不可达,也并不是非死不可,还是可以抢救一下,当一个对象没有与GCroot相连的引用链时,它将被第一次标记,然后判断对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法(没有就没法抢救了),或者finalize()方法已经被虚拟机调用过,则不必执行,如果有必要执行,该对象就会被放到一个叫做F-Queue的队列里,并且由一个低优先级的Finalizer线程去执行它。但是不保证会等待方法执行结束。finalize方法是对象逃脱的最后机会,稍后GC会对F-Queue中的对象进行二次标记,只要对象重新链接上了引用链,它就会被移出即将回收的集合。

2 什么时候应该使用它

finalize()方法中一般用于释放非资源(如打开的文件资源、数据库连接等),或是调用非Java方法(native方法)时分配的内存(比如C语言的malloc()系列函数)。

3 为什么应该避免使用它

首先,由于finalize()方法的调用时机具有不确定性,从一个对象变得不可到达开始,到finalize()方法被执行,所花费的时间这段时间是任意长的。我们并不能依赖finalize()方法能及时的回收占用的资源,可能出现的情况是在我们耗尽资源之前,gc却仍未触发,因而通常的做法是提供显示的close()方法供客户端手动调用。另外,重写finalize()方法意味着延长了回收对象时需要进行更多的操作,从而延长了对象回收的时间。

4 让对象再活一次

利用finalize()方法最多只会被调用一次的特性,我们可以实现延长对象的生命周期。

image-20210805095903866

5)查看类的vtable

方法1:Alt+R 进入 Inspector 工具,输入刚才的 Class 内存地址,看到如下界面

6)验证方法地址

通过 Tools -> Class Browser 查看每个类的方法定义

7)小结

当执行 invokevirtual 指令时,

  1. 先通过栈帧中的对象引用找到对象

  2. 分析对象头,找到对象的实际 Class

  3. Class 结构中有 vtable(虚方法表),它在类加载的链接阶段就已经根据方法的重写规则生成好了

  4. 查表得到方法的具体地址

  5. 执行方法的字节码

2.11 异常处理
try-catch
public class Demo3_11_1 {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        }
    }
}

image-20210805102347892

  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,

一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型(异常范围),

如果一致,进入 target 所指示行号

  • 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置

执行catch (Exception e)中的语句

single-catch
public class Demo3_11_2 {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (ArithmeticException e) {
            i = 30;
        } catch (NullPointerException e) {
            i = 40;
        } catch (Exception e) {
            i = 50;
        }
    }

}

因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用,节省栈帧的内存

image-20210805103353273

image-20210805103619215

multi-catch

多个异常可以写在一个catch块中

public class Demo3_11_3 {

    public static void main(String[] args) {
        try {
            Method test = Demo3_11_3.class.getMethod("test");
            test.invoke(null);
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    public static void test() {
        System.out.println("ok");
    }
}

image-20210805103945730

finally
public class Demo3_11_4 {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        } finally {
            i = 30;
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D1njzoWm-1628668126858)(jvm01.assets/image-20210805112108617.png)]

练习 - finally 面试题
finally 出现了 return

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y3aM7TLQ-1628668126859)(jvm01.assets/image-20210805113816526.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wiqo18M6-1628668126860)(jvm01.assets/image-20210805113451085.png)]

由于 fifinally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准

至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常

public class Demo3_12_1 {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }

    public static int test() {
        try {
            int i = 1/0;
            return 10;
        } finally {
            return 20;
        }
    }
}
finally 对返回值影响

finally 不带return的会把异常抛出去

public class Demo3_12_2 {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }

    public static int test() {
        int i = 10;
        try {
            return i;
        } finally {
            i = 20;
        }
    }
}

image-20210805140606631

2.12 synchronized
public class Demo3_13 {

    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}

方法级别的 synchronized 不会在字节码指令中有所体现

image-20210805142820103

image-20210805143038942

3、编译期处理(语法糖)

**语法糖 :其实就是指 java 编译器把 .java 源码编译为 .class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担

算是 java 编译器给我们的一个额外福利(给糖吃嘛)

注意:

  • 以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。

  • 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。

3.1 默认构造器

image-20210805143809486

3.2 JDK 5 自动拆装箱

image-20210805144215553

3.3 JDK 5 加入泛型

泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理image-20210805150144702

image-20210805145658816

image-20210805150354128

泛型集合取值:方法参数上带的泛型和返回值上的信息,使用反射可以获取到的

image-20210805150617554

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class Candy3 {
    public static void main(String[] args) throws Exception {
        List<Integer> list = new ArrayList<>();
        list.add(10); // 实际调用的是 List.add(Object e)
        Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);

        Method test = Candy3.class.getMethod("test", List.class, Map.class);
        Type[] types = test.getGenericParameterTypes();
        for (Type type : types) {
            if (type instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) type;
                System.out.println("原始类型 - " + parameterizedType.getRawType());
                Type[] arguments = parameterizedType.getActualTypeArguments();
                for (int i = 0; i < arguments.length; i++) {
                    System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
                }
            }
        }

    }

    public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
        return null;
    }
}
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object
3.4 JDK 5 可变参数
public class Candy4 {
    public static void foo(String... args) {
        String[] array = args; // 直接赋值
        System.out.println(array);
    }
    public static void main(String[] args) {
        foo("hello", "world");
    }
}

image-20210805151348842

注意: 如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会传递 null 进去。

3.5 JDK 5 foreach 循环

仍是 JDK 5 开始引入的语法糖,

数组的循环遍历:

public class Candy5_1 {
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦
        for (int e : array) {
            System.out.println(e);
        }
    }
}

// 实际指针移动取值

image-20210805153016227

对于集合而言:

image-20210805152917572

3.6 JDK 7 switch 字符串&枚举
字符串底层转换

从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖

public class Candy6_1 {
    public static void choose(String str) {
        switch (str) {
            case "hello": {
                System.out.println("h");
                break;
            }
            case "world": {
                System.out.println("w");
                break;
            }
        }
    }
}

注意 switch 配合 String 和枚举使用时,变量不能为null,

原因分析完语法糖转换后的代码应当自然清楚

会被编译器转换为:

image-20210805154556188

执行了两遍 switch,

第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应byte 类型,

第二遍才是利用 byte 执行进行比较。

为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢?

**hashCode 是为了提高效率(因为绝大多数字符串哈希值都是唯一的),**减少可能的比较;而 equals 是为了防止 hashCode 冲突,

例如 BM 和 C. 这两个字符串的hashCode值都是2123 ,如果有如下代码:

public class Candy6_2 {
    public static void choose(String str) {
        switch (str) {
            case "BM": {
                System.out.println("h");
                break;
            }
            case "C.": {
                System.out.println("w");
                break;
            }
        }
    }
}

会被编译器转换为

image-20210805155127422

枚举底层转换
enum Sex {
    MALE, FEMALE;
}
public class Candy7 {
    public static void foo(Sex sex) {
        switch (sex) {
            case MALE:
                System.out.println("男"); break;
            case FEMALE:
                System.out.println("女"); break;
        }
    }
}

image-20210805155242457

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jd1fgRTr-1628668126872)(jvm01.assets/image-20210805155541351.png)]

3.7 JDK 7 枚举类

JDK 7 新增了枚举类,以前面的性别枚举为例:

image-20210805161042573

image-20210805161122721

3.8 JDK 7 try-with-resources

JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources`:

try(资源变量 = 创建资源对象){
    
} catch( ) {
    
}

其中资源对象需要实现 AutoCloseable 接口,

例如 InputStream 、 OutputStream 、 Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,**使用 try-with- resources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,**例如:

public class Candy9 { 
    public static void main(String[] args) { 
        try(InputStream is = new FileInputStream("d:\\1.txt")) {
            System.out.println(is); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
    } 
}

会被转换为:

image-20210805164816355

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HgTlDrfk-1628668126876)(jvm01.assets/image-20210805170825056.png)]

为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fifianlly 中如果抛出了异常)

public class Test6 { 
    public static void main(String[] args) { 
        try (MyResource resource = new MyResource()) { 
            int i = 1/0;
        } catch (Exception e) { 
            e.printStackTrace();
        }
    } 
}

class MyResource implements AutoCloseable {
    public void close() throws Exception { 
        throw new Exception("close 异常");
    } 
}

输出:

java.lang.ArithmeticException: / by zero 
    at test.Test6.main(Test6.java:7) 
    Suppressed: java.lang.Exception: close 异常 
        at test.MyResource.close(Test6.java:18) 
        at test.Test6.main(Test6.java:6)
3.9 方法重写时的桥接方法

我们都知道,方法重写时对返回值分两种情况

  • 父子类的返回值完全一致

  • 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)

synthetic bridge 在JVM内部生产

image-20210805171439460

3.10 匿名内部类

image-20210805171708309

image-20210805171903990

引用局部变量的匿名内部类,源代码:

image-20210805171953723

image-20210805172219097

注意这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 fifinal 的

因为在创建Candy11$1 对象时,将 x 的值赋值给了 Candy11$1 对象的 val&x 属 性 , 所以x 不 应 该 再 发 生 变化了 , 如 果 变化 , 那 么 val&x 属性没有机会再跟着一起变化。

4、类加载阶段

image-20210805214745144 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序进行,而解析阶段则不一定,它在某些情况下可能在初始化阶段后在开始,因为java支持运行时绑定。

4.1 加载

加载过程(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象)。这里的数据源可能是各种各样的形态,比如 jar 文件,class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程

将类的字节码载入方法区中:

内部采用 C++ 的 instanceKlass结构描述 java 类,它的重要 field 有:

  • _java_mirror 即 java 的类镜像(c++交互的 ),例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用(想访问String.class,就要先访问镜像对象–> 访问String.class)

  • _super 即父类

  • _fields 即成员变量

  • _methods 即方法

  • _constants 即常量池

  • _class_loader 即类加载器

  • _vtable 虚方法表

  • _itable 接口方法表

  • 如果这个类还有父类没有加载,先加载父类,加载和链接可能是交替运行的

注意instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中。可以通过前面介绍的 HSDB 工具查看。

image-20210805174416503

正常我们用java代码写的类在JVM中是用InstanceKlass的实例表示,用来描述Java类的信息并存在元空间(metaSpace)。

img

4.2 链接

连接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转入 JVM 运行的过程中。

4.2.1.验证

验证类是否符合 JVM规范,安全性检查

用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行

修改魔数

image-20210805184444347

直接编译 加载时执行验证magic值报格式错误

提前阻止不合法的类继续运行

image-20210805184609749

4.2.2.准备
// 为 static 变量分配空间,设置默认值  
public class Load8 {
    static int a;  // 准备阶段完成
    static int b = 10;   // 类的构造方法中复制  类的加载而加载
    
    // 演示 final 对静态变量的影响
    static final int c = 20;
    static final String d = "hello";
    static final Object e = new Object();
}
  • jdk1.6以前静态变量存储在方法区,1.6后和类对象存储在堆中

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾

  • static 变量分配空间和赋值是两个步骤**,分配空间在准备阶段完成,赋值在初始化阶段完成(类的构造方法中)**

  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成

  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成(如:对象)

Classfile /C:/Users/ahcfl/Desktop/JVM/代码/jvm/out/production/jvm/cn/itcast/jvm/t3/load/Load8.class
  Last modified 2021-8-5; size 527 bytes
  MD5 checksum d31906fae8139de94847a4759aff7886
  Compiled from "Load8.java"
public class cn.itcast.jvm.t3.load.Load8
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#27         // java/lang/Object."<init>":()V
   #2 = Fieldref           #5.#28         // cn/itcast/jvm/t3/load/Load8.b:I
   #3 = Class              #29            // java/lang/Object
   #4 = Fieldref           #5.#30         // cn/itcast/jvm/t3/load/Load8.e:Ljava/lang/Object;
   #5 = Class              #31            // cn/itcast/jvm/t3/load/Load8
   #6 = Utf8               a
   #7 = Utf8               I
   #8 = Utf8               b
   #9 = Utf8               c
  #10 = Utf8               ConstantValue
  #11 = Integer            20
  #12 = Utf8               d
  #13 = Utf8               Ljava/lang/String;
  #14 = String             #32            // hello
  #15 = Utf8               e
  #16 = Utf8               Ljava/lang/Object;
  #17 = Utf8               <init>
  #18 = Utf8               ()V
  #19 = Utf8               Code
  #20 = Utf8               LineNumberTable
  #21 = Utf8               LocalVariableTable
  #22 = Utf8               this
  #23 = Utf8               Lcn/itcast/jvm/t3/load/Load8;
  #24 = Utf8               <clinit>
  #25 = Utf8               SourceFile
  #26 = Utf8               Load8.java
  #27 = NameAndType        #17:#18        // "<init>":()V
  #28 = NameAndType        #8:#7          // b:I
  #29 = Utf8               java/lang/Object
  #30 = NameAndType        #15:#16        // e:Ljava/lang/Object;
  #31 = Utf8               cn/itcast/jvm/t3/load/Load8
  #32 = Utf8               hello
{
  static int a;
    descriptor: I
    flags: ACC_STATIC

  static int b;
    descriptor: I
    flags: ACC_STATIC

  static final int c;
    descriptor: I
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: int 20

  static final java.lang.String d;
    descriptor: Ljava/lang/String;
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: String hello

  static final java.lang.Object e;
    descriptor: Ljava/lang/Object;
    flags: ACC_STATIC, ACC_FINAL

  public cn.itcast.jvm.t3.load.Load8();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/load/Load8;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2                  // Field b:I
         5: new           #3                  // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."<init>":()V
        12: putstatic     #4                  // Field e:Ljava/lang/Object;
        15: return
      LineNumberTable:
        line 7: 0
        line 10: 5
}
SourceFile: "Load8.java"
4.2.3.解析

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

java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

/**
 * 解析的含义
 */
public class Load2 {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
       ClassLoader classloader = Load2.class.getClassLoader();
        // 类加载器创建的对象  不会触发解析和初始化的  所以不会解析对象D
       Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
        
        //new C();  // 将常量池中的符号引用解析为直接引用  此时会解析对象D
        System.in.read();
    }
}

class C {
    D d = new D();
}

class D {

}

image-20210805212346436

new C();  // 将常量池中的符号引用解析为直接引用

image-20210805212120156

4.3 初始化

**初始化阶段(initialization),这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。**再来谈谈双亲委派模型,简单说就是当加载器(Class-Loader)试图加载某个类型的时候,除非父类加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载 Java 类型。

()V 方法 类的初始化方法

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2PkpGMLV-1628668126891)(jvm01.assets/image-20210805212853713.png)]

类初始化发生的时机

类初始化是【懒惰的】,一下情况会触发初始化:

  • main 方法所在的类,总会被首先初始化

  • 首次访问这个类的静态变量或静态方法时会初始化

  • 子类初始化,如果父类还没初始化,会触发初始化

  • 子类访问父类的静态变量,只会触发父类的初始化

  • Class.forName

  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串),链接的准备阶段完成不会触发初始化

  • 类对象.class 不会触发初始化

  • 创建该类的数组不会触发初始化

  • 类加载器的oadClass方法不会触发

  • Class.forName的参数2为false时

从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始
public class Load4 {
    public static void main(String[] args) {
        System.out.println(E.a);
        System.out.println(E.b);
        System.out.println(E.c);

    }
}

class E {
    public static final int a = 10;
    public static final String b = "hello";
    public static final Integer c = 20;  // Integer.valueOf(20)
    static {
        System.out.println("init E");
    }
}
典型应用 - 完成懒惰初始化单例模式

实现特点是:

懒惰实例化

初始化时的线程安全是有保障的 (类加载器)

public class Load9 {
    public static void main(String[] args) {
//        Singleton.test();
        Singleton.getInstance();
    }

}

class Singleton {

    public static void test() {
        System.out.println("test");
    }

    private Singleton() {}

    private static class LazyHolder{
        private static final Singleton SINGLETON = new Singleton();
        static {
            System.out.println("lazy holder init");
        }
    }

    public static Singleton getInstance() {
        return LazyHolder.SINGLETON;
    }
}

5、类加载器

image-20210805221329470

5.1 启动类加载器

-Xbootclasspath 表示设置 bootclasspath

其中 /a:. 表示将当前目录追加至 bootclasspath 之后。可以用这个办法替换核心。

java -Xbootclasspath:

java -Xbootclasspath/a:<追加路径>

java -Xbootclasspath/p:<追加路径>

package cn.itcast.jvm.t3.load;

public class F {
    static {
        System.out.println("bootstrap F init");
    }
}
public class Load5_1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
        System.out.println(aClass.getClassLoader()); // AppClassLoader  ExtClassLoader
    }
}

输入:java -Xbootclasspath/a:.

输出:

E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. 
cn.itcast.jvm.t3.load.Load5 
bootstrap F init 
null  // 为null说明 打印的是启动类加载器  java不能直接对启动类加载器进行访问
5.2 扩展类加载器

演示:

第一步:

package cn.itcast.jvm.t3.load;

public class G {
    static {
//        System.out.println("ext G init");
        System.out.println("classpath G init");
    }
}

/**
 * 演示 扩展类加载器
 * 在 C:\Program Files\Java\jdk1.8.0_91 下有一个 my.jar
 * 里面也有一个 G 的类,观察到底是哪个类被加载了
 */
public class Load5_2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
        System.out.println(aClass.getClassLoader());
    }
}

执行 Load5_2输出:

classpath G init 
sun.misc.Launcher$AppClassLoader@18b4aac2  // 加载的G是应用类加载器

第二步:同一个包下同名的类

package cn.itcast.jvm.t3.load;

public class G {
    static {
        System.out.println("ext G init");
        //System.out.println("classpath G init");
    }
}

执行后,生成 G.class文件 , 再打成 jar 包

到 G.class输出目录下执行命令

E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class 
已添加清单 
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)

将 jar 包拷贝到 JAVA_HOME/jre/lib/ext ( JAVA_HOME,jdk存放的位置)

再把G类中的 //System.out.println(“classpath G init”); 注释去掉 。

看类加载器使用的是扩展类加载器,还是应用类加载器。

重新执行 Load5_2,输出:

ext G init 
sun.misc.Launcher$ExtClassLoader@29453f44 // 扩展类加载器
5.3 双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则:由下到上询问,由上到下加载

image-20210810085107655

image-20210810085146492

跟踪loadClass()源码
package cn.itcast.jvm.t3.load;

import java.util.ServiceLoader;

public class Load5_3 {
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(Load5_3.class.getClassLoader());
        Class<?> aClass = Load5_3.class.getClassLoader().loadClass("cn.itcast.jvm.t3.load.H");
        System.out.println(aClass.getClassLoader());

    }
}

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有

  2. sun.misc.Launcher$AppClassLoader // 2 处,委派上级

sun.misc.Launcher$ExtClassLoader.loadClass()

  1. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有

  2. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有

  3. sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 fifindClass 方法,是在

​ JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher$AppClassLoader

的 // 2 处

  1. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 fifindClass 方法,在

classpath 下查找,找到了

由下到上询问

image-20210810090325560

image-20210810090621584

当扩展类加载器的parent为null 走else 因为BootstrapClassLoader直接访问是null值

BootstrapClassLoader类加载器是C++编写的,所以不能直接访问

   if (parent != null) {
       c = parent.loadClass(name, false);
   } else {
       c = findBootstrapClassOrNull(name); // 执行的本地方法
   }

-------------------------
    // 调用方法中调用的findBootstrapClass()方法   
    // return null if not found
    private native Class<?> findBootstrapClass(String name);

如果返回null执行下面代码

由上到下加载
 if (c == null) {
     // If still not found, then invoke findClass in order
     // to find the class.
     long t1 = System.nanoTime();
     c = findClass(name);

     // this is the defining class loader; record the stats
     sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
     sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
     sun.misc.PerfCounter.getFindClasses().increment();
 }

ExtClassLoader没有这个 H.java类,那么抛出异常被AppClassLoader捕捉 不做任何处理

 catch (ClassNotFoundException e) {
     // ClassNotFoundException thrown if class not found
     // from the non-null parent class loader
 }

/*
查找具有指定二进制名称的类。 这个方法应该被遵循加载类委托模型的类加载器实现覆盖,并且在检查请求类的父类加载器后将被loadClass方法调用。 默认实现抛出ClassNotFoundException 。
参数:
name - 类的二进制名称
返回:
生成的Class对象
抛出:
ClassNotFoundException – 如果找不到类
自从:
1.2
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
链接指定的类。 类加载器可能会使用此(误导性命名的)方法来链接类。 如果类c已经被链接,那么这个方法简单地返回。 否则,将按照The JavaLanguage Specification的“执行”一章中的描述链接该类。
参数:
c – 要链接的类
if (resolve) {  //  默认为false
	resolveClass(c);
}

image-20210810095121246

5.4 线程上下文类加载器&应用类加载器
JDBC 加载 Driver 驱动

我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写

Class.forName(“com.mysql.jdbc.Driver”)

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?

让我们追踪一下源码:

image-20210810100316223

基于SPI思想,打破了双亲委派机制,使用当前线程直接获取 应用类加载器,不回去询问启动类加载器。

打破类加载器的双亲委派模式,jdbc中DriverManager直接在AppClassloder中加载类

image-20210810100642906

image-20210810100716196

先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此

可以顺利完成类加载

再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)

约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

image-20210810102000460

ServiceLoader.load(接口类型.class); 找到所以的接口实现类,迭代遍历得到实现类的实例对象

SPI思想Service Provider Interface

**通过SPI来得到实现类,体现的是【面向接口编程+解耦】的思想,**在下面一些框架中都运用了此思想:

JDBC

Servlet 初始化器

Spring 容器

Dubbo(对 SPI 进行了扩展)

**接着看 ServiceLoader.load 方法:**获取线程上下文类加载器

JVM默认把应用程序类加载器赋值给当前线程 ,这里线程调用就可以获取

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取线程上下文类加载器 
    ClassLoader cl = Thread.currentThread().getContextClassLoader(); 
    return ServiceLoader.load(service, cl);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,

它内部又是由Class.forName 调用了线程上下文类加载器完成类加载,

具体代码在 ServiceLoader 的内部类LazyIterator 中:

image-20210810104905928

5.5 自定义类加载器

问问自己,什么时候需要自定义类加载器

1)想加载非 classpath 随意路径中的类文件

2)都是通过接口来使用实现,希望解耦时,常用在框架设计

3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  1. 继承 ClassLoader 父类

  2. 要遵从双亲委派机制,重写 findClass 方法

​ 注意不是重写 loadClass 方法,否则不会走双亲委派机制

  1. 读取类文件的字节码

  2. 调用父类的 defifineClass 方法来加载类

  3. 使用者调用该类加载器的 loadClass 方法

示例:

准备好两个类文件放入 E:\myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下:

image-20210810105900046

遵从双亲委派机制,重写 findClass 方法

package cn.itcast.jvm.t3.load;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Load7 {
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> c1 = classLoader.loadClass("MapImpl1");
        Class<?> c2 = classLoader.loadClass("MapImpl1"); 
        System.out.println(c1 == c2); // true 类名,加载器对象相同 第一次已经加载 第二次会从缓存中直接拿取

        MyClassLoader classLoader2 = new MyClassLoader();
        Class<?> c3 = classLoader2.loadClass("MapImpl1");
        System.out.println(c1 == c3); // false 类名,加载器对象不同,相互隔离的

        c1.newInstance();
    }
}

class MyClassLoader extends ClassLoader {

    @Override // name 就是类名称
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "e:\\myclasspath\\" + name + ".class";

        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path), os);
            // 得到字节数组
            byte[] bytes = os.toByteArray();
            // byte[] -> *.class
            return defineClass(name, bytes, 0, bytes.length);

        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到", e);
        }
    }
}

输出:

image-20210810110921859

6、运行期优化

6.1 JIT即时编译
6.1.1分层编译(TieredCompilation)
public class JIT1 {

    // -XX:+PrintCompilation -XX:-DoEscapeAnalysis
    public static void main(String[] args) {
        for (int i = 0; i < 200; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                new Object();
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\n",i,(end - start));
        }
    }
}

原因是什么呢?JVM 将执行状态分成了 5 个层次:

0 层,解释执行(Interpreter)

1 层,使用 C1 即时编译器编译执行(不带 profifiling)

2 层,使用 C1 即时编译器编译执行(带基本的 profifiling)

3 层,使用 C1 即时编译器编译执行(带完全的 profifiling)

4 层,使用 C2 即时编译器编译执行(效率可以提高10-100倍)

profifiling :是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】【循环的回边次数】等

即时编译器(JIT)与解释器的区别

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  • JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT 会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。

执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来)优化之

逃逸分析:

进入C2阶段**的一种优化手段称之为【逃逸分析】,**发现新建的对象是否逃逸,使用 C2 即时编译器编译执行来提高执行效率

    for (int j = 0; j < 1000; j++) {
                new Object();  // 例如这里,在循环内的局部遍历 ,外部没有引用,不会逃逸
            }

可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析

再运行刚才的示例代码,编译阶段就不会进入C2阶段

观察结果,执行效率基本不会降低

参考资料:https://docs.oracle.com/en/java/javase/12/vm/java-hotspot-virtual-machine-performance-enhancements.html#GUID-D2E3DC58-D18B-4A6C-8167-4A1DFB4888E4

6.1.2方法内联(Inlining)

image-20210810114332278

测试:

public class JIT2 { 
    // -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining 打印内联
    // -XX:CompileCommand=dontinline,*JIT2.square 禁用某个包下的方法的内联 (不加禁用整个jvm内联优化)
    // -XX:+PrintCompilation

    public static void main(String[] args) {

        int x = 0;
        for (int i = 0; i < 500; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                x = square(9);

            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\t%d\n",i,x,(end - start));
        }
    }

    private static int square(final int i) {
        return i * i;
    }
}

输出: 将近到300 次的时候,进行了内联优化,把次热点方法的返回默认为一个常量赋值

image-20210810114944860

加参数:-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining 打印内联 输出:

如果方法太长,是不会进行内联的

image-20210810115237858

6.1.3字段优化

JMH 基准测试请参考:http://openjdk.java.net/projects/code-tools/jmh/

演示:

导入依赖

 <dependencies>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>${jmh.version}</version>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>${jmh.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

package test;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

@Warmup(iterations = 2, time = 1)  // 热身,让程序预热
@Measurement(iterations = 5, time = 1) // 5轮测试
@State(Scope.Benchmark)
public class Benchmark1 {

    int[] elements = randomInts(1_000);

    private static int[] randomInts(int size) {
        Random random = ThreadLocalRandom.current();
        int[] values = new int[size];
        for (int i = 0; i < size; i++) {
            values[i] = random.nextInt();
        }
        return values;
    }

    @Benchmark
    public void test1() {
        for (int i = 0; i < elements.length; i++) {
            doSum(elements[i]);
        }
    }

    @Benchmark
    public void test2() {
        int[] local = this.elements;
        for (int i = 0; i < local.length; i++) {
            doSum(local[i]);
        }
    }

    @Benchmark
    public void test3() {
        for (int element : elements) {
            doSum(element);
        }
    }

    static int sum = 0;

    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    static void doSum(int x) {
        sum += x;
    }


    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(Benchmark1.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

image-20210810135650268

image-20210810135925353

6.2 反射优化
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Reflect1 {

    public static void foo() {
        System.out.println("foo...");
    }

    public static void main(String[] args) throws NoSuchMethodException, 
            InvocationTargetException, IllegalAccessException, IOException {
        Method foo = Reflect1.class.getMethod("foo");
        for (int i = 0; i <= 16; i++) {
            System.out.printf("%d\t", i);
            foo.invoke(null);
        }
        System.in.read();
    }
}

foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现

image-20210810141625704

本地的方法访问器的invoke执行了15次后,把本地方法访问器,替换成运行期间动态生成的方法访问器

image-20210810142026774

进行了反射优化

image-20210810142543889

查阅运行期间动态生成的类的字节码

当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到

类名为 sun.reflflect.GeneratedMethodAccessor1

可以使用阿里的 arthas 工具:

java -jar arthas-boot.jar 
[INFO] arthas-boot version: 3.1.1 
[INFO] Found existing java process, please choose one and hit RETURN. 
* [1]: 13065 cn.itcast.jvm.t3.reflect.Reflect1
    
$ jad sun.reflect.GeneratedMethodAccessor1

第17次已经不走NativeMethodAccessorImpl 了,

动态生成的GeneratedMethodAccessor1直接使用类名.静态方法访问

image-20210810144007355

image-20210810144250405

注意:

通过查看 ReflectionFactory 源码可知

sun.reflect.noInflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首

次生成比较耗时,如果仅反射调用一次,不划算)

sun.reflect.inflationThreshold 可以修改膨胀阈值

image-20210810144407923

五、内存模型(JMM)

1、Java内存模型概述

【java 内存结构】与【java 内存模型】傻傻分不清,

【java 内存模型】是 **Java MemoryModel(JMM)**的意思。

关于它的权威解释,请参考 https://download.oracle.com/otn-pub/jcp/memory_model-1.0-pfd-spec-oth-JSpec/memory_model-1_0-pfd-spec.pdf?AuthParam=1562811549_4d4994cbd5b59d964cd2907ea22ca08b

简单的说,JMM 定义了一套在多线程读写共享数据(成员变量、数组)时,>对数据的可见性、有序性、和原子性的规则和保障。

1.0JMM中名词解释

指令重排

  1. 什么是指令重排?

    在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。

  2. 指令重排的原因分析

    主要还是**编译器以及CPU为了优化代码或者执行的效率而执行的优化操作;**应用条件是单线程场景下,对于并发多线程场景下,指令重排会产生不确定的执行效果。

    CPU指令重排

    多条汇编指令执行时, 考虑性能因素, 会导致执行乱序。

    一条汇编指令的执行是可以分为很多步骤的, 分为不同的硬件执行

    • 取指 IF
    • 译码和取寄存器操作数 ID
    • 执行或者有效地址计算 EX (ALU逻辑计算单元)
    • 存储器访问 MEM
    • 写回 WB (寄存器)

    硬件优化(如写吸收,批操作)

    cpu2修改了变量T, 而cpu1却从高速缓存cache中读取了之前T的副本, 导致数据不一致.

    编译器优化

    我们可以通过修改JAVA_HOME/jre/lib/i386/jvm.cfg, 将jvm调整为server模式验证下.

    修改内容如下图所示, 将-server调整到-client的上面.

    -server KNOWN
    -client KNOWN
    -hotspot ALIASED_TO -client
    -classic WARN
    -native ERROR
    -green ERROR

    主要是Java虚拟机层面的可见性, 两者区别在于当jvm运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器,而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对彻底,

    会导致程序启动慢, 但服务起来之后, 性能更高, 同时有可能带来可见性问题.

    如果光靠sychronized和volatile来保证程序执行过程中的原子性, 有序性, 可见性, 那么代码将会变得异常繁琐.

    JMM提供了Happen-Before规则来约束数据之间是否存在竞争, 线程环境是否安全。

  3. 如何防止指令重排

    volatile关键字可以保证变量的可见性,因为对volatile的操作都在Main Memory中,而Main Memory是被所有线程所共享的,这里的代价就是牺牲了性能,无法利用寄存器或Cache,因为它们都不是全局的,无法保证可见性,可能产生脏读。
    volatile还有一个作用就是局部阻止重排序的发生,对volatile变量的操作指令都不会被重排序,因为如果重排序,又可能产生可见性问题。
    在保证可见性方面,锁(包括显式锁、对象锁)以及对原子变量的读写都可以确保变量的可见性。但是实现方式略有不同,例如同步锁保证得到锁时从内存里重新读入数据刷新缓存,释放锁时将数据写回内存以保数据可见,而volatile变量干脆都是读写内存。

  4. 可见性

    这里提到的**可见性是指前一条程序指令的执行结果,可以被后一条指令读到或者看到,称之为可见性。**反之为不可见性。这里主要描述的是在多线程环境下,指令语句之间对于结果信息的读取即时性。

  5. 原子性

​ 原子性是指一个操作是不可中断的. 即使是在多个线程一起执行的时候,

一个操作一旦开始,就不会被其它线 程干扰。

6.有序性

​ 有序性是指在单线程环境中, 程序是按序依次执行的。而在多线程环境中, 程序的执行可能因为指令重排而出 现乱序。

  1. Happen-Before先行发生规则

    顺序原则

    一个线程内保证语义的串行性; a = 1; b = a + 1;

    volatile规则

    volatile变量的写,先发生于读,这保证了volatile变量的可见性,

    锁规则

    解锁(unlock)必然发生在随后的加锁(lock)前.

    传递性

    A先于B,B先于C,那么A必然先于C.

    线程启动, 中断, 终止

    线程的start()方法先于它的每一个动作.

    线程的中断(interrupt())先于被中断线程的代码.

    线程的所有操作先于线程的终结(Thread.join()).

    对象终结

    对象的构造函数执行结束先于finalize()方法.

1.1 原子性

原子性在线程时讲过,下面来个例子简单回顾一下:

问题提出

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作。

image-20210810151110999

image-20210810151144021

image-20210810151555206

image-20210810151819790

image-20210810152049702

解决方法

synchronized 解决并发问题:

synchronized( 对象 ) { 
要作为原子操作代码 
} 
package cn.itcast.jvm.t4.avo;

public class Demo4_1 {

    static int i = 0;
    static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                for (int j = 0; j < 50000; j++) {
                    i++;
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (obj) {
                for (int j = 0; j < 50000; j++) {
                    i--;
                }
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(i);
    }
}

image-20210810153210248

如何理解呢:你可以把 obj 想象成一个房间,线程 t1,t2 想象成两个人。

当线程 t1 执行到 synchronized(obj) 时就好比 t1 进入了这个房间,并反手锁住了门,在门内执行count++ 代码。

这时候如果 t2 也运行到了 synchronized(obj) 时,它发现门被锁住了,只能在门外等待。

当 t1 执行完 synchronized{} 块内的代码,这时候才会解开门上的锁,从 obj 房间出来。t2 线程这时才可以进入 obj 房间,反锁住门,执行它的 count-- 代码。

注意:上例中 t1 和 t2 线程必须用 synchronized 锁住同一个 obj 对象,如果 t1 锁住的是 m1 对象,t2 锁住的是 m2 对象,就好比两个人分别进入了两个不同的房间,没法起到同步的效果。

1.2 可见性
退不出的循环

先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

public class Demo4_2 {

    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(run){
                // ....
                //System.out.println(1);
            }
        });
        t.start();

        Thread.sleep(1000);
        run = false; // 线程t不会如预想的停下来
    }
}
问题分析

JIT的优化

image-20210810153749670

image-20210810153809107

image-20210810153920967

解决方法

volatile(易变关键字)

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。

image-20210810154833112

可见性

前面例子体现的实际就是可见性,它保证的是在多个线程之间**,**

注意:一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况: 上例从字节码理解是这样的:

image-20210810154933557

注意: **synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。**但缺点是synchronized是属于重量级操作,性能相对更低。

如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?

synchronized关键字的作用 打破JIT的优化 再次从主从中获取 run的值

image-20210810160242834

1.3 有序性
问题提出

image-20210810160659540

image-20210810160749660

这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现:

借助 java 并发压测工具 jcstress https://wiki.openjdk.java.net/display/CodeTools/jcstress

package test;

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;

//@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {

    int num = 0;
    volatile boolean ready = false;
    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }

}

进入终端

mvn clean install 
java -jar target/jcstress.jar

image-20210810162022646

解决方法

volatile 修饰的变量,可以禁用指令重排

image-20210810163314496

有序性理解

image-20210810163527344

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性

例如著名的 double-checked-locking (双重检查锁)模式实现单例

image-20210810163747557

image-20210811082814022

对INSTANCE使用 volatile修饰,可以禁用指令重排,但注意在JDK5+的版本的volatile才有效

1.4 happens-before规则

image-20210811091832002

image-20210811092043878

image-20210811092458795

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zKLazLQr-1628668126939)(jvm01.assets/image-20210811092925271.png)]

image-20210811092833518

对变量默认值(0,false,null)的写,对其它线程对该变量的读可见

具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z

变量都是指成员变量或静态成员变量

2、CAS与原子类

2.1 CAS概念

CAS 即 Compare and Swap ,它体现的一种乐观锁的思想,(比较交换)(无锁并发)比如多个线程要对一个共享的整型变量执行 +1 操作:

image-20210811094911776

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。结合 CAS 和 volatile 可以实现无

锁并发,适用于竞争不激烈、多核 CPU 的场景下。

  • 因为没有使用 synchronized,所以线程不会陷入阻塞,(不会进行上下文切换)这是效率提升的因素之一

  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

**CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令,**下面是直接使用 Unsafe 对象进

行线程安全保护的一个例子:

package cn.itcast.jvm.t6;

import sun.misc.Unsafe;
import java.lang.reflect.Field;

/**
 * @Author: ahcfl
 * @Description:
 */

public class TestCAS {
    public static void main(String[] args) throws InterruptedException {
        DataContainer dc = new DataContainer();
        int count = 5;
        dc.increase();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < count; i++) {
                dc.increase();
            }
        });

        t1.start();
        t1.join();
        System.out.println(dc.getData());
    }
}


class DataContainer {
    private volatile int data;
    static final Unsafe unsafe;
    static final long DATA_OFFSET;

    static {
        try {
            //Unsafe 对象不能直接调用,只能通过反射获得
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
        try {
            //data 属性在 DataContainer 对象中的偏移量,用于 Unsaf直接访问该属性
            DATA_OFFSET = unsafe.objectFieldOffset(DataContainer.class.getDeclaredField("data"));
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }
    }

    public void increase() {
        int oldValue;
        while (true) {
            //获取共享变量旧值,可以在这一行加入断点,修改 data
            //调试来加深理解
            oldValue = data;
            //cas 尝试修改 data 为 旧值 + 1,如果期间旧值被别的线程改了,返回 false
            if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue + 1)) {
                return;
            }
        }
    }

    public void decrease() {
        int oldValue;
        while (true) {
            oldValue = data;
            if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - 1)) {
                return;
            }
        }

    }

    public int getData() {
        return data;
    }
}
2.2 乐观锁与悲观锁

CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,

我吃亏点再重试呗。

synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁

你们都别想改,我改完了解开锁,你们才有机会。

2.3 原子操作类

juc(java.util.concurrent)中提供了原子操作类(无锁并发),可以提供线程安全的操作,

例如:AtomicInteger、AtomicBoolean等,它们底层就是采用 CAS 技术 + volatile 来实现的。

可以使用 AtomicInteger 改写之前的例子:

import java.util.concurrent.atomic.AtomicInteger;

public class Demo4_4 {
    // 创建原子整数对象
    private static AtomicInteger i = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                i.getAndIncrement();  // 获取并且自增  i++
//                i.incrementAndGet();  // 自增并且获取  ++i
            }
        });

        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                i.getAndDecrement(); // 获取并且自减  i--
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

3、synchronized 优化

JDK6对synchronized 底层做了一系列优化,在某种情况下更优于CAS。

Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。Mark Word 平时存

储这个对象的 哈希码 、 分代年龄 ,当加锁时,这些信息就根据情况被替换为标记位 、 线程锁记录指

针 、 重量级锁指针 、 线程ID 等内容。

3.1 轻量级锁

如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以用轻量级锁(锁重入)来化。

这就好比:学生(线程 A)用课本占座,上了半节课,出门了(CPU时间到),回来一看,发现课本没变,说明没

有竞争,继续上他的课。 如果这期间有其它学生(线程 B)来了,CPU会告知(线程A)会有并发访问(放课本是不安全的),线程A 随即升级为重量级锁,进入重量级锁的流程。

而重量级锁就不是那么用课本占座那么简单了,可以想象线程 A 走之前,把座位用一个铁栅栏围起来。

例如:假设有两个方法同步块,利用同一个对象加锁

3

每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

image-20210811110357161

image-20210811110645129

3.2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

image-20210811110911374

image-20210811111026118

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nKajmv77-1628668126944)(jvm01.assets/image-20210811111236489.png)]

3.3 重量锁(自旋)

重量级锁竞争的时候,还可以使用自旋(线程不会停)来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

在 Java 6 之后自旋锁是自适应的(比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能)

image-20210811111456486

​ jvm内部进行自旋功能的控制

自旋成功的情况

image-20210811113020405

自旋失败的情况

image-20210811113731735

3.4 偏向锁

image-20210811114438086

-XX:-UseBiasedLocking 禁用偏向锁

可以参考这篇论文:https://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf

假设有两个方法同步块,利用同一个对象加锁

image-20210811114839590

3.5 其它优化
3.5.1减少上锁时间

同步代码块中尽量短

3.5.2减少锁的粒度

image-20210811115401932

3.5.3锁粗化

多次循环进入同步块不如同步块内多次循环 另外 JVM 可能会做如下优化,把多次 append 的加锁操作

粗化为一次(因为都是对同一个对象加锁,没必要重入多次)

new StringBuffer().append("a").append("b").append("c");
3.5.4锁消除

JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候

就会被即时编译器忽略掉所有同步操作。

3.5.5读写分离

CopyOnWriteArrayList ConyOnWriteSet

参考:

https://wiki.openjdk.java.net/display/HotSpot/Synchronization

http://luojinping.com/2015/07/09/java锁优化/

https://www.infoq.cn/article/java-se-16-synchronized

https://www.jianshu.com/p/9932047a89be

https://www.cnblogs.com/sheeva/p/6366782.html

https://stackoverflflow.com/questions/46312817/does-java-ever-rebias-an-individual-lock

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程小栈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值