Java - JVM内存模型及GC(垃圾回收)机制

JVM内存模型

JVM堆内存划分(JDK1.8以前)


JVM堆内存划分(JDK1.8之后)


主要变化在于:

  • java8没有了永久代(虚拟内存),替换为了元空间(本地内存)。
  • 常量池:1.7又把他放到了堆内存中;1.8之后出现了元空间,它又回到了方法区。
    年轻代:

新生成的对象都放在年轻代,主要存放一些生命周期比较短的对象

新生代一般分三个区:一个Eden区,两个 Survivor区:

大部分对象在Eden区中生成,当Eden区满时,还存活的对象将被复制到Survivor区,当这个 Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到老年代(Tenured)。

同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
老年代:

在年轻代中经历多次垃圾回收后仍存活的对象会被放入老年代中,一般存放生命周期较长的对象
java7永久代:

用于存放静态文件,如Java类、方法等。永久带对垃圾回收没有显著影响,一般不做垃圾回收,在JVM内存中划分空间。
java8元空间:

类似于永久带,不过它是直接使用物理内存而不占用JVM堆内存。


垃圾回收机制

Java不用像C++一样自己释放内存,通过垃圾回收器GC来进行内存的回收释放。

不需要进行垃圾回收:程序计数器、JVM栈、本地方法栈。

需要进行回收垃圾的区:堆和方法区

方法区,也不太需要被回收, 它存放的是类对象, 主要工作就是 "类加载", 但是很少会涉及到 "类卸载", 需要 GC 但不迫切。堆,才是 GC工作的主战场, 很多 new 出来的对象, 用完之后, 就需要被及时回收!!
什么时候进行垃圾回收?

该类的所有实例对象都已经被回收。

加载该类的ClassLoader已经被回收。

该类对应的反射类java.lang.Class对象没有被任何地方引用。
几种垃圾收集器:

Minor GC:新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发stop-the-world,但是它的回收速度很快。

Major GC:Tenured区GC,用于回收老年代,出现Major GC通常会出现至少一次Minor GC。

Full GC:是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。

垃圾回收机制流程

GC主要处理的是年轻代与老年代的内存清理操作,元空间(永久代)一般很少用GC。具体流程如下:

① 当一个新对象产生,需要内存空间,为该对象进行内存空间申请。

② 首先判断Eden区是否有有内存空间,有的话直接将新对象保存在Eden区。

③ 如果此时Eden区内存空间不足,会自动触发MinorGC,将Eden区不用的内存空间进行清理,清理之后判断Eden区内存空间是否充足,充足的话在Eden区分配内存空间。

④ 如果执行MinerGC发现Eden区不足,判断存活区,如果Survivor区有剩余空间,将Eden区部分活跃对象保存在Survivor区,随后继续判断Eden区是否充足,如果充足在Eden区进行分配。

⑤ 如果此时Survivor区也没空间了,继续判断老年区,如果老年区空间充足,则将Survivor区中活跃对象保存到老年代,而后存活区有空余空间,随后Eden区将活跃对象保存在Survivor区之中,在Eden区为新对象开辟空间。

⑥ 如果老年代满了,此时将产生MajorGC进行老年代内存清理,进行完全垃圾回收。

⑦ 如果老年代执行MajorGC发现依然无法进行对象保存,此时会进行OOM异常(OutOfMemoryError)。

上面流程就是整个垃圾回收机制流程,总的来说,新创建的对象一般都会在Eden区生成,除非这个创建对象太大,那有可能直接在老年区生成。


如何判断一个对象是否为 "垃圾"

引用计数方案:

引用计数非 Java, 别的编程语言中使用的方法, 此处也简单介绍一下, 因为在 <<深入理解 Java 虚拟机>> 这本书中, 这两种方法都提到了.

"引用计数": 是使用额外的计数器, 记录某个对象, 被多少个引用指向. 如果某一时刻, 计数器为 0 了, 就说明此时没有引用指向它了, 此时这个对象就可以视为 "垃圾" 了.

引用计数就类似上图, 每新产生一个引用, 引用计数就 + 1, 每销毁一个引用, 引用计数就-1,

销毁可以理解为如果引用是局部变量, 出了作用域就是销毁了. 如果引用是成员变量, 则该引用对应的对象被销毁时, 引用本身才被销毁.
"引用计数" 的缺陷

1 . 在多线程中, 需要修改同一个引用计数, 需要考虑到线程安全问题.

2 . 有可能会造成不必要的开销!! 如果本身是大的对象, 多引入一个引用计数器, 负担不大; 如果本身是小的对象, 并且数量还多, 引入引用计数器就会造成不小的空间开销.

3. 可能会带来循环引用的问题 (最致命的)

有一个 Test 类, 类中有一个 Test 类型的成员变量, 操作1 是将这个类实例化 2 个对象, 操作 2 是分别将两个对象中的引用类型的成员变量互相指向对方.

此时计数器为 2, 当 a, b 对象某一时刻被销毁时:

此时, 当前这俩对象引用计数都不为 0, 因此就都不会被当成垃圾, 但是这俩引用的地址都在对方手里, 就导致无法使用这俩对象, 最终就成了既不能被回收, 又不能被使用, 非常类似于 "死锁". 正因为引用计数有上述三个缺陷, 所以在 Java 中就不太合适, 就没有采取这个方案.
可达性分析

Java 中真正采用判断对象是否为垃圾的方案就是 "可达性分析".

