JVM学习之内存管理

2023年 8月 5日 32.9k 0

个人Blog地址: www.re1ife.top/index.php/a…
欢迎大家光临

1. 运行时数据区域

Jvm 在执行Java程序时,会把它管理的内存划分为若干不同的区域。各有分工、

1.1 程序计数器

程序计数器(PCR) : 一块较小的内存空间,可以看作当前线程所执行字节码的行号指示器。

JVM概念模型中,字节码解释器会改变计数器选区下条需要执行的字节码指令。

JVM的多线程是由线程轮流切换、分配处理器执行时间实现的,一个处理器(一个内核)在同一时刻只执行一条指令。

切换后为了恢复到原来的位置,因此每个线程都有一个独立的PCR. 线程私有内存

计数器实际记录的是正在执行的虚拟机字节码指令的地址。若执行本地方法, 计数器为空。

本地方法: 用非Java语言(如C、C++等)实现的方法,也称为本地代码(Native Code)

1.2 Java 虚拟机栈

VM Stack:也是线程私有的。生命周期与线程相同。

虚拟机栈描述了Java方法执行的线程内存模型:

每个方法被执行时,Java 虚拟机会同步创建一个栈帧(Stack Frame)存储局部变量表、操作栈、动态连接、方法出口等。每方法执行完毕对应一个栈帧在VM Stack从入栈到出栈的过程。

  • 栈帧: 方法运行时期重要的数据结构

  • 局部变量表:存放编译器各种Java 基本数据类型、对象引用(Reference) .

  • 数据类型以局部变量槽(Slot)存储在局部变量表中

  • long 与 double 占据两个变量槽,其他的类型占用一个

  • 局部变量表的内存空间在编译期间就分配好了

  • 大小是指slot的数量的多少,具体 64bit 或 32bit由虚拟机实现

  • 注:基本类型的数组不再局部变量表,在Java堆中。

  • 两类异常:

  • StackOverflowError: 线程请求栈深度大于虚拟机允许深度
  • OutOfMemeoryError: 若JVM Stack 容量可动态扩展,而栈扩展无法申请到足够内存
  • HotSpot 虚拟机栈容量不可变,Classic 虚拟机可扩展

1.3 本地方法栈

本地方法栈与虚拟机栈作用相似。

区别:

  • VM Stack:为虚拟机执行Java方法服务

  • NativeMethod Stack:为虚拟机使用的本地方法服务。

有些VM 如 HotSpot将本地方法栈和VM栈合二为一。

1.4 Java Heap

Java Heap: 虚拟机管理的内存最大的一块,用来存放对象实例,“几乎”所有对象实例在这里分配内存。

在《Java 虚拟机规范》描述了:所有的的对象实例及数组都应该在堆上分配。

  • 但是随着JIT技术进步以及逃逸分析技术的日益强大,栈上分配、标量替换导致也不是这么绝对了。

  • 栈上分配:一种内存分配技术,它将对象的内存分配在线程栈上,而不是在堆上。这种技术可以减轻Java堆的负担,同时也可以提高程序的性能。栈上分配的前提条件是对象的生命周期非常短暂,即对象在方法内被创建并在方法结束时被销毁。由于这种技术可以避免频繁地访问堆,从而减少了内存访问的开销,因此可以提高程序的性能。

  • 标量替换:一种优化技术,它将一个对象拆分成多个基本类型的属性,并将这些属性分配在栈上或寄存器中,而不是在堆上。

  • 逃逸分析:一种静态分析技术,它可以分析程序中对象的生命周期,判断对象是否会被外部引用,即“逃逸”出当前方法或线程。如果对象不会逃逸,那么可以将对象分配在栈上或寄存器中。

Java 堆是被所有线程共享的一块内存区域,虚拟机启动时创建。也是垃圾收集器管理的内存区域,因此也叫GC堆

Tips:现代垃圾收集器大部分基于分代收集理论,会出现 新生代老年代永久代Eden空间等,仅仅是部分垃圾收集器的共同特性与设计风格。

从内存分配角度看,所有线程共享的Java堆中可以划分多个线程私有的分配缓冲区(Thread Local Allocation Buffer)来提升效率。

但是不会改变堆中存储的内容---对象实例。

Java 堆逻辑上是连续而物理上不连续的。同时还可以被实现为固定大小、可扩展的。 通过 -Xmx-Xms设定。

异常: 内存中没有空间来进行对象实例的分配会抛出OutOfMemeoryError.

1.5 方法区

方法区:线程共享区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

《Java虚拟机规范》将其描述为堆一个了了逻辑部分。

JDK 8 以前,Java 程序员习惯在HotSpot上开发,很多人都更愿意把方法区称呼为“永久代”,但其实并不等价。

当时的HotSpot虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已

1.6 运行时常量池

运行时常量池: 方法区的一部分。

常量池表: 位于Class文件信息里,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

2. HotSpot 虚拟机对象

Java 是一门面向对象的语言,时时刻刻都会有对象被创建。

2.1 对象创建

第一步

