JVM01 --- 内存与垃圾回收篇

JVM01 --- 内存与垃圾回收篇

  • 1.JVM与JAVA体系结构
  • 2.类加载子系统
  • 3.运行时数据区概述及线程
  • 4.程序计数器
  • 5.虚拟机栈(重点)
  • 6.本地方法接口
  • 7.本地方法栈
  • 8.堆(重要)
  • 9.方法区
  • 10.直接内存
  • 11.执行引擎
  • 12.String Table
  • 13.垃圾回收
    • 垃圾回收的相关算法
      • 1 -- 标记阶段:引用计数算法
      • 2 -- 标记阶段:可达性分析算法
      • 3 -- 对象的finalization机制
      • 4 -- MAT与JProfiler的GC Roots溯源
      • 5 -- 清除阶段:标记--清除算法
      • 6 -- 清除阶段:复制算法
      • 7 -- 清除阶段:标记 -- 压缩算法
      • 8 -- 小结
      • 9 -- 分代收集算法
      • 10 -- 增量收集算法、分区算法
      • 11 -- 垃圾回收器的相关概念


1.JVM与JAVA体系结构

JVM:跨语言的平台。JVM不关心其内部运行的程序到底是何种编程语言写的,它只关心“字节码”文件。Java不是最强大的语言,但JVM是最强大的虚拟机。
JVM01 --- 内存与垃圾回收篇_第1张图片

  • JVM作用
    Java虚拟机就是二进制字节码的运行环境,负责装载字节码到内部,解释/编译为其对应平台的机器指令执行。
  • JVM特点
    一次编译,到处运行;自动内存管理;自动垃圾回收功能。
  • JVM的位置
    JVM01 --- 内存与垃圾回收篇_第2张图片
  • JVM的整体结构
    JVM01 --- 内存与垃圾回收篇_第3张图片
  • JVM的架构模型
    由于跨平台性的设计,Java指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
  • JVM的生命周期
    虚拟机的启动:Java虚拟机的启动时通过引导类加载器创建一个初始类来完成的,这个类是由虚拟机的具体实现指定的。
    虚拟机的执行:一个运行中的Java虚拟机有着一个清晰的任务:执行java程序;程序开始执行时他才运行,程序结束时他就停止;执行一个所谓的Java程序的时候,真真正正执行的是一个叫Java虚拟机的进程。
    虚拟机的退出:1 程序正常执行结束;2 程序在执行过程中遇到了异常或错误而异常终止;3 由于操作系统出现错误而导致java虚拟机进程终止;4 某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java安全管理器业允许这次exit或halt操作;5 除此之外,JNI(Java Native Interface)规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机的退出情况。

2.类加载子系统

  • 类加载过程图:
    JVM01 --- 内存与垃圾回收篇_第4张图片
    链接:
    JVM01 --- 内存与垃圾回收篇_第5张图片
    初始化:
  • 初始化阶段就是执行类构造器方法< clinit >()的过程。
  • 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
  • 构造器方法中指令按语句在源文件中出现的顺序执行。
  • < clinit >()不同于类的构造器。(关联:构造器是虚拟机视角下的< init >())
  • 若该类具有父类,JVM会保证子类的 < clinit >()执行前,父类的 < clinit >()已经执行完毕。
  • 虚拟机必须保证一个类的 < clinit >()方法在多线程下被同步加锁。
  • JVM01 --- 内存与垃圾回收篇_第6张图片

类加载器的分类:

  • JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)自定义类加载器(User-Defined ClassLoader)
  • 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
  • 关于ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)。

获取ClassLoader的途径:
方式一:后去当前类的ClassLoader。 => class.getClassLoader()
方式二:获取当前线程上下文的ClassLoader。 => Thread.currentTread().getContextClassLoader()
方式三:获取系统的ClassLoader。 => ClassLoader.getSystemClassLoader()
方式四:获取调用者的ClassLoader。 => DriverManager.getCallerClassLoader()

双亲委派机制:

  • 工作原理:
    1)如果一个类加载器收到了类加载请求,他并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
    2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
    3)若父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。
    JVM01 --- 内存与垃圾回收篇_第7张图片
  • 双亲委派机制优势:
    避免类的重复加载;保护程序安全,防止核心API被随意篡改。

3.运行时数据区概述及线程

JVM01 --- 内存与垃圾回收篇_第8张图片

