简介JVM
1.回收的是什么? 对象
1.1 什么是对象
Java 万物皆对象
1.2对象里有什么?
1.2.1 从代码上看
属性:表示有什么
方法 : 表示能干什么
1.2.2 从jvm底层看
对象头
MarkWord:存储 :对象哈希码、GC 分代年、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等
synchronized 用的锁就是存在 Mark Word 中,在运行期间,Mark Word 里存储的数据会随着锁标志位的变化而变化,会有以下五种变化。注意无锁态和偏向锁的锁标志位相同,额外增加了一个字节来判断。在 32 位的 HotSpot 虚拟机 中对象未被锁定的状态下,Mark Word 的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits 用于存储对象分代年龄,2Bits 用于存储锁标志位,1Bit 固定为 0,表示非偏向锁。
Klass (数组对象的话还有一个length):用来确定该对象是哪个类的实例。
对象实际数据:存储对象的各种类型的字段内容(包括从父类继承的)
对齐填充:对齐数据不是必然存在的,只起占位符的作用,没有特别的含义
访问方式:
句柄
在Java 堆中将会划分出一块内存作为【句柄池】,Java栈中reference中存储的就是对应的句柄地址
句柄中包含了 :对象实例数据 和 类型数据各自的具体地址信息
直接指针
reference中存储的就是对象的地址
比较
直接指针最大的好处时:速度快,节省额一次指针定位的时间开销
句柄: reference中存储的时稳定句柄地址,在对象被移动(垃圾收集时移动对象是很普遍的行为)时只会改变句柄中的实例数据指针,reference不会改变
Hot Spot 使用的时直接指针方式
2.对象生命周期
2.1 存放在哪?
对象的实例(instantOopDesc)保存在堆上,对象的元数据(instantKlass)保存在方法区,对象的引用保存在栈上
2.2 生命周期
2.2.1 加载
1.通过一个类的全限定名来获取定义此类的二进制字节流
2.将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
2.2.2 验证
目的:为了保证class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
操作内容:
- 文件格式验证: 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
- 元数据验证:对字节码描述的信息镜像语义分析,以保证其描述的信息符合Java语言规范的要求。
- 字节码验证:(最复杂的一个阶段)通过数据流和控制流分析,确定程序语义是合法的。符合逻辑的。
- 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段-----解析阶段发生。符号引用验证可以看做对类自身以外(常量池中各种符号引用)的信息进行匹配性校验,确保解析动作能正常执行。
2.2.3 准备
正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的的内存都将在方法区中进行分配。
- 类变量指的是被static修饰的变量
- 类变量不包含实例变量,实例变量将会在对象实例化的时随着对象一起分配在Java堆中,令这里的初始值对于基本类型一般是0或false,引用类型是null。
2.2.4 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
-
符号引用: 用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用是能无歧义地定位到目标即可。符号引用于虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中
-
直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用于虚拟机实现的内存布局有关,同一个符号引用在不同虚拟机实例翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经在内存中存在。
-
解析动作主要针对
-
类或接口解析
-
字段解析
-
类方法解析
-
接口方法解析
-
方法类型
-
方法句柄
-
调用点限定符
-
2.2.5 初始化
-
类加载过程的最后一步,真正开始执行类中定义的Java程序代码(或者说字节码)
-
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者而已从另一个角度来表达:初始化阶段是执行类构造器< clinit >()方法的过程:
1)< clinit >()方法是有编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,编辑器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
2)< clinit >()方法与类的构造函数不同,它不需要显示地调用父类构造函数,虚拟机会保证在子类的< clinit >()方法执行之前,父类的< clinit >()方法已经执行完毕。因此在虚拟机中第一个被执行的< clinit >()方法的类肯定是java.lang.Object。
3) 由于父类的< clinit >()方法先执行,也就意味着弗雷中定义的静态语句块要优先于子类的变量赋值操作。
4) < clinit >()方法对于类或接口来说并不是必须的,如果一个来中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成< clinit >()方法。
5) 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口也有< clinit >()方法,但接口与类不同的是:执行接口< clinit >()方法时不需要父接口的< clinit >()方法。只有当父接口中定义的变量使用时,父接口才会初始化。
6) 虚拟机会保证一个类的< clinit >()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的< clinit >()方法,其他线程都是阻塞等待,直到活动线程执行< clinit >()方法完毕(其他线程进入后不会再次执行< clinit >()方法,同一个类加载器下,一个类型只会初始化一次)。
还有剩下的2个过程 使用以及卸载
2.3 类加载器
- 对于任意一个类,都需要有加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间
- 从Java虚拟机的角度来讲,只存在两种不同的类加载器:
- 一种是启动类加载器(Bootstrap ClassLoader),这个类加载器是使用C++语言实现,是虚拟机自身一部分;
- 另一种就是所有其他类的类加载器,这些都是有Java语言实现,独立于虚拟机外部,并且全部继承抽象类java.lang.ClassLoader。
从Java开发人员角度来看:
-
启动类加载器(Bootstrap ClassLoader)
-
扩展类加载器(Extension ClassLoader)
-
应用程序类加载器(Application ClassLoader)
-
有必要,还可以加入自己定义的类加载器
类加载器双亲委派模型
- 要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而都是使用组合关系来复用父加载器的代码
- 双亲委派模型并不是一个强制的约束模型,而是Java设计者推荐给开发者的类加载器实现,在Java中大部分类加载器都遵循这个模型,但也有例外:
- 双亲委派模型是在JDK1.2之后才被引入,为了向前兼容。在java.lang.ClassLoader添加了一个新的protected方法findClass(),而这个方法的唯一逻辑就是调用自己的loadClass()
- 由于模型的自身缺陷导致,双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类越有上层的加载器进行加载),基础类之所以被称为“基类”,是因为它们总是作为被用户代码调用的API,但当基础类调用用户的代码,则会出现失败的问题,所以引入线程上下文类加载器(Thread Context ClassLoader),这个类可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置。此类情况多用在JNDI、JDBC、JCE等
- 由于用户对程序动态性的追求(如代码热替换,模块热部署等)。
3.JVM 组成
3.1 运行时数据区域
运行时数据区从线程的角度来说,可以分为线程私有、线程共享两大部分:
-
线程共享
-
堆(GC 堆)
- 新生代
- ****Eden空间
- From Survivor 空间
- To Survivor 空间
- 老年代
物理上不连续,但逻辑上是连续的、存放对象的实例、其他参数 、可能抛出的异常: OOMError
- 新生代
-
方法区
- 存放被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等
- 运行时常量池
- 存放编译期生成的各种字面量和符号引用,在类加载后进入方法区的运行时常量池
-
-
线程私有
-
程序计数器
- 一块较小的内存空间 可以看作当前线程所执行的字节码的行号指示器
- 如果执行的是一个Native方法,则线程的这个计数器值是空
- 此区域是唯一一个Java虚拟机规范中没有任务OOM Error情况的区域
-
虚拟机栈
-
与线程的生命周期一致
-
每个方法在执行的同时会创建一个栈帧(动态连接、方法返回地址、一些额外的附加信息 称为 栈帧信息)
-
局部变量表
- 存放方法内定义的局部变量和方法的参数
- 容量是以slot【变量槽】为最小单位
-
操作数栈
- 后入先出的栈
- 在方法执行的过程中,或有各种字节码指令往操作数栈中写入或提取,即入栈和出栈
-
动态连接
- 每一个栈帧都持有一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中【动态链接】
-
方法返回地址
-
一些额外的附加信息
-
-
为了进行内存的共享,有可能多个栈帧之间会有 【重合区域 】
-
一个线程的方法调用链可能很长【导致OOM异常的原因 】,只有栈顶的栈帧才是有效的栈帧,与之关联的方法是当前方法
-
-
本地方法栈
- 与虚拟机栈类似,不过是与Native方法服务
- 可能抛出的异常: StackOverflowError OOMError
-
3.2 执行引擎
3.3 本地库接口
3.4 本地方法库
4.垃圾回收标准
4.1 对象已死? 如何确认
JVM 关于对象是否已死,有一些方法论:
-
引入计数法算法
- 为每个对象添加引用计数器,每当有一个引用该对象的时候,计数器加1
- 弊端: 当对象之间相互引用时,导致无法被回收
-
可达性分析算法
-
通过一系列称为“GC Roots”的对象作为起始点,当对象没有通往 GC Roots的引用链时,表示对象不可用
-
GCRoots的对象包括下面几种:
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中JNI(Native方法)引用的对象。
-
4.2 对象的引用状态
- 强引用
- 代码中普遍存在的类似"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用
- 描述有些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收
- Java中的类SoftReference表示软引用
- 弱引用
- 描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
- Java中的类WeakReference表示弱引用
- 虚引用
- 这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系
- Java中的类PhantomReference表示虚引用
4.3 对象死亡过程
注意:即使在可达性分析算法中不可达的对象,也不是“非死不可”,此时处于“缓刑”阶段
至少要经过两次标记过程:
- 如果对象在进行可达性分析时,没有可达GC Root的链接,那么将会被标记一次并且进行筛选
- 条件? 是否有必要执行 finalize方法
- 没必要执行
没有覆盖finalize 方法
finalize 方法已经被虚拟机调用过 - 有必要执行
1.将对象放置在F-Queue的队列中,稍后由虚拟机创建线程去执行
2.不承诺等待线程运行结束
3.finalize 是对象逃脱死亡的唯一 一次机会 finalize 方法中重新引用上新链接,则跳出标记队列
4.GC 进行再次标记
5.回收
- 没必要执行
另外 方法区也是可以回收的 但仅是可以
5.GC 算法
-
标记清除
标记需要回收的对象、清除被标记的对象、简单、直接
缺点:容易造成内存碎片
-
复制算法
将内存先一分为两、每次只用其中一块内存,用完后还存活的对象复制到另一块内存中
缺点:内存利用率低 -
标记-整理算法
- 先将已死对象标记,并清除,然后将还存活的对象都向一端移动,然后清理调边界意外的内存,保证了内存的连续性
-
分代算法
- 将存活对象分代
- 新生代
- 老年代
- 将存活对象分代
6.GC 算法实现细节
- 理论上通过GC 算法和垃圾回收标准 就可以进行正确的垃圾回收,不过还是有一些执行细节需要考量
- 由可达性分析引出GC Root,在实际生产中,GC Root 主要是在 < 全局的引用> 与< 执行上下文> 中,需要逐个检查会消耗很多时间
- 另外可达性分析 还对 GC 停顿敏感,某一时刻的可达性是需要从全局来确认,表示引用关系不断变化的话 是不能进行可达性分析的
这一点导致 GC 时需要 Stop The World 的重要原因
解决方案:精确式GC 直接通过OopMap的数据结构来达到目的
HotSpot 在“ 特定的位置”记录OopMap的信息,这些位置叫做《安全点》
- 程序并不是什么地方都能停下来,只有在安全的时才能暂停
- 安全点方案
1.抢先式中断:不需要线程的执行代码主动区配合,在GC发生时,首先把所有线程全部中断,如果由线程不在安全点,则让它“跑”到安全点 。 现在几乎没有使用这种方式了
2.主动式中断:
1.设置中断标记
2.线程执行时主动区轮询这个标志,为真时则自动挂起
3.轮询标志和安全点时重合的
安全区域
指的时在一段代码中,引用关系不会发生变化,在这个区域内任意地方开始GC 都是安全的
7.垃圾收集器
从上面的垃圾收集的方法论,那么下面就是垃圾收集的实际模型了,下面来一张基于1.7 JDK的垃圾收集器种类
从图中可以看出来:
7.1 新生代中使用的
- Serial(标记复制)
- ParNew (Serial 的多线程版)
- Parallel Scavenge(复制算法,多线程)被称为“吞吐量优先”收集器
- 关注于达到一个可控制的吞吐量(=运行用户代码时间 / (运行用户代码时间+垃圾收集时间))
- 其他收集器关注:缩短垃圾收集时用户线程的停顿时间
7.2 老年代中使用的
- Serial Old(标记-整理)
- ParNew Old
- Parallel Old 复制算法,多线程)
- CMS (Concurrent Mark Sweep) 是一种获取最短回收停顿时间为目标的收集器
下面主要详细的看看CMS、G1 两种垃圾收集器的一些细节
7.3 CMS (Concurrent Mark Sweep)
注意CMS 使用的使用标记-清除算法,它的实现主要是有四个步骤。
7.3.1 四个步骤
- 初始标记
- 需要“Stop The World”
- 仅只是标记一下GC Roots 能直接关联到的对象
- 并发标记
- 进行GC Root Tracing 的过程
- 重新标记
- 仍需要“Stop The World”
- 修正并发标记期间因用户程序运作而导致标记记录产生变动的那一部分对象的标记记录
- 并发清除
从上面的四个过程中可以发现初始标记、重新标记仍然需要“Stop The World”,初始标记仅是标记一下GC Roots n能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing 的过程,而重新标记阶段是为了修正那些之前已经被标记的对象,不能处理新产生的垃圾对象。
所以CMS也有一些缺点:
- 对CPU资源非常敏感,并发时会占用一部分CPU资源
- CMS 默认启动的回收线程数:(CPU数量 +3)/ 4,在4CPU以上时,并发回收垃圾收集线程不少于25%,并随着CPU的数量增加而下降,当不足4个(如2个),CMS对用户线程影响可能变得很大。
- 无法处理浮游垃圾 “Floating Garbage”
- 指的是在CMS 并发清理阶段用户线程还在运行着,并伴随产生的新的垃圾,这部分的垃圾并未被标记,所以不会被处理
- 针对上面的现象,JVM 提供一个参数-XX:CMSInitiatingOccupancyFraction,内存使用率到达该值就会触发GC ,以便降低内存回收的次数
- 可能导致“Concurrent Mode Failure” ,JVM 会启动后备方案:临时启动Serial Old收集器,停顿时间会很长
- 标记-清除方法会导致内存碎片化
- 默认提供一个参数:-XX:+UseCMSCompactAtFullCollection (默认开启),用于内存碎片的合并整理
- -XX:CMSFullGCsBeforeCompaction ( 默认值0) 执行多少次不压缩的Full GC后,跟着来一次带压缩的GC
7.4 G1
G1是一款面向服务端应用的垃圾收集器。其具备以下的特点:
-
并行与并发
- 充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩减Stop-The-World的停顿时间,部分收集器需要停顿Java线程执行的GGC动作,G1 仍可以通过并发的方式让Java程序继续执行
-
分代收集
- 分代概念在G1中依然保留,虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不太的方式去处理新建的对象和已经存活了一段时间、熬过多次GC的就对象以获取更好的收集效果
-
空间整合
- G1整体上来看是基于“标记-整理”算法实现的
- 从局部(两个Region之间)上来看是基于“复制”算法实现的
- 综上G1不会产生内存空间碎片
-
可预测的停顿
- 降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒
之前的收集器的范围都是整个新生代或者整个老年代,而G1不再是这样。G1收集器使用是,Java堆的内存布局就与其他收集器有很大的差别。
它将整个Java堆划分为多个大小相等的独立区域(Region),虽然留有新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。通过这种方式,G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这就是Garbage-First名称的由来),其大致可划分以下几个步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
7.5 扩展 ZGC
ZGC全称是Z Garbage Collector,是一款可伸缩(scalable)的低延迟(low latency garbage)、并发(concurrent)垃圾回收器,旨在实现以下几个目标:
- 停顿时间不超过10ms
- 停顿时间不随heap大小或存活对象大小增大而增大
- 可以处理从几百兆到几T的内存大小
-server -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xlog:age*,gc*=info:file=gc-%t.log:time,tid,tags:filecount=3,filesize=20m -Djava.io.tmpdir=/tmp'
ZGC为什么可以这么优秀,主要是因为以下几个特性。
- Concurrent
- ZGC只有短暂的STW,大部分的过程都是和应用线程并发执行,比如最耗时的并发标记和并发移动过程。
- Region-based
- ZGC中没有新生代和老年代的概念,只有一块一块的内存区域page,以page单位进行对象的分配和回收。
- Compacting
- 每次进行GC时,都会对page进行压缩操作,所以完全避免了CMS算法中的碎片化问题。
- NUMA-aware
- 现在多CPU插槽的服务器都是Numa架构,比如两颗CPU插槽(24核),64G内存的服务器,那其中一颗CPU上的12个核,访问从属于它的32G本地内存,要比访问另外32G远端内存要快得多。
- Using colored pointers
- 和以往的标记算法比较不同,CMS和G1会在对象的对象头进行标记,而ZGC是标记对象的指针。
- 其中低42位对象的地址,42-45位用来做指标标记。
- Using load barriers
- 因为在标记和移动过程中,GC线程和应用线程是并发执行的,所以存在这种情况:对象A内部的引用所指的对象B在标记或者移动状态,为了保证应用线程拿到的B对象是对的,那么在读取B的指针时会经过一个 “load barriers” 读屏障,这个屏障可以保证在执行GC时,数据读取的正确性。
ZGC默认支持NUMA架构,在创建对象时,根据当前线程在哪个CPU执行,优先在靠近这个CPU的内存进行分配,这样可以显著的提高性能,在SPEC JBB 2005 基准测试里获得40%的提升。
ZGC默认支持NUMA架构,在创建对象时,根据当前线程在哪个CPU执行,优先在靠近这个CPU的内存进行分配,这样可以显著的提高性能,在SPEC JBB 2005 基准测试里获得40%的提升。
ZGC目前只在Linux/x64上可用,如果有足够的需求,将来可能会增加对其他平台的支持。