当 Java 虚拟机遇到一条字节码 new

  • 首先检查指令参数是否在常量池中定位一个类的符号引用,检查引用类是否被加载、解析与初始化。否则执行类加载过程
  • 类加载之后,虚拟机为新生对象分配内存,对象需要的内存大小,在类加载完成之后可以确定。
    • 如何分配?
      • 指针碰撞(BumpThePointer):堆中的内存都是绝对规整,所有被用过的内存放在一边,用一个指针用来作为分界指示点。分配多少空间,移动相同纪律。
      • 空闲列表(FreeList):堆中内存不规整,在分配的时候从列表中找一块足够的空间,然后更新列表。
  • JVM 分配方式由Java堆是否规整决定 -> 由采用的垃圾收集器是否有空间压缩整理(Compact) 能力决定。

    • Serial、ParNew等带压缩整理过程收集器使用指针碰撞。
    • CMS这种基于清除算法收集器,采用空闲列表的算法。

    除了如何分配内存之外,如何在频繁创建对象下,保证指针的的移动的并发安全?

  • 分配内存动作同步处理:虚拟机采用 CAS+失败重试 保证更新 的原子性。
  • 按线程划分不同空间:每个线程在Java堆中预先分配一小块内存——本地线程分配缓冲。本地线程缓冲区使用完之后同步锁定。
  • 内存分配完成之后,虚拟机会将分配的内存初始化为0。

    第二步

    JVM会对对象进行一些设置比如:对象HashCode(调用时生成)、实例属于哪一个类、如何才能找到类的元数据信息以及对象GC分代年龄等信息。

    以上的信息都存储在对象头中,从之前的 JUC学习中我们知道,对于锁的状态(偏向锁、重向锁)等。

    完成以上步骤,开始进入到构造函数。所有字段此时都默认为0,new 执行之后执行 () 方法,按照程序编写的逻辑初始化。

    2.2 对象内存布局

    对象在堆内存中有三个部分: 对象头(Header)、实例数据(InstanceData)、对齐填充(Padding).

    JVM.jpg

    Java 中的对象头可以分为两类:普通对象与数组。

    普通对象的对象头:

    |--------------------------------------------------------------|
    |                     Object Header (64 bits)                  |
    |------------------------------------|-------------------------|
    |        Mark Word (32 bits)         |    Klass Word (32 bits) |
    |------------------------------------|-------------------------|
    
    

    数组的对象头:

    |---------------------------------------------------------------------------------|
    |                                 Object Header (96 bits)                         |
    |--------------------------------|-----------------------|------------------------|
    |        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
    |--------------------------------|-----------------------|------------------------|
    

    两者相同的部分是:Mark Workd, Klass Word。

    Klass Word 更多是与类相关的数据,类是静态的。而MarkWord则是对象运行时动态的数据,并且在32Bit与64Bit下的是不同的。

    |-------------------------------------------------------|--------------------|
    |                  Mark Word (32 bits)                  |       State        |
    |-------------------------------------------------------|--------------------|
    | identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
    |-------------------------------------------------------|--------------------|
    |  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
    |-------------------------------------------------------|--------------------|
    |               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
    |-------------------------------------------------------|--------------------|
    |               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
    |-------------------------------------------------------|--------------------|
    |                                              | lock:2 |    Marked for GC   |
    |-------------------------------------------------------|--------------------|
    
    
    
    |------------------------------------------------------------------------------|--------------------|
    |                                  Mark Word (64 bits)                         |       State        |
    |------------------------------------------------------------------------------|--------------------|
    | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |
    |------------------------------------------------------------------------------|--------------------|
    | thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |
    |------------------------------------------------------------------------------|--------------------|
    |                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |
    |------------------------------------------------------------------------------|--------------------|
    |                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |
    |------------------------------------------------------------------------------|--------------------|
    |                                                                     | lock:2 |    Marked for GC   |
    |------------------------------------------------------------------------------|--------------------|
    

    里面存放了 identity_hashcode: 就是对象hash值、age对象的年龄,biased_lock 与 lock共同组成了对于这个对象的锁的状态的描述。

    下面这张图是上面的翻译版本。由于可用的空间比较细,但是需要表示的内容变多了。因此MarkWord会在复用空间。

    image.png

    具体锁相关的只是可以去看看JUC部分的知识。

    • 相关字段的解释:
      • lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。
      • biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
      • age4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
      • identity_hashcode25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
      • thread:持有偏向锁的线程ID
      • epoch:偏向时间戳。
      • ptr_to_lock_record:指向栈中锁记录的指针
      • ptr_to_heavyweight_monitor:指向管程Monitor的指针。

    然后的实例数据部分,才是真正对于我们来说有效的数据,所有字段都记录了起来,存储顺序首到虚拟机分配策略和Java内部的设定有关。

    分配顺序:longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs)

    第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。HotSpot虚拟机自动内存管理系统要求对象的其实地址必须是8字节。

    学过《计算机系统基础》的,是比较明白我说的意思的。

    2.3 对象的访问定位

    创建了对象之后如何使用?

    Java 程序会通过栈上的 reference 数据来操作堆上的对象。

    • refernce: 一个指向对象的应用。

    目前主流的访问方式:

    • 句柄方式:Java堆可能会划分一块内存作为句柄池,refernce中存储的是句柄地址,句柄中包含对象实例数据、类型数据各自的地址

      • 如图
        image.png
    • 直接指针方式:只需要访问一次,不需要间接访问一次句柄池,直接根据reference中的内存地址访问对象。但是放置对象更加困难。

    image.png
    快是真的快!因为Java中会经常访问到对象,多一次访问综合起来还是会有不小的开销的。

    3. GC与内存分配策略

    垃圾回收器(Garbage Collection): 用来对内存动态分配和回收的。

    Java 程序会频繁创建对象,有些对象生命周期结束之后,GC为了复用空间会♻️回收对象。

    因此我们应该如何判断对象已经死亡了呢?

    3.1 对象的去世流程

    首先死亡第一步要在医学上检验这个是不是真的去世了。

    去世检验

    目前主流的去世判断方法主要有下面两种:

  • 引用计数法(JVM基本没用)
    • 在对象中添加引用计数器,有人引用就+1,失效-1. 当计数器为0时就可以回收了。
    • 优点: 原理简单、判定效率高(只需判断是否为0)
    • 缺点:
    • 占用额外空间
    • 其他情况待考虑
    • ...
  • [!INFO]
    引用计数法不能很好的解决,对象之间的相互依赖。

    我们举个小例子:

    class A{
    	private class B;
    }
    
    class B{
    	private class A;
    }
    

    如果存在 a、b对象,两者会相互依赖,引用计数器就无法为0。

  • 可达性分析法
    是常用的方法
    把所有对象建成一个树的结构,根对象为GC Root. 根据之间引用的关系从上往下遍历,如果不能访问到的对象则可以被回收。
  • 如下图所示:

    GCRootSetPic

    可作为根节点的条件:

    • 位于虚拟机栈的栈帧中的本地变量表中所引用到的对象(其实就是我们方法中的局部变量)同样也包括本地方法栈中JNI引用的对象。
    • 类的静态成员变量引用的对象。
    • 方法区中,常量池里面引用的对象,比如我们之前提到的String类型对象。
    • 被添加了锁的对象(比如synchronized关键字)
    • 虚拟机内部需要用到的对象。

    示意图如下

    一旦已经存在的根节点不满足存在的条件时,那么根节点与对象之间的连接将被断开。就是GC Roots和对象1之间断开。

    也能较好的解决相互依赖的问题:

    最终核验

    死亡是一件大事,上面的检验完成之后。并不是立刻马上就入殡了。
    JVM规定了至少检验两次之后,才能入殡防止诈尸。

    第一次被标记之后,会进行一次筛选是否有必要执行 finalize() 方法。

    • 对象没有覆盖 finalize()
    • finalize() 已经被虚拟机调用
      上述两种情况都没必要执行。

    如果有必要执行,JVM会将其放到一个 F-Queue ,在一个虚拟机建立的、低调度优先级的Finalizer线程执行finalize()。

    对象可以覆盖finalize方法来抢救一下,但是只能抢救一次。因为finalize()不会执行第二遍。

    以下行为不推荐
    拯救对象:

    public class test{
    	public static test SAVE_HOOK = null;
    	
    	public void isAlive() {  
    		System.out.println("yes, i am still alive :)");
    	}
    
    	@Override  
    	protected void finalize() throws Throwable {
    	
    		super.finalize();  
    		System.out.println("finalize method executed!"); 
    		test.SAVE_HOOK = this;
    		
    	}
    
    	public static void main(String []args) throws Throwable { 
    		SAVE_HOOK = new test();
    
    		//自救
    		SAVE_HOOK = null;  
    		System.gc();  
    		// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它 Thread.sleep(500);
    
    		if (SAVE_HOOK != null) { 
    			SAVE_HOOK.isAlive();
    		} else {  
    			System.out.println("no, i am dead :(");
    		}
    	}
    }
    

    执行结果:

    finalize method executed! 
    yes, i am still alive :)
    

    有关方法区的回收

    部分人认为方法区没有垃圾收集行为,实际上Java虚拟机规范也不要求虚拟机在方法区实现。

    通常来说,对于方法区的回收其实收益是比较小的相较于堆中的回收。

    [!INFO]
    堆中的新生代回收率可以达到 70%-99%

    所以方法区的回收有点吃力不讨好。

    3.2 垃圾收集算法

    各个平台虚拟机内存操作方法各异。

    分代收集理论

  • 弱分代假说: 绝大多数对象都是朝生夕灭的。
  • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
  • 上面两个假说奠定多款常用GC的设计原则:将Java堆的划分出不同的区域,然后将回收对象熬过GC收集次数(Age) 分配到不同的区域。
    比如这个区域都是朝生夕灭,可以较低代价回收大量空间。若是难以消亡的对象,那把它们集中放在一块, 虚拟机用较低的频率来回收这个区域,同时兼顾了垃圾收集的时间开销和内存的空间。

    根据这个由此发展出了:标记——复制算法,标记——清楚算法,标记——整理算法等。

    上面两种假说可以分别对应:新生代(Young Generation)、老年代(Old Generation)。但是后面的GC并没有遵循这一设计,因为对象之间可能跨代引用。

    例如新生代对象被老年代引用,若要找出存活对象,不得不固定的GC Roots之外,还要遍历整个老年代中所有对象来保证准确,会增加负担。

    因此增加了一个假说:

  • 跨代引用假说: 跨代引用相对于同代引用占极少数
  • 因此若存在跨代引用,应该是同时消亡或生存。

    如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了。

    因此不需要去扫整个老年代,只需在新生代上建立一个记忆集,将老年代划分为若干块,表示哪儿存在跨代,只有包含跨代引用的小块里会被加入GC Roots扫描。

    • Partial GC:不完整Java堆垃圾收集
      • 新生代收集(Minor GC/Youn GC): 新生代垃圾收集
      • 老年代收集(Major GC/Old Gc)
      • 混合收集(Mixed GC)
    • Full GC

    具体收集算法

  • 标记清除
  • 这是最基础的垃圾收集算法。
    首先表基础所有需要回收的对象,标记完成之后统一回收对象。或者相反,统一收回未标记对象。

    但是这个缺点有点明显:
    a. 效率不稳定,大量的对象需要回收的话,会频繁标记和清除
    b. 内存空间碎片话,标记清除之后产生大量不连续内存碎片

  • 标记-复制
  • Fenichel提出了半区复制 的垃圾收集算法。

    将内存按容量分为大小相等的两块,只使用其中一块。若一块用完了,将还存活的对象复制到另外一块,然后将已使用的一次清楚。

    如果都是存活的话,会有很大的内存复制开销,但是多数对象是可回收的。只需按顺序移动堆顶指针来分配内存。

    目前大多数JVM优先使用此收集算法,IBM研究发现 新生代中98%对象熬不过第一次收集,因此没必要1:1划分新生代内存空间。

    因此针对此现象,将新生代分为一块较大的Eden空间与两块较小Survivor空间。
    每次分配内存时只使用Eden和其中一块Survivor.

    HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1

  • 标记—整理
    在老年代,对象存活率高,因此标记复制方法,会进行较多次复制操作。
  • 若不想浪费50%空间,就要有额外空间担保。

    image.png

    标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。

    经典垃圾收集器

    image.png

    上图中收集器处于不同分代,若俩之间有连线则可以搭配使用。

  • Serial 收集器
    Serial收集器是最基础、历史最悠久的收集器。
  • 这是一个单线程收集器,在收集时会暂停其他所有工作线程直到结束。

    image.png

    这其实体验很不好,就好比没过几分钟你电脑就卡死一下然后又正常。

    虽然这很不好,并且HotSpot虚拟机开发团队一直在努力构思优秀的垃圾收集器,但是它仍然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器。

    因为它非常的简单和高效。

  • ParNew 收集器
  • 其实就是Seriral收集器的多线程版本,但是在收集时仍然要停止所有的用户线程。

    image.png

    在JDK7 之前的遗留系统是首选的新生代收集器。

    在JDK5 时发布了CMS 收集器,这是第一款真正意义上并发的垃圾收集器,支持GC线程与用户线程同时工作。但是却无法与 JDK1.4中的Parallel Scavenge收集器配合。因此只能选择ParNewSerial

    ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果。

  • Parallel Scavenge/Old
  • 一款基于标记-复制支持并行收集的新生代收集器。

    PS收集器目标是达到一个可控制吞吐量。

    $$吞吐量 = frac{运行用户代码时间}{运行用户代码时间 + 运行垃圾收集时间}$$
    

    而 CMS等收集器是注重尽可能缩短垃圾收集时用户线程停顿时间。

    在JDK6时也推出了其老年代收集器Parallel Old,采用标记整理算法实现:

    image-20230306165555265

    与ParNew收集器不同的是,它会自动衡量一个吞吐量,并根据吞吐量来决定每次垃圾回收的时间,这种自适应机制,能够很好地权衡当前机器的性能,根据性能选择最优方案。

    目前JDK8采用的就是这种 Parallel Scavenge + Parallel Old 的垃圾回收方案。
    4. Serial Old收集器
    这是Serial的老年代版本,同样也是标记-整理算法。

    image.png

    主要意义也是供客户端模式下的HotSpot虚拟机使用。
    在服务端下也可以作为CMS失败后的后备收集器。

  • CMS 收集器
  • CMS收集器一种以获取最短回收停顿时间为目标的收集器。

    大量的Java应用集中在互联网网站服务端上,因此希望系统停顿尽量的短。

    CMS收集过程有四个步骤:

  • 初始标记(CMS initial mark)
    • 标记 GC Roots 可以直接关联到的对象,速度较快
  • 并发标记(CMS concurrent mark)
    • 从GC Roots直接关联的对象遍历整个对象图
    • 耗时较长无需暂停用户线程
  • 重新标记(CMS remark)
    • 为了修正并发标记期间,因为用户线程继续运作的导致的标记改动部分
    • 比初始标记时间长,比并发标记时间短
  • 并发清除(CMS concurrent)
    • 清除标记的死亡对象,不需要移动存活的对象
  • 运行示意图:
    image.png

    CMS的缺点:

    • 对处理器资源非常敏感,并发阶段虽然不会暂停用户线程,但是会占用部分线程导致应用变慢。
      • 默认启动的回收线程数:(处理器核心数量+3)/4
      • 当核心数 < 4时影响较大
    • 无法处理“浮动垃圾”,标记中用户线程仍然会产生新的垃圾对象, 只能等待下次标记。
    • 会产生大量的碎片空间,需要 -XX:+UseCMS-CompactAtFullCollection 来触发Full GC时合并碎片空间。
  • Garbage First收集器
  • [!INFO]
    垃圾收集器技术发展历史上的里程碑式的成果

    JDK7 Update 4 是G1第一个商用版本,到了JDK8 Update 40 G1 提供了并发的类卸载支持。

    成为了Oracle官方称为的 全功能垃圾收集器。

    HotSpot开发团队对于G1目标是替换CMS.

    身为CMS接班人,Developers 希望那个有一款能建立 Pause Prediction Model的收集器。

    • 持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标

    G1 面向的并不是新生代、老年代或者Java堆。G1 面向堆内任何部分组成回收集(Collection Set)进行回收。

    因此它的回收标准不是xx代,而是哪儿的垃圾数量最多。

    G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。

    除此之外,Region中有一个Humongous特殊区域,来存储大对象(大小超过一个Region容量一半的对象)。

    Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB。

    G1收集器分区示意图

    它的回收过程与CMS大体类似:

    image-20230306165641872

    分为以下四个步骤:

    • 初始标记(暂停用户线程):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
    • 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
    • 最终标记(暂停用户线程):对用户线程做一个短暂的暂停,用于处理并发标记阶段漏标的那部分对象。
    • 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多个收集器线程并行完成的。

    Other: 引用

    JDK 1.2 之后Java将引用分为了:强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种。

    • 强引用: Object obj = new Object()
      • 这种就是强引用,只要引用关系还在就不会被回收。
    • 软引用:描述一些有用但非必须的对象
      • 只有即将发生内存溢出异常时才会列进二次回收。
    • 弱引用:描述非必须对象
      • 被弱引用关联的对象只能生存到下一次垃圾收集发生为止
    • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。

    1. 运行时数据区域

    Jvm 在执行Java程序时,会把它管理的内存划分为若干不同的区域。各有分工、

    1.1 程序计数器

    程序计数器(PCR) : 一块较小的内存空间,可以看作当前线程所执行字节码的行号指示器。

    JVM概念模型中,字节码解释器会改变计数器选区下条需要执行的字节码指令。

    JVM的多线程是由线程轮流切换、分配处理器执行时间实现的,一个处理器(一个内核)在同一时刻只执行一条指令。

    切换后为了恢复到原来的位置,因此每个线程都有一个独立的PCR. 线程私有内存

    计数器实际记录的是正在执行的虚拟机字节码指令的地址。若执行本地方法, 计数器为空。

    本地方法: 用非Java语言(如C、C++等)实现的方法,也称为本地代码(Native Code)

    1.2 Java 虚拟机栈

    VM Stack:也是线程私有的。生命周期与线程相同。

    虚拟机栈描述了Java方法执行的线程内存模型:

    每个方法被执行时,Java 虚拟机会同步创建一个栈帧(Stack Frame)存储局部变量表、操作栈、动态连接、方法出口等。每方法执行完毕对应一个栈帧在VM Stack从入栈到出栈的过程。

    • 栈帧: 方法运行时期重要的数据结构

    • 局部变量表:存放编译器各种Java 基本数据类型、对象引用(Reference) .

    • 数据类型以局部变量槽(Slot)存储在局部变量表中

    • long 与 double 占据两个变量槽,其他的类型占用一个

    • 局部变量表的内存空间在编译期间就分配好了

    • 大小是指slot的数量的多少,具体 64bit 或 32bit由虚拟机实现

    • 注:基本类型的数组不再局部变量表,在Java堆中。

    • 两类异常:

    • StackOverflowError: 线程请求栈深度大于虚拟机允许深度
    • OutOfMemeoryError: 若JVM Stack 容量可动态扩展,而栈扩展无法申请到足够内存
    • HotSpot 虚拟机栈容量不可变,Classic 虚拟机可扩展

    1.3 本地方法栈

    本地方法栈与虚拟机栈作用相似。

    区别:

    • VM Stack:为虚拟机执行Java方法服务

    • NativeMethod Stack:为虚拟机使用的本地方法服务。

    有些VM 如 HotSpot将本地方法栈和VM栈合二为一。

    1.4 Java Heap

    Java Heap: 虚拟机管理的内存最大的一块,用来存放对象实例,“几乎”所有对象实例在这里分配内存。

    在《Java 虚拟机规范》描述了:所有的的对象实例及数组都应该在堆上分配。

    • 但是随着JIT技术进步以及逃逸分析技术的日益强大,栈上分配、标量替换导致也不是这么绝对了。

    • 栈上分配:一种内存分配技术,它将对象的内存分配在线程栈上,而不是在堆上。这种技术可以减轻Java堆的负担,同时也可以提高程序的性能。栈上分配的前提条件是对象的生命周期非常短暂,即对象在方法内被创建并在方法结束时被销毁。由于这种技术可以避免频繁地访问堆,从而减少了内存访问的开销,因此可以提高程序的性能。

    • 标量替换:一种优化技术,它将一个对象拆分成多个基本类型的属性,并将这些属性分配在栈上或寄存器中,而不是在堆上。

    • 逃逸分析:一种静态分析技术,它可以分析程序中对象的生命周期,判断对象是否会被外部引用,即“逃逸”出当前方法或线程。如果对象不会逃逸,那么可以将对象分配在栈上或寄存器中。

    Java 堆是被所有线程共享的一块内存区域,虚拟机启动时创建。也是垃圾收集器管理的内存区域,因此也叫GC堆

    Tips:现代垃圾收集器大部分基于分代收集理论,会出现 新生代老年代永久代Eden空间等,仅仅是部分垃圾收集器的共同特性与设计风格。

    从内存分配角度看,所有线程共享的Java堆中可以划分多个线程私有的分配缓冲区(Thread Local Allocation Buffer)来提升效率。

    但是不会改变堆中存储的内容---对象实例。

    Java 堆逻辑上是连续而物理上不连续的。同时还可以被实现为固定大小、可扩展的。 通过 -Xmx-Xms设定。

    异常: 内存中没有空间来进行对象实例的分配会抛出OutOfMemeoryError.

    1.5 方法区

    方法区:线程共享区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

    《Java虚拟机规范》将其描述为堆一个了了逻辑部分。

    JDK 8 以前,Java 程序员习惯在HotSpot上开发,很多人都更愿意把方法区称呼为“永久代”,但其实并不等价。

    当时的HotSpot虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已

    1.6 运行时常量池

    运行时常量池: 方法区的一部分。

    常量池表: 位于Class文件信息里,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

    2. HotSpot 虚拟机对象

    Java 是一门面向对象的语言,时时刻刻都会有对象被创建。

    2.1 对象创建

    第一步

    当 Java 虚拟机遇到一条字节码 new

  • 首先检查指令参数是否在常量池中定位一个类的符号引用,检查引用类是否被加载、解析与初始化。否则执行类加载过程
  • 类加载之后,虚拟机为新生对象分配内存,对象需要的内存大小,在类加载完成之后可以确定。
    • 如何分配?
      • 指针碰撞(BumpThePointer):堆中的内存都是绝对规整,所有被用过的内存放在一边,用一个指针用来作为分界指示点。分配多少空间,移动相同纪律。
      • 空闲列表(FreeList):堆中内存不规整,在分配的时候从列表中找一块足够的空间,然后更新列表。
  • JVM 分配方式由Java堆是否规整决定 -> 由采用的垃圾收集器是否有空间压缩整理(Compact) 能力决定。

    • Serial、ParNew等带压缩整理过程收集器使用指针碰撞。
    • CMS这种基于清除算法收集器,采用空闲列表的算法。

    除了如何分配内存之外,如何在频繁创建对象下,保证指针的的移动的并发安全?

  • 分配内存动作同步处理:虚拟机采用 CAS+失败重试 保证更新 的原子性。
  • 按线程划分不同空间:每个线程在Java堆中预先分配一小块内存——本地线程分配缓冲。本地线程缓冲区使用完之后同步锁定。
  • 内存分配完成之后,虚拟机会将分配的内存初始化为0。

    第二步

    JVM会对对象进行一些设置比如:对象HashCode(调用时生成)、实例属于哪一个类、如何才能找到类的元数据信息以及对象GC分代年龄等信息。

    以上的信息都存储在对象头中,从之前的 JUC学习中我们知道,对于锁的状态(偏向锁、重向锁)等。

    完成以上步骤,开始进入到构造函数。所有字段此时都默认为0,new 执行之后执行 () 方法,按照程序编写的逻辑初始化。

    2.2 对象内存布局

    对象在堆内存中有三个部分: 对象头(Header)、实例数据(InstanceData)、对齐填充(Padding).

    JVM.jpg

    Java 中的对象头可以分为两类:普通对象与数组。

    普通对象的对象头:

    |--------------------------------------------------------------|
    |                     Object Header (64 bits)                  |
    |------------------------------------|-------------------------|
    |        Mark Word (32 bits)         |    Klass Word (32 bits) |
    |------------------------------------|-------------------------|
    
    

    数组的对象头:

    |---------------------------------------------------------------------------------|
    |                                 Object Header (96 bits)                         |
    |--------------------------------|-----------------------|------------------------|
    |        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
    |--------------------------------|-----------------------|------------------------|
    

    两者相同的部分是:Mark Workd, Klass Word。

    Klass Word 更多是与类相关的数据,类是静态的。而MarkWord则是对象运行时动态的数据,并且在32Bit与64Bit下的是不同的。

    |-------------------------------------------------------|--------------------|
    |                  Mark Word (32 bits)                  |       State        |
    |-------------------------------------------------------|--------------------|
    | identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
    |-------------------------------------------------------|--------------------|
    |  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
    |-------------------------------------------------------|--------------------|
    |               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
    |-------------------------------------------------------|--------------------|
    |               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
    |-------------------------------------------------------|--------------------|
    |                                              | lock:2 |    Marked for GC   |
    |-------------------------------------------------------|--------------------|
    
    
    
    |------------------------------------------------------------------------------|--------------------|
    |                                  Mark Word (64 bits)                         |       State        |
    |------------------------------------------------------------------------------|--------------------|
    | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |
    |------------------------------------------------------------------------------|--------------------|
    | thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |
    |------------------------------------------------------------------------------|--------------------|
    |                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |
    |------------------------------------------------------------------------------|--------------------|
    |                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |
    |------------------------------------------------------------------------------|--------------------|
    |                                                                     | lock:2 |    Marked for GC   |
    |------------------------------------------------------------------------------|--------------------|
    

    里面存放了 identity_hashcode: 就是对象hash值、age对象的年龄,biased_lock 与 lock共同组成了对于这个对象的锁的状态的描述。

    下面这张图是上面的翻译版本。由于可用的空间比较细,但是需要表示的内容变多了。因此MarkWord会在复用空间。

    image.png

    具体锁相关的只是可以去看看JUC部分的知识。

    • 相关字段的解释:
      • lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。
      • biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
      • age4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
      • identity_hashcode25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
      • thread:持有偏向锁的线程ID
      • epoch:偏向时间戳。
      • ptr_to_lock_record:指向栈中锁记录的指针
      • ptr_to_heavyweight_monitor:指向管程Monitor的指针。

    然后的实例数据部分,才是真正对于我们来说有效的数据,所有字段都记录了起来,存储顺序首到虚拟机分配策略和Java内部的设定有关。

    分配顺序:longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs)

    第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。HotSpot虚拟机自动内存管理系统要求对象的其实地址必须是8字节。

    学过《计算机系统基础》的,是比较明白我说的意思的。

    2.3 对象的访问定位

    创建了对象之后如何使用?

    Java 程序会通过栈上的 reference 数据来操作堆上的对象。

    • refernce: 一个指向对象的应用。

    目前主流的访问方式:

    • 句柄方式:Java堆可能会划分一块内存作为句柄池,refernce中存储的是句柄地址,句柄中包含对象实例数据、类型数据各自的地址

      • 如图
        image.png
    • 直接指针方式:只需要访问一次,不需要间接访问一次句柄池,直接根据reference中的内存地址访问对象。但是放置对象更加困难。

    image.png
    快是真的快!因为Java中会经常访问到对象,多一次访问综合起来还是会有不小的开销的。

    3. GC与内存分配策略

    垃圾回收器(Garbage Collection): 用来对内存动态分配和回收的。

    Java 程序会频繁创建对象,有些对象生命周期结束之后,GC为了复用空间会♻️回收对象。

    因此我们应该如何判断对象已经死亡了呢?

    3.1 对象的去世流程

    首先死亡第一步要在医学上检验这个是不是真的去世了。

    去世检验

    目前主流的去世判断方法主要有下面两种:

  • 引用计数法(JVM基本没用)
    • 在对象中添加引用计数器,有人引用就+1,失效-1. 当计数器为0时就可以回收了。
    • 优点: 原理简单、判定效率高(只需判断是否为0)
    • 缺点:
    • 占用额外空间
    • 其他情况待考虑
    • ...
  • [!INFO]
    引用计数法不能很好的解决,对象之间的相互依赖。

    我们举个小例子:

    class A{
    	private class B;
    }
    
    class B{
    	private class A;
    }
    

    如果存在 a、b对象,两者会相互依赖,引用计数器就无法为0。

  • 可达性分析法
    是常用的方法
    把所有对象建成一个树的结构,根对象为GC Root. 根据之间引用的关系从上往下遍历,如果不能访问到的对象则可以被回收。
  • 如下图所示:

    GCRootSetPic

    可作为根节点的条件:

    • 位于虚拟机栈的栈帧中的本地变量表中所引用到的对象(其实就是我们方法中的局部变量)同样也包括本地方法栈中JNI引用的对象。
    • 类的静态成员变量引用的对象。
    • 方法区中,常量池里面引用的对象,比如我们之前提到的String类型对象。
    • 被添加了锁的对象(比如synchronized关键字)
    • 虚拟机内部需要用到的对象。

    示意图如下

    一旦已经存在的根节点不满足存在的条件时,那么根节点与对象之间的连接将被断开。就是GC Roots和对象1之间断开。

    也能较好的解决相互依赖的问题:

    最终核验

    死亡是一件大事,上面的检验完成之后。并不是立刻马上就入殡了。
    JVM规定了至少检验两次之后,才能入殡防止诈尸。

    第一次被标记之后,会进行一次筛选是否有必要执行 finalize() 方法。

    • 对象没有覆盖 finalize()
    • finalize() 已经被虚拟机调用
      上述两种情况都没必要执行。

    如果有必要执行,JVM会将其放到一个 F-Queue ,在一个虚拟机建立的、低调度优先级的Finalizer线程执行finalize()。

    对象可以覆盖finalize方法来抢救一下,但是只能抢救一次。因为finalize()不会执行第二遍。

    以下行为不推荐
    拯救对象:

    public class test{
    	public static test SAVE_HOOK = null;
    	
    	public void isAlive() {  
    		System.out.println("yes, i am still alive :)");
    	}
    
    	@Override  
    	protected void finalize() throws Throwable {
    	
    		super.finalize();  
    		System.out.println("finalize method executed!"); 
    		test.SAVE_HOOK = this;
    		
    	}
    
    	public static void main(String []args) throws Throwable { 
    		SAVE_HOOK = new test();
    
    		//自救
    		SAVE_HOOK = null;  
    		System.gc();  
    		// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它 Thread.sleep(500);
    
    		if (SAVE_HOOK != null) { 
    			SAVE_HOOK.isAlive();
    		} else {  
    			System.out.println("no, i am dead :(");
    		}
    	}
    }
    

    执行结果:

    finalize method executed! 
    yes, i am still alive :)
    

    有关方法区的回收

    部分人认为方法区没有垃圾收集行为,实际上Java虚拟机规范也不要求虚拟机在方法区实现。

    通常来说,对于方法区的回收其实收益是比较小的相较于堆中的回收。

    [!INFO]
    堆中的新生代回收率可以达到 70%-99%

    所以方法区的回收有点吃力不讨好。

    3.2 垃圾收集算法

    各个平台虚拟机内存操作方法各异。

    分代收集理论

  • 弱分代假说: 绝大多数对象都是朝生夕灭的。
  • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
  • 上面两个假说奠定多款常用GC的设计原则:将Java堆的划分出不同的区域,然后将回收对象熬过GC收集次数(Age) 分配到不同的区域。
    比如这个区域都是朝生夕灭,可以较低代价回收大量空间。若是难以消亡的对象,那把它们集中放在一块, 虚拟机用较低的频率来回收这个区域,同时兼顾了垃圾收集的时间开销和内存的空间。

    根据这个由此发展出了:标记——复制算法,标记——清楚算法,标记——整理算法等。

    上面两种假说可以分别对应:新生代(Young Generation)、老年代(Old Generation)。但是后面的GC并没有遵循这一设计,因为对象之间可能跨代引用。

    例如新生代对象被老年代引用,若要找出存活对象,不得不固定的GC Roots之外,还要遍历整个老年代中所有对象来保证准确,会增加负担。

    因此增加了一个假说:

  • 跨代引用假说: 跨代引用相对于同代引用占极少数
  • 因此若存在跨代引用,应该是同时消亡或生存。

    如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了。

    因此不需要去扫整个老年代,只需在新生代上建立一个记忆集,将老年代划分为若干块,表示哪儿存在跨代,只有包含跨代引用的小块里会被加入GC Roots扫描。

    • Partial GC:不完整Java堆垃圾收集
      • 新生代收集(Minor GC/Youn GC): 新生代垃圾收集
      • 老年代收集(Major GC/Old Gc)
      • 混合收集(Mixed GC)
    • Full GC

    具体收集算法

  • 标记清除
  • 这是最基础的垃圾收集算法。
    首先表基础所有需要回收的对象,标记完成之后统一回收对象。或者相反,统一收回未标记对象。

    但是这个缺点有点明显:
    a. 效率不稳定,大量的对象需要回收的话,会频繁标记和清除
    b. 内存空间碎片话,标记清除之后产生大量不连续内存碎片

  • 标记-复制
  • Fenichel提出了半区复制 的垃圾收集算法。

    将内存按容量分为大小相等的两块,只使用其中一块。若一块用完了,将还存活的对象复制到另外一块,然后将已使用的一次清楚。

    如果都是存活的话,会有很大的内存复制开销,但是多数对象是可回收的。只需按顺序移动堆顶指针来分配内存。

    目前大多数JVM优先使用此收集算法,IBM研究发现 新生代中98%对象熬不过第一次收集,因此没必要1:1划分新生代内存空间。

    因此针对此现象,将新生代分为一块较大的Eden空间与两块较小Survivor空间。
    每次分配内存时只使用Eden和其中一块Survivor.

    HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1

  • 标记—整理
    在老年代,对象存活率高,因此标记复制方法,会进行较多次复制操作。
  • 若不想浪费50%空间,就要有额外空间担保。

    image.png

    标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。

    经典垃圾收集器

    image.png

    上图中收集器处于不同分代,若俩之间有连线则可以搭配使用。

  • Serial 收集器
    Serial收集器是最基础、历史最悠久的收集器。
  • 这是一个单线程收集器,在收集时会暂停其他所有工作线程直到结束。

    image.png

    这其实体验很不好,就好比没过几分钟你电脑就卡死一下然后又正常。

    虽然这很不好,并且HotSpot虚拟机开发团队一直在努力构思优秀的垃圾收集器,但是它仍然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器。

    因为它非常的简单和高效。

  • ParNew 收集器
  • 其实就是Seriral收集器的多线程版本,但是在收集时仍然要停止所有的用户线程。

    image.png

    在JDK7 之前的遗留系统是首选的新生代收集器。

    在JDK5 时发布了CMS 收集器,这是第一款真正意义上并发的垃圾收集器,支持GC线程与用户线程同时工作。但是却无法与 JDK1.4中的Parallel Scavenge收集器配合。因此只能选择ParNewSerial

    ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果。

  • Parallel Scavenge/Old
  • 一款基于标记-复制支持并行收集的新生代收集器。

    PS收集器目标是达到一个可控制吞吐量。

    $$吞吐量 = frac{运行用户代码时间}{运行用户代码时间 + 运行垃圾收集时间}$$
    

    而 CMS等收集器是注重尽可能缩短垃圾收集时用户线程停顿时间。

    在JDK6时也推出了其老年代收集器Parallel Old,采用标记整理算法实现:

    image-20230306165555265

    与ParNew收集器不同的是,它会自动衡量一个吞吐量,并根据吞吐量来决定每次垃圾回收的时间,这种自适应机制,能够很好地权衡当前机器的性能,根据性能选择最优方案。

    目前JDK8采用的就是这种 Parallel Scavenge + Parallel Old 的垃圾回收方案。
    4. Serial Old收集器
    这是Serial的老年代版本,同样也是标记-整理算法。

    image.png

    主要意义也是供客户端模式下的HotSpot虚拟机使用。
    在服务端下也可以作为CMS失败后的后备收集器。

  • CMS 收集器
  • CMS收集器一种以获取最短回收停顿时间为目标的收集器。

    大量的Java应用集中在互联网网站服务端上,因此希望系统停顿尽量的短。

    CMS收集过程有四个步骤:

  • 初始标记(CMS initial mark)
    • 标记 GC Roots 可以直接关联到的对象,速度较快
  • 并发标记(CMS concurrent mark)
    • 从GC Roots直接关联的对象遍历整个对象图
    • 耗时较长无需暂停用户线程
  • 重新标记(CMS remark)
    • 为了修正并发标记期间,因为用户线程继续运作的导致的标记改动部分
    • 比初始标记时间长,比并发标记时间短
  • 并发清除(CMS concurrent)
    • 清除标记的死亡对象,不需要移动存活的对象
  • 运行示意图:
    image.png

    CMS的缺点:

    • 对处理器资源非常敏感,并发阶段虽然不会暂停用户线程,但是会占用部分线程导致应用变慢。
      • 默认启动的回收线程数:(处理器核心数量+3)/4
      • 当核心数 < 4时影响较大
    • 无法处理“浮动垃圾”,标记中用户线程仍然会产生新的垃圾对象, 只能等待下次标记。
    • 会产生大量的碎片空间,需要 -XX:+UseCMS-CompactAtFullCollection 来触发Full GC时合并碎片空间。
  • Garbage First收集器
  • [!INFO]
    垃圾收集器技术发展历史上的里程碑式的成果

    JDK7 Update 4 是G1第一个商用版本,到了JDK8 Update 40 G1 提供了并发的类卸载支持。

    成为了Oracle官方称为的 全功能垃圾收集器。

    HotSpot开发团队对于G1目标是替换CMS.

    身为CMS接班人,Developers 希望那个有一款能建立 Pause Prediction Model的收集器。

    • 持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标

    G1 面向的并不是新生代、老年代或者Java堆。G1 面向堆内任何部分组成回收集(Collection Set)进行回收。

    因此它的回收标准不是xx代,而是哪儿的垃圾数量最多。

    G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。

    除此之外,Region中有一个Humongous特殊区域,来存储大对象(大小超过一个Region容量一半的对象)。

    Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB。

    G1收集器分区示意图

    它的回收过程与CMS大体类似:

    image-20230306165641872

    分为以下四个步骤:

    • 初始标记(暂停用户线程):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
    • 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
    • 最终标记(暂停用户线程):对用户线程做一个短暂的暂停,用于处理并发标记阶段漏标的那部分对象。
    • 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多个收集器线程并行完成的。

    Other: 引用

    JDK 1.2 之后Java将引用分为了:强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种。

    • 强引用: Object obj = new Object()
      • 这种就是强引用,只要引用关系还在就不会被回收。
    • 软引用:描述一些有用但非必须的对象
      • 只有即将发生内存溢出异常时才会列进二次回收。
    • 弱引用:描述非必须对象
      • 被弱引用关联的对象只能生存到下一次垃圾收集发生为止
    • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论