课程笔记:JVM(尚硅谷)


在这里插入图片描述

一、内存与垃圾回收篇

1.1 JVM与Java体系结构

虚拟机分为系统虚拟机程序虚拟机,VMware就属于系统虚拟机,程序虚拟机的典型代表就是Java虚拟机,它专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令我们称为Java字节码指令

Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。

Java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种是基于寄存器的指令集架构。由于跨平台性的设计,Java的指令都是根据栈来设计的,优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

JVM的生命周期:

  1. Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
  2. 虚拟机的执行:执行一个所谓的Java程序的时候,执行的实际是Java虚拟机的进程
  3. 虚拟机的退出:
    1. 程序正常执行结束
    2. 程序在执行过程中遇到了异常或错误而异常终止
    3. 操作系统出现错误导致Java虚拟机进程终止
    4. 某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作
    5. 除此之外,JNI规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机的退出情况

JVM发展历程:

  1. Classic虚拟机:只有解释器,只能外挂JIT,二选一
  2. Exact VM:准确式内存管理
  3. HotSpot VM:默认虚拟机,热点探测技术
  4. JRockit:专注于服务器端使用,全部代码都靠即时编译器编译后执行
  5. J9:广泛用于IBM的各种Java产品
  6. KVM和CDC/CLDC Hotspot:低端设备
  7. Azul VM:与特定硬件平台绑定、软硬件配合的专有虚拟机
  8. Liquid VM:不需要操作系统的支持
  9. Apache Harmony:IBM和Intel联合开发的开源JVM
  10. Microsoft JVM:为了在IE3浏览器中支持Java Applets
  11. TaobaoJVM:基于OpenJDK HotSpot VM发布的国内第一个优化、深度定制且开源的高性能服务器版Java虚拟机
  12. Dalvik VM:谷歌开发的,应用于Android系统,基于寄存器架构
  13. Graal VM:Run Programs Faster Anywhere

JVM架构:
在这里插入图片描述

1.2 类加载子系统

类加载子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识,加载的类信息存放于一块称为方法区的内存空间。

类的加载过程分加载、链接和初始化,链接又有验证、准备和解析

JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)

引导类加载器、扩展类加载器、系统类加载器,对于用户自定义类来说,默认使用系统类加载器进行加载,核心类库使用引导类加载器进行加载

启动类加载器(引导类加载器):用来加载Java的核心库

扩展类加载器:Java语言编写,派生于ClassLoader类

自定义加载器的原因:隔离加载类、修改类加载的方式、扩展加载源、防止源码泄漏

用户自定义类加载器实现步骤:

  1. 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
  2. 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中
  3. 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

获取ClassLoader的几种途径:

  1. 获取当前类的ClassLoader:clazz.getClassLoader
  2. 获取当前线程上下文的ClassLoader:Thread.currentThread().getContextClassLoader
  3. 获取系统的ClassLoader:ClassLoader.getSystemClassLoader
  4. 获取调用者的ClassLoader:DriverManager.getCallerClassLoader

双亲委派机制:

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载道内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

双亲委派机制工作原理:

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

双亲委派机制的优势:

  1. 避免类的重复加载
  2. 保护程序安全,防止核心API被随意篡改

沙箱安全机制:保证对Java核心源代码的保护

1.3 运行时数据区概述及线程

Java虚拟机定义了若干种程序运行期间会使用道的运行时数据区,其中有一些会随着虚拟机启动而创建,随机虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

每个JVM只有一个Runtime实例,即为运行时环境

再Hotspot JVM里,每个线程都与操作系统的本地线程直接映射

Hotspot JVM后台系统线程主要有:

  1. 虚拟机线程
  2. 周期任务线程
  3. GC线程
  4. 编译线程
  5. 信号调度线程

1.4 程序计数器

Program Counter Register

JVM种的PC寄存器是对物理PC寄存器的一种抽象模拟,用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令

每一个线程一个PC寄存器

1.5 虚拟机栈

栈是运行时的单位,而堆是存储的单位

生命周期和线程一致,主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回

访问速度仅次于程序计数器

对于栈来说不存在垃圾回收问题