4.程序计数器

  • 关于PC寄存器的常见问题:
    JVM01 --- 内存与垃圾回收篇_第9张图片
    JVM01 --- 内存与垃圾回收篇_第10张图片

5.虚拟机栈(重点)

  • 栈是运行时单位,而堆是存储的单位。即栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪里。
  • Java虚拟机栈:Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,一次次的方法调用,就相当于一个个的栈帧出栈。是线程私有的。
  • 生命周期:生命周期和线程一致。
  • 作用:主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。(局部变量,成员变量(或属性);基本数据变量,引用类型变量(类、数组、接口))

栈的优点: 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。JVM直接对Java栈的操作只有两个:每个方法的 执行 ,伴随着 进栈 (入栈、压栈);方法执行 结束 后的 出栈 工作。对于栈而言不存在垃圾回收问题。

栈中可能出现的异常:
Java虚拟机规范允许Java栈的大小是动态的或是固定不变的

  • 如果采用固定大小的Java虚拟机栈,那每个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
  • 若Java虚拟机可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError(也就是OOM)异常。

栈中都存储什么?

  • 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。
  • 在这个线程上正在执行的每个方法都各自对应一个栈帧。
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

栈的运行原理

  • JVM直接对Java栈的操作只有两个,就是对栈帧的压栈
    出栈
    ,“先进先出”的原则。
  • 在一条活动线程中,一个世界点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
  • 如果在盖方法中调用了其他方法,对应的新的栈帧则会被创建出来,放在栈顶端,成为新的当前帧。

栈帧的内部结构
每个栈帧中存储着:

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack)(或表达式栈)
  • 动态链接(Dynamin Linking)(或指运行时常量池的方法引用)
  • 方法返回地址(Return Address)(或方法正常退出或异常退出的定义)
  • 一些附加信息

关于Slot的理解

  • 参数值的存放总是在局部变量表数组的index0开始,到数组长度-1的索引结束。
  • 局部变量表,最基本的存储单元时Slot(变量槽)
  • 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
  • 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
  • byte, short, char在存储前转换为int, boolean也被转为int, 0表示false,非0表示true。
  • long和double则占据两个slot。
  • JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成为访问到局部变量表中制定的局部变量值
  • 当一个实例方法被调用时,她的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上。
  • 若需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。
  • 若当前帧是由构造方法或实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
    JVM01 --- 内存与垃圾回收篇_第11张图片
  • Slot的重复利用栈帧中的局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
public class SlotTest{
	public void localVar1(){
		int a = 0;
		System.out.println(a);
		int b = 0;
	}
	public void localVar2(){
        int a = 0;
        System.out.println(a);
        
        //此时的b就会复用a的槽位
        int b = 0;
    }
}
  • 在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
  • 局部变量表中的变量也是重要的垃圾回收根节点,只要局部变量表中直接或间接引用的对象都不会被回收。

操作数栈
JVM01 --- 内存与垃圾回收篇_第12张图片
JVM01 --- 内存与垃圾回收篇_第13张图片
方法返回地址

  • 存放调用方法的pc寄存器的值。
  • 一个方法的结束,有两种方式:正常执行完成;出现未处理的异常,非正常退出。
  • 无论通过哪一种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确认,栈帧中一般不会保存这部分信息。
  • 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC 寄存器值等,让调用者方法继续执行下去。正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

栈相关的面试题:

  • 举例栈溢出的情况? (Stack0verflowError) 通过它-Xss设置栈的大小;OOM。
  • 调整栈大小,就能保证不出现溢出吗? 不能。
  • 分配的栈内存越大越好吗? 不是。
  • 垃圾回收是否会涉及到虛拟机栈? 不会的。
  • 方法中定义的局部变量是否线程安全?具体问题具体分析。

6.本地方法接口

JVM01 --- 内存与垃圾回收篇_第14张图片
什公是本地方法?
筒单地讲,一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如c。这个特征并非Java所特有,很多其它的编程语言都有这机制,比如在C++中,可以用extern “C” 告知C+ +编译器去调用一个c的函数。

“A native method is a Java method whose implementation is provided by non-java code。”

在定义一个native method吋, 并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面实现的。

本地接ロ的作用是融合不同的編程语言为Java所用,它的初衷是融合C/C++程序。

