HotSpot VM的内存模型和GC算法

JVM GC (Garbage Collection 垃圾收集) 可以自动的回收内存,保证 JVM 中的内存空间充足,防止内存泄漏溢出。JVM 帮助程序员去管理内存有很大好处:1. 防止没有回收不用的对象的内存而产生内存泄漏;2.防止人为的回收两次回收了错误的内存。本文记录了 Oracle 的 JVM HotSpot 的内存区域划分和垃圾收集算法

Hotspot VM 内存区域的划分

jvm

  1. 程序计数器 (Program Counter Register):记录当前线程执行字节码行号(运行到哪了),不同线程在CPU中轮流运行需要记录接下来要运行的位置

  2. 虚拟机栈 (JVM Stack):一个线程中要调用很多方法,方法之间也存在相互调用,每调用一个方法就会向JVMS 中插入一个栈帧,一个方法的调用到结束就对应这个栈帧在 JVMS 中的入栈到出栈。栈帧中存放着函数内的局部变量,操作数栈,常量池引用。当线程请求的栈深度大于 JVM 栈的最大深度时就会触发StackOverflowError,如递归的层数过大或无法终止

  3. 本地方法栈 (Native Method Statck):调用C语言所用到的栈

    以上的是每个线程独有的内存区域,以下是线程共有的内存区

  4. 堆区 (Heap):存放大多数 new 出来的对象 (其他情况:栈上分配TLAB),字符串常量池

  5. 方法区 (Method Area):存储已被加载的类的元数据,如:属性,方法,常量池,静态变量,这部分内存不在JVM中,而是在本地内存

注: 在jdk1.7之前,方法区 Method Area 用永久代 (PermGen) 的形式实现,它是 Heap 中的一块内存区,用于存放类的元数据以及常量池 , jdk1.8 之后用元空间 (MetaSpace) 代替了永久代的形式去实现 Method Area ,MetaSpace 在本地内存中,默认没有最大空间限制,所以几乎不会发生(大大减少了)OOM。移除永久代,一是为了和 JRockit 融合,JRockit 没有永久代的概念;二是永久代受参数大小的限制,参数在配置时也受到 JVM 设置内存大小的限制,导致在使用中可能出现内存溢出。当然,如果不设置 MetaSpace 的内存的上限则受到本地内存的限制

​ HotSpot 在jdk1.7之后将常量池从方法区中移动到 Heap 中

JVM GC 要干啥

  1. 确定哪些内存要回收
  2. 何时执行 GC
  3. 怎样执行 GC

哪些内存需要回收

对象创建时在堆上分配一块内存,等这个对象不再使用了要将这块内存回收好分配给新创建的对象,所以GC是发生在Heap上的,那么哪些对象是不再使用的呢?现在采用下面的2方法

  1. Reference Counting(计数引用):即没有引用指向这个对象了,这个对象一定是不再使用了,但是可能出现循环引用的情况,如下图中三个不再使用的对象相互引用,计数引用永远发现不了他们而导致内存泄漏

    jvm

  2. Root Search (根可达):从一些根出发,能遍历到的对象是存活的,遍历不到的是死亡的。根包括:在 JVM Stack,native method stack, run-time constant pool, static references in metaspace 中的引用

垃圾收集算法

Mark-Sweep 标记清除算法

先标记出需要回收的对象,再清除被标记的对象

优缺点:实现简单,但:1. 产生碎片,装不下新对象时会触发 full GC 压缩碎片,而 full GC 很耗时;2. 对于大内存的服务器,需要遍历很大内存才能标记完所有对象

Copying 拷贝复制

将内存分成两部分 A, B;先将创建的对象放入 A 中,当 A 无法放入对象时,将 A 中存活的对象拷贝到 B 中依次排列,这样 B 区域不需要压缩即可得到连续的可用区域;再将 A 一次性的清空;由 B 向 A 重复这个过程