虚拟机栈常见的异常:StackOverflowError、OutOfMemoryError

使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度

栈中的数据都是以栈帧(Stack Frame)的格式存在,每个方法对应一个栈帧在这里插入图片描述
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能再一个栈帧之中引用另外一个线程的栈帧

Java方法由两种返回函数的方式,一种是正常的函数返回,使用return指令,另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。

栈帧的内部结构:

  1. 局部变量表(Local Variables)
  2. 操作数栈(Operand Stack)
  3. 动态链接(Dynamic Linking)(指向运行时常量池的方法引用)
  4. 方法返回地址(Return Address)(方法正常退出或者异常退出的定义)
  5. 一些附加信息

局部变量表

定义为一个数字数组,主要用于存储方法参数和定义再方法体内的局部变量,局部变量表所需的容量大小是在编译器确定下来的,并保存在方法的Code属性的maximu local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。

局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

局部变量表最基本的存储单元是Slot,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot

如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index位0的slot处,其余的参数按照参数表顺序排列

栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后声明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收

操作数栈:

主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间

每一个操作数栈都有拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就定义好了,保存在方法的Code属性中,为max_stack的值

如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令

栈顶缓存:

栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低堆内存的读/写次数,提升执行引擎的执行效率

动态链接

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接,比如:invokedynamic指令

方法的调用:

在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关

静态链接:字节码文件被装载进JVM内部时,如果被调用的目标在编译器可知,且运行期保持不变

动态链接:被调用的方法在编译器无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用

对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)

静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法,其他方法成为虚方法

虚拟机中提供了以下几条方法调用指令:

普通调用指令:

  1. invokestatic:调用静态方法,解析阶段确定唯一方法版本
  2. invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本
  3. invokevirtual:调用所有虚方法
  4. invokeinterface:调用接口方法

动态调用指令:

  1. invokedynamic:动态解析出需要调用的方法,然后执行

每个类中都有一个虚方法表,表中存放着各个方法的实际入口

方法返回地址:

存放调用该方法的pc寄存器的值,方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址,而通过异常退出的,返回地址时要通过异常表来确定,栈帧中一般不会保存这部分信息

栈的相关面试题:

  1. 举例栈溢出的情况
  2. 调整栈大小,就能保证不出现溢出吗
  3. 分配的栈内存越大越好吗
  4. 垃圾回收是否会涉及到虚拟机栈
  5. 方法中定义的局部变量是否线程安全

1.6 本地方法接口

本地方法:Native Method就是一个Java调用非Java代码的接口,本地接口的作用时融合不同的编程语言为Java所用

1.7 本地方法栈

Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用,也是线程私有的

当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界

1.8 堆

所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)

现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:新生区(Young Generation Space)、养老区(Tenure generation space)、元空间(Meta Space),新生区又被划分为Eden区和Survivor区(Survivor0和Survivor1)
在这里插入图片描述
-Xms用于表示堆区的起始内存,-Xmx则用于表示堆区的最大内存,默认初始内存为物理内存/64,最大内存为物理内存/4

-XX:NewRatio:设置新生代和老年代的比例,默认是2

-XX:SurvivorRatio:设置新生代中Eden区与Survivor区的比例

-XX:-UseAdaptiveSizePolicy:关闭自适应的内存分配策略

几乎所有的Java对象都在Eden区被new出来的,绝大部分的Java对象的销毁都在新生代进行

-Xmn:设置新生代最大内存大小

对象分配过程:

如果对象再Eden出生并经过第一次MinorGC后仍存活,并且能被Survivor容纳的话,就被移动到Survivor空间中,并将对象年龄设为1。对象再Survivor区每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中

垃圾回收分类:

部分收集(Partial GC):

  1. 新生代收集(Minor GC / Young GC):Eden区满时触发
  2. 老年代收集(Major GC / Old GC):Major GC速度比Minor GC慢10倍以上
  3. 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集

整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集

TLAB

由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的,为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度

JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内

JVM将TLAB作为内存分配的首选,通过-XX:+UseTLAB控制,默认开启,TLAB空间的内存非常小,仅占有整个Eden空间的1%

