JVM(Java Virtual Machine)
1.JVM概述
作为一个优秀的java程序员,掌握Jvm的理论知识是最基本的能力.
1.1 虚拟机
虚拟机:虚拟计算机;软件,用来执行一系列虚拟计算机指令.大体分为系统虚拟机和程序虚拟机.
java技术核心就是jvm,因为java程序都运行在jvm内部.
VMware就属于系统计算机,完全对物理计算机仿真,提供了一个可运行完整操作系统的软件平台.
java虚拟机就属于程序虚拟机,为执行计算机程序设计,java虚拟机执行的指令成为java字节码指令.拥有独立的运行机制.
1.2 JVM作用
二进制字节码的运行环境,装载字节码至其内部,编译/解释为对应平台上的机器码指令执行
JVM的特点:
-
一次编译到处执行
-
自动内存管理
-
自动垃圾回收
现在的JVM不仅可以执行java字节码文件,还可以执行其他语言编译的字节码文件,是一个跨语言平台.
1.3 Jvm的位置
1.4 JVM的四个组成部分
1.类加载区(ClassLoader)
2.运行时数据区(Runtime Data Area)
3.执行引擎(Execution Engine)
4.本地库接口(Native Interface)
程序在执行之前先要把 java 代码转换成字节码(class 文件)jvm 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area),而字节码文件是 jvm 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine) 将字节码翻译成底层系统指令再交由 CPU 去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这 4 个主要组成部分的职责与功能
而我们通常所说的 JVM 组成指的是运行时数据区(Runtime Data Area),因为通常需要程序员调试分析的区域就是“运行时数据区”,或者更具体的来说就是“运行时数据区”里面的 Heap(堆)模块
1.5 Java 代码执行流程
1.6 JVM架构模型
2 类加载
2.1 类加载器子系统的作用
类加载器子系统负责从文件系统或者网络中加载 class 文件, class 文件在文件开头有特定的文件标识(字节码文件都以 CA FE BA BE 标识开头)。
classLoader 只负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量
2.2 类加载ClassLoader的角色
- class file 存在于硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板
在执行的时候是要加载 JVM 当中来,根据这个模板实例化出 n 个一模一样的实
例. - class file 加载到 JVM 中,被称为 DNA 元数据模板,放在方法去中.
- 在.class–>JVM–>最终称为元数据模板,此过程就要有一个运输工具(类加载器 Class Loader),扮演一个快递员的角色)
2.3 加载过程
2.3.1 加载
- 通过类名(地址)获取此类的二进制字节流.
- 将这个字节流所代表的静态存储结构转换为方法区(元空间)的运行时结构.
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口.
2.3.2 链接
验证:检验被加载的类是否有正确的内部结构,并和其他类协调一致;
准备:准备阶段则负责为类的静态属性分配内存,并设置默认初始值;
不包含用 final 修饰的 static 实例变量,在编译时进行初始化. 不会为实例变量初始化
解析:将类的二进制数据中的符号引用替换成直接引用(符号引用是用一组符号描述所引用的目标;直接引用是指向目标的指针).
2.3.3 初始化
- 创建类的实例,也就是 new 一个对象
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(Class.forName(“”))
- 初始化一个类的子类(会首先初始化子类的父类)
2.4 类加载器分类
JVM 支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
2.4.1 引导类加载器(启动类加载器)
这个类加载器使用 C/C++语言实现,嵌套在 JVM 内部.它用来加载 java 核心类库.并不集成于 java.lang.ClassLoader 没有父加载器. 负责加载扩展类加载器和应用类加载器,并为他们指定父类加载器. 出于安全考虑,引用类加载器只加载包名为 java,javax,sun 等开头
2.4.2 扩展类加载器
Java 语言编写的,由 sun.misc.Launcher$ExtClassLoader 实现. 派生于 ClassLoader 类. 上层类加载器为引用类加载器. 从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 系统安装目录的jre/lib/ext 子目录(扩展目录)下加载类库.如果用户创建的 jar 放在此目录下,也会自动由扩展类加载器加载.
2.4.3 应用程序类加载器(系统类加载器)
Java 语言编写的,由 sun.misc.Launcher$AppClassLoader 实现. 派生于 ClassLoader 类. 上层类加载器为扩展类加载器. 加载我们自己定义的类. 该类加载器是程序中默认的类加载器. 通过类名.class.getClassLoader(),ClassLoader.getSystemClassLoader()
来获得
ClassLoader 类,它是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器)
2.5 双亲委派机制
Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要该类时才会将它的 class 文件加载到内存中生成 class 对象.而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式.
工作原理:
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行.
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器.
- 如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制. 如果均加载失败,就会抛出
ClassNotFoundException
异常。
双亲委派机制的优点:
1.安全 避免用户自己编写的类动态替换java核心类;
2.避免全限定命名的类重复加载(使用findLoadClass()
判断当前类是否已经加载)
2.6 沙箱安全机制
作用:防止恶意代码污染java源代码
比如上面我们定义了一个类名为 String 所在包也命名java.lang,因为这个类本来是属于 jdk 的,如果没有沙箱安全机制的话,这个类将会污染到系统中的String,但是由于沙箱安全机制,所以就委托顶层的引导类加载器查找这个类,如果没有的话就委托给扩展类加载器,再没有就委托到系统类加载器.但是由于
String 就是 jdk 的源代码,所以在引导类加载器那里就加载到了,先找到先使用,所以就使用引导类加载器里面的 String,后面的一概不能使用,这就保证了不被恶意代码污染.
JVM判断两个对象属于同一个类
1.全类名(地址)相同 2.类的加载器相同
2.7 类的主动/被动使用
JVM规定,每个类或接口被首次主动使用时才对其进行初始化
主动使用:
- 通过new关键字初始化类,必然进行类的加载并且初始化
- 初始化类的静态变量,包括读取和更新
- 调用类的静态方法
- 对某个类进行反射操作(Class.forName())
- 初始化子类导致父类被初始化
- 执行该类的main函数
被动使用:
其实除了上面的几种主动使用其余就是被动使用了
1.引用该类的静态常量,注意是常量,不会导致初始化,但是也有意外,这里的常量是指已经指定字面量的常量,对于那些需要一些计算才能得出结果的常量就会导致初始化,比如:
public final static int NUMBER = 5 ; //不会导致类初始化.被动使用
public final static int RANDOM = new Random().nextInt() ; //会导致类的初始化.主动使用
2.构造某个类的数组时不会导致该类的初始化,比如:
Student[] students = new Student[10] ;
主动使用和被动使用的区别在于类是否会被初始化.
3 运行时数据区
3.1 运行时数据区组成概述
JVM 的运行时数据区,不同虚拟机实现可能略微有所不同,但都会遵从 Java 虚拟机规范,Java 8 虚拟机规范规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区域:
程序计数器(较小的内存空间 速度快 不会溢出 线程是有 记录下条指令的位置);
java虚拟机栈(方法执行时创建线帧(Stack Frame)用于储存java方法);
本地方法栈(执行本地方法Native)(两个栈方法私有);
java堆内存(存放实例对象 线程共享);
方法区(存储已被类加载的类信息,变量,静态常量,即时编译后的代码等数据)
内存区域是很重要的系统资源,是硬盘和 CPU 的中间桥梁,承载着操作系统和应用程序的实时运行.JVM 内存布局规定了 Java 在运行过程中内存申请,分配,管理的策略,保证了 JVM 的高效稳定运行.不同的 JVM 对于内存的划分方式和管理机制存在着部分差异,我们现在以使用最为流行的 HotSpot 虚拟机为例讲解;
3.2 程序计数器(Program Counter Register)
3.2.1 概述
JVM 中的程序计数寄存器(Program Counter Register)中的 Register 命名源于CPU 的寄存器,寄存器存储指令相关的现场信息.CPU 只有把数据装载到寄存器才能运行.这里,并非是广义上所指的物理寄存器,或许将其翻译为 PC 计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会.JVM 中的PC 寄存器是对物理 PC是对物理 PC 寄存器的一种抽象
3.2.2 作用
程序计数器用来存储下一条指令的地址,也即将要执行的指令代码.由执行引擎读取下一条指令
- 它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域.
- 在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程生命周期保持一致.
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法.程序计数器会存储当前线程正在执行的Java方法的JVM指令地址.如果是在执行native方法,则是未指定值(undefined).
- 它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成.
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令.
- 它是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域.
3.2.3 面试题Demo
- 使用程序计数器存储字节码指令地址有什么用?为什么使用程序计数器记录当前线程的执行地址呢?
因为 CPU 需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪儿开始继续执行.
JVM 的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令.
- 程序计数器为什么被设定为线程私有的?
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的
方法,CPU 会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?
为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个程序计数器,这样一来各个线程之间便可以独立计算,
3.3 Java虚拟机栈(Java Virtual Machine Stacks)
3.3.1 虚拟机栈出现的背景
前面已经讲过,由于跨平台性的设计,Java 的指令都是根据栈来设计的.不同平台CPU 架构不同,所以不能设计为基于寄存器的. 基于栈的指令设计优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样功能需要更过的指令集
3.3.2 分清堆和栈
栈是运行时的单位,而堆是存储的单位.
即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据. 堆解决的是数据存储的问题,即数据怎么放,放在哪儿.
3.3.3 Java虚拟机栈是什么?
Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈.每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次方法的调用. Java 虚拟机栈是线程私有的. 生命周期和线程一致.
3.3.4 Java虚拟机栈的作用
主管 Java 程序的运行,它保存方法的局部变量(8 种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回
3.3.5 栈的特点
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器. JVM 直接对 java 栈的操作只有两个:调用方法,进栈. 执行结束后,出栈.对于栈来说不存在垃圾回收问题.
3.3.6 栈中出现什么异常?
StackOverflowError
:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError
:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存
3.3.7 栈中储存什么
每个线程都有自己的栈,栈中的数据都以栈帧为单位存储. 在这个线程上正在执行的每个方法都各自对应一个栈帧.
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息.
3.3.8 栈的运行原理
-
JVM 直接对 java 栈的操作只有两个,就是对栈帧的压栈和出栈,遵循”先进后出”/后进先出的原则.
-
在一条活动的线程中,一个时间点上,只会有一个活动栈.即只有当前在执行的方法的栈帧(栈顶)是有效地,这个栈帧被称为当前栈(Current Frame),与当前栈帧对应的方法称为当前方法(Current Method),定义这个方法的类称为当前类(Current Class).
-
执行引擎运行的所有字节码指令只针对当前栈帧进行操作.
-
如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,成为新的当前栈帧.
不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法).如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧. Java 方法有两种返回的方式,一种是正常的函数返回,使用 return 指令,另一种是抛出异常.不管哪种方式,都会导致栈帧被弹出.
3.3.9 栈帧的内部结构
每个栈帧中存储着:
- 局部变量表(Local Variables)
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。 - 操作数栈(Operand Stack)(或表达式栈)
栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。 - 动态链接(Dynamic Linking) (或指向运行时常量池的方法引用)
因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。 - 方法返回地址(Retuen Address)(或方法正常退出或者异常退出的定义)
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。 - 一些附加信息
3.3.10 面试题Demo
什么情况下会出现栈溢出(StackOverflowError)?
栈溢出就是方法执行时创建的栈帧超过了栈的深度。那么最有可能的就是方法递归调用产生这种结果。
通过调整栈大小,就能保证不出现溢出吗?
不能
分配的栈内存越大越好吗?
并不是的,只能延缓这种现象的出现,可能会影响其他内存空间
垃圾回收机制是否会涉及到虚拟机栈?
不会
3.4 本地方法栈(Native Method Stack)
-
Java 虚拟机栈管理 java 方法的调用,而本地方法栈用于管理本地方法的调用.
-
本地方法栈也是线程私有的.
-
允许被实现成固定或者是可动态扩展的内存大小.内存溢出方面也是相同的.
如果线程请求分配的栈容量超过本地方法栈允许的最大容量抛出StackOverflowError.
如果本地方法可以动态扩展,并在扩展时无法申请到足够的内存会抛出OutOfMemoryError.
-
本地方法是用 C 语言写的.
-
它的具体做法是在 Native Method Stack 中登记 native 方法,在Execution Engine (执行引擎)执行时加载本地方法
3.5 Java堆内存
3.5.1 堆内存概述
- 一个 JVM 实例只存在一个堆内存,堆也是 Java 内存管理的核心区域.
- Java 堆区在 JVM 启动时的时候即被创建,其空间大小也就确定了,是 JVM 管理的最大一块内存空间. 堆内存的大小是可以调节. 例如: -Xms:10m(堆起始大小) -Xmx:30m(堆最大内存大小) 一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率.
- 《Java 虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的.
- 所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区.
- 《Java 虚拟机规范》中对 Java 堆的描述是:所有的对象实例都应当在运行时分
配在对上. - 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除.
- 堆,是 GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域.
3.5.2 堆内存区域划分
Java8 及之后堆内存分为:新生区(新生代)+老年区(老年代)
新生区分为 Eden(伊甸园)区和 Survivor(幸存者)区
3.5.3 为什么分区(代)
将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及 GC 频率。针对分类进行不同的垃圾回收算法,对算法扬长避短
3.5.4 对象创建内存分配过程
为新对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如何分配,在哪分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片.
- new 的新对象先放到伊甸园去,此区大小有限制.
- 当伊甸园的空间填满时,程序有需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁.再加载新的对象放到伊甸园区.
- 然后将伊甸园区中的剩余对象移动到幸存者 0 区.
- 如果再次出发垃圾回收,此 时上次幸存下来存放到幸存者 0 区的对象,如果没有回收, 就会被放到幸存者 1 区,每次会保证有一个幸存者区是空的.
- 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区.
- 什么时候去养老区呢?默认是 15 次,也可以设置参数
-XX:MaxTenuringThreshold= - 在老年区,相对悠闲,当养老区内存不足时,再次触发 GC:Major GC,进行养老区的内存清理.
- 若养老区执行了 Major GC 之后发现依然无法进行对象保存,就会产生 OOM 异常.
Java.lang.OutOfMemoryError:Java heap space
例如:
public static void main(String[] args) {
List<Integer> list = new ArrayList();
while(true){
list.add(new Random().nextInt());
}
}
3.5.5 新生代和老年区配置比例
- 默认**-XX:NewRatio**=2,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3
- 可以修改**-XX:NewRatio**=4,表示新生代占 1,老年代占 4,新生代占整个堆的 1/5
- 当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优
在 HotSpot 中,Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 : 1,当然开发
人员可以通过选项-XX:SurvivorRatio
调整这个空间比例。比如-XX:SurvivorRatio=8新生区的对象默认生命周期超过 15
3.5.6 分带收集思想Minor GC,Major GC,Full GC
JVM 在进行 GC 时,并非每次都新生区和老年区一起回收的,大部分时候回收的都是指新生区.针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类型:一种是部分收集, 一种是整堆收集.
部分收集:不是完整收集整个 java 堆的垃圾收集.其中又分为:
- 新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集.
- 老年区收集(Major GC / Old GC):只是老年区的垃圾收集.
- 混合收集(Mixed GC):收集整个新生区以及部分老年区的垃圾.
整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集. 整堆收集出现的情况:
-
System.gc();时
-
老年区空间不足
-
方法区空间不足
开发期间尽量避免整堆收集
3.5.7 TLAB机制
为什么有 TLAB(Thread Local Allocation Buffer)
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据. 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的.
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度.
什么是 TLAB?
TLAB 的全称是 Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。
如果设置了虚拟机参数-XX:UseTLAB
,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。
JVM 使用 TLAB 来避免多线程冲突,在给对象分配内存时,每个线程使用自己的 TLAB,这样可以避免线程同步,提高了对象分配的效率。
TLAB 空间的内存非常小,缺省情况下仅占有整个 Eden 空间的 1%,也可以通过选项-XX:TLABWasteTargetPercent
设置 TLAB 空间所占用 Eden 空间的百分比大小.
3.5.8 堆空间的参数设置
官网地址:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial
查看所有参数的默认初始值-XX:+PrintFlagsFinal
查看所有参数的最终值(修改后的值)- -
Xms:初始堆空间内存(默认为物理内存的 1/64)
-Xmx:最大堆空间内存(默认为物理内存的 1/4)
-Xmn:设置新生代的大小(初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio:设置新生代中 Eden 和 S0/S1 空间比例
-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails 输出详细的 GC 处理日
3.5.9 字符串常量池
字符串常量池为什么要调整位置?
JDK7 中将字符串常量池放到了堆空间中。因为永久代的回收效率很低,在 Full GC 的时候才会执行永久代的垃圾回收,而 Full GC 是老年代的空间不足、永久代不足时才会触发。这就导致 StringTable 回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
3.6 方法区
3.6.1 方法区的简述
方 法 区 , 是 一 个 被 线 程 共 享 的 内 存 区 域 。 其 中 主 要 存 储 加 载 的 类 字 节 码 、class/method/field 等元数据、static final 常量、static 变量、即时编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。
Java 虚拟机规范中明确说明:”尽管所有的方法区在逻辑上是属于堆的一部分,但对于HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开. 所以,方法区看做是一块独立于 java堆的内存空间
方法区在 JVM 启动时被创建,并且它的实际的物理内存空间中和 Java 堆区一样都可以是不连续的. 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展. 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出, 虚拟机同样会抛出内存溢出的错误:
java.lang.OutOfMemoryError:Metaspace
public static void main(String[] args) {
String temp = "world";
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String str = temp + temp;
temp = str;
str.intern();
}
}
方法区,栈,堆的交互关系
3.6.2 方法区设置大小
Java 方法区的大小不必是固定的,JVM 可以根据应用的需要动态调整.
- 元 数 据 区 大 小 可 以 使 用 参 数
-XX:MetaspaceSize
和
-XX:MaxMataspaceSize
指定,替代上述原有的两个参数. - 默认值依赖于平台,windows 下,
-XXMetaspaceSize
是 21MB, -XX:MaxMetaspaceSize
的值是-1,级没有限制.- 这个
-XX:MetaspaceSize
初始值是 21M 也称为高水位线 一旦
触及 就会触发 Full GC. - 因此为了减少 FullGC 那么这个
-XX:MetaspaceSize
可以设置一个较高的值
3.6.3 方法区内部结构
方法区它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存,运行常量池等。
通过反编译字节码文件查看.
反编译字节码文件,并输出值文本文件中,便于查看。参数 -p 确保能查看
private 权限类型的字段或方法`javap -v -p Demo.class > tes`
3.6.4 方法区垃圾回收
- 有些人认为方法区(如 Hotspot 虚拟机中的元空间或者永久代)是没有垃圾
收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,
提到过可以不要求虚拟机在方法区中实现垃圾收集。 - 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。
方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型。
回收废弃常量与回收 Java 堆中的对象非常类似。(关于常量的回收比较简单,重点是类的回收)
下面也称作类卸载
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
1.该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。
2.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
4 执行引擎
4.1 执行引擎基本概述
1.执行引擎是 Java 虚拟机核心的组成部分之一。
2.JVM 的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被 JVM 所识别的字节码指令、符号表,以及其他辅助信息。
3.那么,如果想要让一个 Java 程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者。
注意区分概念:
1.前端编译:从 Java 程序员-字节码文件的这个过程叫前端编译. 2.执行引擎这里有两种行为:一种是解释执行,一种是编译执行(这里的是后端编译)
4.2 什么是解释器?什么是JIT编译器?
**解释器:**当 Java 虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
**JIT(Just In Time Compiler)编译器:**就是虚拟机将源代码一次性直接编译成和本地机器平台相关的机器语言,但并不是马上执行。
4.3 为什么说java语言是半编译半解释型?
起初将 Java 语言定位为“解释执行”还是比较准确的。再后来,Java 也发展出可以直接生成本地代码的编译器。现在 JVM 在执行 Java 代码的时候,通常都会将解释执行与编译执行二者结合起来进行。
原因:
- JVM 设计者们的初衷仅仅只是单纯地为了满足 Java 程序实现跨平台特性,因此避免采用静态编译的方式由高级语言直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。
- 解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行,执行效率低。
- JIT 编译器将字节码翻译成本地代码后,就可以做一个缓存操作,存储在方法区的 JIT 代码缓存中(执行效率更高了).
- 是否需要启动 JIT 编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。
- JIT 编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升 Java 程序的执行性能。
- 一个被多次调用的方法,或者是一-个方法体内部循环次数较多的循环体都可以被称之为“热点代码”。目前 HotSpot VM 所采用的热点探测方式是基于计数器的热点探测
JIT 编译器执行效率高为什么还需要解释器?
1.当程序启动后,解释器可以马上发挥作用,响应速度快,省去编译的时间,立即执行。
2.编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间,但编译为本地代码后,执行效率高。就需要采用解释器与即时编译器并存的架构来换取一个平衡点。
5 本地库接口
5.1 什么是本地方法
简单来讲,一个 Native Method 就是一个 java 调用非 java 代码的接口,一个Native Method 是这样一个 java 方法:该方法的底层实现由非 Java 语言实现,
比如 C。这个特征并非 java 特有,很多其他的编程语言都有这一机制在定义一个 native method 时,并不提供实现体(有些像定义一个 Javainterface),因为其实现体是由非 java 语言在外面实现的。
关键字 native 可以与其他所有的 java 标识符连用,但是 abstract 除外
5.2 为什么要使用本地方法
Java 使用起来非常方便,然而有些层次的任务用 java 实现起来不容易,或者我们对程序的效率很在意时,问题就来了。
1.与 java 环境外交互:
有时 java 应用需要与 java 外面的环境交互,这是本地方法存在的主要原因。 你可以想想 java 需要与一些底层系统,如擦偶偶系统或某些硬件交换信息时的情况。本地方法正式这样的一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解 java 应用之外的繁琐细节。
2.与操作系统交互(比如线程最后要回归于操作系统线程)
JVM 支持着 java 语言本身和运行库,它是 java 程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用 java 实现了 jre 的与底层系统
的交互,甚至 jvm 的一些部分就是用 C 写的。还有,如果我们要使用一些 java语言本身没有提供封装的操作系统特性时,我们也需要使用本地方法。
3.Sun’s Java
Sun 的解释器是用 C 实现的,这使得它能像一些普通的 C 一样与外部交互。jre大部分是用 java 实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread
的 setPriority()
方法是用 Java 实现的,但是它实现调用的事该类里的本地方法 setPriority()
。这个本地方法是用 C 实现的,并被植入 JVM
内 部 , 在 Windows 95 的 平 台 上 , 这 个 本 地 方 法 最 终 将 调 用 Win32setPriority()API
。这是一个本地方法的具体实现由 JVM 直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后
被 JVM 调用。
6 垃圾回收
6.1 垃圾回收基本概述
6.1.1 垃圾回收简述
- Java 和 C++语言的区别,就在于垃圾收集技术和内存动态分配上,C++语言没有垃圾收集技术,需要程序员手动的收集。
- 垃圾收集,不是 Java 语言的伴生产物。早在 1960 年,第一门开始使用内存动态分配和垃圾收集技术的 Lisp 语言诞生。
- 关于垃圾收集有三个经典问题:
哪些内存需要回收?
什么时候回收?
如何回收? - 垃圾收集机制是 Java 的招牌能力,极大地提高了开发效率。
6.1.2 什么是垃圾?
垃圾:运行程序中没有任何指针指向的对象
垃圾对象不及时清除会占用内存空间,其他对象也不能使用该空间直到应用程序结束,甚至会导致内存溢出
6.1.3 为什么需要GC?
1.对于高级语言来说,不进行垃圾回收,内存迟早被消耗完
2.除了释放没用的对象,垃圾回收能清除内存中的记录碎片,jvm将清理后的内存分给新对象
3.随着程序面对的业务越来越庞大,复杂,GC就能保证应用程序的正常运行
6.1.4 早期垃圾回收
早期c/c++时代,垃圾回收基本都是人工进行的,通过new关键字进行内存申请,delete关键字进行内存释放
MibBridge *pBridge= new cmBaseGroupBridge();
//如果注册失败,使用 Delete 释放该对象所占内存区域
if(pBridge->Register(kDestroy)!=NO ERROR)
delete
这种方法可以灵活控制释放内存时间,但是对于开发人员就有了管理负担,人为操作容易产出内存泄漏,因此垃圾回收机制就出现了,这种自动化的内存分配和回收方法就成了现在开发语言的标准
6.1.5 java垃圾回收机制
6.1.5.1 自动内存管理
自动内存管理的优点
自动内存管理,无需开发人员手动参与内存的分配与回收,降低内存泄漏和内存溢出的风险.
自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发
6.1.5.2 关于自动内存管理的担忧
过度依赖"自动"会弱化开发人员在程序出现溢出时的定位解决问题能力
6.1.5.3应该关心那些区域的垃圾回收?
垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收,其中,Java 堆是垃圾收集器的工作重点
从次数上讲:
频繁收集 Young 区
较少收集 Old 区
基本不收集元空间(方法区)
6.2 垃圾回收相关算法
6.2.1 垃圾标记阶段算法
1.标记阶段的目的(判断对象是否存活)
这就引用了引用计数算法和可达性分析算法进行判断对象是否存活
2.引用技术算法
对每一个对象保存一个整型的引用计数器属性,用于记录对象被引用情况
原理:对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1。只要对象 A 的引用计数器的值为 0,即表示对象 A 不可能再被使用,可进行回收。
优点:实现简单,垃圾对象便于识别;判定效率高,回收木有延迟性
缺点:
(1)字段储存计数器,增加了存储空间开销
(2)每次赋值都需要更新计数器,花费时间过多
(3)无法处理循环引用的情况(在.Java 的垃圾回收器中没有使用这类算法)
3.可达性分析算法(根搜索算法,追踪性垃圾收集)
(1)不仅实现简单,执行效率高,有效解决在引用计数算法中的循环引用问题,防止内存泄漏
可达性分析实现思路
通过GC Roots对象为起点,想下搜索,能到达的对象是不可回收的对象,不能到达的对象是可回收的对象,以此判断对象是否存活
GC Roots可以是哪些元素?
1.虚拟机栈中引用的对象
2.本地方法栈中JNI(本地方法)引用的对象
3.方法区中类静态属性引用的对象
4.方法区中常量引用的对象
5…所有被同步锁 synchronized 持有的对象
6.java 虚拟机内部的引用。
基 本 数 据 类 型 对 应 的 Class 对 象 , 一 些 常 驻 的 异 常 对 象 ( 如 :NullPointerException、OutofMemoryError),系统类加载器
对象的finalization
机制
对象销毁前的回调方法:finalize();
java语言提供了对象终止(finalization)机制允许开发人员提供对象被销毁之前的自定义处理逻辑
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的 finalize()方法。
finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等
Object 类中 finalize() 源码
永远不要主动调用某个对象的 finalize()方法,应该交给垃圾回收机制调用。理
由包括下面三点:
1.在 finalize()时可能会导致对象复活。
2.finalize()方法的执行时间是没有保障的,它完全由 GC 线程决定,极端情况下,
若不发生 GC,则 finalize()方法将没有执行机会。
3.一个糟糕的 finalize()会严重影响 GC 的性能。比如 finalize 是个死循环。
protected void finalize() throws Throwable { }
永远不要主动调用某个对象的 finalize()方法,应该交给垃圾回收机制调用。理由:
1.在 finalize()时可能会导致对象复活。
2.finalize()方法的执行时间是没有保障的,它完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize()方法将没有执行机会。
3.一个糟糕的 finalize()会严重影响 GC 的性能。比如 finalize 是个死循环。
生存还是死亡?
由于 finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态
1.可触及的:从根节点开始,可以到达这个对象。
2.可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
3.不可触及的:对象的 finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为 finalize()只会被调用一次。
以上 3 种状态中,是由于 finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。
6.2.2 垃圾回收阶段算法
常见的jvm种三种垃圾收集算法:
标记-清除算法(Mark-Sweep)
复制算法(Copying)
标记-压缩算法(Mark-Compact)
1.标记-清除算法(Mark-Sweep)
过程:
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为 stop the world),然后进行两项工作,第一项则是标记,第二项则是清除
标记:采用可达性分析算法进行标记
清除:有一个集合专门记录垃圾位置,如果有新对象进来,则覆盖垃圾位置
优缺点:
优点:易理解
缺点:执行效率不高;执行GC,需要停止应用程序;处理后的空闲空间不连续,有"碎片"
2.复制算法(Copying)
过程:
将内存分为两块,使用其中一块,在垃圾回收时,将未回收的放入空闲区
优缺点:
优点:实现简单(没有标记 清除过程);效率高;空间连续性,无"碎片"问题
缺点:需要两倍空间,时间要求也高
3.标记-压缩算法(Mark-Compact)
背景
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。
如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以 JVM 的设计者需要在此基础之上进行改进。
过程:
第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间
优缺点:
优点:消除内存区域分散缺点;给新对象分配内存时,jvm只用一个内存起始地址即可;消除了复制算法当中,内存减半的高额代价
缺点:从效率上来说,标记-整理算法要低于复制算法;移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址;移动过程中,需要全程暂停用户应用程序。即:STW
三个算法的比较:
4.分区收集
不同区域采取不同算法
新生区,幸存区 采用复制算法
老年区 采用标记清除,标记压缩
5.增量算法
垃圾回收时,用户线程需要停止(stw) 所有行为暂时挂起 每次回收分垃圾回收线程和用户线程交替执行,减少停顿时间
缺点 回收垃圾的量减少
6.3 垃圾回收的相关概念
6.3.1 System.gc()的理解
在默认情况下,通过 System.gc()或 Runtime.getRuntime().gc() 的调用,会显式触发 Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
然而 System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用(不能确保立即生效)。JVM 实现者可以通过 System.gc() 调用来决定 JVM 的 GC 行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()。
6.3.2 内存溢出和内存泄漏
内存溢出
内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。
由于 GC 一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现 OOM 的情况。
大多数情况下,GC 会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的 Full GC 操作,这时候会回收大量的内存,供应用程序继续使用。
Javadoc 中对 OutofMemoryError 的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
内存泄漏
对象不在被应用程序使用,但是垃圾回收机制不能将其移除,一直占用内存空间
6.3.3 Stop-the-World
Stop-the-World,简称 STW,指的是 GC 事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为 STW。
可达性分析算法中枚举根节点(GC Roots)会导致所有 Java 执行线程停顿,
为什么需要STW?
垃圾回收标记算法,需要对某一个时间节点上的内存对象状态进行分析,定位垃圾
6.3.4 对象引用
6.3.4.1 对象引用简述
我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存中;如果内存空间在进行垃圾收集后还是很紧张,则可以抛弃这些对象。
既偏门又非常高频的面试题:强引用、软引用、弱引用、虚引用有什么区别?具体使用场景是什么?
在 JDK1.2 版之后,Java 对引用的概念进行了扩充,将引用分为:
强引用(Strong Reference)
软引用(Soft Reference)
弱引用(Weak Reference)
虚引用(Phantom Reference)
这4种引用强度依次逐渐减弱。除强引用外,其他3种引用均可以在java.lang.ref
包中找到它们的身影。如下图,显示了这 3 种引用类型对应的类,开发人员可以在应用程序中直接使用它们
Reference 子类中只有终结器引用是包内可见的,其他 3 种引用类型均为public,可以在应用程序中直接使用.
**强引用(StrongReference):**最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。宁可报 OOM,也不会 GC 强引用
**软引用(SoftReference):**对软引用标记,垃圾回收之后,内存不够,回收软引用,如果还是不够,则报OOM
**弱引用(WeakReference):**被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
**虚引用(PhantomReference):**幽灵引用,直接回收。
6.3.4.2 强引用
在 Java 程序中,最常见的引用类型是强引用(普通系统 99%以上都是强引用),也就是我们最常见的普通对象引用,也是默认的引用类型。
当在 Java 语言中使用 new 操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。
只要强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象。
只要强引用的对象是可达的,jvm 宁可报 OOM,也不会回收强引用。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以当做垃圾被收集了,当然具体回收时机还是要看垃圾收集策略。
相对的,软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下,都是可以被回收的。所以,强引用是造成 Java 内存泄漏的主要原因之一。
public class StrongReferenceTest {
public static void main(String[] args) {
StringBuffer str = new StringBuffer ("Hello,world");
StringBuffer str1 = str;
str = null;
System.gc();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(str1);
}
}
6.3.4.3 软引用(Soft Reference):内存不足即回收
软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。注意,这里的第一次回收是不可达的对象
软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列(Reference Queue)。类似弱引用,只不过 Java 虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。
Object obj = new Object();// 声明强引用
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; //销毁强引
6.3.4.4 弱引用(Weak Reference)发现即回收
弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在系统 GC 时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。
但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。
// 声明强引用
Object obj = new Object();
WeakReference<Object> sf = new WeakReference<>(obj);
obj = null; //销毁强引用
弱引用对象与软引用对象的最大不同就在于,当 GC 在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC 总是进行回收。弱引用对象更容易、更快被 GC 回收。
6.3.4.5 虚引用(Phantom Reference):对象回收跟踪
也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个
一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是 null 。即通过虚引用无法获取到我们的数据为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。
虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。
// 声明强引用
Object obj = new Object();
// 声明引用队列
ReferenceQueue phantomQueue = new ReferenceQueue();
// 声明虚引用(还需要传入引用队列)
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj= null;
6.4 垃圾回收器
6.4.1 垃圾回收器简述
垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM 来实现。
由于 JDK 的版本处于高速迭代过程中,因此 Java 发展至今已经衍生了众多的 GC 版本。
7从不同角度分析垃圾收集器,可以将 GC 分为不同的类型。
6.4.2 垃圾回收器分类
按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。
串行回收指的是在同一时间段内只允许有一个 CPU 用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
和串行回收相反,并行收集可以运用多个 CPU 同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用"stop-the-world"机制。
按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。
并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
独占式垃圾回收器(stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束
按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。
6.4.3 GC性能指标
吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间)
垃圾收集开销:垃圾收集所用时间与总运行时间的比例。
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
收集频率:相对于应用程序的执行,收集操作发生的频率。
内存占用:Java 堆区所占的内存大小。
快速:一个对象从诞生到被回收所经历的时间
6.4.4 HotSpot垃圾收集器
串行回收器:Serial,Serial old
并行回收器:ParNew,Parallel scavenge,Parallel old
并发回收器:CMS、G1
新生代收集器:Serial,ParNew.Parallel scavenge;
老年代收集器:Serial old.Parallel old.cMS;
整堆收集器:G1;
6.4.4.1 Serial 垃圾收集器(单线程)
只开启一条 GC 线程进行垃圾回收,并且在垃圾收集过程中停止一切用户线程(Stop The World)。
一般客户端应用所需内存较小,不会创建太多对象,而且堆内存不大,因此垃圾收集器回收时间短,即使在这段时间停止一切用户线程,也不会感觉明显卡顿。因此 Serial 垃圾收集器适合客户端使用。
由于 Serial 收集器只使用一条 GC 线程,避免了线程切换的开销,从而简单高效
6.4.4.2 ParNew 垃圾收集器(多线程)
ParNew 是 Serial 的多线程版本。由多条 GC 线程并行地进行垃圾清理。但清理过程依然需要 Stop The World。
ParNew 追求“低停顿时间”,与 Serial 唯一区别就是使用了多线程进行垃圾收集,在多 CPU 环境下性能比 Serial 会有一定程度的提升;但线程切换需要额外的开销,因此在单 CPU 环境中表现不如 Seria
6.4.4.3 Parallel Scavenge 垃圾收集器(多线程)
Parallel Scavenge 和 ParNew 一样,都是多线程、新生代垃圾收集器。
但是两者有巨大的不同点:
Parallel Scavenge:追求 CPU 吞吐量,能够在较短时间内完成指定任务,因此适合没有交互的后台计算。
ParNew:追求降低用户停顿时间,适合交互式应用。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
追求高吞吐量,可以通过减少 GC 执行实际工作的时间,然而,仅仅偶尔运行 GC 意味着每当 GC 运行时将有许多工作要做,因为在此期间积累在堆中的对象数量很高。单个 GC 需要花更多的时间来完成,从而导致更高的暂停时间。而考虑到低暂停时间,最好频繁运行 GC 以便更快速完成,反过来又导致吞吐量下降。
6.4.4.4 Serial Old 垃圾收集器(单线程)
Serial Old 收集器是 Serial 的老年代版本,都是单线程收集器,只启用一条 GC线程,都适合客户端应用。它们唯一的区别就是:Serial Old 工作在老年代,使用“标记-整理”算法;Serial 工作在新生代,使用“复制”算法。
6.4.4.5 Parallel Old 垃圾收集器(多线程)
Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量
6.4.4.6 CMS 回收器(低延迟)
CMS(Concurrent Mark Sweep,并发标记清除)收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
- 初始标记:Stop The World,仅使用一条初始标记线程对所有与 GCRoots 直接关联的对象进行标记。
- 并发标记:使用多条标记线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象。速度很慢。
- 重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。
- 并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象。这个过程非常耗时。
并发标记与并发清除过程耗时最长,且可以与用户线程一起工作,因此,总体上说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
CMS 的优点:
并发收集
低延迟
CMS 的弊端:
- 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的青况下,不得不提前触发 Ful1 GC.
- CMS 收集器对 CPU 资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
- CMS 收集器无法处理浮动垃圾。可能出现"Concurrent Mode Failure"失败而导致另一次 Full GC 的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS 将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行 GC 时释放这些之前未被回收的内存
6.4.4.7 G1(Garbage First)回收器(区域划分代式)
既然我们已经有了前面几个强大的 GC,为什么还要发布 Garbage First(G1)GC?
原因就在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC 就不能保证应用程序正常进行,而经常造成 STW 的 GC 又跟不上实际的需求,所以才会不断地尝试对 GC 进行优化。G1(Garbage-First)垃圾回收器是在 Java7 update 4 之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一.
与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。
官方给 G1 设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。
为什么名字叫做 Garbage First(G1)呢?
因为 G1 是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的 Region 来表示 Eden、幸存者 0 区,幸存者1 区,老年代等。
G1 GC 有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region. 由于这种方式的侧重点在于回收垃圾最大量的(Region),所以我们给 G1 一个名字:垃圾优先(Garbage First)。
G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备多核 CPU 及大容量内存的机器,以极高概率满足 Gc 停顿时间的同时,还兼具高吞吐量的性能特征。
从整体上看,G1 是基于“标记-整理”算法实现的收集器,从局部(两个 Region之间)上看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
这里抛个问题:
一个对象和它内部所引用的对象可能不在同一个 Region 中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析?
并不!每个 Region 都有一个 Remembered Set,用于记录本区域中所有对象引用的对象所在的区域,进行可达性分析时,只要在 GC Roots 中再加上Remembered Set 即可防止对整个堆内存进行遍历。
如果不计算维护 Remembered Set 的操作,G1 收集器的工作过程分为以下几个步骤:
- 初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
- 并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢。
- 最终标记:Stop The World,使用多条标记线程并发执行。
- 筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行
为什么删除永久代?元空间是什么?
容易造成内存溢出,难以确定类的大小
元空间本质和永久代类似,他的出现就是为了解决类加载器元数据过多导致的OOM问题
GC分代年龄为什么最大是15岁?
我们平时写代码,编写的只是对象的实例数据,但其实Java对象除了自身的实例数据外,还包括头信息和对齐字节,如下图所示:
这就是一个MarkWord,因为头信息采用4个bit位来保存年龄,4个bit位能表示的最大数就是15
创作不易,大佬们留步… 动起可爱的双手,来个赞再走呗 (๑◕ܫ←๑)