JVM 内存结构与内存模型

Posted by sincosmos on July 3, 2020

JVM 内存结构

JVM 运行时数据区分为栈区、堆区、方法区、本地方法栈、程序计数器。 JVM 的线程在执行方法时,首先会在栈区申请栈帧用于存放方法中的局部变量、操作数等数据。当线程栈大小设置为固定值,如果线程执行所需的栈空间超过设定值时(例如深度的递归调用)就会出现 StackOverflowError。当线程栈空间设置为动态增长,如果 JVM 申请的栈内存大小超过其可用内存时则会出现 OutOfMemoryError。 JVM 将新创建的对象保存在堆区。一般的,JVM 根据对象的产生时间、对象大小、存活时间等特征将对象放在堆内存中不同区块儿,这些不同的区块儿即不同的代,这样的内存管理策略也称为分代管理。JVM 有默认的内存管理策略,用户在启动 java 程序时,也可以传入参数来调整其内存管理方式。
下面是一条示例命令,可以结合该命令了解 JVM 的内存管理策略。

java -Xmx4g –Xms4g –Xmn1200m -XX:NewSize=1200m -XX:NewRatio=4 -XX:SurvivorRatio=8 –Xss512k -XX:MaxPermSize:256m -XX:PermSize=100m  -XX:MaxTenuringThreshold=15 -jar example.jar
  • -Xmx4g 堆内存最大可使用空间 4G
  • –Xms4g 堆内存初始化申请空间 4G,一般和 Xmx 相等,去除 heap 伸缩带来的性能消耗。
  • –Xmn1200m 设置年轻代最大为 1200M。老年代 = 堆内存 - 年轻代。分代管理是 JVM 内存管理的核心概念,老年代与年轻代的划分对性能影响较大,Sun 官方建议年轻代占整个堆内存的 3/8。
  • -XX:NewSize=1200m,年轻代初始化大小为 1200M,默认和 Xmn 相等。
  • -XX:NewRatio=4 设置年轻代(包括 Eden, 两个 Survivor 区:From 和 To)与老年代的比值为 1:4,年轻代占整个堆的 1/5。这个参数和 Xmn 是重复的,其优先级低于 Xmn。
  • -XX:SurvivorRatio=8 设置年轻代中 Survivor 和 Eden 的比值为 2:8
  • –Xss512k 每个线程的堆栈大小设置为 512KB。每个线程在执行方法调用时,都会为方法申请一个栈帧,该参数设定了一个线程最大可申请的栈空间。
  • -XX:MaxPermSize:256m -XX:PermSize=100m 分别设置永久带最大为 256M,初始化大小为 256M。在 jdk 1.8 之前,永久带是用来实现方法区,它从堆中分配空间,存储类信息、常亮池、静态变量等。在 jdk 1.8 中,永久带被取消,使用元空间(metaspace)来实现方法区,元空间不在堆中申请空间,而是直接使用本地内存。
  • -XX:MaxTenuringThreshold=15 设置对象从年轻代晋升到老年代,需要在年轻代 Survivor 的 From 和 To 区中经历 minor GC 的次数下限。

GC 过程

GC 是应用程序职能外的、可能会比较耗时的工作,因此为了尽可能提高程序性能,GC 的耗时越少越好。Sun 的 JVM 之所以采用了分代内存管理模式,也是为了减少 GC 的总耗时。 在 Java 的世界里,大部分对象的生命期很短(die young),而没有 die young 的对象一般会存活较久(live long)。基于该特点,JVM 将堆分为年轻代和老年代(1:4),在年轻代划分出 Eden 区和 Survivor 区(8:2),又将 Survivor 区等分为 From 区和 To 区。 这样,所有新对象(除了大对象,大对象直接在老年代创建)都在年轻代的 Eden 区创建,当 Eden 区空间分配完后,将 Eden 区存活对像(由于 die young,存活对象很少)复制到 From 区,然后清空 Eden 区,这样新对象又能在 Eden 区创建;如果 Eden -> From 复制过程中,From 区空间占满,则将 From 区和 Eden 去存活对象(die Young 存活对象较少)复制到 To 区,清空 Eden 和 From 区,同时此时的 To 区将变为新的 From 区,清空后的 From 区变为 To 区。如果一个对象在 From 和 To 区反复翻滚,超过 MaxTenuringThreshold 时,那它很可能是 live long 的对象,则将该对象移动到老年代。上面对年轻代的垃圾回收称为 minor GC。 当老年代的空间占满时,则需要对老年代进行垃圾回收,清除其中的垃圾对象,释放老年代空间。对老年代进行垃圾回收的过程成为 major GC。 Heap minor GC 耗时要比 major GC 少得多。对垃圾对象的扫描上,minor GC 和 major GC 都是从 GC root 开始扫描,但 minor GC 扫描的对象要少得多,首先是因为年轻代比老年代小很多;其次 minor GC 只要发现某个对象被老年代的对象引用即可认为该对象是存活的;再次由于存活的对象很少,minor GC 需要复制的对象也很少。 我们可以看到 minor GC 采用标记-复制算法进行垃圾回收,总是会有 To 区的一片内存空间是闲置的,Major GC 发生时,老年代存活的对象可能占大多数,因此一般不能预留出对等的大片空间用于复制;并且存活对象较多时,复制对象会耗费大量时间,因此不适用标记-复制算法,对老年代的回收一般采用标记-交换算法(会使内存碎片化)或标记-整理算法。