为什么要使用Native Method ?
Java使用起来非常方便,然而有些层次的任务用Java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。

  • 与Java环境外交互:
    有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。 你可以想想Java需要与一些底层系统,如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口, 而且我们无需去了解Java应用之外的繁琐的细节。
  • 与操作系统交互:
    JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释
    器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用c写的。还有,如果我们要使用一些Java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
  • Sun’ s Java
    Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。 jre大部分是用Java实现的,它也通过一些本地方法与外界交互。例如:类java. lang. Thread的setPriority() 方法是用Java实现的,但是它实现调用的是该类里的本地方法setPriority0().这个本地方法是用c实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用win32 SetPriority() API。 这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library) 提供,然后被JVM调用。

现状
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用web Service等等, 不多做介绍。

7.本地方法栈

  • Java虛拟机栈用于管理Java方法的调用,而本地方法栈用于理本地方法的调用。
  • 本地方法栈,也是线程私有的。
  • 允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
    ➢如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个stackoverflowError 异常。
    ➢如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个OutOfMemoryError 异常。
  • 本地方法是使用C语言实现的。
  • 它的具体做法是Native Method Stack中 登记native方法,在Execution Engine 执行时加载本地方法库。
    JVM01 --- 内存与垃圾回收篇_第15张图片
  • 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
    ➢本地方法可以通过本地方法接口来访问虛拟机内部的运行时数据区。
    ➢它甚至可以直接使用本地处理器中的寄存器。
    ➢直接从本地内存的堆中分配任意数量的内存。
  • 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
  • 在Hotspot JVM中, 直接将本地方法栈和虚拟机栈合二为一。

8.堆(重要)

