JVM----自动内存管理机制

一.简介

Java 虚拟机,简称 JVM, 是一个虚构的计算机,可模拟实际计算机操作系统上的功能并运行在不同的操作系统上,因此只要 Java 语言编译生成在 JVM 运行的字节码,JVM 就可以根据运行的对应的系统将字节码解释成具体系统平台的机器指令,从而实现跨平台运行。
image.png

二.内存区域

当执行一个 Java 程序的时候,首先会将 Java 文件编译成 class 文件, 这就是一个字节码文件,然后通过类加载器将 Class 文件加载到虚拟机中运行。 JVM 将运行时的内存区域划分为几个部分:
image.png

1.程序计数器

程序计数器是线程执行字节码的指示器,每个线程有一个独立的程序计数器,是一块较小的内存空间,存放着正在执行的字节码指令或者下一条需要执行的字节码指令。如果是一个 Java 方法,则记录正在执行的虚拟机字节码指令的地址,如果是一个 Native 则这个计数器的值就为空。

程序计数器是唯一一个没有规定任何 OutOfMemoryError (内存溢出) 的区域。

2.虚拟机栈

虚拟机栈是 Java 方法执行时的内存模型,与线程的生命周期相同,同样是线程独有的,在执行一个一个方法的时候就会创建一个栈帧,这个栈帧包含了局部变量表,操作数栈,方法出口等信息,方法调用和执行完成就是一个栈帧在虚拟机栈中的入栈和出栈过程。

虚拟机栈中规定的异常状况有两种:

  • 线程请求的栈深度大于虚拟机允许的深度就抛出 StackOverflowError
  • 虚拟机栈内存可以动态扩展,如果扩展的时候不能申请到足够的内存就抛出 OutOfMemoryError

3.本地方法栈

本地方法栈的作用和虚拟机栈类似,只不过本地方法栈是 Native 方法执行的内存模型,而虚拟机栈是 Java 方法的内存模型。

4.方法区

方法区市一个线程共享的内存区域,用于存储被虚拟机加载后的类的信息,常量,静态变量等数据,通常定义在 Class 文件中

运行时常量池

这个方法区的一部分,用于存放编译期生成的符号引用,字面量,运行时常量具有动态性,运行期间可以将新的常量放入常量池,而 Class 文件不具备这个特性。

方法区中的异常:当方法区,包括常量池,无法申请到足够的内存的时候就会抛出 OutMemoryError 异常。

5.堆

Java 堆是也是所有线程的共享的一块区域,,用于存放对象的实例,即几乎所有的对象都在这里分配内存。同时 Java 堆也是垃圾收集器管理的主要的区域,因此也叫 GC 堆(Garbage Collected Heap ),由于垃圾收集器可以采用不同的方式,对 Java 堆还进行了进一步的划分。可分为新生代/老年代,或者划分为 Eden 空间/From Survivor空间/To Survivor 空间。
image.png

Java 堆在物理上可以不连续,只要逻辑连续,同时可以设置固定大小,也可以设置为可扩展

  • 设置 Xmx 最大值
  • 设置 Xms 最小值

如果一个实例在堆中没有完成分配,且堆也没有办法进行扩展,就抛出 OutOfMemoryError 异常。

二.对象的创建

1.对象的内存布局

前面说过实例对象是存储在 java 堆中的,而在具体的内存区域中,一个对象的存储又可以划分 3 个区域:对象头,实例数据,对齐填充。

image.png

  • 对象头,包含了两个部分的内容,一个是对象自身的运行数据,包括哈希码,GC 分代,锁状态等,这部分称为 Mark Word。另一个部分是类型指针,指向类元数据,可以确定对象是哪个类的实例。
  • 实例数据,这是一个对象存储的真正的信息,包括继承父类的和自己本身的。
  • 对齐填充,起着占位符的作用,为了对象的起始地址是 8 个字节的整数倍。

2.对象的创建过程