优点:一次 GC 后不会产生碎片,在内存中移动对象速度快,回收效率高

缺点:浪费一半的内存

Mark-Compact 标记压缩

先标记存活对象,将存活对象向一端移动,清除边界以外的内存,可以得到连续的可用区域,效率比 Copying 低

分代的收集算法

根据对象存活时间的不同,将存活时间短的放在 eden 区,将存活时间长的放在 tenured 区。eden 区中由于对象存活时间短,每次要回收的对象很多,执行 GC 频繁,采用效率最高的 Copying,tenured 区每次要回收的对像不多,执行 GC 不频繁,采用 Mark-Compact 算法,如下图:

jvm

上图中我们先假设是初始的内存为空的状态,new 出特别大的对象直接放在老年代 tenured 区,普通对象放在新生代eden区,eden 区的对象在经过一次GC后将存活的对象拷贝到 Surviror1 区,下一次 GC 时,eden区和 Survivor1 中存活的对象拷贝到 Survivor2 中,S1到S2的存活对象的年龄增大 1,依此类推,下次 GC 将 eden 和 S2 的存活对象拷贝到 S1 中。当对象的年龄达到一定数目就放到 tenured 区,在新生代中使用的收集算法是 Copying,老年代使用 Mark-Compact

GC的种类与何时触发GC

在 Heap 按对象的生命周期划分成新生代和老年代的情况下

  1. 当新生代对象满时,触发的 GC 称为 Minor GC 或 Young GC
  2. 老年代空间不足:创建的大对象无法直接放入 tenured 区或新生代 GC 后晋升为老年代的对象放不下时,会触发 Full GC 也叫 Major GC,或者程序调用System.gc();时会建议执行 Full GC

无论 Minor GC 还是 Full GC 都会使程序 STW(Stop the World) 即等待 GC 完成后才继续运行,由于 Full GC 的时间比 Minor GC 慢很多,所以要减少 Full GC 发生的次数,比如减少大对象的创建,释放一些使用完的资源,不要调用System.gc();

垃圾收集器

垃圾收集器是 JVM 对垃圾收集算法的具体实现,现有的垃圾收集器有以下几种:

  1. Serial
  2. Parallel
  3. CMS
  4. G1
  5. ZGC

垃圾收集器我还不是很懂啊,下一篇将具体写一下

补充

栈上分配

有些对象的作用域始终都在一个方法中,它们的生命周期和方法一样:从方法被调用到方法的结束,对于作用域不会逃逸出方法的对象,将对象分配到栈上,减少gc负担

栈上分配要开启逃逸分析标量替换

TLAB(Thread Local Allocation Buffer)

TLAB(Thread Local Allocation Buffer),线程本地分配缓冲区,为了缓解多个线程同时在堆上申请空间使得分配效率降低的问题。TLAB 是eden 区的一份空间,很小,JVM 默认会为每个线程分配一块 TLAB 空间,当线程需要创建对象(非栈上分配的对象)时,会先尝分配到试该线程的 TLAB 区,如果 TLAB 区不够再分配到堆中。由于 TLAB 区很小,当剩余空间小于一个阈值 (refill_waste) 中,需要为线程重新创建空的 TLAB,原来的 TLAB 则舍弃,被舍弃的 TLAB 即使有少许的剩余空间也无法分配对象

对象内存分配的两种方法

如何将一块指定大小的内存从堆中划分出来存储一个对象呢:

  1. 碰撞指针 (Bump the Pointer)

    将 heap 的内存分成规整的两部分:已使用放在一边,空闲的放在另一边,中间放一个指针作为分界点,分配内存时将指针向空闲空间移动对象需要的大小

  2. 空闲列表 (Free List)

    如果堆的内存并不是规整的,已用内存和空闲内存是相互交错,就无法简单的使用碰撞指针了,需要维护一个列表,记录可用的内存块,分配时找到足够大的空间划分给对象,并更新列表中的记录

您的支持鼓励我继续创作!