JVM中堆空间的参数设置

-XX:+PrintFlagsInitial:查看所有参数的默认值
-XX:+PrintFlagsFinal:查看所有参数的最终值
-Xms:初始堆空间内存(默认为物理内存的1/64)
-Xmx:最大堆空间内存(默认为物理内存的1/4)
-Xmn:设置新生代的大小
-XX:NewRatio:设置新生代与老年代在堆结构的占比
-XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails:输出详细的GC处理日志
-XX:+PrintGC:打印GC简要信息
-XX:HandlePromotionFailure:是否设置空间分配担保

堆不是分配对象存储的唯一选择

随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所以的对象都分配到堆上也渐渐变得不那么“绝对了”

如果经过逃逸分析后发 现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配

逃逸分析:代码优化

  1. 栈上分配
  2. 同步省略,锁消除
  3. 分离对象或标量替换

1.9 方法区

尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择区进行垃圾收集或者进行压缩,所以方法区看作是一块独立于堆的内存空间

方法区的大小决定了系统可以保存多少个类

元空间不在虚拟机设置的内存中,而是使用本地内存

jdk8及以后:-XX:MetaspaceSize-XX:MaxMetaspaceSize

解决OOM

  1. 要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏还是内存溢出
  2. 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Root是的引用链
  3. 检查虚拟机的堆参数,尝试减少运行时的内存消耗

方法区的内部结构

类型信息、常量、静态变量、即时编译器编译后的代码缓存

常量池

常量池可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型

运行时常量池

运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池,具备动态性

jdk1.8及以后,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆

StringTable

jdk7中将StringTable放到了堆空间中,因为永久代的回收效率很低,在full gc的时候才会触发,而full gc是老年代的空间不足、永久代不足时才会触发。这就导致StringTable的回收效率不高,而开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足,放到堆里,能及时回收内存

方法区的垃圾回收

方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型

判断一个类型是否属于“不再被使用的类”需要同时满足下面三个条件:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
  2. 加载该类的类加载器已经被回收
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

1.10 对象的实例化内存布局与访问定位

对象的实例化
在这里插入图片描述
对象的内存布局
在这里插入图片描述
对象的访问定位
在这里插入图片描述

1.11 直接内存

直接内存是Java堆外的、直接向系统申请的内存空间,通向访问直接内存的速度会优于Java堆

Java的NIO库允许Java程序使用直接内存,用于数据缓冲区

1.12 执行引擎

执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以

执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器

解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行,分为古老的字节码解释器和模版解释器

JIT(Just In Time Compiler)编译器:虚拟机将源代码直接编译成和本地机器平台相关的机器语言

当虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。并且随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。

热点代码使用JIT,采用基于计数器的热点探测,HotSpot VM将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter),方法调用计数器用于统计方法的调用次数,回边计数器则用于统计循环体执行的循环次数。

Java虚拟机运行在Client模式下,使用C1编译器,在Server模式下,使用C2编译器

1.13 StringTable

String声明为final,不可被继承,实现了Serializable,可序列化,实现了Comparable,可比较

String的String Pool是一个固定大小的Hashtable,可以通过双引号声明,或者调用intern()方法

String的内存分配

jdk1.6在永久代,jdk1.7及以后在堆区

字符串拼接操作

  1. 常量与常量的拼接结果在常量池,原理是编译期优化
  2. 常量池中不会存在相同内容的常量
  3. 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
  4. 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入串中,并返回此对象地址

intern()的使用

如果不是用双引号声明的String对象,可以使用String提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中

public class StringIntern {
   public static void main(String[] args) {
   		String s = new String("1");
   		s.intern();
   		String s2 =1;
   		System.out.println(s == s2) // jdk1.6:false, jdk1.7:false

   		String s3 = new String("1") + new String("1");
   		// jdk1.6: 创建了一个新的对象“11”,也就有了新地址
   		// jdk1.8: 此时常量中并没有创建“11”, 而是创建一个指向堆空间中new String("11")的地址
   		s3.intern();
   		String s4 = "11"
   		System.out.println(s3 == s4) //jdk1.6:false, jdk1.7:true
   }
}