垃圾回收器

垃圾回收器是对垃圾回收算法和垃圾回收过程的实现。JVM 中可用的垃圾回收器有以下几种。

  1. -XX:+UseSerialGC 该参数将使 JVM 采用串行垃圾回收器。该垃圾回收器将暂停所有其它 jvm 线程,使用单个回收线程进行垃圾回收。串行垃圾回收器对年轻代采用标记-复制算法进行垃圾回收,对老年代使用标记-压缩算法进行回收。该回收器最为古老,现在用的比较少。
  2. -XX:+UseParNewGC 该参数将使 JVM 采用串行多线程垃圾回收器,它是 SerialGC 的升级版本,和旧版本的不同就是它在进行垃圾回收时使用多线程以提高效率。
  3. -XX:+UseConcMarkSweepGC 使用 CMS 垃圾回收器。它是一种以获取最短停顿时间为目标的垃圾回收器,它将垃圾回收过程分为 4 步:初始标记,并发标记,重新标记,并发清除。其中初始标记和重新标记阶段仍然需要暂停其它 jvm 线程,其它阶段正常的 jvm 工作线程是可以正常工作的。CMS 是一种以获取最短回收停顿时间为目标的收集器,初始标记仅仅标记 GC Roots 能直接关联到的对象,因此需要的STW 时间很短,重新标记则是为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记,它的时间会稍长于初始标记,但远比并发标记时间短。它基于标记-清除算法,适用于老年代的收集。标记清除算法会产生较多的内存碎片,因此不适用于新生代的垃圾收集。
  4. -XX:+UseG1GC 使用 G1 垃圾回收器。G1 是 Java 领域目前最先进的垃圾回收器。G1 重新定义了堆空间,扩展了原有的分代模型,将堆划分为一个个区域,这样在垃圾回收时不必在全堆范围内进行。G1 保留了新生代和老年代的概念,但二者不再是物理隔离的,它们都是由不连续的 region 组成。region 是将堆空间划分成相等的区域后形成的每个区域。如果应用程序符合以下特征,可以考虑将垃圾回收器从 CMS 或 ParallelOld 收集器迁移到 G1 以获取更好的性能。1)实时数据占用了超过半数的堆空间;2)对象分配率或从年轻代晋升到老年代的速度变化明显;3) 期望将 GC 或停顿的时间限制到 0.5s 或 1s 以下。

Java 内存模型

Java 内存模型(JMM)和 JVM 内存结构是不同的概念。JMM 是 Java 程序运行期对待计算机 CPU 缓存、高速缓存、主内存的方式。我们知道,不同的计算机和操作及系统处理上述各种缓存的方式不同,为了保证 Java 程序的平台无关性,JMM 用来屏蔽不同硬件和操作系统带来的内存访问差异。
JMM 定义了线程和驻内存的抽象关系:线程之间通向的变量存储在主内存,每个线程都有一个线程私有的工作内存,工作内存存储了该线程读/写共享变量的副本。工作内存是 JMM 的抽象概念,是对 CPU 缓存、高速缓存、写缓冲区、寄存器等设备的抽象。 线程的工作内存中保存了它使用到的变量的副本,线程对变量的所有操作必须在工作内存进行。因此,线程读变量时要注意同步其它线程对相应变量的更新;线程写变量时要注意及时将写结果从工作内存同步到主内存,并解决其它线程并发写的冲突问题。 在线程工作内存与主内存的交互中,Java内存模型定义了8种操作来完成,虚拟机实现必须保证每一种操作都是原子的、不可再拆分的(double和long类型例外)。

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。