JVM内存分区

JVM的内存管理机制是如何进行垃圾回收的

执行一个Java进程对应启动一个JVM进程; Java虚拟机使用的是HotSpot虚拟机;18年oracle公开了GraalVM;

Jvm内存结构

栈代表了处理逻辑,而堆代表了数据; 从线程角度可以分为2个区域:

  • 线程私有区;
    • 程序计数器;
    • 虚拟机栈;
    • 本地方法栈;
  • 线程共享区;
    • Heap区;
    • Non-heap 方法区;(元空间)
    • 直接内存;

1. 线程私有区:栈

每个线程运行时所使用的内存结构,包含:程序计数器、多个栈帧对应每次方法调用,其中包含一个活动栈帧,对应正在执行的方法,以及调用Native方法的本地方法栈;

  • 垃圾回收不涉及栈内存;
  • 且栈内存并不是越大越好;
    -Xss 1024k
    -Xss 1m
  • 栈帧内局部变量是线程安全的;

栈的结构:

  1. 程序计数器:记录当前线程下一条要执行的指令;
  2. 虚拟机栈:用来存储局执行方法执行所需要的数据(生命周期跟随线程);
    • 存储单元为栈帧·,每一个方法执行到结束,对应一个栈帧的入栈出栈
    • 每个栈帧:包含部变量表、操作数栈、动态链接、方法出口等信息
    • 局部变量表:用来生成此方法所需要的所有局部变量,提前给定内存空间;(编译期确定)
    • 操作数栈:存储方法中的计算过程的中间结果;
    • 动态链接:指向常量池中的符号引用,如需要调用别的方法,就通过动态链接找到方法引用;
    • 方法出口
  3. 本地方法栈:与虚拟机栈的作用类似,用于执行Native方法(非Java方法);
    • 如Unsafe类,用于与操作系统交互;
    • Object下的
      wait()
      notify()
      notifyAll()
      都是Native方法;

2. 线程共享区:堆

主要用于存放对象实例以及数组;(垃圾回收的主要区域)

  • 年轻代
  • 老年代
  • 字符串常量池(JDK1.8):存放编译/运行时创建的字面量:
    new String("xx")

2.1 年轻代

年轻代目标:尽可能快速的收集掉那些生命周期短的对象,减少存入老年代的对象;

结构:Eden、Survivor(两个)

年轻代存储过程:

  1. 大多数新创建的对象都位于Eden内存空间中;
  2. 当Eden区满时,触发MinorGC,存活的对象被复制到Survivor区中的一个
  3. 之后每经历一次MinorGC,gc年龄+1(gc年龄初始为1,记录在对象头中)
  4. gc年龄达到阈值仍存活的对象,则可进入老年代;也可能由于空间不足,提前进入老年代,默认50%;即按照年龄排序后,超出50%的大年龄对象,会在MinorGC时移入老年代 (复制算法:对象从Survivor一个区移动到另一个区,并回收不可达对象)

2.2 老年代

目标:存储生命周期长的大对象(需要大量内存),不会轻易触发MajorGC

根据使用的垃圾收集器不同,收集算法也不同;通常为FullGC;

如:MajorGC只有CMS收集器有,如果使用G1收集器,还会触发MixedGC;

2.3 堆的内存分配方式

JVM使用不同的垃圾收集器,堆内存的分配方式也不同:

1、指针碰撞:使用带有整理的算法收集器,采用此分配;堆内存规整;(Serial、Parallel) 2、空闲列表:使用基于清除的算法,采用此分配,有内存碎片,不规整;(CMS) 3、本地线程分配缓存(TLAB):G1

2.4 老年代空间担保机制

MinorGC触发时,会检测:

1、老年代空闲内存是否大于年轻代对象总和? 如果大于,则正常执行MinorGC,如果小于,则判断第二个条件:

2、老年代空闲内存是否大于之前每次MinorGC后进入老年代的总对象的平均大小 如果大于,则正常执行MinorGC,如果小于,则执行FullGC

3. 线程共享区:运行时常量池

常量池的目的:

  1. 避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
  2. 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。

字符串常量池(堆中):String Pool;专门为字符串开辟的内存空间;在堆空间存放字符串对象的一张字符串表;

  • 当需要创建字符串常量时,先看字符串常量池有没有该字符串;存在则使用;
  • 不存在,则实例化要创建的字符串,并放入池中;
  • 字符串常量池不会参与GC;常驻内存;

常量池的相关参数:

  • -XX:+PrintStringTableStatistics
    :打印常量池信息;
  • -XX:StringTableSize=60009
    :限制StringTable的bucket个数,99.9不需要此参数,设置太大太小,都会影响性能;

4. 线程共享区:方法区(Non-Heap)

方法区是虚拟机规范中定义的一个抽象概念,虚拟机规范中也并没有规定方法区一定要在堆内存中,不同的虚拟机的方法区有不同的实现方式;

Hotspot虚拟机的实现:

  • 永久代(JDK7):堆内存;包含:类的元信息、常量池、静态变量;
  • 元空间(JDK8):堆外本地内存;包含:类的元信息;(常量池、静态变量存放在堆中)

元空间工作方式

元空间存储:

  1. class文件常量池:已经被虚拟机加载的类的元数据信息:类名、方法、字段信息、字节码;
    • [符号引用]就是字符串;
    • 直接引用,可以被程序识别的内存地址;
  2. 运行时常量池:每个类都有一个运行时常量池;从class常量池中构建的运行时使用的常量引用和符号引用

元空间特点

  • 不占用JVM内存,占用本地内存
  • 为每一个类加载器分配一块内存,此内存与对应的类加载器生命周期相同
  • 元空间默认不会卸载Class,只会通过FullGC来回收元空间中不再使用的class信息(对应的类加载器已死)

5. 线程共享区:直接内存

  • 不受JVM管理;
  • 回收成本高,读写性能高,少一次内存拷贝;常用于数据缓冲,如NIO的ByteBuffer;
  • 直接内存也会内存溢出:
    java.lang.OutOfMemoryError: Direct buffer memory

直接内存不由GC管理,由

Unsafe
类进行管理

  • 直接内存手动分配:
    unsafe.allocateMemory()
  • 直接内存手动释放:
    unsafe.freeMemory()

当ByteBuffer被GC回收时,其使用直接内存也会被释放,但并不是GC回收了直接内存 直接内存的自动释放,是由一个

Cleaner
虚引用完成的;
ByteBuffer
内有一个
Cleaner
引用,当GC触发,Cleaner虚引用就会被回收,从而触发直接内存的释放;

直接内存相关JVM参数

-XX:+DisableExplicitGC
:禁止显式调用GC

  • 禁止使用此参数,会导致直接内存无法回收,会造成直接内存长时间得不到释放;
  • 一定要手动释放内存,请使用
    Unsafe

6. 相关问题

3.1 为什么要分年轻代和老年代?

不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

1、如果不考虑GC性能的话,完全不需要新生代,全部对象创建在一个区域,一起回收即可;

2、因为每次回收都需要遍历所有存活对象,对于生命周期长的对象而言,这种遍历是没有效果的,他们依旧存在

3.2 为什么用元空间代替永久代

即:为什么元空间不占用Heap内存?

1、类的元数据内存回收调优困难

2、程序运行过程中,动态加载频繁,如果动态加载类太多,容易OOM;不放入堆区,可以不限制其内存;