jdk1.6中,intern()将这个字符串对象尝试放入字符串常量池

  1. 如果字符串常量池中有,则不会放入。返回已有的的字符串常量池中对象的地址
  2. 如果没有,会把此对象复制一份,放入字符串常量池,并返回字符串常量池中的对象地址
    jdk1.7起,intern()将这个字符串对象尝试放入字符串常量池
  3. 如果字符串常量池中有,则不会放入,返回已有的字符串常量池中的对象的地址
  4. 如果没有,则会把对象的引用地址复制一份,放入字符串常量池,并返回字符串的引用地址

对于程序中大量存在的字符串,尤其存在很多重复字符串时,使用intern()可以节省内存空间

1.14 垃圾回收概述

垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾

1.15 垃圾回收相关算法

垃圾标记阶段之引用计数(Reference Counting)算法

对每一个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况

优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性

缺点:

  1. 需要单独的字段存储计数器,增加了存储空间的开销
  2. 每次赋值都需要更新计数器,伴随着加法和减法,增加了时间开销
  3. 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法

垃圾标记阶段之可达性分析算法

或叫根搜索算法、追踪性垃圾收集算法

所谓“GC Roots”根集合就是一组必须活跃的引用

基本思路:

  1. 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
  2. 使用可达性分析算法后,内存的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
  3. 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象
  4. 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象

GC Roots包括以下几类元素:

  1. 虚拟机栈中引用的对象,比如:各个线程被调用的方法中使用到的参数、局部变量等
  2. 本地方法栈内JNI(本地方法)引用的对象
  3. 方法区中类静态属性引用的对象,比如:Java类的引用类型静态变量
  4. 方法区中常量引用的对象,比如:字符串常量池(String Table)里的引用
  5. 所有被同步锁synchronized持有的对象
  6. Java虚拟机内部的引用,比如:基本数据类型对应的Class对象,一些常驻的异常对象,系统类加载器
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root

如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的画分析结果的准确性就无法保证。这也是导致GC进行时必须“Stop The World”的一个重要原因

当垃圾回收一个对象之前,会调用finalize()方法,由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态:

  1. 可触及的:从根节点开始,可以到达这个对象
  2. 可复活的:对象的所有引用都被释放,但是对象有可能再finalize()中复活
  3. 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只能被调用一次

MAT查看GCRoot,JProfiler可以GCRoot溯源

标记-清除(Mark-Sweep)算法

堆中的 有效内存空间被耗尽的时候,停止整个程序,STW,然后进行两项工作,第一项是标记,第二项则是清除

  1. 标记:Collector从引用根结点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象
  2. 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收

缺点:

  1. 效率不算高
  2. 进行GC的时候,需要停止整个程序,导致用户体验差
  3. 清理出来的空闲内存不是连续的,产生内存碎片,需要维护一个空闲列表

垃圾清除阶段之复制(Coping)算法

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收

S0,S1区使用此复制方法

优点:

  1. 没有标记和清除阶段,实现简单,运行高效
  2. 复制过去以后保证空间的连续性,不会出现“碎片”问题

缺点:

  1. 需要两倍的内存空间
  2. 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小

垃圾清除阶段之标记-压缩(Mark-Compact)算法

复制算法适合新生代,标记-压缩方法适合老年代垃圾回收

标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此也可以称为标记-清除-压缩算法

分代收集(Generational Collecting)

垃圾回收器区分新生代和老年代使用

增量收集(Incremental Collecting)算法

如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行,每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

分区算法

将整个堆空间划分成连续的不同小区间,每一个小区间独立使用,独立回收

1.16 垃圾回收相关概念

System.gc()的理解

显式触发Full GC,无法保证堆垃圾收集器的立即调用

内存溢出(OOM)

OutOfMemoryError:没有空闲内存,并且垃圾收集器也无法提供更多内存

内存泄漏(Memory Leak)

对象不用,且GC不能回收

尽管内存泄漏不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现OutOfMemory异常,导致程序崩溃

