JVM 内存管理总结归纳
前言
什么是JVM ,首先J 是什么,VM 又是什么,它又有哪些特点? 我们的JVM内存是怎么划分的,划分的每一部分的职责是什么,主要存储什么? 我们常见的垃圾回收算法有哪些,其优缺点是什么? JVM 的一些参数配置有什么,怎么配置?
一、JVM 概述
虚拟机:VM,Virtual Machine
- 逻辑上,一台虚拟的计算机
- 实际上,一个软件,能够执行一系列虚拟的计算指令
- 系统虚拟机
- 对物理计算机的仿真
- 如 VMWare ,Oracle VirtualBox 等
- 软件虚拟机
JVM现在主要有 Oracle 的 JDK(Hospot VM / JRockit VM) 和 OpenJDK
二、JVM 内存分类
2.1.Java自动内存管理
① C / C++ ,malloc 申请内存和 free释放内存 ② 由于程序员疏忽或程序异常,导致内存泄露
① Java / C# ,采用内存自动管理 ② 程序员只需要申请使用,系统会检查无用的对象并回收内存 ③ 系统统一管理内存,内存使用相对较高,但也会出现异常
JVM内存模型图: 图片来自知乎@creep
JVM内存:
① 程序计数器(Program Counter Register) ② Java 虚拟机栈 (JVM Stack) ③ 本地方法栈(Native Method Stack)
① 堆 (Heap) ② 方法区 (Method Area) 运行时常量池
① 程序计数器(Program Counter Register)
- Program Counter Register,一块小内存,每个线程都有
- PC 存储当前方法
- 线程正在执行的方法称为该线程的当前方法
- 当前方法为本地(native,C语言写的方法)方法时,pc 值未定义(undefined)
- 当前方法为非本地方法时,pc 包含了当前正在执行指令的地址
- 当前唯一一块不会引发OutOfMemoryError异常
②JVM 栈(JVM Stack,Java栈)
- 每个线程都有自己独立的Java 虚拟机栈,线程私有
- -Xss 设置每个线程堆栈大小
- Java 方法的执行基于栈
- 每个方法从 调用到完成对应 一个栈帧在栈中入栈、出栈的过程
- 栈帧存储局部变量表、操作数栈等
- 局部变量表存放方法中存在 “栈” 里面的东西
- 引发的异常
- 栈的深度超过虚拟机规定的深度, StackOverflowError 异常
- 无法扩展内存, OutOfMemoryError 异常
③本地方法栈(Native Method Stacks)
- 存储native 方法的执行信息,线程私有
- VM规范没有对本地方法栈做明显规定
- 引发的异常
- 栈的深度超过虚拟机设定的深度, StackOverflowError 异常
- 无法扩展内存, OutOfMemoryError 异常
④堆(Heap)
- 虚拟机启动时创建,所有线程共享,占地最大
- 对象实例和数组都是在堆上分配内存
- 垃圾回收的主要区域
- 设置大小
- 引发的异常
- 无法满足内存分配的要求,OutOfMemoryError 异常
⑤方法区(Method Area)
- 存储JVM已经加载类的结构,所有线程共享
- 运行时常量池、类信息、常量、静态变量等
- JVM 启动时创建,逻辑上属于堆(Heap)的一部分
- 很少做垃圾回收
- 引发的异常
- 无法满足内存分配要求,OutOfMemoryError 异常
⑥ 运行时常量池(Run-Time Constant Pool)
三、JVM内存参数
JVM 默认运行参数:
- 支持JVM运行的重要配置,根据操作系统/物理硬件的不同而不同
- 使用 -XX: +PrintFinal 显示VM 的参数
- 如 java -XX:+PrinntFinal -version | findstr HeapSize 查看堆内存的配置参数
程序启动的两类参数:
- 程序参数:程序需要,存储在 main 函数的形参数组中
- 虚拟机参数:更改默认配置,用以指导进程运行
堆(Heap)
- 共享,内存大户,存储所有的对象和数组
- -Xms 初始堆值,-Xmx 最大堆值
JVM 栈
- 线程私有的,存储类中每个方法的内容
- -Xss 最大栈值
方法区(Method Area)
- 存储类信息、常量池等
- 1.7 以前,永久区(Perm),-XX:PermSize,-XX:MaxPermSize
- 1.8 及以后,元数据区,-XX:MetaspaceSize,-XX:MaxMetaspaceSize
四、Java 对象引用
垃圾收集器:
- JVM 内置有垃圾收集器
- GC,Garbage Collector
- 自动清除无用的对象,回收内存
- 垃圾收集器的工作职责(John McCarthy,Lisp 语言)
- 什么内存需要收集(判定无用的对象)
- 什么时候回收(何时启动,不影响程序正常执行)
- 如何回收(回收过程,要求速度快 / 时间短 / 影响小)
判定无用对象,进行回收:
- 基于对象引用判定无用对象
- 对象引用链
通过一系列的称为 “GC Roots” 对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说,就是从GC Roots 到这个对象不可到达)时,则证明此对象是不可用的。
GC Roots 对象包括:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中引用的对象
强引用:
- 例如 Object obj = new Object(); Object obj2 = obj;
- 只要强引用还存在,对象就不会被回收,哪怕发生OOM (Out Of Memory 内存溢出)异常
软引用:
- 描述有用但并非必须的对象
- 在系统发生内存溢出异常之前,会把这些对象列为可回收
- JDK 提供了 SoftReference 类来实现软引用
弱引用:
- 描述非必须的对象,比软引用强度更弱些
- 被弱引用关联的对象只能生存到下一次垃圾收集发生之前
- JDK 提供了 WeakReference 类来实现弱引用
虚引用:
- 最弱的引用关系,JDK 提供 PhantomReference 实现虚引用
- 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,用于对象回收跟踪
- 随时随地都有可能被回收,说不定现在定义了,下次用的时候就已经被回收了
五、垃圾收集算法
1. 引用计数法
- 一种古老的算法
- 每个对象都有一个引用计数器
- 有引用,计数器加一,当引用失效,计数器减一
- 计数器为0 对象,将会被回收
优点:简单,效率高 缺点:无法识别对象之间相互循环引用
2. 标记-清除
- 标记阶段: 标记出所有需要回收的对象
- 回收阶段: 统一回收所有被标记的对象
优点:简单 缺点:效率不高,内存碎片 这里可以看到,清除后会有内存碎片
3. 复制算法
- 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
- 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面。
- 然后把已使用过的内存空间一次清理掉
优点:简单,高效 缺点:1 .可用内存少 2. 对象存活率高时,复制操作比较多
4. 标记-整理
- 标记阶段:与“标记-清除” 算法一样
- 整理阶段: 让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
优点:1. 避免碎片产生 2. 无需两块相同内存 缺点:1. 计算代价大,标记清除+碎片整理 2. 更新引用地址
5. 分代收集
- Java 对象的生命周期不同,有长有短
- 根据对象存活周期,将内存划分为新生代和老年代
- 新生代 (Young Generation)
主要存放短暂生命周期的对象 新创建的对象都先放入新生代,大部分新建对象在第一次 gc 时被回收 - 老年代(Tenured Generation)
一个对象经过几次 gc 仍存活,则放入老年代 这些对象可以活很长时间,或者伴随程序一生,需要常驻内存的,可以减少回收次数
针对各个年代特点采用合适的收集算法
我们新创建的对象首先会放在 Eden Space 和 From Space 中,当进行一次gc 之后,我们会把Eden 和 Space 这两个区存活的对象,放到 To 区中,然后清空Eden 和 Space ,下次的时候新创建的对象会放在 Eden 和 To 区中,然后gc过后再把幸存的对象放到 From 区中,如此往复。
- 老年代:标记清除或标记整理
六、JVM堆内存参数设置
堆内存参数:
- -Xms 初始堆大小
- -Xmx 最大堆大小
- -Xmn 新生代大小
- -XX: SurvivorRatio 设置 eden 区 / from(to) 的比例
- -XX:NewRation 设置老年代 / 新生代比例
- -XX:+PrintGC / -XX:+PrintGCDetails 打印GC 的过程信息
HotSpot 现有的垃圾收集器(JDK 13)
- 串行收集器 (Serial Collector)
- 并行收集器(Parallel Collector)
- CMS 收集器 (Concurrent Mark Sweep Collector)
- G1 收集器(Garbage-First Collector)
- Z 收集器 (Z Garbage Collector)
|