文章目录
- 前言
- 1、JAVA内存区域
- 2、什么是类加载?类加载的过程?什么是类加载器?类加载器有哪些?
- 3、什么是双亲委派机制?(重要)
- 4、什么是沙箱安全机制?
- 5、对象的创建
- 6、为对象分配内存
- 7、对象的访问定位
- 8、内存泄露与内存溢出
- 9、垃圾回收GC
- 11、新生代为何划分Eden和Survivor?为什么设置两个Survivor?
- 12、JVM中一次完整的GC流程是怎样的?
- 13、finalize()方法
- 14 逃逸分析
- 15、栈、堆、方法区的交互关系
- 16、 JVM监控及诊断的GUI工具
前言
其中内存模型,类加载机制,GC是重点,性能调优部分更加偏向应用方面,重点出实践能力,编译器优化和执行模式部分偏向理论基础,这些都是重要的知识点。
了解:
- 内存模型各个部分的作用,保存哪些数据。
- 类加载双亲委派加载机制,常用加载器分别加载哪种类型的类。
- GC分代回收的思想和依据以及不同的垃圾回收算法的回收思路和适合场景。
- 性能调优常有JVM优化参数作品,参数调优的依据,常用的JVM分析工具能分析哪些问题以及使用方法。
- 执行模式解释/编译/混合模式的优缺点,Java7提供的分层编译技术,JIT即时编译技术,OSR栈上替换,C1/C2编译器针对的场景
- 针对的是server模式,优化更激进.新技术方面Java10的graal编译器。
- 编译器优化javac的编译过程,ast抽象语法树,编译器优化和运行器优化。
1、JAVA内存区域
1.1、说一下 JVM 的主要组成部分及其作用?
JVM内存结构 = 类加载器 + 执行引擎 + 运行时数据区域 。
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
- Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
- Execution engine(执行引擎):执行classes中的指令。
- Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
- Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
作用:首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
JAVA程序运行机制步骤:
- 首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;
- 再利用编译器将源代码编译成字节码文件,字节码文件的后缀名为.class;
- 运行字节码的工作是由解释器(java命令)来完成的。
java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。
其实可以一句话来解释:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
1.2 、说一下 JVM 运行时数据区
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域:
不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范, Java 虚拟机规范规定的区域分为以下 5 个部分:
1、程序计数器(PC寄存器)
- 作用:记录当前线程所执行到的字节码的行号。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 意义:JVM的多线程是通过线程轮流切换并分配处理器来实现的,对于我们来说的并行事实上一个处理器也只会执行一条线程中的指令。所以,为了保证各线程指令的安全顺利执行,每条线程都有独立的私有的程序计数器(为什么要线程计数器?因为线程是不具备记忆功能)。
- 存储内容:当线程中执行的是一个Java方法时,程序计数器中记录的是正在执行的线程的虚拟机字节码指令的地址。当线程中执行的是一个本地方法时,程序计数器中的值为空。
- 可能出现异常:此内存区域是唯一一个在JVM上不会发生内存溢出异常(OutOfMemoryError)的区域。
使用程序计数器存储字节码指令地址有什么用?为什么使用PC寄存器记录当前线程的执行地址?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就要知道接着从哪开始继续执行,JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么会被设计为线程私有?
多线程在一个特定的时间段内只会执行其中的一个线程的方法,CPU会不停的做任务切换,这样必然导致经常中断或恢复,为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然就是为每一个线程都分配一个PC寄存器这样一来各个线程之间便可以进行独立的运算,从而不会出现相互干扰的情况。
2、JAVA虚拟机栈(重要)
- 作用:描述Java方法执行的内存模型。每个方法在执行的同时都会开辟一段内存区域用于存放方法运行时所需的数据,成为栈帧,一个栈帧包含如:局部变量表、操作数栈、动态链接、方法出口等信息。
- 意义:JVM是基于栈的,所以每个方法从调用到执行结束,就对应着一个栈帧在虚拟机栈中入栈和出栈的整个过程。
- 存储内容:局部变量表(编译期可知的各种基本数据类型、引用类型和指向一条字节码指令的returnAddress类型)、操作数栈、动态链接、方法出口等信息。值得注意的是:局部变量表所需的内存空间在编译期间完成分配。在方法运行的阶段是不会改变局部变量表的大小的。
- 可能出现的异常:Java虚拟机规范允许Java栈的大小是动态的或者十固定不变的,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常。如果在动态扩展内存的时候无法申请到足够的内存,就会抛出OutOfMemoryError异常。
使用 -Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
栈的特点有哪些?
- 栈是一种快速分配存储方式,访问速度仅次于程序计数器。
- JVM直接对Java栈的操作只有两个:一是每个方法执行,伴随这进栈。二是执行结束后的出栈。
- 对于栈来说不存在垃圾回收问题,但是存在OOM。
3、本地方法栈(Native Method Stack)
- 作用:为JVM所调用到的Native即本地方法服务。(虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;Native 关键字修饰的方法是看不到的,Native 方法的源码大部分都是 C和C++ 的代码)。
- 可能出现的异常:和虚拟机栈出现的异常很相像。
4、Java 堆(Java Heap)
- 作用:所有线程共享一块内存区域,在虚拟机开启的时候创建。
- 意义:1、存储对象实例,更好地分配内存。2、垃圾回收(GC)。堆是垃圾收集器管理的主要区域。更好地回收内存。
- 存储内容:存放对象实例,几乎所有的对象实例都在这里进行分配。堆可以处于物理上不连续的内存空间,只要逻辑上是连续的就可以。值得注意的是:在JIT编译器等技术的发展下,所有对象都在堆上进行分配已变得不那么绝对。有些对象实例也可以分配在栈中。
- 可能出现的异常:实现堆可以是固定大小的,也可以通过设置配置文件设置该为可扩展的。 如果堆上没有内存进行分配,并无法进行扩展时,将会抛出OutOfMemoryError异常。
5、方法区(Methed Area)
- 作用:用于存储运行时常量池、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 意义:对运行时常量池、常量、静态变量等数据做出了规定。
- 存储内容:运行时常量池(具有动态性)、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 可能出现的异常:当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
1.3、详细介绍下Java虚拟机栈
- Java虚拟机是线程私有的,它的生命周期和线程相同。
- 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中他又要存储,局部变量,操作数栈,动态链接,出口等。
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量、操作数栈、动态链接和方法返回地址等信息。每一个方法从调用开始到执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
每一个栈帧都包括了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的 Code 属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
1.3.1、局部变量表
局部变量表也被称之为局部变量数组或者本地变量表。
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、方法引用。以及returnAddress类型。
在局部变量表中,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot
byte,short,char在存储前被转换位int,boolean也被转换为int,0表示false,非零表示true。
方法嵌套的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用的次数越多。
局部变量表中的变量只在当前方法调用中有效。当方法调用结束的后,随着方法栈的销毁,局部变量表也会随之销毁。
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
局部变量表所需的容量大小是在编译期就确定下来的,并且在运行期间不会改变,为保存在方法的Code属性的maxinum local variables 数据项。
局部变量表Slot的理解:
- 参数存放总是在局部变量数组的index0开始,到数组长达-1的索引结束。
- 局部变量表,最基本的存储单元是Slot(变量槽)
关于Slot的理解:
1.3.2、操作数栈
1.3.3、动态链接(或指向运行时常量池的方法引用)
1.3.4、方法返回地址
1.4、详细介绍一下JAVA的堆
- JAVA的堆是JAVA虚拟机所管理的内存中最大的一块,是被所有的线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象的实例。
- Java 堆可以细分为:新生代(Eden 空间、From Survivor、To Survivor 空间)和老年代。进一步划分的目的是更好地回收内存,或者更快地分配内存。
- 通过 -Xms设定程序启动时占用内存大小,通过 -Xmx 设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常。
- 通过 -Xss 设定每个线程的堆栈大小。设置这个参数,需要评估一个线程大约需要占用多少内存,可能会有多少线程同时运行等。
1.5、栈和堆的区别?(重点)
1.6、详细介绍一下Hotspot虚拟机的方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区逻辑上属于堆的一部分。
对方法区进行垃圾回收的主要目标是对常量池的回收和对类的卸载。
设置方法区内存的大小
永久代:
方法区是 JVM 的规范,而永久代(PermGen)是方法区的一种实现方式,并且只有 HotSpot 有永久代。而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有永久代。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp页面比较多的情况,容易出现永久代内存溢出。
元空间:
JDK 1.8 的时候,HotSpot 的永久代被彻底移除了,使用元空间替代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。两者最大的区别在于:元空间并不在虚拟机中,而是使用直接内存。
为什么要将永久代替换为元空间呢?
永久代内存受限于 JVM 可用内存,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是相比永久代内存溢出的概率更小。
方法区的垃圾回收(了解)
1.7、什么是直接内存?
直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
直接内存的读写操作比堆内存快,可以提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到直接内存。
直接内存的大小可以通过-XX:MaxDirectMemorySize进行设置。
如果不指定,默认与堆的最大值-Xmx参数值是一致的。
1.8、什么是运行时常量池?
运行时常量池是方法区的一部分,在类加载之后,会将编译器生成的各种字面量和符号引用放到运行时常量池。在运行期间动态生成的常量,如 String 类的 intern()方法,也会被放入运行时常量池。
1.9、什么情况下会出现占内存溢出?
当线程请求的栈深度超过虚拟机允许的最大深度时,会抛出StackOverFlowError异常,通过参数-Xss可以调整JVM栈的大小。
1.10、执行引擎(了解)
JIT编译器
2、什么是类加载?类加载的过程?什么是类加载器?类加载器有哪些?
类的加载指的是将类的class文件中的二进制数据读到内存中,将其放在运行时数据区的方法区中,然后在堆区创建一个对象,这个对象封装了类在方法区内的数据结构,并且提供了访问方法区内的类信息的接口。
加载:
类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象。
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
验证:
确保Class文件的字节流中包含的信息符合虚拟机规范,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。
准备:
准备:创建类或者接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显示初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的JVM 指令。
- 如果 static 变量是 final 的基本类型或者字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成。
- 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成。
解析:
虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用用于描述目标,直接引用直接指向目标的地址。
初始化:
初始化阶段就是执行类构造器< clinit >方法的过程。< clinit >()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成的,由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有一下四种类加载器:
- 启动类加载器:用来加载 Java 核心类库,无法被 Java 程序直接引用。
- 扩展类加载器:它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器:它根据应用的类路径来加载 Java 类。可通过ClassLoader.getSystemClassLoader()来获取它。
- 用户自定义类加载器:通过继承 java.lang.ClassLoader类的方式实现。
3、什么是双亲委派机制?(重要)
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终 将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
优势:
- 避免了类的重复加载,确保一个类的全局唯一性,Java类随着它的类加载器一起具备一种带有优先级的层次关系,通过这种层级可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子类加载器在加载一次了
- 保护程序安全,防止核心API被随意篡改。
4、什么是沙箱安全机制?
作用:主要用来防止恶意代码污染java源代码。
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
5、对象的创建
对象创建的几种方式:
对象创建的流程:
虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有, 必须先执行相应的类加载。类加载通过后,接下来分配内存。若Java堆中内存是 绝对规整的,使用“指针碰撞“方式分配内存;如果不是规整的,就从空闲列表 中分配,叫做”空闲列表“方式。划分内存时还需要考虑一个问题-并发,也有 两种方式: CAS同步处理,或者本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。然后内存空间初始化操作,接着是做一些必要的对象设置(元信 息、哈希码…),后执行方法。
Java对象头里面有什么?
- 运行时元数据:哈希值,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳。
- 类型指针:指向类元数据,确定该对象所属的类型。
- 如果是数组的话,还会记录数组的长度。
6、为对象分配内存
类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java 堆是否规整,有两种方式:
- 指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
- 空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录 那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对 象,并在分配后更新列表记录。
选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所 采用的垃圾收集器是否带有压缩整理功能决定。
7、对象的访问定位
Java程序需要通过 JVM 栈上的符号引用访问堆中的具体对象。对象的访问方式取决 于 JVM 虚拟机的实现。目前主流的访问方式有 句柄 和 直接指针 两种方式。
指针: 指向对象,代表一个对象在内存中的起始地址。
句柄: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。
句柄访问:
Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息,具体构造如下图所示:
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是 非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
直接指针:(HotSpot 中采用的就是这种方式)
如果使用直接指针访问,引用中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。
优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。
8、内存泄露与内存溢出
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说, Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。
但是, 即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露, 尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
内存溢出:申请内存时,没有足够的内存可以使用。
两者的关系是,内存泄漏的增多,最终会导致内存溢出。
内存泄漏分类:
- 经常发生:发生内存泄漏的代码会被多次执行,每次执行,泄漏一块内存
- 偶然发生:在某些特定的情况下发生
- 一次性:发生内存泄漏的方法只会执行一次
- 隐式泄漏:一直占着内存不放,直到执行结束,严格的说这个不算内存泄漏,因为最终释放掉了,但是如果执行时间特别长,也可能导致内存耗尽。
8.1 Java中内存泄漏的8种情况(重要)
9、垃圾回收GC
9.1 如何判断一个对象是否存活?
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不能再被任何途径使用的对象)。判断对象是否存活有两种方法:引用计数法和可达性分析。
引用计数法:(java中没有使用这种算法)
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。这种方法很难解决对象之间相互循环引用的问题。
优点:实现简单,垃圾对象便于识别;判定效率高,回收没有延迟性。
缺点:
- 需要单独的字段存储计数器,增加了存储空间的开销
- 每次赋值都需要更新计数器,伴随着加法和减法的操作,这增加了时间的开销
- 无法处理循环引用(致命缺陷)
可达性分析:
通过GC Root对象为起点,从这些节点向下搜索,搜索所走过的路径叫引用链,当一个对象到GC Root没有任何的引用链相连时,说明这个对象是不可用的。
9.2 可作为GC Roots的对象有哪些?(重点)
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 所有被同步锁(synchronized关键字)持有的对象。
9.3 强引用、软引用、弱引用、虚引用是什么,有什么区别?
强引用:垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。本质上是一个标记引用,主要用来跟踪对象被垃圾回收的活动,虚引用必须和引用队列配合使用。当虚引用指向的对象被垃圾回收器回收后,Java虚拟机将会把这个虚引用加入到与之关联的引用队列中。
9.4 Minor GC 、Major GC和 Full GC简介
9.5 内存分配策略
- 对象优先在 Eden 分配,大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
- 大对象直接进入老年代,大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。可以设置JVM参数 -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
- 长期存活的对象进入老年代,通过参数 -XX:MaxTenuringThreshold 可以设置对象进入老年代的年龄阈值。对象在 Survivor 中每经过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度,就会被晋升到老年代中。
- 动态对象年龄判定,虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor
中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold 中要求的年龄。 - 空间分配担保,在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如
果条件成立的话,那么 Minor GC 可以确认是安全的。如果不成立的话虚拟机会查看HandlePromotionFailure 的值是否允许担保失败。如果允许,那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
-XX:HandlePromotionFailure 是否设置空间分配担保
空间分配担保:
9.6 Full GC 的触发条件是什么?
对于 Minor GC,其触发条件比较简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 触发条件相对复杂,有以下情况会发生 Full GC:
- 调用 System.gc():只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
- 老年代空间不足:老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
- 空间分配担保失败:使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
9.7 垃圾回收算法有哪些?(重要)
9.7.1 标记清除算法
执行过程:当堆中的有效内存空间被耗尽的时候,就会停止整个程序(STW),然后进行两项工作,第一项是标记,第二项是清除。
- 标记:Collector从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的Header中记录为可达对象。
- 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
缺点:
- 效率不高。
- 在进行GC的时候,需要停止整个应用程序,导致用户体验差。
- 这种方式清理出来的空闲内存是不连续的,产生内存碎片,需要维护一个空闲列表。
9.7.2 复制算法
核心思想:
将活着的内存空间分为两块,每次只使用其中的一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
9.7.3 标记压缩算法
9.7.4 对比三种算法
9.7.5 分代收集算法
9.8 System.gc()的理解
9.9 STW的理解
9.10 垃圾回收的并发与并行
并行(Parallel):指的是多条垃圾收集线程并行工作,但是此时用户线程仍然处于等待状态,如:ParNew、Parallel Scavenge、Parallel Old。
串行(Serial):单线程执行,如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收。回收完,在启动程序的线程。
并发(Concurrent):指的是用户线程与垃圾收集线程同时执行(但不一定时并行,可能会交替执行),垃圾回收线程在执行时不会停顿用户线程的运行。
9.11 有哪些垃圾回收器,优缺点是什么?(重点)
9.11.1 垃圾回收器的组合关系图
垃圾回收器主要分为以下几种:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1。
查看默认的垃圾回收器方式:
9.11.2 7种垃圾回收器的特点是什么?
9.11.3 Serial 收集器
单线程收集器,使用一条垃圾收集线程去完成垃圾收集工作,在进行垃圾收集工作的时候必须暂停其他所有的工作线程(STW)直到它收集结束。
特点:简单高效,内存损耗小,没有线程交互的开销,单线程收集效率高;需暂停所有的工作线程,用户体验不好。
`
9.11.4 ParNew 收集器
Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
ParNew是很多JVM运行在Server模式下新生代的默认垃圾收集器。
9.11.5 Parallel Scavenge 收集器和Parallel Old 收集器
新生代收集器,基于复制清除算法实现的收集器。吞吐量优先收集器,也是能够并行收集的多线程收集器,允许多个垃圾回收线程同时运行,降低垃圾收集时间,提高吞吐量。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间))。Parallel Scavenge 收集器关注点是吞吐量,高效率的利用 CPU 资源。CMS 垃圾收集器关注点更多的是用户线程的停顿时间。
Parallel Scavenge与ParNew的区别是什么?
- 和ParNew收集器不同,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,它也被称为吞吐量优先的垃圾收集器
- 自适应调节策略也是两者的一个重要区别。
Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。多线程垃圾收集,使用标记-整理算法,但同样也是基于并行回收和STW机制。在注重吞吐量以及CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
参数设置:
9.11.6 CMS收集器
CMS(Concurrent Mark Sweep )并发标记清除,目的是获取最短应用停顿时间。第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程基本上同时工作。在并发标记和并发清除阶段,虽然用户线程没有被暂停,但是由于垃圾收集器线程占用了一部分系统资源,应用程序的吞吐量会降低。
什么是浮动垃圾呢?
答:在并发标记阶段本来可达的对象,由于用户线程的作用变得不可达了,即产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终导致这些新产生的垃圾对象没有被及时回收。
在重新标记阶段会修正由于用户线程运作而导致对象的标记产生变动的记录,那为什么还有浮动垃圾产生?
答:由于标记阶段是从 GC Roots开始标记可达对象,那么在并发标记阶段可能产生两种变动:
1)本来可达的对象,变得不可达了(重新标记阶段不处理)
2)本来不可达的对象,变得可达了
如果在并发标记阶段中,用户线程new了一个对象,而它在初始标记和并发标记中无法从GC Roots可达,如果没有重新标记阶段将这个对象标记为可达,那么在并发清除阶段被回收,这是严重的错误。
相比之下,浮动垃圾是可容忍的问题,那为什么重新标记阶段不处理第一种变动呢?由于从可达变为不可达的变化需要重新从GC Roots开始遍历,相当于再次完成初始标记和并发标记的工作,这样会造成增加重新标记阶段的开销,所带来的暂停时间是追求低延迟的CMS不能容忍的。
9.11.7 G1(Garage First)收集器(重点)
参数设置:
适用场景:
G1回收器回收过程
Remember Set
回收过程一:年轻代GC
过程二:并发标记过程
过程三:混合回收
过程四:FullGC
9.11.8 CMS收集器和G1收集器的区别
- CMS收集器是老年代的收集器,一般配合新生代的Serial和ParNew收集器一起使用;G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
- CMS收集器是一种以获取 最短回收停顿时间 为目标的收集器, G1收集器 可预测垃圾回收的停顿时间 。
- CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片;而G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
- CMS和G1的回收过程不一样,垃圾回收的过程不一样。CMS是 初始标记、并发标记、重新标记、并发清理 ;G1是初始标记、并发标记、最终标记、筛选回收。
10、调优
10.1 什么情况下需要调优?
- Heap内存(老年代)持续上涨达到设置的最大内存值
- Full GC次数频繁
- GC停顿时间过长
- 应用出现OutOfMemory等内存异常
- 应用中有使用本地缓存且占用大量内存空间
- 系统吞吐量与响应性能不高或下降
- 应用的cpu占用过高不下或内存占用过高不下
10.2 常用的JVM调优参数有哪些?
堆栈内存相关
- -Xms 设置初始堆的大小
- -Xmx 设置最大堆的大小
- -Xmn 设置年轻代大小,相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值
- -Xss 每个线程的堆栈大小
- -XX:NewSize 设置年轻代大小(for 1.3/1.4)
- -XX:MaxNewSize 年轻代最大值(for 1.3/1.4)
- -XX:NewRatio 年轻代与年老代的比值(除去持久代)
- -XX:SurvivorRatio Eden区与Survivor区的的比值
- -XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。
- -XX:MaxTenuringThreshold设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代
垃圾收集器相关
- -XX:+UseParallelGC:选择垃圾收集器为并行收集器。
- -XX:ParallelGCThreads=20:配置并行收集器的线程数
- -XX:+UseConcMarkSweepGC:设置年老代为并发收集。
- -XX:CMSFullGCsBeforeCompaction=5 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行5次GC以后对内存空间进行压缩、整理。
- -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
辅助信息相关
- -XX:+PrintGCDetails 打印GC详细信息
- -XX:+HeapDumpOnOutOfMemoryError让JVM在发生内存溢出的时候自动生成内存快照,排查问题用
- -XX:+DisableExplicitGC禁止系统System.gc(),防止手动误触发FGC造成问题.
- -XX:+PrintTLAB 查看TLAB空间的使用情况
10.2 如何排查OOM问题?
排查 OOM 的方法:
- 增加JVM参数 -XX:+HeapDumpOnOutOfMemoryError 和 - XX:HeapDumpPath=/tmp/heapdump.hprof ,当 OOM 发生时自动 dump 堆内存信息到指定目录;
- jstat 查看监控 JVM 的内存和 GC 情况,评估问题大概出在什么区域;
- 使用 MAT 工具载入 dump 文件,分析大对象的占用情况 。
11、新生代为何划分Eden和Survivor?为什么设置两个Survivor?
- 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
- Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
- 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)
12、JVM中一次完整的GC流程是怎样的?
- Java堆划分为老年代和新生代
- 新生代 划分为Eden和两个Survivor(S0、S1)
- 当 Eden区的空间满了, Java虚拟机会触发一次Minor GC,以收集新生代的垃圾,存活下来的对象,则会转移到 Survivor区
- 大对象(需要大量连续内存空间的Java对象,如那种很长的字符串)直接进入老年态;
- 如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,年龄设为1,每熬过一次Minor GC,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。即长期存活的对象进入老年态。
- 老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和年老代。
- Major GC 发生在老年代的GC,清理老年区,经常会伴随至少一次Minor GC,比Minor GC慢10倍以上。
13、finalize()方法
14 逃逸分析
15、栈、堆、方法区的交互关系
16、 JVM监控及诊断的GUI工具
16.1 VisualVM
16.2 JProfiler