举例:

  1. 单例模式:单例的生命周期和应用程序是一样长的,单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生
  2. 一些提供close的资源未关闭导致内存泄漏:数据库连接,网络连接和io连接必须手动close,否则是不能回收的

Stop The World

GC事件发生过程中,会产生应用程序的停顿,停顿产生时整个应用程序线程都会被暂停,没有任何响应,称为STW

垃圾回收的并发与并行

并发:用户线程和垃圾收集线程同时执行,CMS,G1

并行:指多条垃圾收集线程同时工作,但此时用户线程仍处于等待状态

安全点和安全区域

安全点(Safepoint):只有在特定的位置才能停顿下来开始GC, 通过会根据“是否具有让程序长时间执行的特征”为标准,如方法调用、循环跳转和异常跳转

安全区域(Safe Region):扩展了的Safepoint

引用

强引用(Strong Reference):最传统的“引用”,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象

StringBuffer str = new StringBuffer("Hello, 尚硅谷");

软引用(Soft Reference): 在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收,如果这次回收后还没有足够的内存,才会抛出内存溢出异常

通常用来实现内存敏感的缓存

Object obj = new Object(); // 声明强引用
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 销毁强引用

弱引用(Weak Reference):被弱引用关联的对象只能生存到下一次垃圾收集之前,当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象

虚引用(Phantom Reference):一个对象是否又虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

终结器引用(Final Reference):用以实现对象的finalize()方法

1.17 垃圾回收器

垃圾回收器分类

按线程分:串行垃圾回收器和并行垃圾回收器

按照工作模式分:并发式垃圾回收器和独占式垃圾回收器

按碎片处理方式:压缩式垃圾回收器和非压缩式垃圾回收器

按工作的内存空间分:年轻代垃圾回收器和老年代垃圾回收器

评估GC的性能指标

吞吐量、垃圾收集开销、暂停时间、收集频率、内存占用、快速

吞吐量:运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)

暂停时间:STW时间

标准:在最大吞吐量优先的情况下,降低停顿时间

不同的垃圾回收器概述
在这里插入图片描述
垃圾收集器的组合关系
在这里插入图片描述
Serial回收器:串行回收

Serial收集器采用复制算法、串行回收和“Stop-the-World”机制的方式执行内存回收

Serial Old收集器同样采用了串行回收和“Stop-the-World”机制,只不过内存回收算法使用的是标记-压缩算法

优势:简单而高效,运行在Client端是不错的选择

使用-XX:+UseSerialGC开启

ParNew回收器:并行回收

并行回收,复制算法、Stop the World

-XX: +UseParNewGC:开启ParNew GC

-XX:ParallelGCThreads:指定并行运行的线程数

Parallel Scavenge回收器:吞吐量优先

达到一个可控制的吞吐量,自适应调节策略也是Parallel Scavenge和ParNew的一个重要区别

对应Parallel Old收集器

Java8中,是默认的垃圾回收器

CMS(Concurrent-Mark-Sweep)回收器:低延迟

实现了让垃圾收集线程与用户线程同时工作

采用标记-清除算法,并且也会STW

分为初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段

优点:并发收集、低延迟

缺点:内存碎片、对CPU资源非常敏感、无法处理浮动垃圾

G1回收器:区域化分代式

延迟可控的情况下获得尽可能高的吞吐量

G1 GC有计划地避免在整个Java堆中进行全区域地垃圾收集。G1跟踪各个Region里面地垃圾堆积地价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region

优势:并行性与并发性、分代收集、空间整合、可预测的停顿时间模型

使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB等。可以通过-XX:G1HeapRegionSize设定,所有的Region大小相同,且在JVM生命周期内不会改变。

垃圾回收过程主要包括如下三个环节:

  1. Young GC
  2. Concurrent Marking
  3. Mixed GC

Remembered Set

进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set,就可以保证不进行全局扫描,也不会有遗漏

ZGC:可伸缩的低延迟GC

二、字节码与类的加载篇

2.1 概述

字节码文件的跨平台

2.1 虚拟机的基石:Class文件

字节码是一种二进制的类文件,内容是JVM的指令

JVM指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode),以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成

