前言
网上的资料比较杂,且少有成体系的资料,因此收集资料并按照自己的理解做了一下整合,本文为自我理解与总结,不代表标准答案。
从hello world开始
java从编写’System.out.println(“hello world”)’ 开始,并编译运行,在这期间到底发生了什么。
.java文件从编译到结束全过程(类加载过程)
一个.java文件的完整周期是 java编码成jvm可识别的.class文件, 然后大致流程是: 加载–>连接–>初始化–>使用–>卸载
1. 加载过程
1、通过全类名获取定义此类的二进制字节流(类似反射)
2、将字节流所代表的静态存储结构转换为JVM方法区的运行时数据结构
3、在内存中生成一个代表该类的 java.lang.Class 对象,作为方法区这些数据的访问入口(这样代码才能通过全类名访问到该对象)
PS:数组类型不通过类加载器创建,它由JAVA虚拟机直接创建,数组的加载可通过自定义类加载器控制加载方式
1.1 加载过程中所需要的类加载器
JVM中有三个类加载器,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
1、BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。
2、ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
3、AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
1.2 类加载器的模型设计(双亲委派模型)
每一个类都有一个对应它的类加载器,在加载类时系统会先判断该类是否已经被加载过,如加载过则直接返回,否则才尝试加载,加载时会首先调用父类的loadClass()方法,当父类加载器无法处理时,才由自己来处理。
举例: 自定义加载器(判断该类是否加载过,未加载过,委托上级处理) –> AppClassLoader(应用程序类加载器,同样判断是否加载过,未加载则委托上级处理) —> ExtensionClassLoader –> BootstrapClassLoader
整个过程就是依次向上传递,(如上级无法处理)再依次向下传递
好处:
1、双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类)
2、保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类(即系统无法区分是jdk自带的Object对象还是用户自建的Object对象)。
自定义类加载器:继承 ClassLoader
2. 类加载验证过程
1、文件格式验证,验证是否符合class文件规范,版本号是否支持,常量池中常量是否有不支持的类型
2、元数据验证,对字节码语义进行分析,以保存其符合java语言规范要求,例如:此类是否有父类(java.langObject除外),是否继承了被final 修饰的类等等。
3、字节码验证,比较复杂的一个阶段,会对类的方法体逻辑进行验证,例如类型转换是否有效,字节码指令跳转异常等。
4、符号引用验证,验证符号引用等否找到对应类,类权限是否能被引用等。
总结:验证编译后的代码是否合法,是否能正常运行
3. 类加载准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,* 这个时候进行内存分配的仅包括类变量(静态变量) * ,这里初始化变量值是数据类型的零值,不是实际值,如果是final修饰,则直接赋予实际值
4. 解析阶段
解析阶段时将java虚拟机中变量池中的符号引用替换为直接引用的过程。简单来说就是把类名限定转换为实际内存中的Class对象引用。
JAVA虚拟机为每个类都准备了一张方法表来存储方法的地址,当需要调用某个类时只需要知道这个方法的位置即可,通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置
总结:java通过全类名获得类对象,通过解析操作
5. 初始化阶段
初始化阶段是执行初始化方法
1、初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
2、只有主动去使用类才会初始化类(也就是NEW对象的时候)
3、虚拟机启动时会先初始化Main方法这个类
4、……
6. 内存如何分配
当类被加载完毕后如何分配内存?
虚拟机执行到new对象指令时才开始加载类并分配内存,其流程为:类加载检查 –> 判断是否已加载类(未加载类,则先加载类) –> 已加载类,开始分配内存 –> 执行初始化 –> 设置对象头 –> 执行init方法。
6.1. 类加载检查
当主动使用一个类(new 一个对象)时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过(也就是检查类是否已经被加载过)。如果没有,那必须先执行相应的类加载流程。
6.2. 开始分配内存
如果未被加载过,则在类加载检查通过后,接下来虚拟机将为新生对象分配内存,分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
指针碰撞 :
- 适用场合 :堆内存规整(即没有内存碎片)的情况下。
- 原理 :用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
- 使用该分配方式的 GC 收集器:Serial, ParNew
空闲列表 :
- 适用场合 : 堆内存不规整的情况下。
- 原理 :虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
- 使用该分配方式的 GC 收集器:CMS
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是”标记-清除”,还是”标记-整理”(也称作”标记-压缩”),值得注意的是,复制算法内存也是规整的。
PS:默认对象分配在Eden区,大对象直接分配在老年代
内存分配并发问题
虚拟机采用两种方式来保证线程安全:
- CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB: 为每一个线程预先在 Eden 区分配一块内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
也就是说每个线程都会在堆的Eden区预先分配一块内存给对象使用,当不够用时再使用乐观锁竞争分配来保证操作的原子性。
6.3. 初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
6.4. 设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
6.5. 执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,
6.6. 对象的访问定位
建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有:使用句柄、直接指针。
句柄
如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。直接指针
如果使用直接指针访问,reference 中存储的直接就是对象的地址。
简单来说就是当我们要使用一个类时JVM会通过这两种方式获取到对象,使用句柄的好处是中间隔了一层,对象被移动时只会改变句柄中的实例数据指针,而直接指针的优势就是速度快。
6.7. 逃逸分析
一般情况下new出来的对象都是分配在堆上的,但在满足一定的条件,会先分配在栈上。
我们知道当堆空间不足时会触发FULL GC,而FULL GC会严重影响性能。
有些对象其实是临时产生的,只在某个方法中使用,如果可以将这些对象随着栈的弹出而一同被销毁,那么就减轻了垃圾回收的压力。
为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象会不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存。随栈帧出栈而销毁,减轻GC的压力。
什么是逃逸分析
当创建的对象只在方法中使用,不被其它类所使用时则属于逃逸对象。如下代码:
1 | public class Test { |
test1由于该对象返回了引用地址,有被外部使用,因此不属性逃逸对象,而test里的User并没有被其它任何地方使用,因此属于逃逸对象,可以将基分配在栈中。
如何开启逃逸分析
JDK7之后默认开启逃逸分析
1 | -XX:+DoEscapeAnalysis 开启逃逸分析 |
6.8. 标量替换
如果一个对象通过逃逸分析能过确定他可以在栈上分配,但是我们知道一个线程栈的空间默认也就1M,栈帧空间就更小了。而对象分配需要一块连续的空间,经过计算如果这个对象可以放在栈帧上,但是栈帧的空间不是连续的,
对于一个对象来说,这样是不行的,因为对象需要一块连续的空间。那怎么办呢?这时JVM做了一个优化,即便在栈帧中没有一块连续的空间方法下这个对象,他也能够通过其他的方式,让这个对象放到栈帧里面去,这个办法就是标量替换。
标量替换不是将整个User对象放到栈帧中,而是将User中的成员变量拿出来分别放在每一块空闲空间中。这种不是放一个完整的对象,而是将对象打散成一个个的成员变量放到栈帧上,当然会有一个地方标识这个属性是属于那个对象的,这就是标量替换.
1 | -XX:+EliminateAllocations 开启标量替换 |
JDK7之后默认开启。
6.9. 标量替换与聚合量
标量替换与聚合量是逃逸分析中对变量或对象的定义。
JAVA的基本类型就属于标量,如:int,long等基本类型,对象属于可进一步分解的量,因此属于聚合量
7. 卸载
卸载类需要满足 3 个要求:
1、该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
2、该类没有在其他任何地方被引用
3、该类的类加载器的实例已被 GC
7.1. 什么情况下对象会被GC(垃圾回收)
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
判断对象死亡的算法有两种:引用计数法和可达性分析算法。
- 引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 ,当引用失效,计数器就减 1,任何时候计数器为 0 的对象就是不可能再被使用的。
- 可达性分析算法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
哪些对象可以作为 GC Roots 呢?
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈(Native 方法)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
所有被同步锁持有的对象
对象可以被回收,就代表一定会被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
判断对象死亡的算法总结
引用计数法通过简单的计数判断,可达性分析算法通过引用链判断对象,不可达对象不一定会被回收,该对象会放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
垃圾回收中的引用类型
无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。
- 强引用(StrongReference)
代码中直接new 出来的对象就属于强引用,JVM GC不会回收它,当内存空间不足的时候,java虚拟机宁可抛出OOM异常,也不会回收具有强引用的对象来释放内存。
- 软引用(SoftReference)
如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
定义一个软引用对象:
1 | Object o=new Object(); |
- 弱引用(WeakReference)
无论内存是否足够,只要发生GC 弱引用的对象一定被回收.
1 | Object o=new Object(); |
- 虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
虚引用与引用队列
1 | Object o=new Object(); |
7.2. 垃圾收集算法
标记-清除算法
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
效率问题
空间问题(标记清除后会产生大量不连续的碎片)
标记-复制算法
为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
复制算法
标记-整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
标记-整理算法
分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
7.3. 空间分配担保机制
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,
如果大于,则此次Minor GC是安全的
如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。
7.4. 垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
JVM有许多垃圾收集器,每种收集器都对应了不同的使用场景,收集器列表如下:
- Serial 收集器
- ParNew 收集器
- Parallel Scavenge 收集器
- Serial Old 收集器
- Parallel Old 收集器
- CMS 收集器
- G1 收集器
- ZGC 收集器
下面是网上收集的各个垃圾收集器的具体介绍:
Serial 收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
新生代采用标记-复制算法,老年代采用标记-整理算法。
Serial 收集器
虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。
ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
新生代采用标记-复制算法,老年代采用标记-整理算法。
ParNew 收集器
它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
并行和并发概念补充:
并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。
Parallel Scavenge 收集器
Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。 那么它有什么特别之处呢?
-XX:+UseParallelGC
使用 Parallel 收集器+ 老年代串行
-XX:+UseParallelOldGC
使用 Parallel 收集器+ 老年代并行
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用标记-复制算法,老年代采用标记-整理算法。
Parallel Scavenge 收集器
这是 JDK1.8 默认收集器
使用 java -XX:+PrintCommandLineFlags -version 命令查看
-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version “1.8.0_211”
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能
Serial Old 收集器
Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
CMS 垃圾收集器
从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
对 CPU 资源敏感;
无法处理浮动垃圾;
它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
G1 收集器
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点:
并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
G1 收集器的运作大致分为以下几个步骤:
初始标记
并发标记
最终标记
筛选回收
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
ZGC 收集器
与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。
在 ZGC 中出现 Stop The World 的情况会更少!
总结
总过程如下:
程序员编写JAVA代码,保存为.java文件 –> JDK再将java文件编译成.class文件 –> 虚拟机加载.class文件 –> JVM执行类加载器(参考上面的类加载过程,new一个对象时触发) –> 类加载完后会开始分配内存(Main方法类会在虚拟机启动时就初始化) –>
对象内存分配完后执行init方法(执行构造方法) –> 但对象未被引用时可能会被回收(取决于回收算法) –> 卸载(前提是满足条件)。
- 本文作者: reiner
- 本文链接: https://reiner.host/posts/d137c087.html
- 版权声明: 转载请注明出处,并附上原文链接