一个对象的创建通常是由 new 开始的,具体的过程可分为下面几个部分:

  • 1.首先,会检查 new 指令的参数能否在方法区的常量池中找到对应类的符号引用,并检查这个符号引用的类是否被加载,解析和初始化过,没有就进行这三个步骤。
  • 2.一个类加载后需要的内存大小就可以确定,接着会为这个对象分配内存,内存分配完成后会首先初始化为 0 .根据 Java 堆中内存是不是规整的有两种不同的内存划分方式:

      • 堆内存是规整的,使用指针碰撞的方式。即用过内存放在一边,没有通过的放在一边,中间使用一个指针作为分界点,分配时通过指针移动一段合适的距离划分新的内存空间。
      • 堆内存不是规整的,使用空闲列表。即通过一个列表记录哪些内存是可以用的,从表中找到一块区域直接划分并更新列表。
  • 3.设置对象信息,主要是对象头中的信息,包括这个对象属于哪个类的实例,类的元数据的信息,对象的哈希码,GC 分代年龄,锁信息等。
  • 4.执行数据的初始化,即 init 方法。

3.对象的访问

对象在内存分配完成后就可以使用在 栈中的引用类型 (reference 数据)来操作具体的对象,这种访问方式可以分为来个两种方式,使用句柄和使用直接指针。

  • 使用句柄,会在 Java 堆中划分一份区域为句柄池,作为 reference 数据和真正的对象的一个桥梁。使用句柄的优势是即使对象的内存地址被移动,只会改变句柄中的实例数据的指针,而reference 不会改变。

    image.png

  • 使用直接指针,就是指reference 直接指向对象的地址,它的优势是可以减少开销,速度比较快。
    image.png

三.垃圾收集

1.简介

在 Java 语言中,不需要直接控制内存的回收,Java 程序的内存分配和回收都是由 JRE 在后台自动进行的 JRE 会负责回收那些不再使用的内存,这种机制称为垃圾回收(Garbage Collection ,GC )通常在CPU 空闲或者内存不足的时候就会进行回收。垃圾回收机制能自动释放内存空间,减轻了编写 Java 程序的负担,(C/C++ 需要显示进行垃圾回收),同时垃圾回收保证了程序的完成性,垃圾回收是 java 语言安全性策略的一个重要的部分。

2.回收的对象

在进行一次垃圾回收的时候首先就是要找到所有的对象,并判断对象是否可进行回收。通常由两种算法;

(1).引用计数算法

这种算法是通过给对象增加一个引用的计数器,只要有一个指针指向这个对象,这个计数器就加 1 ,这个引用失效就减 1.如果一个对象的计数为 0 的时候就可以进行回收。

这个算法存在的问题就是当两个对象互相含有对方的引用,但是又没有被其他对象引用的时候,计数器就不为 0 ,又不能被垃圾回收

1
2
3
graph LR
A-->B
B--> A

(2)可达性算法

这个算法首先会记录一系列 GC Root 对象,这些对象通常就是还有被使用的对象,然后以这些对象为起点向下搜索,搜索的对象的路径称为引用链,如果一些对象到 GC Root 之间没有任何引用链,那么这个对象就可以回收。
image.png

(3)对象的引用

对象被定义为两种状态:没有引用可回收,有引用不可回收,会显得内存回收不够灵活,因此希望对象如果在内存足够的时候在 GC 时 继续保留在内存中,如果进行 GC 后内存还是不足就直接回收。针对这种情况,对对象的引用进行了扩充。将一个引用划分为强引用,软引用,弱引用,虚引用。

  • 强引用,只要强引用存在就不会回收掉引用的对象
  • 软引用:在系统将发生内存溢出前会将这些对象列入第二次回收的范围
  • 弱引用:无论内存是否足够都会回收
  • 虚引用:虚引用不会对对象回收产生影响,它的作用是在回收时受到一个系统通知。

(4).对象的死亡

一个对象的死亡是经过两次标记过程的,即在进行可达性算法的时候会即使不可达也不一定就立即回收。
image.png

3.垃圾收集算法

JVM 并没有明确指定使用哪种算法,但是任何一种算法的作用都是发现无用的对象,回收被无用对对象占用的内存空间,使得空间能够再次被使用。

1.对象的划分

在 Java 堆中通常根据对象的使用频率将对象划分为不同的区域。
image.png
每一个对象有一个年龄计数器,发生一次 GC 且存活就将计数器加 1 .对于长期存活的对象就可从新生代进入老年代。对于新生代和老年代就可以采取不同的垃圾回收

  • Minor GC ,新生代的垃圾回收,执行比较频繁,速度比较快
  • Major GC/ Full GC,老年代的垃圾回收,通常老年代的会伴随着新生代回收。

2.垃圾收集算法

(1)标记-清除算法