*堆的核心概述

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
  • Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
    ➢堆内存的大小是可以调节的
  • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
  • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB) 。
  • 《Java虛拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应
    当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated )
    ➢我要说的是: “几乎”所有的对象实例都在这里分配内存。- -从实际
    使用角度看的。
  • 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
  • 堆,是GC ( Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
  • 内存细分:
    JVM01 --- 内存与垃圾回收篇_第16张图片
    堆空间大小的设置
  • Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大
    家可以通过选项"- Xmx"和”- Xms"来进行设置。
    ➢"-Xms"用于表示堆区的起始内存,等价于-XX: InitialHeapSize
    ➢"-Xmx"则用于表示堆区的最大内存,等价于-XX :MaxHeapSize
  • 一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常
  • 通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
  • 默认情况下,初始内存大小:物理电脑内存大小/ 64;最大内存大小:物理电脑内存大小/ 4
    年轻代和老年代
    JVM01 --- 内存与垃圾回收篇_第17张图片
    JVM01 --- 内存与垃圾回收篇_第18张图片
    JVM01 --- 内存与垃圾回收篇_第19张图片
    JVM01 --- 内存与垃圾回收篇_第20张图片

总结

  • 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to。
  • 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。

Minor GC、Major GC、Full GC
JVM01 --- 内存与垃圾回收篇_第21张图片
JVM01 --- 内存与垃圾回收篇_第22张图片
JVM01 --- 内存与垃圾回收篇_第23张图片
JVM01 --- 内存与垃圾回收篇_第24张图片
JVM01 --- 内存与垃圾回收篇_第25张图片

9.方法区

方法区被看作是一块独立于Java堆的内存空间。
运行时数据区结构图
JVM01 --- 内存与垃圾回收篇_第26张图片
从线程共享与否的角度看:
JVM01 --- 内存与垃圾回收篇_第27张图片
栈、堆、方法区之间的交互关系:
JVM01 --- 内存与垃圾回收篇_第28张图片
JVM01 --- 内存与垃圾回收篇_第29张图片

方法区的基本理解:

  • 方法区(Method Area) 与Java堆一 样,是各个线程共享的内存区域。
  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样,都可以是不连续的。
  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误: java.lang.OutofMemoryError:PermGen space或者java.lang.OutOfMemoryError: Metaspace
  • 关闭JVM就会释放这个区域的内存。

如何解决这些OOM?
JVM01 --- 内存与垃圾回收篇_第30张图片

运行时常量池 VS 常量池

  • 方法区,内部包含了运行时常量池。
  • 字节码文件,内部包含了常量池。
  • 要弄清楚方法区,需要理解清楚ClassFile,因为加载类的信息都在方法区
  • 要弄清楚方法区的运行时常量池,需要理解清楚ClassFile中的常量池。
    https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
  • 常量池中又什么?
    JVM01 --- 内存与垃圾回收篇_第31张图片
    小结:
    常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
    运行时常量池:
    JVM01 --- 内存与垃圾回收篇_第32张图片

面试题
JVM01 --- 内存与垃圾回收篇_第33张图片
JVM01 --- 内存与垃圾回收篇_第34张图片
JVM01 --- 内存与垃圾回收篇_第35张图片
对象的实例化
JVM01 --- 内存与垃圾回收篇_第36张图片
对象的内存布局
JVM01 --- 内存与垃圾回收篇_第37张图片

10.直接内存

直接内存概述

  • 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
  • 直接内存是在Java堆外的、直接向系统申请的内存区间。
  • 来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存
  • 通常,访问直接内存的速度会优于Java堆。即读写性能高。
    ➢因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。
    ➢Java的NIO库允许Java程序使用直接内存,用于数据缓冲区
    JVM01 --- 内存与垃圾回收篇_第38张图片
    JVM01 --- 内存与垃圾回收篇_第39张图片
  • 也可能导致0utOfMemoryError异常
  • 由于直接内存在Java堆外,因此它的大小不会直接受限于-Xmx指定的最大堆大小,但是系统内存是有限的,Java堆 和直接内存的总和依然受限于操作系统能给出的最大内存。
  • 缺点
    ➢分配回收成本较高
    ➢不受JVM内存回收管理
  • 直接内存大小可以通过MaxDi rectMemorySize设置
  • 如果不指定, 默认与堆的最大值-Xmx参数值一致

11.执行引擎

  • 执行引擎是Java虛拟机核心的组成部分之一 。
  • “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
  • JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。那么,如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。

执行引擎的工作过程
JVM01 --- 内存与垃圾回收篇_第40张图片

  • 从外观上来看,所有的Java虛拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果。

Java代码编译和执行的过程
JVM01 --- 内存与垃圾回收篇_第41张图片
问题:什么是解释器( Interpreter),什么是JIT编译器?
解释器:当Java虛拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
JIT (Just In Time Compiler)编译器:就是虚拟机将源代码直接编;译成和本地机器平台相关的机器语言。
问题:为什么说Java是半编译半解释型语言?
JDK1.0时代,将Java语言定位为“解释执行”还是比较准确的。再后来,Java也发展出可以直接生成本地代码的编译器。
现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。

12.String Table

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

String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop"; //编译期优化
若拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果。new了一个新对象,则是开辟了一个新空间,就有一个新地址。
String s5 = s1 + "hadoop"; //s1为变量
String s6 = "javaEE" + s2; //s2为变量
String s7 = s1 + s2;
System.out.println(s3 == s4);//true;
System.out.println(s3 == s5);//false;
System.out.println(s3 == s6);//false;
System.out.println(s3 == s7);//false;
System.out.println(s5 == s6);//false;
System.out.println(s5 == s7);//false;
System.out.println(s6 == s7);//false

intern():判断字符串常量池中是否存在javaEEhadoop值,若存在,则返回常量池中javaEEhadoop的地址;
若字符串常量池中不存在javaEEhadoop,则在常量池中加载一份javaEEhadoop,并返回此对象的地址。

String s8 = s6.intern();
System.out.println(s3 == s8);//true
//只要在拼接的时候有一个是变量,那么结果就是在堆当中
/********************************************/
1、字符串拼接操作不一定使用的是StringBuilder
   若拼接符号左右两边都是字符串常量或常量引用,则任然使用编译期优化,即非StringBuilder的方式。
   
2、针对于final修饰类、方法、基本数据类型、引用数据类型的量结构时,能使用上final的时候建议使用上。


final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4); //true

题目:
new String(“ab”)会创建几个对象?看字节码,就知道是两个对象。(自行去看字节码)

  • 一个对象是:new关键字在堆空间创建的。
  • 另一个对象是:字符串常量池中的对象。字节码指令:ldc

思考:new String(“a”) + new String(“b”)呢?(自行去看字节码)

  • 对象1:new StringBuilder()
  • 对象2:new String(“a”)
  • 对象3:常量池中的“a”
  • 对象4:new String(“b”)
  • 对象5:常量池中的"b"
  • 深入剖析:StringBuilder的toString():
    对象6:new String(“ab”)
  • 强调一下,toString()的调用,在字符串常量池中,没有生成"ab"
String s = new String("1");
s.intern();//调用此方法之前,字符串常量池中已存在“1”
String s2 = "1";
System.out.println(s == s2);//jdk6:false jdk7/8:false

