目录
四、内存溢出(项目中什么情况下会内存溢出,怎么解决的项目中什么情况下会内存溢出,怎么解决的)
一、JVM 内存结构
1.概览
线程私有
①程序计数器
②虚拟机栈
线程共享
①堆
②方法区
-
执行 javac 命令编译源代码为字节码
-
执行 java 命令
-
创建 JVM,调用类加载子系统加载 class,将类的信息存入方法区
-
创建 main 线程,使用的内存区域是 JVM 虚拟机栈,开始执行 main 方法代码
-
如果遇到了未见过的类,会继续触发类加载过程,同样会存入方法区
-
需要创建对象,会使用堆内存来存储对象
-
不再使用的对象,会由垃圾回收器在内存不足时回收其内存
-
调用方法时,方法内的局部变量、方法参数所使用的是 JVM 虚拟机栈中的栈帧内存
-
调用方法时,先要到方法区获得到该方法的字节码指令,由解释器将字节码指令解释为机器码执行
-
调用方法时,会将要执行的指令行号读到程序计数器,这样当发生了线程切换,恢复时就可以从中断的位置继续
-
对于非 java 实现的方法调用,使用内存称为本地方法栈(见说明)
-
对于热点方法调用,或者频繁的循环代码,由 JIT 即时编译器将这些代码编译成机器码缓存,提高执行性能
-
说明
-
加粗字体代表了 JVM 虚拟机组件
-
对于 Oracle 的 Hotspot 虚拟机实现,不区分虚拟机栈和本地方法栈
2.会发生内存溢出的区域
-
不会出现内存溢出的区域 – 程序计数器
-
出现 OutOfMemoryError 的情况
-
堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收
-
方法区内存耗尽 – 加载的类越来越多,很多框架都会在运行期间动态产生新的类
-
虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时
-
-
出现 StackOverflowError 的区域
-
JVM 虚拟机栈,原因有方法递归调用未正确结束、反序列化 json 时循环引用
-
3.方法区、永久代、元空间
-
方法区是 JVM 规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
-
永久代是 Hotspot 虚拟机对 JVM 规范的实现(1.8 之前)
-
元空间是 Hotspot 虚拟机对 JVM 规范的另一种实现(1.8 以后),使用本地内存作为这些信息的存储空间
从这张图学到三点
-
当第一次用到某个类时,由类加载器将 class 文件的类元信息读入,并存储于元空间
-
X,Y 的类元信息是存储于元空间中,无法直接访问
-
可以用 X.class,Y.class 间接访问类元信息,它们俩属于 java 对象,我们的代码中可以使用
从这张图可以学到
-
堆内存中:当一个类加载器对象,这个类加载器对象加载的所有类对象,这些类对象对应的所有实例对象都没人引用时,GC 时就会对它们占用的对内存进行释放
-
元空间中:内存释放以类加载器为单位,当堆中类加载器内存释放时,对应的元空间中的类元信息也会释放
二、JVM 内存参数
对于JVM内存配置参数:-Xmx10240m -Xms10240m -Xmn5120m -XX:SurvivorRatio=3 其最小内存值和Survivor区总大小分别是
堆内存,按大小设置
解释:
-
-Xms 最小堆内存(包括新生代和老年代)
-
-Xmx 最大对内存(包括新生代和老年代)
-
通常建议将 -Xms 与 -Xmx 设置为大小相等,即不需要保留内存,不需要从小到大增长,这样性能较好
-
-XX:NewSize 与 -XX:MaxNewSize 设置新生代的最小与最大值,但一般不建议设置,由 JVM 自己控制
-
-Xmn 设置新生代大小,相当于同时设置了 -XX:NewSize 与 -XX:MaxNewSize 并且取值相等
-
保留是指,一开始不会占用那么多内存,随着使用内存越来越多,会逐步使用这部分保留内存。下同
堆内存,按比例设置
解释:
-
-XX:NewRatio=2:1 表示老年代占两份,新生代占一份
-
-XX:SurvivorRatio=4:1 表示新生代分成六份,伊甸园占四份,from 和 to 各占一份
元空间内存设置
解释:
* class space 存储类的基本信息,最大值受 -XX:CompressedClassSpaceSize 控制
* non-class space 存储除类的基本信息以外的其它信息(如方法字节码、注解等)
* class space 和 non-class space 总大小受 -XX:MaxMetaspaceSize 控制
注意:
* 这里 -XX:CompressedClassSpaceSize 这段空间还与是否开启了指针压缩有关,这里暂不深入展开,可以简单认为指针压缩默认开启
代码缓存内存设置
解释:
-
如果 -XX:ReservedCodeCacheSize < 240m,所有优化机器代码不加区分存在一起
-
否则,分成三个区域(图中笔误 mthod 拼写错误,少一个 e)
-
non-nmethods - JVM 自己用的代码
-
profiled nmethods - 部分优化的机器码
-
non-profiled nmethods - 完全优化的机器码
-
线程内存设置
三、JVM 垃圾回收
1.三种垃圾回收算法
① 标记清除法
解释:
1. 找到 GC Root 对象,即那些一定不会被回收的对象,如正执行方法内局部变量引用的对象、静态变量引用的对象
2. 标记阶段:沿着 GC Root 对象的引用链找,直接或间接引用到的对象加上标记
3. 清除阶段:释放未加标记的对象占用的内存
要点:
-
标记速度与存活对象线性关系
-
清除速度与内存大小线性关系
-
缺点是会产生内存碎片
② 标记整理法
解释:
1. 前面的标记阶段、清理阶段与标记清除法类似
2. 多了一步整理的动作,将存活对象向一端移动,可以避免内存碎片产生
特点:
-
标记速度与存活对象线性关系
-
清除与整理速度与内存大小成线性关系
-
缺点是性能上较慢
③ 标记复制法
解释:
1. 将整个内存分成两个大小相等的区域,from 和 to,其中 to 总是处于空闲,from 存储新创建的对象
2. 标记阶段与前面的算法类似
3. 在找出存活对象后,会将它们从 from 复制到 to 区域,复制的过程中自然完成了碎片整理
4. 复制完成后,交换 from 和 to 的位置即可
特点:
-
标记与复制速度与存活对象成线性关系
-
缺点是会占用成倍的空间
2.GC 与分代回收算法
- GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度
- GC 要点
- ①回收区域是堆内存,不包括虚拟机栈,在方法调用结束会自动释放方法占用内存
- ②判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象
- ③GC 具体的实现称为垃圾回收器
- ④GC 大都采用了分代回收思想,理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收,根据这两类对象的特性将回收区域分为新生代和老年代,不同区域应用不同的回收策略
- ⑤根据 GC 的规模可以分成 Minor GC,Mixed GC,Full GC
3.分代回收与 GC 规模
- 分代回收
- ①伊甸园 eden,最初对象都分配到这里,与幸存区合称新生代
- ②幸存区 survivor,当伊甸园内存不足,回收后的幸存对象到这里,分成 from 和 to,采用标记复制算法
- ③老年代 old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)
- GC 规模
- ①Minor GC 发生在新生代的垃圾回收,暂停时间短
- ②Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
- ③Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免
分代回收过程演示
1.伊甸园 eden,最初对象都分配到这里,与幸存区 survivor(分成 from 和 to)合称新生代
2.当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象
3.将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放
4. 将 from 和 to 交换位置
5. 经过一段时间后伊甸园的内存又出现不足
6.标记伊甸园与 from(现阶段没有)的存活对象
7.将存活对象采用复制算法复制到 to 中
8.复制完毕后,伊甸园和 from 内存都得到释放
9.将 from 和 to 交换位置
10.老年代 old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)
4.三色标记与并发漏标问题
1.用三种颜色记录对象的标记状态
①黑色 – 已标记
②灰色 – 标记中
③白色 – 还未标记
2.漏标问题 – 记录标记过程中变化
①Incremental Update
①只要赋值发生,被赋值的对象就会被记录
②Snapshot At The Beginning,SATB
①新加对象会被记录
②被删除引用关系的对象也被记录
5.垃圾回收器
四、内存溢出(项目中什么情况下会内存溢出,怎么解决的项目中什么情况下会内存溢出,怎么解决的)
- 误用线程池导致的内存溢出
- 查询数据量太大导致的内存溢出
- 动态生成类导致的内存溢出
五、类加载
1.类加载过程的三个阶段
-
加载
-
将类的字节码载入方法区,并创建类.class 对象
-
如果此类的父类没有加载,先加载父类
-
加载是懒惰执行
-
-
链接
-
验证 – 验证类是否符合 Class 规范,合法性、安全性检查
-
准备 – 为 static 变量分配空间,设置默认值
-
解析 – 将常量池的符号引用解析为直接引用
-
-
初始化
-
静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值,会被合并成一个
<cinit>
方法,在初始化时被调用 -
static final 修饰的基本类型变量赋值,在链接阶段就已完成
-
初始化是懒惰执行
-
名称 | 加载哪的类 | 说明 |
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
2.双亲委派机制
所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器
* 能找到这个类,由上级加载,加载后该类也对下级加载器可见
* 找不到这个类,则下级类加载器才有资格执行加载
双亲委派的目的有两点
1. 让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到 jdk 提供的核心类
2. 让类的加载有优先次序,保证核心类优先加载
六、四种引用(对象引用类型分为哪几类?)
1.强引用
①普通变量赋值即为强引用,如 A a = new A();
②通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收
2.软引用(SoftReference)
①例如:SoftReference a = new SoftReference(new A());
②如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象
③软引用自身需要配合引用队列来释放
④典型例子是反射数据
3.弱引用(WeakReference)
①例如:WeakReference a = new WeakReference(new A());
②如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象
③弱引用自身需要配合引用队列来释放
④典型例子是 ThreadLocalMap 中的 Entry 对象
4.虚引用(PhantomReference)
①例如: PhantomReference a = new PhantomReference(new A());
②必须配合引用队列一起使用,当虚引用引用的对象被回收时,会将虚引用对象入队,由 Reference Handler 线程释放其关联的外部资源
③典型例子是 Cleaner 释放 DirectByteBuffer 占用的直接内存
七、finalize
1.finalize的理解
- 一般的回答是:它是 Object 中的一个方法,子类重写它,垃圾回收时此方法会被调用,可以在其中进行一些资源释放和清理工作
- 较为优秀的回答是:将资源释放和清理放在 finalize 方法中非常不好,非常影响性能,严重时甚至会引起 OOM,从 Java9 开始就被标注为 @Deprecated,不建议被使用了
2.finalize 原理
- 对 finalize 方法进行处理的核心逻辑位于 java.lang.ref.Finalizer 类中,它包含了名为 unfinalized 的静态变量(双向链表结构),Finalizer 也可被视为另一种引用对象(地位与软、弱、虚相当,只是不对外,无法直接使用)
- 当重写了 finalize 方法的对象,在构造方法调用之时,JVM 都会将其包装成一个 Finalizer 对象,并加入 unfinalized 链表中
- Finalizer 类中还有另一个重要的静态变量,即 ReferenceQueue 引用队列,刚开始它是空的。当狗对象可以被当作垃圾回收时,就会把这些狗对象对应的 Finalizer 对象加入此引用队列
- 但此时 Dog 对象还没法被立刻回收,因为 unfinalized -> Finalizer 这一引用链还在引用它嘛,为的是【先别着急回收啊,等我调完 finalize 方法,再回收】
- FinalizerThread 线程会从 ReferenceQueue 中逐一取出每个 Finalizer 对象,把它们从链表断开并真正调用 finallize 方法
- 由于整个 Finalizer 对象已经从 unfinalized 链表中断开,这样没谁能引用到它和狗对象,所以下次 gc 时就被回收了
3.finalize 缺点
-
无法保证资源释放:FinalizerThread 是守护线程,代码很有可能没来得及执行完,线程就结束了
-
无法判断是否发生错误:执行 finalize 方法时,会吞掉任意异常(Throwable)
-
内存释放不及时:重写了 finalize 方法的对象在第一次被 gc 时,并不能及时释放它占用的内存,因为要等着 FinalizerThread 调用完 finalize,把它从 unfinalized 队列移除后,第二次 gc 时才能真正释放内存
-
有的文章提到【Finalizer 线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐】这个显然是错误的,FinalizerThread 的优先级较普通线程更高,原因应该是 finalize 串行执行慢等原因综合导致