JVM内存管理

JVM 内存布局

Cgq2xl4VrjWAPqAuAARqnz6cigo666.png

  • JVM 堆中的数据是共享的,是占用内存最大的一块区域。
  • 可以执行字节码的模块叫作执行引擎。
  • 执行引擎在线程切换时怎么恢复?依靠的就是程序计数器。
  • JVM 的内存划分与多线程是息息相关的。像我们程序中运行时用到的栈,以及本地方法栈,它们的维度都是线程。
  • 本地内存包含元数据区和一些直接内存。
虚拟机栈

在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。

一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。每个栈帧,都包含四个区域:

  • 局部变量表
  • 操作数栈
  • 动态连接
  • 返回地址
本地方法栈

是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。

程序计数器

存的,就是当前线程执行的进度

还存储了当前正在运行的流程,包括正在执行的指令、跳转、分支、循环、异常处理等。

是 JVM 上最大的内存区域

存储对象

垃圾回收,操作的对象就是堆。

随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)

对象创建的时候,到底是在堆上分配,还是在栈上分配呢?

  • 对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。

  • 对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。

    我们上面提到,每个线程拥有一个虚拟机栈。当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。

    注意,像 int[] 数组这样的内容,是在堆上分配的。数组并不是基本数据类型。

元空间
方法区

存储类的信息、常量池、方法数据、方法代码

总结

JVM 的运行时区域是栈,而存储区域是堆。


类的加载机制

Cgq2xl4cQNeAO_j6AABZKdVbw1w802.png

类加载过程

加载、验证、准备、解析、初始化。

加载

将外部的 .class 文件,加载到 Java 的方法区内

验证

不符合规范的将抛出 java.lang.VerifyError 错误

准备

将为一些类变量分配内存,并将其初始化为默认值。此时,实例对象还没有分配内存,所以这些动作是在方法区上进行的。

解析

将符号引用替换为直接引用的过程

你可以把通讯录里的女友手机号码,类比为符号引用,把面对面和你吃饭的人,类比为直接引用。

解析阶段负责把整个类激活

  • 类或接口的解析
  • 类方法解析
  • 接口方法解析
  • 字段解析
初始化

初始化成员变量

开始执行一些字节码

static 语句块,只能访问到定义在 static 语句块之前的变量

JVM 会保证在子类的初始化方法执行之前,父类的初始化方法已经执行完毕

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class A {
static {
System.out.println("1");
}
public A(){
System.out.println("2");
}
}

public class B extends A {
static{
System.out.println("a");
}
public B(){
System.out.println("b");
}

public static void main(String[] args){
A ab = new B();
ab = new B();
}
}
1
2
3
4
5
6
1
a
2
b
2
b

static 字段和 static 代码块,是属于类的,在类的加载的初始化阶段就已经被执行。

类信息会被存放在方法区,在同一个类加载器下,static 代码块只会执行一次

类加载器

几个类加载器
  • Bootstrap ClassLoader 启动类加载器 任何类的加载行为, 加载核心类库,也就是 rt.jar、resources.jar、charsets.jar 等。随着 JVM 启动。
  • Extention ClassLoader 扩展类加载器 加载 lib/ext 目录下的 jar 包和 .class 文件 是个 Java 类,继承自 URLClassLoader
  • App ClassLoader Java 类的默认加载器 加载 classpath 下的其他所有 jar 包和 .class 文件 我们写的代码,会首先尝试使用这个类加载器进行加载。
  • Custom ClassLoader 自定义加载器,支持一些个性化的扩展功能
双亲委派机制

除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委派给它的父加载器进行加载。这样一层层向上传递,直到祖先们都无法胜任,它才会真正的加载。

Cgq2xl4cQNeAG0ECAAA_CbVCY1M014.png

首先使用 parent 尝试进行类加载,parent 失败后才轮到自己。

这个方法是可以被覆盖的,也就是双亲委派机制并不一定生效。

一些自定义加载器
  • tomcat
  • SPI 机制 JDBC加载驱动类
小结

Java 自带的三个类加载器

main 方法的线程上下文加载器,其实是 Application ClassLoader

一般情况下,类加载是遵循双亲委派机制的


垃圾回收

GC Roots

在发生 GC 的时候,一个对象,JVM 总能够找到引用它的祖先。找到最后,如果发现这个祖先已经名存实亡了,它们都会被清理掉。而能够躲过垃圾回收的那些祖先,比较特殊,它们的名字就叫作 GC Roots。