String s3 = new String("1") + new String("1");//s3变量记录的地址为new String("11")
//执行完上一行代码之后,字符串常量池中,是否存在“11”呢? 不存在!
s3.intern();//在字符串常量池中生成“11”、如何理解:jdk6:创建了一个新的对象“11”,也就有新的地址。
	        //                               jdk7/8:此时常量中并没有创建“11”,而是创建了一个指向堆空间中new String("11")的地址。
	        
String s4 = "11";//s4变量记录的地址:使用的是上一行代码执行时,在常量池中生成的“11”的地址。
System.out.println(s3 == s4);//jdk6:false  jdk7/8:true;

总结String的intern()的使用:

  • jdk1.6中,将这个字符串对象尝试放入串池。
    ➢如果串池中有,则并不会放入。返回已有的串池中的对象的地址
    ➢如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
  • Jdk1.7起,将这个字符串对象尝试放入串池。
    ➢如果串池中有,则并不会放入。返回已有的串池中的对象的地址
    ➢如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址

这章节的重点是intern()的使用。

13.垃圾回收

大厂面试题

什么是垃圾呢?
垃圾就是运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出

想要学习GC ,首先需要理解为什么需要GC ?

  • 对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。
  • 除了释放没用的对象, 垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象
  • 随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。

Java的垃圾回收机制

  • 自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险
    ➢没有垃圾回收器,java也会和cpp- 样,各种悬垂指针,野指针,泄露问题
    让你头疼不已。
  • 自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发
  • oracle官网关于垃圾回收的介绍➢https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/

GC的作用域方法区
JVM01 --- 内存与垃圾回收篇_第42张图片

  • 垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收。其中,Java堆是垃圾回收器的工作重点。
  • 从次数上讲:
    – 频繁收集Young区
    – 较少收集old区
    – 基本不动perm区(或元空间)

垃圾回收的相关算法

1 – 标记阶段:引用计数算法

  • 在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
  • 那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
  • 判断对象存活一般有两种方式:引用计数算法可达性分析算法
  • 引用计数算法(Reference Counting) 比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
  • 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
  • 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
  • 缺点:
    ➢它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
    ➢每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
    ➢引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。

2 – 标记阶段:可达性分析算法

  • 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
  • 相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)。
  • 所谓"GC Roots"根集合就是一-组必须活跃的引用。
  • 基本思路:
    ➢可达性分析算法是以根对象集合(GC Roots) 为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
    ➢使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
    ➢如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
    ➢在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。

在Java语言中,GC Roots包括以下几类元素:

  • 虚拟机栈中引用的对象,
    ➢比如:各个线程被调用的方法中使用到的参数、局部变量等。

  • 本地方法栈内JNI (通常说的本地方法)引用的对象

  • 方法区中类静态属性引用的对象
    ➢比如: Java类的引用类型静态变量

  • 方法区中常量引用的对象
    ➢比如:字符串常量池(String Table)里的引用

  • 所有被同步锁synchronized持有的对象

  • Java虚拟机内部的引用。
    ➢基本数据类型对应的Class对象,- -些常驻的异常对象(如:Nul lPointe rException、OutOfMemoryError) ,系统类加载器。

  • 反映java虚拟机内部情况的JMXBean、 JVMTI中注册的回调、本地代码缓存等。

  • 除了这些固定的GCRoots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(Partial GC)
    ➢如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。

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

注意

  • 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。

  • 这点也是导致GC进行时必须“Stop The World"的一个重要原因。
    ➢即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

3 – 对象的finalization机制

  • Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
  • 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。,
  • finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
  • 由于finalize ()方法的存在,虚拟机中的对象一般处于三种可能的状态。
  • 如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:
    可触及的:从根节点开始,可以到达这个对象。
    可复活的:对象的所有引用都被释放,但是对象有可能在finalize ()中复活。
    不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize() 只会被调用一次。
  • 以上3种状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。

4 – MAT与JProfiler的GC Roots溯源

5 – 清除阶段:标记–清除算法

背景:
标记-清除算法( Mark- Sweep )是一种非常基础和常见的垃圾收集算法,该算法被J. McCarthy等人在1960年提出并并应用于Lisp语言。
执行过程:
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world) ,然后进行两项工作,第一项则是标记,第二项则是清除。

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

