JVM学习之内存管理

个人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堆中。

  • 两类异常:

    1. StackOverflowError: 线程请求栈深度大于虚拟机允许深度
    2. 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

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

JVM 分配方式由Java堆是否规整决定 -> 由采用的垃圾收集器是否有空间压缩整理(Compact) 能力决定。

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

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

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

内存分配完成之后,虚拟机会将分配的内存初始化为0。

第二步

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

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

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

2.2 对象内存布局

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

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

普通对象的对象头:

java 复制代码
|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|

数组的对象头:

java 复制代码
|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|

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

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

txt 复制代码
|-------------------------------------------------------|--------------------|
|                  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   |
|-------------------------------------------------------|--------------------|
txt 复制代码
|------------------------------------------------------------------------------|--------------------|
|                                  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会在复用空间。

具体锁相关的只是可以去看看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中存储的是句柄地址,句柄中包含对象实例数据、类型数据各自的地址

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

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

3. GC与内存分配策略

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

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

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

3.1 对象的去世流程

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

去世检验

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

  1. 引用计数法(JVM基本没用)
    • 在对象中添加引用计数器,有人引用就+1,失效-1. 当计数器为0时就可以回收了。
    • 优点: 原理简单、判定效率高(只需判断是否为0)
    • 缺点:
      1. 占用额外空间
      2. 其他情况待考虑
      3. ...

[!INFO] 引用计数法不能很好的解决,对象之间的相互依赖。

我们举个小例子:

java 复制代码
class A{
	private class B;
}

class B{
	private class A;
}

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

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

如下图所示:

可作为根节点的条件:

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

示意图如下

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

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

最终核验

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

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

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

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

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

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

java 复制代码
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 :(");
		}
	}
}

执行结果:

txt 复制代码
finalize method executed! 
yes, i am still alive :)

有关方法区的回收

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

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

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

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

3.2 垃圾收集算法

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

分代收集理论

  1. 弱分代假说: 绝大多数对象都是朝生夕灭的。
  2. 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

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

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

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

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

因此增加了一个假说:

  1. 跨代引用假说: 跨代引用相对于同代引用占极少数

因此若存在跨代引用,应该是同时消亡或生存。

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

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

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

具体收集算法

  1. 标记清除

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

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

  1. 标记-复制

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

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

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

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

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

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

  1. 标记---整理 在老年代,对象存活率高,因此标记复制方法,会进行较多次复制操作。

若不想浪费50%空间,就要有额外空间担保。

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

经典垃圾收集器

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

  1. Serial 收集器 Serial收集器是最基础、历史最悠久的收集器。

这是一个单线程收集器,在收集时会暂停其他所有工作线程直到结束。

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

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

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

  1. ParNew 收集器

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

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

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

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

  1. Parallel Scavenge/Old

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

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

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

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

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

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

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

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

  1. CMS 收集器

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

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

CMS收集过程有四个步骤:

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

运行示意图:

CMS的缺点:

  • 对处理器资源非常敏感,并发阶段虽然不会暂停用户线程,但是会占用部分线程导致应用变慢。
    • 默认启动的回收线程数:(处理器核心数量+3)/4
    • 当核心数 < 4时影响较大
  • 无法处理"浮动垃圾",标记中用户线程仍然会产生新的垃圾对象, 只能等待下次标记。
  • 会产生大量的碎片空间,需要 -XX:+UseCMS-CompactAtFullCollection 来触发Full GC时合并碎片空间。
  1. 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。

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

分为以下四个步骤:

  • 初始标记(暂停用户线程):仅仅只是标记一下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堆中。

  • 两类异常:

    1. StackOverflowError: 线程请求栈深度大于虚拟机允许深度
    2. 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

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

JVM 分配方式由Java堆是否规整决定 -> 由采用的垃圾收集器是否有空间压缩整理(Compact) 能力决定。

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

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

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

内存分配完成之后,虚拟机会将分配的内存初始化为0。

第二步

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

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

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

2.2 对象内存布局

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

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

普通对象的对象头:

java 复制代码
|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|

数组的对象头:

java 复制代码
|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|

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

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

