03-JVM内存管理
1. 物理内存与虚拟内存
- 物理内存,即RAM(随机存储器)
- 寄存器,用于存储计算机单元执行指令(如浮点、整数等运算)
- 地址总线,连接处理器和RAM
- 虚拟内存使用多个进程在同时运行时可以共享物理内存
2. 内核空间与用户空间
- 内核空间:操作系统运行时所使用的用于程序调度,虚拟内存的使用或连接硬件资源等的程序逻辑
- 用户空间:用户运行程序所占的内存空间
3. Java 内存组件
- Java 堆
用于存储 Java 对象的内存区域;-Xmx
表示堆的最大大小;-Xms
表示堆的初始大小;
一旦分配完成,就不能再内存不够时再向操作系统重新申请。 - 线程
每个线程创建时,JVM 会为线程创建一个堆栈,通常在256kb~756kb
之间。 - 类和类加载器
在 Sun SDK 中被存储在堆中,这个区域叫持久代
(PermGen区)。
① 只有 HotSpot 才有 PermGen space
② JRockit(Oracle)、J9(IBM)并没有 PermGen space
③ JDK1.8 中 PermSize 和 MaxPermGen 已经无效,JDK1.8 使用元空间替代 PermGen 空间,元空间并不在虚拟机中,而是使用本地内存
默认的 3 个类加载器:Bootstrap ClassLoader
/ExtClassLoader
/AppClassLoader
- NIO
JDK1.4 版本之后引入了一种基于通道和缓冲区来执行 I/O 的新方式;
使用 java.nio.ByteBuffer.allocateDirect() 方法分配内存,分配的是本机内存
而不是 Java 堆内存;
每次分配内存时,都会调用操作系统的 os::malloc() 函数。 - JNI
JNI 使得本机代码(如C语言程序)可以调用 Java 代码,也就是 native memory (本机内存)。
4. JVM 内存结构
4.1 内存结构
- 程序计数器
线程私有。是一块较小的内存空间,它可以看做当前线程所执行的字节码的行号指示器,主要作用用来选择执行指令
。 - 栈
线程私有。它的生命周期与线程相同,它里面有局部变量表
存放编译期可知的各种基本数据类型。 - 堆
线程共享。是虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域;在虚拟机启动的时候创建;此内存区域的唯一目的就是存放对象实例
,几乎所有的对象实例都在这里分配内存;Java堆也是垃圾收集管理的主要区域
。 - 方法区
线程共享。它用于存储被虚拟机加载的类信息、常量、静态变量、及编译器编译后的代码
等数据,它不会频繁的GC。 - 运行时常量池
代表运行时每个 class 文件中的常量表
:数字常量、方法、字段的引用。 - 本地方法栈
本地方法栈为虚拟机使用到的native
方法服务内存区域。
4.2 年轻代,老年代,持久代,元空间
Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
在 Java 中,**堆
**被划分成两个不同的区域:年轻代
( Young )、老年代
( Tenured)。
年轻代
年轻代用来存放新近创建的对象,尺寸随堆大小的增大和减小而相应的变化,默认值是保持为堆大小的1/15,可以通过
-Xmn
参数设置年轻代为固定大小,也可以通过-XX:NewRatio
来设置年轻代与老年代的大小比例,年轻代的特点是对象更新速度快,在短时间内产生大量的“死亡对象”。
年轻代的特点是产生大量的死亡对象,并且要是产生连续可用的空间, 所以使用复制清除算法和并行收集器进行垃圾回收.对年轻代的垃圾回收称作初级回收 (minor gc
)
年轻代 ( Young ) 又被划分为三个区域:Eden
、From Survivor
、To Survivor
。 这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
1 |
|
老年代
当对象在
Eden 出生
后,在经过一次 Minor GC
后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳,则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域
中,然后清理所使用过的 Eden 以及 Survivor 区域,并且将这些对象的年龄设置为1
,以后对象在 Survivor 区每熬过一次 Minor GC
,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。
但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。持久代
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class ,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过
-XX:MaxPermSize=
进行设置。元空间
有的虚拟机并没有持久代,Java8 开始持久代也已经被彻底删除了,取代它的是另一个内存区域也被称为 元空间。
它是本地堆内存中的一部分 它可以通过
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
来进行调整 当到达XX:MetaspaceSize
所指定的阈值后会开始进行清理该区域
如果本地空间的内存用尽了会收到java.lang.OutOfMemoryError: Metadata space
的错误信息。
和持久代相关的 JVM 参数-XX:PermSize
及-XX:MaxPermSize
将会被忽略掉。
参考资料:https://halo.sherlocky.com/archives/java-xin-sheng-dai-lao-nian-dai/
5. JVM 内存回收策略
5.1 回收原则
- 引用计数法
给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1;当引用失效是,计数器值-1。
这种算法使用的场景很多,但是 Java 中没有使用到这种算法,因为这种算法很难解决对象之间的相互引用的情况。 - 可达性分析法
通过一系列成为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径成为引用链
。当一个对象到 GC Roots 没有任何链(即GC Roots到对象不可达)时,则证明此对象是不可用的。
5.2 引用状态
强引用
代码中普遍存在的类似 Object obj = new Object() 这类的引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
软引用
描述有些还有用但非必需的对象。在系统发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。Java 中的类 SoftReference 表示软引用。
弱引用
描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java 中的类 WeakReference 表示弱引用。
虚引用
这个引用存在的唯一目的就是在这个对象被对象收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系。Java 中的类 PhantomReference 表示虚引用。
5.3 方法区垃圾回收
废弃常量
以字面量回收为例,如果一个字符串”abc”已经进入常量池,但是当前系统没有任何一个 String 对象引用了叫做”abc”的字面量,那么,如果发生垃圾回收并且有必要时,”abc”就会被移出常量池。
无用的类
该类所有的实例都已经被回收,即 Java 堆中不存在该类的任何实例;
加载该类的 ClassLoader 已经被回收;
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
5.4 垃圾收集算法
标记-清除(Mark-Sweep)算法
分为
标记
和清除
两个阶段:首先标记出所有要回收的对象,标记完成后统一回收所有被标记的对象。复制(Coping)算法
它可将内存分为两块,每次只用其中一块,当这一块用完了,就将还存活着的对象复制到另外一块上面,然后再把使用过的内存空间一次性清理掉。
标记-整理(Mark-Compact)算法
让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
分代收集算法
大批对象死去、少量对象存活(
新生代
),使用复制算法,复制成本低;对象存活率高、没有额外的空间进行分配担保的(老年代
),采用标记-清理算法 或 标记-整理算法。
5.5 垃圾收集器
G1收集器(支持收集新生代和老年代) - jdk1.7
并行和并发
。使用多个 CPU 来缩短 Stop The World 停顿时间,与用户线程并发。分代收集
。独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果。空间整合
。基于标记-整理算法,无内存碎片产生。可预测的停顿
。能建立和预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。Young Generation
Serial 收集器(jdk1.3)。采用复制算法的单线程收集器
ParNew 收集器 (jdk1.4)。其实就是 Serial 收集器的多线程版本
Parallel Scavenge 收集器(jdk1.4)。一个新生代收集器,复制算法,也是并行多线程收集器;也是虚拟机运行在 Server 模式下的默认垃圾收集器;目标是达到一个可控的吞吐量。Tenured Generation
Parallel Old 收集器(jdk1.6)。Parallel Scavenge 收集器的老年代版本,使用
多线程
和标记-整理
算法。
CMS 收集器(jdk1.5)。Concurrent Mark Sweep 收集器,获取最短回收停顿时间为目标,使用标记-清除
算法。
Serial Old 收集器(jdk1.5)。Serial 收集器的老年代版本,使用单线程
和标记-整理
算法。
5.6 GC
Minor GC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存。
① 当 JVM 无法为一个新的对象分配空间时,会触发 Minor GC,比如当 Eden 满了。所以分配效率越高,越频繁执行 Minor GC;
② 内存池被填满的时候,其中的内容会被全部复制,指针会从 0 开始跟踪空闲内存;
③ 执行 Minor GC 操作时,不会影响 持久代;
④ 所有的 Minor GC 都会触发 stop-the-world,停止应用程序的线程。Major GC
清理老年代。
Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC
、Full GC
( 或称为Major GC
)。Full GC
清理整个堆空间,包括年轻代和老年代。
6. JVM 参数
参数 | 描述 |
---|---|
-Xms | 初始堆大小。如:-Xms256m |
-Xmx | 最大堆大小。如:-Xmx512m |
-Xmn | 新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90% |
-Xss | JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。 |
-XX:NewRatio | 新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3 |
-XX:SurvivorRatio | 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10 |
-XX:PermSize | 持久代(方法区)的初始大小 |
-XX:MaxPermSize | 持久代(方法区)的最大值 |
-XX:+PrintGCDetails | 打印 GC 信息 |
-XX:+HeapDumpOnOutOfMemoryError | 让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用 |