缺点
➢效率不算高
➢在进行GC的时候,需要停止整个应用程序,导致用户体验差
➢这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表
注意:何为清除?
➢这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。

6 – 清除阶段:复制算法

背景:
为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年发表了著名的论文,“使用双存储区的Lisp语言垃圾收集器CA LISP Garbage Col lector Algorithm Using Serial Secondary Storage ”。M. L.Minsky在该论文中描述的算法被人们称为复制(Copying)算法,它也被M. L.Minsky本人成功地引入到了Lisp语言的一个实现版本中。
核心思想:
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

优点:

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

缺点:

  • 此算法的缺点也是很明显的,就是需要两倍的内存空间。
  • 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。

特别的:

  • 如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。

7 – 清除阶段:标记 – 压缩算法

背景:
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
标记一清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。标记-压缩(Mark - Compact)算法由此诞生。
1970 年前后, G. L. Steele 、C. J. Chene和D.S. Wise 等研究者发布标记-压缩算法。在许多现代的垃圾收集器中,人们都使用了标记-压缩算法或其改进版本。
JVM01 --- 内存与垃圾回收篇_第43张图片
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark - Sweep-Compact)算法。
二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
优点:

  • 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
  • 消除了复制算法当中,内存减半的高额代价。

缺点:

  • 从效率上来说,标记-整理算法要低于复制算法。
  • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
  • 移动过程中,需要全程暂停用户应用程序。即: STW

8 – 小结

JVM01 --- 内存与垃圾回收篇_第44张图片

9 – 分代收集算法

前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。分代收集算法应运而生。
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如: String对象, 由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
目前几乎所有的GC都是采用分代收集( Generational Collecting) 算法执行垃圾回收的。
以HotSpot中的CMS回收器为例,CMS是基于Mark- Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial 0ld回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Seria 0ld执行Full GC以达到对老年代内存的整理。
分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年
代。

10 – 增量收集算法、分区算法

上述现有的算法,在垃圾回收过程中,应用软件将处于一种Stop the World的状态。在Stop the World状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting)算法的诞生。
基本思想
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
缺点:
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制Gc产生的停顿时间,将- -块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成
连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制–次回收多少个小
区间。
最后
注意,这些只是基本的算法思路,实际GC实现过程要复杂的多,目前还在发展中的前沿GC都是复合算法,并且并行和并发兼备。

11 – 垃圾回收器的相关概念

System.gc()的理解:
在默认情况下,通过System.gc()或者Runtime. getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收尝试释放被丢弃对象占用的内存。

  • 然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用。JVM实现者可以通过System.gc ()调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()。

内存溢出与内存泄露:

1、内存溢出(OOM)

  • 内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。
  • 由于GC一直在发展,所有一.般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现O0M的情况。
  • 大多数情况下,GC会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的Full GC操作,这时候会回收大量的内存,供应用程序继续使用。
  • javadoc中对outofMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
  • 首先说没有空闲内存的情况:说明Java 虚拟机的堆内存不够。原因有二:
    (1) Java虚拟机的堆内存设置不够。
    比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可
    观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小。我们可以通过参数-Xms、-Xmx来调整。
    (2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
    对于老版本的Oracle JDK,因为永久代的大小是有限的,并且JVM对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现OutOfMemoryError也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似intern字符串缓存占用太多空间,也会导致00M问题。对应的异常信息,会标记出来和永久代相关:“java. lang. OutOfMemoryError: PermGen space"。
    随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的00M有所改观,出现OOM,异常信息则变成了:“java. lang . OutOfMemoryError: Metaspace"。 直接内存不足,也会导致OOM。
  • 这里面隐含着一层意思是,在抛出OutOfMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间。
    ➢例如:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等。
    ➢在java.nio.BIts.reserveMemory()方法中,我们能清楚的看到,System.gc()会被调用,以清理空间。
  • 当然,也不是在任何情况下垃圾收集器都会被触发的
    ➢比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出0utOfMemoryError.

2、内存泄露
也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
但实际情况很多时候一 些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM, 也可以叫做宽泛意义上的“内存泄漏”。
尽管内存泄漏并不会立刻引起程序崩溃,但是一-旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现0utOfMemory异常,导致程序崩溃。
注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟
内存大小取决于磁盘交换区设定的大小。
举例:
1、单例模式
单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
2、一些提供close的资源未关闭导致内存泄漏数据库连接( dataSourse.getConnection()),网络连接( socket)和io连接必须手动close,否则是不能被回收的。
3、Stop The World

  • Stop-the-World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
    ➢可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。
    分析工作必须在一个能确保一 致性的快照中进行
    一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
    如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
  • 被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉
    像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。
  • STW事件和采用哪款GC无关,所有的GC都有这个事件。
  • 哪怕是G1也不能完全避免Stop-the-world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。1
  • STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
  • 开发中不要用System.gc();会导致Stop-the-world的发生。

4、垃圾回收的并行与并发
JVM01 --- 内存与垃圾回收篇_第45张图片
JVM01 --- 内存与垃圾回收篇_第46张图片

5、安全点和安全区域
程序执行时并非在所有地方都能停顿下来开始GC, 只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint )”。
Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择些执行时间较长的指令作为Safe Point, 如方法调用、循环跳转和异常跳转等。
安全点:
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?

  • 抢先式中断:(目前没有虚拟机采用了)
    首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
  • 主动式中断:
    设置一个中断标志,各个线程运行到Safe Point 的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。