Class文件实际上并不一定以磁盘文件的形式存在,是一组字节为基础单位的二进制流

Class文件结构中只有两种数据类型:无符号数

无符号数:最基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值

表:由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以_info结尾。表用于描述由层次关系的复合结构的数据,整个Class文件本质上就是一张表,由于表没有固定长度,所有通过会在其前面加上个数说明

2.2 Class文件结构

Class文件的总体结构有:魔数、Class文件版本、常量池、访问标志、类索引、父类索引、接口索引集合、字段表集合、方法表集合、属性表集合

https://docs.oracle.com/javase/specs/index.html

常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)

2.3 字节码指令

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

Java虚拟机的指令由一个字节长度的、代表这某种特定含义的数字(成为操作码)以及跟随其后的零至多个代表此操作所需参数(成为操作数)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码。

加载与存储指令

用于将数据从栈帧的局部变量表和操作数栈之间来回传递

局部变量压栈指令:aload_1、iload 5

常量入栈指令:iconst_<i>(i从-1到5)、bipush、sipush、ldc

出栈装入局部变量表指令:istore_1(n为索引)、fstore

算术指令

iadd、isub、fneg、dcmpg

类型转换指令

一般用于实现用户代码中的显示类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题

宽化类型转换:小范围类型向大范围类型的安全转换,int ⇒ long ⇒ float ⇒ double

窄化类型转换:可能会导致结果具备不同的正负号、不同的数量级,因此,转换过程很可能会导致数值丢失精度

对象的创建与访问指令

创建指令:new、newarray、anewarray、multianewarray(创建多维数组)

字段访问指令:getstatic、putstatic、getfield、putfield

数组操作指令:iastore、iaload、arraylength

类型检查指令:instanceof、checkcast

方法调用与返回指令

invokevirtual:用于调用对象的实例方法,根据对象的实际类型进行分配(虚方法分派),支持多态

invokeinterface:调用接口方法,会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用

invokespecial:调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法,这些方法都是静态类型绑定的,不会再调用时进行动态派发

invokestatic:用于调用命名类中的类方法

invokedynamic:调用动态绑定的方法

方法返回指令:ireturn、areturn、return

操作数栈管理指令

pop、pop2、dup、dup2、dup_x1、swap、nop

控制转移指令

条件跳转指令:ifeq、iflt

比较条件跳转指令:if_icmpeq、if_icmpne

多条件分支跳转:tableswitch、lookupswitch

无条件跳转:goto、goto_w

异常处理指令

athrow

try/catch/finally用异常表来实现,异常表保存了每个异常处理信息,比如起始位置、结束位置、程序计数器记录的代码处理的偏移地址、被捕获的异常类在常量池中的索引

同步控制指令

JVM支持两种同步结构:方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的

方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中

方法内指定指令序列的同步,使用monitorenter和monitorexit

2.4 类的加载过程

在这里插入图片描述
过程一:Loading阶段

所谓加载,简而言之就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型—类模板对象

  1. 通过类的全限定名,获取类的二进制数据流
  2. 解析类的二进制数据流为方法区内的数据结构
  3. 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口

数组类的加载:(1)如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组A的元素类型;(2)JVM使用指定的元素类型和数组维度来创建新的数组类

过程二:Linking阶段

环节1:Verification(验证)

保证加载的字节码是合法、合理并符合规范的

环节2:Preparation(准备)

为类的静态变量分配内存,并将其初始化为默认值

这里不包含基本数据类型字段用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值

如果使用字面量的方式定义一个字符串常量的话,也是在准备环节直接进行显示赋值

环节3:Resolution(解析)

将类、接口、字段和方法的符号引用转为直接引用

过程三:Initialization(初始化)阶段

为类的静态变量赋予正确的初始值

初始化阶段的重要工作是执行类的初始化方法:<clinit>()方法,它是由类静态成员的赋值语句以及static语句块合并产生的

虚拟机总是会试图加载父类的<clinit>方法

使用static + final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行

对于<clinit>()方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性

Java程序对类的使用分为两种:主动使用和被动使用,被动使用不会引起类的初始化