txt 复制代码
|-------------------------------------------------------|--------------------|
|                  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   |
|-------------------------------------------------------|--------------------|
txt 复制代码
|------------------------------------------------------------------------------|--------------------|
|                                  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会在复用空间。

具体锁相关的只是可以去看看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中存储的是句柄地址,句柄中包含对象实例数据、类型数据各自的地址

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

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

3. GC与内存分配策略

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

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

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

3.1 对象的去世流程

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

去世检验

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

  1. 引用计数法(JVM基本没用)
    • 在对象中添加引用计数器,有人引用就+1,失效-1. 当计数器为0时就可以回收了。
    • 优点: 原理简单、判定效率高(只需判断是否为0)
    • 缺点:
      1. 占用额外空间
      2. 其他情况待考虑
      3. ...

[!INFO] 引用计数法不能很好的解决,对象之间的相互依赖。

我们举个小例子:

java 复制代码
class A{
	private class B;
}

class B{
	private class A;
}

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

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

如下图所示:

可作为根节点的条件:

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

示意图如下

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

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

最终核验

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

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

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

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

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

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

java 复制代码
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 :(");
		}
	}
}

执行结果:

txt 复制代码
finalize method executed! 
yes, i am still alive :)

有关方法区的回收

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

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

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

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

3.2 垃圾收集算法

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

分代收集理论

  1. 弱分代假说: 绝大多数对象都是朝生夕灭的。
  2. 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

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

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

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

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

因此增加了一个假说:

  1. 跨代引用假说: 跨代引用相对于同代引用占极少数

因此若存在跨代引用,应该是同时消亡或生存。

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

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

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

具体收集算法

  1. 标记清除

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

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

  1. 标记-复制

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

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

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

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

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

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

  1. 标记---整理 在老年代,对象存活率高,因此标记复制方法,会进行较多次复制操作。

若不想浪费50%空间,就要有额外空间担保。

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

经典垃圾收集器

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

  1. Serial 收集器 Serial收集器是最基础、历史最悠久的收集器。

这是一个单线程收集器,在收集时会暂停其他所有工作线程直到结束。

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

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

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

  1. ParNew 收集器

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

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

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

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

  1. Parallel Scavenge/Old

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

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

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

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

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

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

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

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

  1. CMS 收集器

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

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

CMS收集过程有四个步骤:

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

运行示意图:

CMS的缺点:

  • 对处理器资源非常敏感,并发阶段虽然不会暂停用户线程,但是会占用部分线程导致应用变慢。
    • 默认启动的回收线程数:(处理器核心数量+3)/4
    • 当核心数 < 4时影响较大
  • 无法处理"浮动垃圾",标记中用户线程仍然会产生新的垃圾对象, 只能等待下次标记。
  • 会产生大量的碎片空间,需要 -XX:+UseCMS-CompactAtFullCollection 来触发Full GC时合并碎片空间。
  1. 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。

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

分为以下四个步骤:

  • 初始标记(暂停用户线程):仅仅只是标记一下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()
    • 这种就是强引用,只要引用关系还在就不会被回收。
  • 软引用:描述一些有用但非必须的对象
    • 只有即将发生内存溢出异常时才会列进二次回收。
  • 弱引用:描述非必须对象
    • 被弱引用关联的对象只能生存到下一次垃圾收集发生为止
  • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
相关推荐
AAA 建材批发王哥(天道酬勤)5 小时前
JVM 由多个模块组成,每个模块负责特定的功能
jvm
JavaNice哥11 小时前
1初识别jvm
jvm
涛粒子11 小时前
JVM垃圾回收详解
jvm
YUJIANYUE11 小时前
PHP将指定文件夹下多csv文件[即多表]导入到sqlite单文件
jvm·sqlite·php
逊嘘12 小时前
【Java语言】抽象类与接口
java·开发语言·jvm
鱼跃鹰飞21 小时前
大厂面试真题-简单说说线程池接到新任务之后的操作流程
java·jvm·面试
王佑辉1 天前
【jvm】Major GC
jvm
阿维的博客日记1 天前
jvm学习笔记-轻量级锁内存模型
jvm·cas·轻量级锁
曹申阳1 天前
2. JVM的架构模型和生命周期
jvm·架构