当前位置:   article > 正文

深入理解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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
问题辨析:

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

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

栈内存分配越大越好吗?

  • 不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
  • 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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

调用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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

解决方法: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 进制的,需要转换。
# 会详细定位到出现问题的源码行数
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

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);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
【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.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
【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];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

堆转储 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.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
【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);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
【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!");
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
 javap -v HelloWorld.class 命令反编译查看结果
  • 1

**常量池:**就是一张地址表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
**运行时常量池:**常量池是 *.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"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
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
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
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"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
【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);
	}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
【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);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

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);
        }
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

image-20210723000704066

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

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

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

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

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

考虑是否需要将字符串对象入池,可以通过 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);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
【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(); // 敲一次回车运行
      }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
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 对方法区的实现称为元空间
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

运行在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();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

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

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}
  • 1
  • 2
  • 3

底层是创建了一个 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;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

这里调用了一个 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;
                    }
                });
            }

        }
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

可以看到关键的一行代码, 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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

直接内存的回收机制总结

  • 使用了 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();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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

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

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

意思就是禁止我们手动的 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");
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
Eclipse Memory Analyzer 分析

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

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

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

第二步:

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

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

live:触发一次垃圾回收

file:文件名
16104:进程的id

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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 命令转储文件命令
  • 1

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
        }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

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());
        }
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

在这里插入图片描述

弱引用演示
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() + ",");
        }
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
[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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
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]);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
-----------------------------初始化时的信息-----------------------------------------
# 堆
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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
添加一个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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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   // 指定垃圾回收运行线程数
  • 1
  • 2
  • 3
  • 4
  • 5
  • 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
  • 2
  • 3
【1】G1 垃圾回收阶段
img

分为下面三个阶段:

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

【2】Young Collection

新生代存在 STW:
分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多个小区间,方便控制 GC 产生的停顿时间!
E:eden,S:幸存区([survive](javascript:声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】

推荐阅读
相关标签