JVM
2021-10-25 19:24:43 31 举报
AI智能生成
《深入理解Java虚拟机》以及 宋红康 JVM教学视频 知识点总结
作者其他创作
大纲/内容
类加载器子系统
Class字节码文件,是存储在物理磁盘中(或加载于网络等等),最终都需要加载到虚拟机中之后才能被运行和使用<br>
类加载过程
加载
该阶段,需要完成的三件事情
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构<br>
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
JVM并没有规定字节码的获取来源,例如:
从ZIP压缩包中读取,JAR,EAR,WAR格式等等
从网络中获取,典型的就是Web Applet
在运行时计算生成,例如:动态代理技术,可动态生成一个“*&Proxy”的代理类
由其他文件生成,例如JSP文件生成对应的Class文件
从数据库中读取,该场景相对少见,例如:中间件服务器,可以将程序安装到数据库中完成程序代码在集群间的群发
从加密文件中获取,通过加载中进行解密Class文件
加载方式
虚拟机内置的启动类加载器来完成
用户自定义的类加载去完成
可通过自定义类加载器区控制字符流的获取方式<br>(重写一个类加载器的findClass()或loadClass()方法)
对于数组类而言
数组类本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构建出来的
类加载器
类加载器类型
启动类加载器<br>(Bootstrap Class Loader)
该加载器负责加载:<br>存放在<JAVA_HOME>/lib目录,<br>或则被-XbootclassPath参数所指定的路径存放的,<br>而且是 "Java虚拟机" 能够识别类库加载到虚拟机的内存中
该加载器无法被Java程序直接应用<br>(在Java中无法通过getClassLoader()方法获取到)
拓展类加载器<br>(Extension Class Loader)
类加载器负责加载:<br><JAVA_HOME>\lib\ext目录中<br>或则被java.ext.dirs 系统变量所指定路径中的所有类库
“父类”加载器是启动类加载器
应用程序类加载器<br>(Applicatioon Class Loader)<br>也被称为“系统类加载器”
该加载器负责加载:<br>用户类路径(ClassPath)上所有的类库
该类加载器,为大多数开发者最常用的加载器
"父类"加载器是拓展类加载器
自定义类加载器
继承ClassLoader重写findClass()或loadClass()方法<br>一般来说现在重写findClass()比较多
为了方便也可以继承 URLClassLoader的方式,这样可以省去重写findClass()所带来的麻烦<br>
"父类"加载器是系统类加载器
获得类加载器的方式
<ul><li>获取当前类的ClassLoader</li></ul> clazz.getClassLoader()<br>
<ul><li>获取当前线程上下文的ClassLoader<br>Thread.currentThread().getContextClassLoader()</li></ul>
<ul><li>获取系统类ClassLoader</li></ul> ClassLoader.getSystemClassLoader()<br>
<ul><li>获取调用者的ClassLoader</li></ul> DriverManager.getcallerClassLoader()<br>
验证
<b>验证时连接阶段的第一步,这个阶段的目的时确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息不被当作代码运行后不会危害虚拟机自身安全。</b><br>
验证会大致完成一下四个检验动作
1. 文件格式验证
<b>该验证阶段主要保证输入的字节流能正确地解析并存储于方法区中</b>
一些可能包括的验证点,但是实际上不止这一些
是否已魔数 0xCAFEBABE开头
主、次版本号是否在当前Java虚拟机接受范围之内
常量池的常量中是否有补呗支持的常量类型(检查常量tag标志),等
2. 元数据验证
<b>该阶段主要目的是对类的元数据信息进行语义校验</b>
可能包括的验证点
这个类是否有父类(除了Object类外,所有类应当有父类)
这个类的父类是否继承了不允许被继承的累(final修饰的类)
类中的字段、方法是否与父类产生矛盾,等
3. 字节码验证
<b>该阶段主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。<br>【在第二阶段后,对类的方法体(Class文件中的Code属性) 进行校验分析。】</b>
保证不发生一些危害虚拟机安全的行为
保证任意时刻不会出现<br>在操作栈放置了一个int类型的数据,使用时却按long类型来加载入“本地变量表”中
保证任何跳转指令都不会跳转到方法体以外的字节码指令上
保证方法体中的类型转换总是有效的
StackMapTable
为了避免过多的执行时间消耗在字节码验证阶段,JDK6之后的Javac编译器和Java虚拟机里进行了联合优化<br>把尽可能多的校验辅助措施挪到Javac编译器中进行。于是在方法体Code属性的属性表中新增 “StackMapTable”属性
其具体实现效果
4. 符号引用验证
该阶段发生在虚拟机将符号引用转化为直接应用的时候,这个转化动作发生在"解析"阶段中。<br>验证类是否缺少或被禁止访问它依赖的某些外部类、方法、方法的可访问性。
校验举例
符号引用中通过字符串描述的全限定名是否能找到对应的类
在指定类中是否存在符合方法的字段描述符及简单名称所描述的字段和方法
符号引用中的类、字段、方法的可访问性(private,protected,public)是否可被当前类访问
准备
准备阶段正式为类中定义的变量(类变量,即:静态变量。不包括实例变量)分配内存并设置类变量初始值的阶段
初始值:通常情况下,是数据类型的零值
例如:public static int value = 123;<br>在经过准备阶段时 value会被赋值为int的零值,即 0
当类字段的字段属性表中存在ConstantValue属性时(也就是被final标志的常量),在准备阶段,将会直接指定为所指定的初始值
public static final value = 123<br>在准备阶段中 value 会被赋值为 123
解析
解析阶段是Java虚拟机常量池内的符号引用替换为直接引用的过程
初始化
在准备阶段时,变量已经经过赋值一次系统要求的初始零值,在该阶段,则会更具程序员通过程序编码制定的主观计划去初始化类变量和其他资源。
<clinit>()方法
该阶段就是执行类构造器<clinit>()方法的过程<br><clinit>()方法并不是由程序员在Java直接编写的代码
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的
编译器收集顺序是由语句在源文件中出现的顺序决定的
静态语句块中只能访问到静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此,在Java虚拟机中第一个被执行的类型一定是Object类
<clinit>()方法对于类或接口来说不是必需的,如果一个类没有静态语句块,也没有对变量进行赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
接口也可以生成<clinit>()方法,但是于类有所不同
执行接口<clinit>()方法,不需要先执行父接口的<clinit>()方法
只有当父接口中定义的变量被使用时,父接口才会被初始化
接口的实现类在初始化时,也一样不会执行接口的<clinit>()方法
Java虚拟机必须保证一个类的<clinit>()方法在多线程环境下 被正确的加同步锁
规定有六种情况必须立即对类进行“初始化”<br>
遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果没有初始化,则需要先触发其初始化阶段。
使用了 new 关键字实例化对象的时候
读取或设置一个类型的静态字段<br>(被final修饰的,在编译器就把结果放入常量池的静态字段除外。)
调用一个类型的静态方法的时候
调用反射包的方法对类型进行反射调用的时候
当初始化类时,如果该类的父类没有经行初始化,则需要先触发父类的初始化
虚拟机启动时,虚拟机会先初始化要执行的主类,也就是含有main()方法的那个类
当JDK7新加入动态语言支持时,如果一个java.lang.invoke.MethodHanle实例最后解析的结果为:<br>REF_getStatic、REF_putStatic、REFinvokeStatic、REF_newInvokeSpecial四种类型的方法句柄<br>且这个方法句柄对应类没有进行过初始化,则需先触发其初始化
当接口中定义了JDK8新加入的默认方法(defult)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
委派双亲机制
工作原理<br>
1. 如果一个类加载器收到了一个类加载请求,<br>它并不会自己先去加载,而是把这个请求委托给父类加载器执行<br>
2. 所有加载器都是如此依次递归,<br>因此所有的加载请求最终都应该传送到顶成的类加载器中<br>
3.只有当父类加载器反馈自己无法完成这个加载请求时,<br>子类加载器才会尝试自己去完成加载。
好处
保护系统类不会被破坏
防止重复加载同一个class
直接内存
概念
直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
直接内存是在Java堆外的、直接向系统申请的内存区间
用法
在JDK1.4后加入了NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配对外内存,然后直接通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作
直接内存大小可以通过MaxDirectMemorySize设置
如果不指定,默认与堆的最大值-Xmx参数一致
异常
即使直接内存在Java堆外,不受虚拟机内存大小限制,但是系统内存也是有限的,因此还是会报OOM异常
优缺点
优点
通常,访问直接内存的速度会优于Java堆。即读写性能高。
Java的NIO库允许Java程序使用直接内存,用于数据缓冲区
缺点
分配回收成本较高
不受JVM内存回收管理
执行引擎
概念
在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,甚至还可能会包含几个不同级别的编译器执行引擎。但从外观上看起来,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果
在JVM中的最重要两部分
解释器
即时编译器(JIT)
对“编译期”的解释
Java语言的“编译器”其实时一段“不确定”、“模糊”的操作过程,因为它可能时指前端编译器(或者叫“编译器的前端”)把 .java文件转变成 .class文件过程;
也可能是指虚拟机的“后端运行期编译器”(JITb编译器)把字节码转换成机器码的过程
还可能是指使用静态提前编译器(AOT编译器)直接把.java文件编译成本地机器码的过程
解释器
概念
解释器就像一个编译器,它不会一次把整个程序转译出来,只像一位“中间人”,每次运行程序时都要先转成另一种语言再作运行,因此解释器的程序运行速度比较缓慢。它每转译一行程序叙述就立刻运行,然后再转译下一行,再运行,如此不停地进行下去。
通俗
把程序源代码一行一行的读懂然后执行,发生在运行时,产物是「运行结果」。
JVM中的作用
在JVM中解释器就是将Java字节码一句一句翻译成机器码去执行
解释型语言
Python
JavaScript
PHP等
优缺点
优点
1.解释型语言启动速度快,因为它无需编译可以直接解释<br>
2.易于跨平台,在运行时可根据不同的平台进行解释执行
缺点
1.后续的执行效率不高,因为如果运行重复的代码需要再次解释
2.整体来说执行效率低
编译器
概念
编译器是一种计算机程序,负责把一种编程语言编写的源码转换成另外一种计算机代码,后者往往是以二进制的形式被称为目标代码(object code)。这个转换的过程通常的目的是生成可执行的程序。ge)机器代码的等价程序。
把整个程序源代码翻译成另外一种代码,然后等待被执行,发生在运行之前,产物是「另一份代码」。
编译型语言
C
C++等
优缺点
优点
1.后续执行效率高,已经在一开始已经进行了编译,所以在运行到重复的代码时可以直接执行编译后的代码,无需再次编译
2.总体来说执行效率更高
缺点
1.编译型语言启动比较慢,因为启动时,需要经行一个编译的过程(编译的结果就是目标代码,就是上诉的所谓「另一份代码」)
2.启动时的编译是需要根据硬件,以及平台相关性来决定的<br>
JVM中的编译器分类
前端编译器
JAVAC
Eclipse JDT中的增量式编译器(ECJ)
JIT编译器
HotSpot VM的C1,C2的编译器
AOT编译器
GNU Compiler for the Java(GCJ)、Excelsior JET
Java是半解释半编译的语言
这得益于在JVM中能够同时拥有,编译器(即时编译器),以及解释器
在程序启动的初期,JVM会通过解释器进行翻译执行代码,然后,如果后续有重复的代码(又称作,热点代码),JVM就会使用及时编译器进行编译,然后保存编译的结果下次再需要执行者重复的代码时,就不需要通过解释器逐句翻译执行了。<br>
在编译的同时,JVM还能堆源代码的一些部分进行优化这样可以进一步的提高执行效率
不一定所有的JVM实现都是半解释半编译的,如:JRocket就没有解释器<br>
(Hotspot)即时编译器(JIT)
在HotSpot VM中内嵌又两个JIT编译器,分别为Client Compiler和Server Compile,但大多数情况下我们简称为C1编译器和C2编译器。<br>
默认情况下,我们64位处理器默认开启的时Server模式
使用
指令-client:指定Java虚拟机运行在Client模式下,并使用C1编译器
C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度
指令 -servcer:指定Java虚拟机运在Server模式下,并使用C2编译器
C2进行耗时比较长的优化,可以激进优化。代码的执行效率要更高
AOT编译器
JDK9引入了AOT编译器(静态提前编译器)
它可以在程序运行过程之前,直接将Java类文件转换为机器码,并存放至生成的动态共享库之中
优缺点
优点
Java虚拟机加载已经预编译成二进制库,可以直接执行。不必等待JIT编译器的预热,减少Java应用给人带来“第一次运行慢”的不良体验。
缺点
破坏了java“一次编译,到处运行”,必须为每个不同硬件、OS编译对应的发行包
降低了Java链接过程的动态性,加载的代码在编译期就必须全部已知
编译环境的选择
在HotSpot VM中开发人员可以根据具体的引用场景,通过命令显示地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行
-Xint:完全采用解释器模式执行程序
-Xcomp:完全采用即使编译器模式执行程序。如果即使编译器出现问题,解释器还是会介入执行
-Xminxed:采用解释器+即时编译器的混合模式共同执行程序
垃圾回收机制与算法
标记阶段<br>(该阶段标记哪些对象需要被回收)
引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;<br>当引用失效时,计数器值就减一;任何时刻计数器为零的就是不可能再被使用的。<br>
优缺点
优点
实现原理简单
判定效率高
缺点
有许多特例的情况需要考虑<br>(无法解决循环引用的问题)
市面上的运用
Python
Squirrel等
可达性分析算法
该算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据关系引用关系向下到GC Roots间没有任何引用链相连,或者用图论的话来说就是从 GC Roots到这个对象不可到达时,则证明此对象时不可能再次被使用的。<br>
可达性分析图解
finalize
如果一个对象没有任何引用链相连,则判断为对象不可用,作为可回收对象,通过finalize()自救。
如果一个对象本该被回收,但调用finaize()时,<br>为此对象赋予了与GC Roots的链接,则该对象会被“复活”
如果对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过一次,那么虚拟机会视这两种情况为“没有必要执行”
调用过程
如果该对象被判定为确有必要执行finalize()方法,那么该对象会被执法置放在一个名为F-Queue的队列中
在稍后由一条虚拟机自动建立的、低调度优先级的Finalizer线程去执行他们的finalize()方法
该方法已被官方明确申明不推荐使用,所以在开发中不要使用该方法
在Java语言中,可作为GC Roots的对象包含
1.在虚拟机栈(栈帧中的本地变量表)中引用对象
2.在方法区类静态属性引用的对象
3.在方法区中常量引用的对象,譬如字符串常量池里的引用
4.在本地方法栈中JNI(即,通常所说的Native方法)引用的对象
5.Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻异常对象等,还有系统类加载器
6.所有被同步锁(synchroonized关键字)持有的对象
7.反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合
市面上的运用
JAVA
C#
Lisp等
GC实现的方式
保守式GC
优点
这种可达性分析的方式因为不需要准确的判断出一个指针,所以效率快
缺点
因为是模糊的检查,所以对于一些已经死掉的对象,很可能会被误认为仍有地方引用他们,GC也就自然不会回收他们,从而引起了无用的内存占用,造成资源浪费。
由于不知道疑似指针是否真的是指针,所以它们的值都不能改写;移动对象就意味着要修正指针。换言之,对象就不可移动了
半保守式GC
准确式GC
“准确式GC”所谓的准确,关键就是“类型”,也就是说给定某个位置上的某块数据,要能知道它的准确类型是什么
实现的方式<br>
在java中实现的方式是:从外部记录下类型信息,存成映射表,在HotSpot中把这种映射表称之为OopMap(不同的虚拟机对映射表的叫法不一样)
三色标记算法
适用于,GC与用户线程并发的环境中,在不停止用户线程的情况下,对垃圾进行标记
使用
三种颜色标记
白色
表示对象尚未被垃圾收集器访问过。
在可达性分析刚开始阶段,所有对象都为白色,最后如果还都是白色则为不可达对象
黑色
表示对象已经被垃圾收集器访问过,切这个对象的所有引用都已经被扫瞄过了
如果有其他的对象引用指向了黑色对象,无需重新扫描一遍
黑色对象不可能直接(不仅过灰色对象)指向某个白色对象
灰色
表示对象已经被垃圾收集器访问过了,但这个对象上至少存在一个引用还没有被扫描过
标记的过程
<br>
会产生的问题
多标
此时,用户线程缺点了D中的E引用,E已经是一个不可达对象,却依然显示灰色,无法被回收
漏标
此时,原来G为不可达对象,被建立了与D的引用,此时G就会被回收,造成程序运行的错误
解决方式
当且仅当两个条件同时满足,才会产生"对象消失的问题"
赋值器插入了一条或多条从黑色对象到白色对象的引用
赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
增量更新
破坏了第一个元素:当引用关系发生改变的时候就记录下来,然后并发扫描结束,再将这些记录过的关系引用对象重新扫描<br>
通俗理解:黑色对象新插入了白色对象的引用,就将他编程灰色
CMS收集器就是采用了这总方法
原始快照(SATB)
该方法时破坏了第二个元素:当灰色对象插入新的指向白色对象的关系引用时,就将这个要删除的引用计入下来,在并发扫描结束之后,再将这些引用关系中的灰色的根对象重新扫描一次。<br>
通俗理解:当一个白色对象引用断开连接,就记录一下,下次来查看这个记录,对记录中已经被删除引用的对象进行再次扫描,看看是否有新的引用对象指向它,如果有则将它变成灰色,如果没有则无需更改<br>
引用
强引用
强引用是最传统的引用,无论任何情况下,只要强引用还在,垃圾回收器就永远不会回收掉被引用的对象
Object obj = new Object()
软引用
弱引用是用来描述一些还有有用,但非必须的对象。<br>只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行回收。
1.2JDK版本后:提供SoftReference类来实现软引用
弱引用
弱引用也是用来描述那些非必须的对象,但是它的强度比软引用低一些,被弱引用关联着的对象只能生存到下一次垃圾回收发生为止。无论当前内存是否足够
JDK1.2版本后:提供WeakReference类来实现弱引用<br>
虚引用
又叫“幽灵引用”或“幻影引用”,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
虚引用的唯一目的只是为了能够在这个对象被收集器回收时收到一个和系统通知。
JDK1.2版本后:提供PhantomReference类来实现虚引用
HotSpot VM的算法细节实现<br>
根节点枚举
“根节点枚举”就是在确认根节点(GC Roots)的过程<br>
而且,迄今为止所有的收集器在“根节点枚举”这一步骤时都必须暂停用户线程(STW)
虽然,现在可达性分析算法耗时最长的“查找引用链”的过程已经可以做到与用户线程一起并发,<br>但,“根节点枚举”始终还是必须在一个能保障一致性的快照中才得以进行
什么是OopMap?
目前主流的JVM使用的都是准确式垃圾收集,<br>在扫描标记的时,虚拟机可以准确的知道该数据时什么类型或者是否是指针
OopMap在准确式GC中用来保存类型的映射表<br>
生成映射表的方式
每次都遍历原始的映射表,循环的一个个偏移量扫描过去;这种用法也叫“解释式”;
为每个映射表生成一块定制的扫描代码(想像扫描映射表的循环被展开的样子),以后每次要用映射表就直接执行生成的扫描代码;这种用法也叫“编译式”。
安全点(Safe Point)
可达性分析算法必须是在一个确保一致性的内存快照中进行。如果在分析的过程中对象引用关系还在不断变化,分析结果的准确性就不能保证。<br>安全点意味着在这个点时,所有工作线程的状态是确定的,JVM 就可以安全地执行 GC 。<br>
如何选定安全点
安全点太多,GC 过于频繁,增大运行时负荷;安全点太少,GC 等待时间太长
一般会在如下几个位置选择安全点<br>
循环的末尾
方法临返回前<br>
调用方法之后<br>
抛异常的位置
如何让所有线程在安全点上停止下来呢?
抢断式中断
在 GC 发生时,首先中断所有线程,如果发现线程未执行到 Safe Point,就恢复线程让其运行到 Safe Point 上。
主动式中断
在 GC 发生时,不直接操作线程中断,而是简单地设置一个标志,让各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起。
JVM 采取的就是主动式中断。轮询标志的地方和安全点是重合的。
安全区域(Safe Region)
如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。
Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。
线程在进入 Safe Region 的时候先标记自己已进入了 Safe Region,在这段时间里,虚拟机要发起垃圾收集时就不会管这些声明已经在安全区域内的线程了,等到被唤醒时准备离开 Safe Region 时,先检查能否离开,如果 GC 完成了,那么线程可以离开,否则它必须等待直到收到安全离开的信号为止。
清除阶段<br>
标记-清除算法(Mark-Sweep)
运行流程
1.首先标记出所有需要回收的对象
2.然后统一回收掉所被标记了的对象
缺点
执行效率不稳定(较为缓慢)<br>需要两个过程的遍历,一个是标记,一个是清除
会产生空间碎片化
复制算法
需要将内存按照容量划分为大小相等的两块区域,每次只使用其中一块儿
运行流程
当这一块的内存用完了,就将还存活着的对象,复制到另外一块区域上,再把原来的区域一次清理掉
优缺点
优点
实现简单,运行效率高
因为不需要两个步骤,只需要遍历一次,将存活的对象复制
不会产生空间碎片化
缺点
空间开销比较大
如果,大部分的对象都能够存活则,需要复制的对象会很多,此时会产生较大的复制开销
标记-整理算法(Mark-Compact)
即在标记-清除算法的基础上,做一个整理的过程,以解决标记-清除中的碎片化问题
优缺点
优点
在标记-清除的基础上,解决了碎片化的问题
同时也避免了,复制算法的空间浪费问题
缺点
相对于标记-清除效率更低了
因为多了一个整理的过程
降低了系统的吞吐量
在垃圾清理的过程中需要,停止用户线程(被描述为“Stop The World”)<br>所以,过长的清理时间会降低系统的吞吐量
在CMS中为了解决不在内存分配上和访问上增加太大额外负担的方案
虚拟机在多数时间都采用标记-清除算法,暂时容忍内存碎片的存在
直到内存空间的碎片化程度已经大到影响内存分配了,在采用标记-整理算法收集一次
System.gc() 或<br>Runtime.getRuntime().gc()
会显示触发Full GC
System.gc无法保证一定能够对垃圾收集器的调用
字节码文件
Class文件时一组以字节为基础单位的二进制流,各个数据严格按照顺序排列紧凑的排列在文件中,中间没有任何分隔符
组成Class文件的两种数据类型
无符号数
分别以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数
无符号数用来描述
数字
索引引用
数量值
按照UTF-8编码构成的字符串值
表
是由多个无符号数或其他表作为数据项构成的复合数据类型
所有表命名都习惯性地以"_info"结尾
整个Class文件本质上也可以视作是一张表
Class文件结构
魔数
用来确定这个文件是否为一个能被虚拟机接受的Class文件
四个字节
CAFEBABE
版本号
用来确定JDK的版本
四个字节
5-6字节是次版本号,7-8字节是主版本号
常量池
常量池容量计数器
四个字节
存放着常量池中常量的个数
从1开始默认保留下标为0的空位置
主要存放两大类常量
字面量
文本字符串
被声明为final的常量值
符号引用
被模块到处或开放的包
类和接口的权限定名
字段的名称和描述符
方法的名称和描述符
方法句柄和方法类型
动态调用点和动态常量
运行时数据区
java虚拟机在执行程序的过程中会将它所管理的内存分成若干个部分管理
运行时数据区组成
程序计数器<br>(PC)寄存器
程序计数器时一块较小的内存空间,它可以看作时当前线程所执行的字节码的行号指示器。<br>通俗讲法:就是它寄存着字节码中下一句要运行的代码的地址,每个线程都有一个程序计数器,分别记录着自己线程的下一跳代码(指令)的地址,当CPU在线程调度,切换时,保证每次的切换都能够切换回上次运行的地方。
如果调用的时本地方法(Native),则该计数器记录的值为空(Undefined)
此内存区域是唯一一个在《Java虚拟机规范》中没规定任何OutOfMenoryError(OOM)情况的区域
程序计数器是线程私有的
Java虚拟机栈(栈)
虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈动态连接、方法出口等信息。当有一个方法需要被执行是就会入栈操作,执行结束则出栈
不同的线程中所包含的栈帧时不能互相引用的<br>
Java虚拟机栈是线程私有的
栈帧弹出的方式
正常的结束return;
抛出异常(未捕获)
异常
StackOverflowError
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
OutOfMemoryError
如果Java虚拟机栈容量可动态扩展,当栈扩张时无法申请到足够内存会抛出OutOfMemoryError异常
设置栈的大小
可以使用 -Xss Size 来设置栈的大小<br>例如 -Xss256k
栈帧的内部存储
操作数栈(表达式栈)<br>
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间<br>
当一个方法刚开始执行时,这个方法的操作数栈是空的,但是操作数栈的大小在编译的时候就确定了
存放
32位数据类型占的栈容量为1
64位数据类型所占的栈容量为2
操作过程
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配
例如:iadd指令,只能用于整型数的加法,且最接近栈顶的两个元素的数据类型必须为int类型
不同的栈帧作为不同方法的虚拟机栈元素,是完全独立的。<br>但是,在大多数虚拟机的实现里都会有进行一些优化处理,令下面的栈帧部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样不仅节约了空间,更重要的是在方法调用时就可以直接公用一部分数据,无需额外的参数复制传递。
局部变量表
局部变量就是定义在每个方法中的变量。定义在类中的叫类变量(static),或者属性(对象属性、实例变量)
所需容量是在编译器就确定下来了的<br>
存放
基本数据类型
对象的引用(并不等同于对象本身)
returnAddress类型(指向了一条字节码指令的地址)
方法的形参
基本存储单位
slot(槽)
32位(4个字节)占一个slot(包括returnAddress类型)
byte,short、char、引用类型在存储前被转化为int类型,boolean 0等于false,非零true<br>
64(8个字节)占用两个slot(double,long)
也可以看作是一个数字数组
在非静态方法(静态方法中不允许调用this),或构造方法中,以“this.xx”这样的形式调用的方式,会将this指向存放在局部变量表的index为0的slot处<br>
slot槽位十可以重复利用的,如果方法中的局部变量过期了(代码运行超过了作用域),则该位置将会被重复利用<br>
方法放反回地址(方法正常退出或异常退出的定义)
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。<br>(通俗点讲,就是回复调用该方法的之前方法)
PC寄存器就是存放着下一条需要执行的指令,该指令需要执行引擎去执行
一个方法有两种退出方式(出栈)
执行引擎遇到任意一个方法返回的字节码指令
在执行方法的过程中遇到了异常,并且这个异常没有在方法体中得到妥善的处理,<br>也就是只要在本地方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,该退出方式称为“异常调用完成”
当你的异常被catch捕获就属于被合理的异常处理了
如果是抛出或者Java虚拟机内部发生了异常都属于未被异常处理
返回字节码分类
return
无返回值类型(void)
ireturn<br>
当返回值是boolean、short、int、byte、char<br>
lreturn
当放回值是long
freturn
当放回值是float
dreturn
当返回值是double
areturn
当返回值是引用类型
其他附加信息
《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息倒栈帧中,例如与调试、性能收集相关的信息,这部分信息完全取决于虚拟机实现
动态链接(指向运行时常量池的方法引用)<br>
每个栈帧都包含一个指向“运行时常量池”中栈帧所属方法的引用,<br>持有了这个引用就可以支持方法在调用过程中的动态链接
当前Class文件常量池符号引用,这些引用指向方法区中的“运行时常量池”中的方法引用
这些符号引用一部分会在类加载阶段或第一次使用的时候被转化为直接引用,这种转化被称为“静态解析”。<br>另外一部分将在每一次运行期间转化为直接引用,这部分就称为“动态连接”<br>
方法调用
方法调用阶段唯一任务就是确定被调用方法的版本(即调用哪一个方法)
所有方法的调用的目标方法在Class文件里面都是一个符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,但是还有一部分类无法在类加载阶段被解析,因为这些方法被调用的版本不可确定,或者在运行阶段被调用时可能是可变的(例如:多态)。
符合“编译期可知,运行期不变”这个要求的方法
静态方法
私有方法
对应不同类型的方法,字节码指令
invokestatic
用于调用静态方法
invokespecial
用于调用实例构造器<init>()方法、私有方法和父类中的方法
invokevirtual
调用所有虚方法
invokeinterface
用于调用接口方法,会在运行时再确定一个实现该接口的对象
invokedynamic
先在运用时动态解析出调用点限定符所引用方法,然后再执行该方法
方法分类
虚方法
在被调用时,无法在类加载阶段转化为直接引用,也就是在类加载的解析阶段,无法被确定版本的方法
非虚方法
在被调用时,会在类加载的时候就可以把符号引用解析为该方法的直接引用。
一个概念
静态类型(外观类型)
静态类型是在编译期可知的
实际类型(运行时类型)
实际类型变化的结果在运行期才可确定
例如:Human human = (new Random()).nextBoolean() ? new Man() : new Woman();
Human human = new Man();<br>Human human = new Woman();
这里的Man和Woman为Human的子类
Humen human就是静态类型
new Man();<br>new Woman();<br>为实际类型
分派
静态分派
所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派
运用
重载
选择那哪一个重载版本是根据,静态类型来决定的,而不是实际类型
所以,Java实际上实在编译期间就已经确定了你所需要调用的方法重载版本
动态分派
调用方法的版本实在运行时才可以确定的
运用
重写
如果一个接口有两个实现类,用该接口调用该方法,则实际的方法调用版本则根据实际参数决定
需要用到指令为invokevirtual指令
invokevirtual解析过程
1.找到操作数栈顶的第一个元素所指向的对象实际类型,记作C
2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,<br>如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回IllegalAccessError异常
3. 否则,按照继承关系从下往上一次对C的各个父类进行第二步的搜索和验重过程
4.如果始终没找到何使的方法,则抛出AbstractMethodError异常
按照宗量数分<br>方法的接收者和方法的参数统称为方法的宗量<br>
多分派
就如Java来说,在编译阶段,选择目标方法的时候需要依据两个点<br>一、是方法的调用者是谁,<br>二、根据方法的参数选择重载的对应的方法<br>
单分派
在运行的阶段,由于在编译时期已经确定了重载对应的方法和调用者<br>这一阶段只需要确定,根据实际参数,调用方法的版本即可
堆
概念
Java堆是虚拟机锁管理的内存中最大的一块,其大小在虚拟机被启动时就确定了
是被所有线程共享的一块内存区域,还能够划分线程私有的缓冲区(TLAB)
唯一目的就是存放对象实例,Java中“几乎”所有对象实例都在这里分配内存
《Java虚拟机规范》规定,堆可以处于内存不连续的内存空间中,但在逻辑上应被视为连续的<br>
堆的分区(不是所有虚拟机固有的)
新生代
伊甸园区(Eden)
概念
几乎所有Java对象都是在Eden区中被New出来的<br>
绝大部分的Java对象的销毁都在新生代<br>
幸存者0区(Survivor 0)<br>
幸存者1区(Survivor 1)
老年代
异常
OutOfMemoryError
如果在Java堆中没有内存完成实力分配,并且堆也无法再扩展时,Java虚拟机将会抛出OOM异常
调优
因为再GC线程启动时会暂停用户线程(STW),所以调优的目的就是尽量让GC垃圾回收的次数减少<br>
调优工具
JDK命令行
Eclipse:Memory Analyzer Tool<br>
Jconsole<br>
VisualVM<br>
Jprofiler<br>
Java Flight Recoder<br>
GCViewer
GC Easy<br>
分代收集理论
按照回收区域分类
部分收集
新生代收集(Minor GC/Young GC)
只是对新生代(Eden,S0/S1)的垃圾收集
触发机制
当新生代空间不足时,就会触发Minor GC,这里的新生代满指Eden满,Survivor满不会引发GC。<br>(每次Minor GC会清理年轻代内存)
大多数Java对象的生命周期都比较短,所以 Minor GC会非常频繁,一般回收速度也比较快。<br>
Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
老年代收集(Major GC/Old GC)
只是对老年代的垃圾收集
目前,只有CMS GC会有单独收集老年代的行为
注意:很多时候Major GC会和Full GC的叫法会混淆使用,Major GC具体指代什么需要具体分辨是老年代回收还是整堆回收
触发机制
指发生在老年代的GC,对象从老年代消失时,我们说“Major GC”或 “Full GC”发生了(上面讲了Major GC 与 Full GC 很多地方叫法混淆)
出现了Major GC,经常会伴随至少一次的Minor GC<br>(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)
也就是老年代空间不足时,会先尝试触发Minor GC。如果空间还不足,则触发Major GC
如果Major GC内存还不足,就报OOM
Mojor GC的速度一般会比Minor GC慢10倍以上,STW的时间更长
混合收集(Mixed GC)
收集整个新生代以及老年代的垃圾收集
目前,只有G1 GC会有这种行为
整体收集(Full GC)
收集整个Java堆和方法区的垃圾收集
内存分配策略
对象优先分配到Eden区
大对象直接分配到老年代<br>
尽量避免程序中出现过多的大对象
长期存活的对象分配到老年代<br>
动态对象年龄判断
如果幸存者区(Survivor)中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。<br>
空间分配担保
谁进行空间担保?
空间担保指的是老年代进行空间分配担保
什么是空间分配担保?
1. 在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
2. 如果大于,则此次Minor GC是安全的
3. 如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败
如果HandlePromotionFailure=true(JDK 7该指令虽然还在,但是不管如何设置都为true一样的效果)
那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;
如果小于
则进行一次Full GC
如果HandlePromotionFalure=false
则进行一次Full GC<br>
为什么要进行空间担保?
是因为新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。
本地方法栈
本地方法栈用于管理本地方法的调用<br>
需要用到时将本地方法栈帧压入本地方法栈,然后通过动态链接去调用本地方法库里面的方法
当调用本地方法时,就不会收到Java虚拟机的限制,和Java虚拟机拥有同样的权限<br>
《Java虚拟机规范中》并没有明确要求本地方法栈所需要用什么语言,具体的实现,以及数据结构等,甚至不要求强制实现本地方法栈<br>
在HotSpot虚拟机中,将本地方法栈和虚拟机栈合二为一<br>
方法区
概念
方法区与Java堆一样,是各个线程共享的内存区域<br>
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是连续的
方法区的大小,跟堆空间一样,可以选择固定大小或者可拓展
关闭JVM就会释放这个区域的内存
《Java虚拟机规范》中明确说明:“尽管所有方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”<br>但对于HotSpotJVM而言,方法区还有一个别名叫作Non-Heap(非堆),目的就是要和堆分开。<br>所以,方法区看作是独立于Java堆的内存空间
在方法区也是存在垃圾回收的,尽管它很少出现
该区域的内存内存回收目标主要是针对
常量池的回收
类型的卸载
异常
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机就会抛出OOM异常
实现方式(HotSpot JVM)
JDK8以前(永久代)
方法区的实现方式为"永久代",所以在此之前好多人讲方法区就曾为“永久代”,实际上两者并不等价
JDK8开始(元空间)
彻底废弃了“永久代的概念”,改用与“JRockit”,“J9”一样在本地内存中实现的“元空间”来代替
在JDK7时,HotSpot,把原来放在“永久代”的“字符串常量池”、“静态变量”等移至Java堆中
大小设置
使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定方法区的大小
默认值(依赖于平台)
windows
-XX:MetaspaceSize是21M,<br>-XXMaxMetaspaceSize的值-1.即没有限制
对于一个64位的服务器端JVM来说其默认的-XX:MetaspaceSize值就是初始的高水位线,一旦触及这高个水位线,<br>Full GC将会被触发并卸载没用的类,<br>然后这个高水位线将会重置。新高水位线的值取决于GC后释放了多少元空间
高水位线会引发Full GC,为了避免过多的发生Full GC,建议将-XX:MeataspaceSize设置位一个相对较高的值
存放(已被虚拟机加载的信息)
类型信息
1. 类型的完整有效名称(全名 = 包名.类型)
2. 类型的直接父类的完整有效名(对于interface或是Object类,都没有父类)
3. 类型的修饰符(public,abstract,final的某个子集)
4. 类型直接接口的一个有序列表
运行时常量池
Class文件中的常量池表和运行时常量池区别
运行时常量池是方法区的一部分
运行时常量池具备动态性
动态性:Java语言并不要求常量一定只有编译器才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中(String类的intern()方法)
常量池表是Class文件的一部分
这里的常量池可以比喻为Class文件里的资源仓库,可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。具体查看在Class文件分支中的讲解
Class文件常量池不具备动态性
在加载类和接口到虚拟机后,就会常见对应的运行时常量池。
JVM为每个已加载的类型(类或接口)都维护了一个常量池。<br>池中的数据像数据项一样是通过索引访问的
运行时常量池中包含多种不同常量
包括编译器就已明确的数值字面量
也包括到运行期解析后才能够获得的方法或字段引用
异常
当常量池无法在申请到内存是会抛出OOM异常
静态变量
这里只的静态变量的引用,即,public static Person person = new Person(); <br>存放的是 person部分(即,静态类型),后面的实体是存放在堆中。
JDK7以后,静态变量就移动至堆中保存,概念上逻辑上(概念上)依然是属于方法区
(JTL)即时编译器编译后的代码缓存等数据
方法信息
1. 方法名称
2. 方法的放回类型(或 void)
3. 方法参数的数量和类型(按顺序)
4.方法修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
5. 方法的字节码、操作数栈大小、局部变量表及大小(abstract和native方法除外)
6. 异常表 (abstract和native方法除外)
记录着每个异常处理的
开始位置
结束位置
代码处理在程序计数器中的偏移地址
被捕获的异常类的常量池索引
域信息
所有域的相关信息
域名称
域类型
域修饰符
域申明顺序
JVM生命周期<br>
虚拟机的启动
Java虚拟机的启动是通过引导类加载器(bootstrap)创建一个初始类(initail)来完成的这个类是由虚拟机具体指定实现的<br>
虚拟机的执行
一个运行着的java虚拟机有一个清晰的任务:执行java程序<br>
程序开始,虚拟机执行,程序结束,虚拟机也停止<br>
所谓执行一个java程序,其实上就是执行一个叫java虚拟机的进程<br>
虚拟机退出
程序正常执行结束,虚拟机退出
程序在执行过程中,遇到异常或错误而异常终止<br>
由于操作系统的错误而导致系统终止<br>
某线程调用Runtime类或System类中的exit方法,或Runtime中的halt方法<br>
对象实例化<br>
对象创建
创建对象的方式
创建对象的步骤
Java对象内存布局
对象头(Header)
实际数据(Instance Data)
Padding
String Pool
在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存、都提供了一种常量池的概念。
常量池就类似一个Java系统级别提供的缓存。8种数据类型的常量池都是系统调节的。<br>String类型的常量池比较特殊。它的主要使用方法有两种。
直接使用双引号声明出来的String对象会直接储存在常量池中<br>比如:String info = “hello String”;
如果不是双引号声明的String对象,可以使用String提供的intern()方法
String Pool是一个固定大小的Hashtable
JDK6以前 StringTable的默认长度是1009。也可以无限制的自定义大小
JDK7以后StringTable的长度默认值是60013,也可以自定义设置大小,但是设置范围的限制(1009~60013)
存放地点
JDK7以前
字符串常量池存放在永久代(方法区)
JDK7时
字符串常量池调整到了Java堆当中
JDK8以后
字符串常量池依然存放在Java堆当中,此时已经取消了永久代的概念,更改为了元空间
内存原理
1.常量与常量的拼接结果在常量池,原理是编译器优化
在编译字节码时就将它自动拼接为一个常量
2. 常量池中不会存在相同内容的常量
3.如果拼接的元素其中一个为变量,则底层会创建StringBuilder的append方法对其进行拼接,最后调用toString()放回
toString()方法底层,虽然通过new String()方式放回String,但是它不会在常量池中创建对应的值
3. 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
JDK6以前
intern()的工作流程
1.查看常量池中是否有这相同字符串的变量
2.如果有则将该对象的引用放回,如果没有则在常量池中创建该对象后再返回该引用(注意此时,会有两个对象,一在堆中,一个在常量池中)
JDK7以后
intern()的工作流程
1.查看常量池中是否有这相同字符串的变量
2.如果有则返回对象该引用,如果没有则将堆中的对象引用保存在字符串常量池中(此时只有一个对象,常量池中引用的是堆中的对象)
4. 通过new String("a")的方式创建对象,实际会创建2个对象,一个在堆中,一个在字符串常量池(实际上常量池也在堆中)
4.通过String a = "a";这样以字面量的方式直接赋值,实际只会创建1个在字符串常量池中的对象
6.调用 toString()虽然底层是通过new String()的方式创建对象 但是不会在字符串常量池中创建该常量
7. 如果在String变量前面添加有final关键字则编译器 在拼接时会进行优化,视为一个字符串(参考第1点)
垃圾回收器
工作模式分类
并发式垃圾回收器
独占式垃圾回收器
碎片处理方式分
压缩式垃圾回收器
压缩式垃圾回收器会在垃圾回收完成之后,对存活对象进行压缩整理,消除回收后的碎片
再分配对象空间使用:指针碰撞
非压缩式垃圾回收器
非压缩式的垃圾回收器不进行这步操作
再分配对象空间使用:空闲列表
工作的内存区间分
年轻代垃圾回收期
老年代垃圾回收器
评估GC的性能指标
吞吐量
运行用户代码时间占总运行时间的比例<br>(总运行时间:程序的运行时间+内存回收的时间)<br>
运行用户代码时间/(运行代码时间+垃圾收集时间)
垃圾收集开销
吞吐量的补数,垃圾收集所用时间与总运行时间的比例
暂停时间
执行垃圾收集时,程序的工作线程被占暂停的时间
收集频率
相对于应用程序的执行,收集操作发生的频率
内存占用
Java堆区所占的内存大小
快速
一个对象从诞生被回收所经历的时间
经典垃圾收集器
Serial
单线程
优缺点
对线程开销少,简单有效,适用于内存资源受限,和处理器核心数教少的环境。(目前,客户端模式下默认使用收集器)<br>
ParNew
多线程
本质上是Serial收集器的多线程并行版本,除了多线程并发没有,其他几乎与Serial保持一致
jdk9以后唯一可以和CMS 一起搭配工作的收集器
Parallel Scavenge
多线程
<br>
他目标是达到一个可控制的吞吐量
是一个吞吐量优先的收集器
CMS
多线程的并发收集器(能够和用户线程并发)
<br>
是一种以获取最短回收停顿时间为目标的收集器,是一种老年代的垃圾收集器
默认启动的线程数是:(处理器核心数量+3)/4
采用的算法是“标记-清除”算法
运行过程
初始标记
需要STW
该阶段主要是标记一下GC Roots能够直接关联到的对象
并发标记
可以和用户线程并发进行
该阶段从GC Roots的直接关联对象开始遍历整个对象图的过程
重新标记
需要STW
由于主要标记垃圾对象是在并发阶段进行的,如果在标记的期间用户线程的变动可能导致变动的标注出现变动
该阶段 主要就是对因用户线程运作而导致标记变动的那一部分对象进行重新标记
并发清除
可以和用户线程并发进行
对以标记的对象进行垃圾清理,由于用户线程还在运行,顾无法进行整理
优缺点
优点
CMS非常注重STW的时间,停顿时间短,所以对用户有良好的体验
缺点
由于是与用户线程并发执行,所以需要占用系统子资源(线程数,内存)所以当内存不够时,可能会导致CMS启动失败,不得不启用备用方案
在单核处理器的环境下 由于需要线程的开销,所以与用户线程同时进行意义不大,甚至还会降低性能
吞吐量相对较低
无法处理“浮动垃圾”而导致另一次完全的STW的Full GC的产生
无法处理"碎片化"的问题,而提前Full GC
处理方式:可以通过参数设置,指定多少次Full GC 时进行一次内存的整理
Parallel Old
多线程
<br>
由于在JDK6之前,Parallel Scavenge唯一能够搭配的就是 Serial Old收集器,但是 Serial Old是一个单线程的处理器,实际上Serial Old 在一定程度上拖累了 Parallel Scavenge所以 Parallel Old 就诞生了<br>
G1
多线程(可以和用户线程并发执行的 )
采用分区算法(取消了原来分代算法的物理划分,但还是保留了逻辑划分)
垃圾清理算法
从整体来看:标记-整理
从region之间来看:复制算法
G1 收集器 内存分布图
既然使用了分区算法,那么在不同的分区之间对象就存在一个相互应用的问题
顾在G1收集器的每一块分区中都存在一个(记忆集)RSet,用来存放其他分区对该分区的引用记录
运行过程
初始标记
仅仅是标记一下GC Roots能够直接关联到的对象并且修改TAMS指针值
并发标记
从GC Roots开始对队中对象进行可达性分析自己,找到需要回收的对象
递归扫描完成后,还需要重新处理SATB记录下在并发时有引用变动的对象
最终标记
需要STW
对并发阶段结果后遗留下来的少量SATB记录进行处理
筛选回收
因为涉及到对象移动,所以需要STW
对Region进行数据统计,对各个Region的回收价值和成本进行排序,更具用户所期望的停顿时间来制定回收计划
可以自由选择任意多个Region进行收集,然后回收的那一部分Region的存活对象复制到空的Region中,清理掉原来的Region
TAMS指针
G1在GC时用户线程时同步进行的,所以就会产生新的对象,G1为每个region设计了两个名为TAMS的指针<br>把region中的一部分空间划分出来用于并发回收过程中新对象的分配
优缺点(与CMS相比较)
优点
停顿时间可以预测:我们指定时间,在指定时间内只回收部分价值最大的空间,而CMS需要扫描整个年老代,无法预测停顿时间
无内存碎片:垃圾回收后会整合空间,CMS采用"标记-清理"算法,存在内存碎片
由于只回收部分region,所以STW时间我们可控,所以不需要与用户线程并发争抢CPU资源,而CMS并发清理需要占据一部分的CPU
缺点
相比Parallel Scavenge/Parallel Old 没有吞吐量优势<br>
由于采用分区算法,所以每个区需要设计RSet,在内存空间上回尝试额外的消耗(20%左右)
其他垃圾处理器
ZGC
0 条评论
下一页