对对象的内存进行标记,然后清除。
image.png
这种算法有两个明显的问题:

  • 标记和清除的效率比较低
  • 清除后产生大量的不连续的碎片空间,在分配一些需要大内存的对像的时候可能会因为内存不够导致再一次的 GC.
(2)复制算法

在新生代的对象很多都有着较低的生命期,因此将内存按 8 :1: 1 划分为 一个Eden 和两个 Survivor ,回收时将Eden 和 Survivor 存活的复制到另一个 Survivor 。如果另一个Survivor 的空间不够就需要向老年代进行分配担保,处于From Survivor 的新生代可进入老年代。
image.png

(3)标记整理

存活的对象向一方移动,然后清理掉端边界的内容。
image.png

(4)分代-收集

分代收集也就是对新生代和老年代采用不同的垃圾收集算法。

3.查找 GC Root

Java 虚拟机中使用 Oopmap 的数据结构记录对象的引用,通过 Oopmap 就可以找到所有的 GC Root 。JVM 只在指令中产生 Safepoint(安全点) 的地方进行一次记录,可避免对每条指令进行记录导致 Oopmap 多次变化。对于执行的线程中的到达安全点,主要有抢先式和主动式两种,而对于没有运行的不代码,就设置为安全区域,Safe Region ,其中的引用关系不会变化,所以可以直接GC 可以不用响应JVM 的中断。

4.垃圾收集器

垃圾收集器是对收集算法的具体实现,在JVM 中包含的收集器有下面几种:
image.png

1.Serial 收集器
  • 单线程收集器,进行垃圾收集的时候会暂停所有的工作线程

  • 简单高效,在内存较少的场景中,线程停顿时间是可以接受的

  • 采取复制算法

image.png

2.ParNew 收集器
  • 多线程版本的Serial ,在进行垃圾回收的时候是通过多个线程进行的
  • 采取复制算法
    image.png
3.Parallel Scavenge 收集器
  • 多线程的收集器,可达到一个可控的吞吐量,高效的利用 CPU 时间,适用在后台运算而不需要太多交互的任务
  • 采取复制算法
4.Parallel Old 收集器
  • Parallel Scavenge 的老年代版本,使用多线程和“标记-整理”算法
  • 适用于注重吞吐量以及 CPU 资源敏感的场合
5.Serial Old 收集器
  • Serial 的老年代版本
    image.png
6.CMS 收集器
  • 是一种以获取最短回收停顿时间为目标的收集器,适用于响应速度快,系统停顿时间最短。

运作过程:

  • 初始标记,标记 GC 能直接关联到的对象,需要停止其他线程。
  • 并发标记,根节点枚举的过程。
  • 重新标记,修正并发标记期间用户程序继续运作而导致标记产生变动的那个部分对象的标记,需要停止其他线程。
  • 并发清除

缺点:

  • 在并发阶段需要暂用一部分 CPU 资源,从而导致应用程序变慢,总吞吐量会降低。
  • 在并发清理阶段,用户线程产生的新的垃圾不会被回收,且需要留一部分的空间给用户空间运行。
  • 使用标记-清除算法,且通过内存碎片整理的方式,合并内存碎片,这个过程不能并发,会延长停顿时间。
    image.png
7.G1 收集器

特点:

  • 并行与并发,充分利用多 CPU多核环境 。
  • 分代收集,管理整个 GC 堆但是可以采用不同的方式去处理不同的对象
  • 空间整合,在整体上 使用 标记整理,在局部采用复制,不会产生内存空间碎片。
  • 可预测的停顿,可指定在一个时间片段内 GC 消耗的时间。
  • 高效,将内存划分为几个区域,跟踪区域垃圾堆积的价值大小,维护一张优先表,优先回收。

过程:

  • 初始标记,GC 关联对象。
  • 并发标记,找出存活对象。
  • 最终标记,修正并发标记期间用户程序继续运作而导致标记产生变动的那个部分对象的标记。
  • 筛选标记,对区域进行筛选,并回收。
    image.png

4.内存分配与回收策略

  • 优先在新生代 Eden 区分配,当 Eden 没有足够的空间的时候就会发起一次 Minor GC
  • 数组或者长字符串等大对象直接进入老年代
  • 长期存活的对象根据对象年龄计数器的数值动态晋升到老年代。
  • 空间分配担保:
    image.png
0%