主动使用

  1. 创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化
  2. 调用类的静态方法时,即当使用了字节码invokestatic指令
  3. 使用类、接口的静态字段时(final修饰特殊考虑)
  4. 当使用java.lang.reflect包中的方法反射类的方法时
  5. 初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  6. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化
  7. 当虚拟机启动时,用户需要指定一个要执行的主类(包括main()方法的那个类),虚拟机会先初始化这个主类
  8. 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类

被动使用

除了以上的情况,其他情况均属于被动使用

  1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化

    当通过子类引用父类的静态变量,不会导致子类初始化

  2. 通过数组定义类引用,不会触发此类的初始化

  3. 引用常量不会触发此类或接口的初始化,因为常量在链接阶段就已经被显式赋值

  4. 调用ClassLoader类的loadClass()方法加载一个类

过程四:类的Using(使用)

过程五:类的Unloading(卸载)

类、类的加载器、类的实例之间的引用关系:在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。另一方面,一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。由此可见,代表某个类的Class实例与其类的加载器之间为双向关联关系。一个类的实例总是引用代表这个类的Class对象,在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象

类的生命周期:当类被加载、链接和初始化后,它的生命周期就开始了,当代表Sample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,类在方法区内的数据也会被卸载,从而结束类的生命周期

2.5 类加载器

ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的

显式加载指的是在代码中通过调用ClassLoader加载class对象

每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成

类加载器的分类

引导类加载器:

  • 使用C/C++语言实现,嵌套在JVM内部
  • 用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)
  • 不继承自java.lang.ClassLoader,没有父加载器
  • 出于安全考虑,Bootstrap启动类加载器只加载java.javax.sun等开头的类
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器

扩展类加载器:

  • Java语言编写,由sun,misc.Launcher$ExtClassLoader实现
  • 继承于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载

系统类加载器:

  • java语言编写,由sun.misc.Launcher$AppClassLoader实现
  • 继承于ClassLoader类
  • 父类加载器为扩展类加载器
  • 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  • 应用程序中的类加载器默认是系统类加载器
  • 用户自定义类加载器的默认父加载器
  • 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器

用户自定义类加载器:

  • 使用自定义类加载器来实现类库的动态加载,加载源可以是本地的jar包,也可以是网络上的远程资源
  • 通过类加载器可以实现非常绝妙的插件机制,如著名的OSGI组件框架、Eclipse的插件机制
  • 自定义加载器能够实现应用隔离,例如Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块
  • 自定义类加载器通常需要继承于ClassLoader

ClassLoader的主要方法

public final ClassLoader getParent():返回该类加载器的超类加载器

public Class<?> loadClass(String name) throws ClassNotFoundException:加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,则返回ClassNotFoundException,该方法中的逻辑就是双亲委派模式的实现

protected Class<?> findClass(String name) throws ClassNotFoundException:查找二进制名称为name的类,返回结果为java.lang.Class类的实例。这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委派机制,该方法会在检查完父类加载器之后被loadClass()方法调用

protected final Class<?> defineClass(String name, byte[] b, int off, int len):根据给定的字节数组b转换为Class的实例,off和len参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。这是受保护的方法,只有在自定义ClassLoader子类中可以使用

protected final void resolveClass(Class<?> c):链接指定的一个Java类,使用该方法可以使用类的Class对象创建完成的同时也被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用

protected final Class<?> findLoadedClass(String name):查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例

private final ClassLoader parent:ClassLoader的实例,表示这个ClassLoader的双亲,在类加载的过程中,ClassLoader可能会将某些请求交予自己的双亲处理

双亲委派机制

优势:

  1. 避免类的重复加载,确保一个类的全局唯一性
  2. 保护程序安全,防止核心API被随意篡改

弊端:顶层的ClassLoader无法访问底层的ClassLoader所加载的类

破坏双亲委派机制

沙箱安全机制

  • 保证程序安全
  • 保护Java原生的JDK代码

自定义类加载器

  1. 隔离加载类
  2. 修改类加载的方式
  3. 扩展加载源
  4. 防止源码泄漏

三、性能监控与调优篇

未完待续…

四、大厂面试篇

未完待续…

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值