可达性分析: 以代码中一些特殊的变量作为起点(GCRoot), 然后以起点出发, 判断哪些对象能够被访问到. 如果对象能被访问到, 就标记为 "可达", 当所有能被访问到的对象都被标记成 "可达" 时, 剩下的对象就是 "不可达" 的了, 就需要被当做垃圾回收了.

>>> 什么样的变量可以被称作 "起点" - GCRoot

  1. 局部变量表中的引用. (栈里面的局部变量)

栈有多个, 每个线程一个栈, 每个栈里面有很多栈帧, 每个栈帧里面有一个自己的局部变量表. 所有线程的所有栈的所有栈帧的所有局部变量表中的所有变量, 都可以视为起点 - GCRoot.

  1. 常量池中对应的对象.

  2. 方法区中, 静态引用类型的成员.

>>> 什么叫做能够被访问到

对于二叉树而言, 只要记住根节点, 就能判断其他结点能否被访问到了.

  1. 如果在代码中写了 root.right.right = null , 那么 F 结点就不可达了, 就可以被回收了.

  2. 如果在代码中写了 root.right = null, 那么 C 结点就不可达了, 同时 F 结点也被一起带走了(回收).

>>> 可达性分析相较于引用计数解决了两个缺陷:

  1. 可达性分析不需要引用计数器, 没有占用额外的空间;

  2. 可达性分析不会涉及到循环引用的问题.


几种垃圾回收算法

标记-清除算法(Mark-Sweep):最基础的GC算法,将需要进行回收的对象做标记,之后扫描,有标记的进行回收,这样就产生两个步骤:标记和清除。这个算法效率不高,而且在清理完成后会产生内存碎片,这样,如果有大对象需要连续的内存空间时,还需要进行碎片整理。
复制算法(Copying):新生代内存分为了三份,Eden区和2块Survivor区,保证有一块Survivor区是空闲的,这样,在垃圾回收的时候,将不需要进行回收的对象放在空闲的Survivor区,然后将Eden区和第一块Survivor区进行完全清理,这样有一个问题,就是如果第二块Survivor区的空间不够大怎么办?这个时候,就需要当Survivor区不够用的时候,暂时借持久代的内存用一下。此算法适用于新生代。
标记-整理(或叫压缩)算法(Mark-Compact):和标记-清楚算法前半段一样,只是在标记了不需要进行回收的对象后,将标记过的对象移动到一起,使得内存连续,这样,只要将标记边界以外的内存清理就行了。此算法适用于持久代。

分代回收算法

分代回收算法是通过区域划分, 实现不同区域和不同的垃圾回收策略, 从而实现更好的垃圾回收。

分带算法给对象引入了 "年龄" 的概念, 此处的年龄单位不是年, 而是对象活过 GC 的轮次. 对象刚创建出来, 没经历过 GC 的洗礼, 就认为年龄是 0, 没经过一轮 GC, 如果没被回收, 年龄就 + 1.

>>> 它是将 "复制算法" 和 "标记-整理算法" 的方案综合起来了, 只是根据对象存活周期的不同将内存划分为 "新生代" 和 "老年代" 两块大的区域. (具体要经过多少轮 GC 才会变成老年代, 我们不关心, 我们只关注策略本身.)

  1. 新创建的对象都放在伊甸区中. 伊甸区中的对象有个特点: 绝大部分对象都活不过一轮 GC ,

基本上就是 "朝生夕死" .

  1. 经过一轮 GC 的考验, 还存活下来的对象就通过 "复制算法" 放到了 "幸存区".

  2. 幸存区中的对象又会经过下一轮 GC 的考验, 且每经过一轮 GC 都会淘汰一部分对象, 没被淘汰的对象, 就继续通过 "复制算法" 拷贝到下一个幸存区中. (在 2 号幸存区考验时, 存活的又复制到 1 号幸存区) 这个过程, 既保留了复制算法的高效, 无内存碎片的优势, 又避免了过多的浪费空间, 因为此处浪费的幸存区的空间, 它相对整体空间来说, 微乎其微.

  3. 当对象在幸存区中经历了多轮 GC , 仍然没有被销毁, 就认为该对象一时半会不会被销毁, 于是就把这个对象拷贝到 "老年代" 了.

  4. 老年代的对象, 也要经历 GC 的考验, 只是考验的频率大大降低了, 如果老年代的对象即将要被回收, 就是用 "标记-整理" 算法, 因为老年代对象被回收的频率不高, 就可以接受 "标记-整理" 算法带来的时间开销.

  5. 还要注意的是: 有一个小的例外, 如果当前有一个特别大的对象, 就不经历上述分代回收过程, 直接就进入老年代, 因为大的对象在复制算法中, 是不太友好的.

相关推荐
救救孩子把7 分钟前
深入理解 Java 对象的内存布局
java
落落落sss10 分钟前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
万物皆字节15 分钟前
maven指定模块快速打包idea插件Quick Maven Package
java
夜雨翦春韭22 分钟前
【代码随想录Day30】贪心算法Part04
java·数据结构·算法·leetcode·贪心算法
简单.is.good27 分钟前
【测试】接口测试与接口自动化
开发语言·python
我行我素,向往自由29 分钟前
速成java记录(上)
java·速成
一直学习永不止步34 分钟前
LeetCode题练习与总结:H 指数--274
java·数据结构·算法·leetcode·数组·排序·计数排序
邵泽明35 分钟前
面试知识储备-多线程
java·面试·职场和发展
Yvemil71 小时前
MQ 架构设计原理与消息中间件详解(二)
开发语言·后端·ruby
程序员是干活的1 小时前
私家车开车回家过节会发生什么事情
java·开发语言·软件构建·1024程序员节