GC Roots 包括:

  • Java 线程中,当前所有正在被调用的方法的引用类型参数、局部变量、临时值等。也就是与我们栈帧相关的各种引用。
  • 所有当前被加载的 Java 类。
  • Java 类的引用类型静态变量。
  • 运行时常量池里的引用类型常量(String 或 Class 类型)。
  • JVM 内部数据结构的一些引用,比如 sun.jvm.hotspot.memory.Universe 类。
  • 用于同步的监控对象,比如调用了对象的 wait() 方法。
  • JNI handles,包括 global handles 和 local handles。

GC Roots 大体可以分为三大类

  • 活动线程相关的各种引用。
  • 类的静态变量的引用。
  • JNI 引用。
典型 OOM 场景

OOM 的全称是 Out Of Memory

最常见的还是发生在堆上

原因

  • 内存的容量太小了,需要扩容,或者需要调整堆的空间。
  • 错误的引用方式,发生了内存泄漏。没有及时的切断与 GC Roots 的关系。比如线程池里的线程,在复用的情况下忘记清理 ThreadLocal 的内容。
  • 接口没有进行范围校验,外部传参超出范围。比如数据库查询时的每页条数等。
  • 对堆外内存无限制的使用。这种情况一旦发生更加严重,会造成操作系统内存耗尽。

典型的内存泄漏场景,原因在于对象没有及时的释放自己的引用。比如一个局部变量,被外部的静态集合引用。


垃圾回收(上)

当我们的内存空间达到一定条件时,会自动触发。这个过程就叫作 GC

负责 GC 的组件,就叫作垃圾回收器

垃圾回收器 只需要保证不要把正在使用的对象给回收掉就可以

先找到活跃的对象,然后把其他不活跃的对象判定为垃圾,然后删除

垃圾回收只与活跃的对象有关,和堆的大小无关

常见的内存回收算法
  • 复制算法(Copy)

复制算法是所有算法里面效率最高的,缺点是会造成一定的空间浪费。

  • 标记-清除(Mark-Sweep)

效率一般,缺点是会造成内存碎片问题。

  • 标记-整理(Mark-Compact)

效率比前两者要差,但没有空间浪费,也消除了内存碎片问题。

所以,没有最优的算法,只有最合适的算法。

大部分对象,可以分为两类:

  • 大部分对象的生命周期都很短;
  • 其他对象则很可能会存活很长时间。

我们把死的快的对象所占的区域,叫作年轻代(Young generation)。把其他活的长的对象所占的区域,叫作老年代(Old generation)。

年轻代

年轻代使用的垃圾回收算法是复制算法

因为年轻代发生 GC 后,只会有非常少的对象存活,复制这部分对象是非常高效的。

老年代

一般使用“标记-清除”、“标记-整理”算法

年轻代垃圾回收器
  • (1)Serial 垃圾收集器

    ​ 处理 GC 的只有一条线程,并且在垃圾回收的过程中暂停一切用户线程。

  • (2)ParNew 垃圾收集器

  • (3)Parallel Scavenge 垃圾收集器

老年代垃圾收集器
  • (1)Serial Old 垃圾收集器

与年轻代的 Serial 垃圾收集器对应,都是单线程版本,同样适合客户端使用。

年轻代的 Serial,使用复制算法。

老年代的 Old Serial,使用标记-整理算法。

  • (2)Parallel Old

Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。

  • (3)CMS 垃圾收集器

CMS(Concurrent Mark Sweep)收集器是以获取最短 GC 停顿时间为目标的收集器

Java8 之后,被G1 等垃圾回收器替换掉

查看java默认使用垃圾回收器

java -XX:+PrintCommandLineFlags -version

以下是一些配置参数:

  • -XX:+UseSerialGC 年轻代和老年代都用串行收集器
  • -XX:+UseParNewGC 年轻代使用 ParNew,老年代使用 Serial Old
  • -XX:+UseParallelGC 年轻代使用 ParallerGC,老年代使用 Serial Old
  • -XX:+UseParallelOldGC 新生代和老年代都使用并行收集器
  • -XX:+UseConcMarkSweepGC,表示年轻代使用 ParNew,老年代的用 CMS
  • -XX:+UseG1GC 使用 G1垃圾回收器
  • -XX:+UseZGC 使用 ZGC 垃圾回收器

线上使用最多的垃圾回收器,就有 CMS 和 G1,以及 Java8 默认的 Parallel Scavenge。

  • CMS 的设置参数:-XX:+UseConcMarkSweepGC。
  • Java8 的默认参数:-XX:+UseParallelGC。
  • Java13 的默认参数:-XX:+UseG1GC。
STW ( Stop the world)

如果在垃圾回收的时候(不管是标记还是整理复制),又有新的对象进入怎么办?

最好的办法就是暂停用户的一切线程

在这段时间,不能 new 对象的,只能等待。表现在 JVM 上就是短暂的卡顿

这叫stw


垃圾回收(下)

G1

实战

进阶