安全区域:
Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?例如线程处于Sleep状态或Blocked 状态,这时候线程无法响应JVM的中断请求,“走” 到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region) 来解决。
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。我们也可以把Safe Region 看做是被扩展了的Safepoint。
实际执行时:
1、当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会 忽略标识为Safe Region状态的线程;
2、当线程即将离开Safe Region时, 会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止;

6、强引用
[既偏门又非常高频的面试题]强引用、软引用、弱引用、虚引用有什么区别?具体使用场景是什么?
Reference子类中只有终结器引用是包内可见的,其他3种引用类型均为public,可以在应用程序中直接使用

  • 强引用(StrongReference) :最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new object()"这种引用关系。无论任何情况下只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用(SoftReference) :在系统将要发生内存溢出之前,将会把这些对象列入回收
    范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用(WeakReference) :被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
  • 虛引用(PhantomReference) :一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得-一个对象的实例。为一个对象设置虛引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

强引用 — 不回收
JVM01 --- 内存与垃圾回收篇_第47张图片
** 7、软引用**
软引用—内存不足即回收
JVM01 --- 内存与垃圾回收篇_第48张图片
8、弱引用
弱引用—发现即回收

9、虚引用
虚引用—对象回收跟踪
JVM01 --- 内存与垃圾回收篇_第49张图片
JVM01 --- 内存与垃圾回收篇_第50张图片
10、垃圾回收器概述

评估GC的性能指标
JVM01 --- 内存与垃圾回收篇_第51张图片
7种经典的垃圾回收器

  • 串行回收器: Serial、 Serial Old
  • 并行回收器: ParNew、 Parallel Scavenge、Parallel Old
  • 并发回收器: CMS、G1

7种经典的垃圾回收器与垃圾分代之间的关系
JVM01 --- 内存与垃圾回收篇_第52张图片
垃圾收集器的组合关系
JVM01 --- 内存与垃圾回收篇_第53张图片
G1垃圾回收器:区域化分代式
为什么名字叫做Garbage First (G1)呢?

  • 因为G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region) (物理上不连续的)。使用不同的Region来表示Eden、幸存者0区,幸存者1区,老年代等。
  • G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
  • 由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First)

G1的优点:
与其他GC收集器相比,G1使用了全新的分区算法,其优点如下所示:
并行与并发
➢并行性: G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此
时用户线程STW
➢并发性: G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况,
分代收集
➢从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
➢将堆空间分为若干个区域(Region) , 这些区域中包含了逻辑上的年轻代和老年代。
➢和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
空间整合
➢CMS:“标记-清除”算法、内存碎片、若干次GC后进行- -次碎片整理
➢G1将内存划分为- -个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一-次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
可预测的停顿时间模型( 即:软实时soft real-time)

  • 这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
    ➢由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
    ➢G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率
    ➢相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。

G1回收器的缺点:
相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint) 还是程序运行时的额外执行负载(Overload)都要比CMS要高。
从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存
应用上则发挥其优势。平衡点在6-8GB之间。

G1使用场景:

G1垃圾回收过程:
JVM01 --- 内存与垃圾回收篇_第54张图片
7种经典垃圾回收器的总结

面试题目:
➢垃圾收集的算法有哪些?如何判断一个对象是否可以回收?
➢垃圾收集器工作的基本流程。

GC日志分析:

你可能感兴趣的:(JVM,java,开发语言,后端)