堆
堆和方法区针对一个 JVM 进程来说是唯一的,也就是一个进程只有一个 JVM ,但是进程包含多个线程,他们是共享同一堆和方法区空间的,每个线程各自包含一套程序计数器、本地方法栈和虚拟机栈。
- 一个进程 一个jvm 一个堆和方法区 一个Runtime实例
Runtime
每个Java应用程序都有一个类的实例Runtime (jvm运行时环境) 单例
Java VisualVM
下图就是使用:Java VisualVM 查看堆空间的内容,通过 jdk/bin提供的插件
Java VisualVM 插件地址
https://visualvm.github.io/pluginscenters.html
参数设置
-Xms10m:最小堆内存
-Xmx10m:最大堆内存
堆的核心概念
一个 JVM 实例只存在一个堆内存,堆也是 Java 内存管理的核心区域。
Java 堆区在 JVM 启动的时候即被创建,其空间大小也就确定了。是 JVM 管理的最大一块内存空间。
- 堆内存的大小是可以调节的。
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
- 所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation
Buffer,TLAB)。
对象实例都分配在堆上吗?
《Java虚拟机规范》中对 Java 堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated)
我要说的是:“几乎” 所有的对象实例都在这里分配内存。——从实际使用角度看的。
- 因为还有一些对象是在栈上分配的
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
堆.-垃圾回收重点区域
堆,是 GC(Garbage Collection 垃圾收集器)执行垃圾回收的重点区域
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
- 也就是触发了 GC 的时候,才会进行回收
- 如果堆中对象马上被回收,那么用户线程就会收到影响,因为有 Stop The World
堆内存细分
Java 7 及之前:
堆内存逻辑上分为三部分:新生区+老年区+永久区
- Young Generation Space 新生区 Young/New 又被划分为 Eden 区和 Survivor 区
- Tenure Generation Space 养老区 Old/Tenure
- Permanent Space 永久区 Perm
Java 8 及之后:
堆内存逻辑上分为三部分:新生区+老年区+元空间
- Young Generation Space 新生区 Young/New 又被划分为 Eden 区和 Survivor 区 -
- Tenure Generation Space 养老区 Old/Tenure
- Meta Space 元空间 Meta
约定:新生区 <-> 新生代 <-> 年轻代 、 养老区 <-> 老年区 <-> 老年代、 永久区 <-> 永久代
堆空间内部结构,JDK 1.8 时从永久代替换成元空间
设置堆内存大小与 OOM
Java 堆区用于存储 Java 对象实例,那么堆的大小在 JVM 启动时就已经设定好了,大家可以通过选项"-Xmx"和"-Xms"来进行设置。
“-Xms" 用于表示堆区的起始内存,等价于 -XX:InitialHeapSize
“-Xmx" 则用于表示堆区的最大内存,等价于 -XX:MaxHeapSize
- 一旦堆区中的内存大小超过 “-Xmx” 所指定的最大内存时,将会抛出 OutOfMemoryError 异常。
通常会将 -Xms 和 -Xmx 两个参数配置相同的值,其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
开发中建议将初始堆内存和最大堆内存设置成相同的值
默认情况下:
- 初始内存大小:物理电脑内存大小/64
- 最大内存大小:物理电脑内存大小/4
public class HeapSpaceInitial {
public static void main(String[] args) {
// 返回Java虚拟机中的堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
// 返回Java虚拟机试图使用的最大堆内存
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms:" + initialMemory + "M");
System.out.println("-Xmx:" + maxMemory + "M");
}
}
输出结果
-Xms:245M
-Xmx:3623M
如何查看堆内存的内存分配情况
- jps -> jstat -gc 进程id
- -XX:+PrintGCDetails
为什么设置初始堆内存为600M,实际只有575M?
答:因为在新生代中,数据存放在 Eden 区和 Survivor 区,其中 Survivor0 和 Survivor1 区只能二选一存放,少了一个25600 / 1024 = 25M。
OutOfMemory 举例
package com.cy.java;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class HeapTest {
byte[] buffer=new byte[new Random().nextInt(1024*200)];
public static void main(String[] args) {
List<HeapTest> list = new ArrayList<>();
while(true) {
list.add(new HeapTest());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
轻年代与老年代
存储在 JVM 中的 Java 对象可以被划分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速 - 生命周期短的,及时回收即可
- 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与 JVM 的生命周期保持一致
Java 堆区进一步细分的话,可以划分为
- 年轻代(YoungGen)
- 老年代(OldGen)
年轻代又可以划分为
- Eden 空间、
- Survivor0 空间和 Survivor1 空间(有时也叫做 From 区、To 区)
下面默认参数开发中一般不会调:
- Eden : From : To -> 8 : 1 : 1
- 新生代 : 老年代 - > 1 : 2
配置新生代与老年代在堆结构的占比。
- 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
- 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优
在 HotSpot 中,Eden 空间和另外两个 Survivor 空间缺省所占的比例是8 : 1 : 1,当然开发人员可以通过选项“-XX:SurvivorRatio”调整这个空间比例。比如-XX:SurvivorRatio=8
为什么默认是8:1:1,而实际当中是6:1:1?
答:因为存在自适应机制,即-XX:-UseAdaptiveSizePolicy(+启用,-禁用),但这种方法一般不能生效,所以一般采用-XX:SurvivorRatio=8
可以使用选项"-Xmn"设置新生代最大内存大小(优先级高于-XX:NewRatio)
对象生命周期
- 几乎所有的 Java 对象都是在 Eden 区被 new 出来的。
- 绝大部分的 Java 对象的销毁都在新生代进行了。
- 有些大的对象在 Eden 区无法存储时候,将直接进入老年代
IBM 公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。
图解对象分配过程
概述
为新对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑
- 内存如何分配、
- 在哪里分配等问题
- 并且由于内存分配算法与内存回收算法密切相关
- 所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片。
图解过程
我们创建的对象,一般都是存放在 Eden 区的,当我们 Eden 区满了后,就会触发 GC 操作,一般被称为 YGC / Minor GC 操作
当我们进行一次垃圾收集后,红色的将会被回收,而绿色的还会被占用着,存放在S0(Survivor From) 区。同时我们给每个对象设置了一个年龄计数器,一次回收后就是1。
同时 Eden 区继续存放对象,当 Eden 区再次存满的时候,又会触发一个 MinorGC 操作,此时 GC 将会把 Eden 和 Survivor From 中的对象进行一次收集,把存活的对象放到 Survivor To区,同时让年龄 + 1
我们继续不断的进行对象生成和垃圾回收,当 Survivor 中的对象的年龄达到15的时候,将会触发一次 Promotion 晋升的操作,也就是将年轻代中的对象晋升到老年代中
思考:幸存区区满了后?
特别注意,在 Eden 区满了的时候,才会触发 Minor GC,而 Survivor 区满了后,不会触发 Minor GC 操作
如果 Survivor 区满了后,将会触发一些特殊的规则,也就是可能直接晋升老年代
举例:以当兵为例,正常人的晋升可能是 : 新兵 -> 班长 -> 排长 -> 连长
但是也有可能有些人因为做了非常大的贡献,直接从 新兵 -> 排长
对象分配的特殊情况
常用的调优工具
总结
- 针对幸存者 S0,S1 区的总结:复制之后有交换,谁空谁是 To
- 关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不再永久代和元空间进行收集
- 新生代采用复制算法的目的:是为了减少内碎片
Minor GC,MajorGC、Full GC
- Minor GC:新生代的 GC
- Major GC:老年代的 GC
- Full GC:整堆收集,收集整个 Java 堆和方法区的垃圾收集
我们都知道,JVM 的调优的一个环节,也就是垃圾收集,我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,容易出现 STW 的问题 而
Major GC 和 Full GC 出现 STW 的时间,是 Minor GC 的10倍以上
JVM 在进行 GC 时,并非每次都对上面三个内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。
针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大种类型:
- 一种是部分收集(Partial GC)
- 一种是整堆收集(Full GC)
部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
- 老年代收集(Major GC/Old GC):只是老年代的圾收集。
目前,只有 CMS GC 会有单独收集老年代的行为。 - 注意,很多时候 Major GC会和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
目前,只有 G1 GC 会有这种行为
整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。
Minor GC
年轻代GC(Minor GC)触发机制:
- 当年轻代空间不足时,就会触发 Minor GC ,这里的年轻代满指的是 Eden 代满,Survivor 满不会引发 GC 。(每次Minor GC 会清理年轻代的内存。)
- 因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
- Minor GC 会引发 STW ,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
STW:Stop The World
Major GC
老年代GC(Major GC/Full GC)触发机制:
- 指发生在老年代的 GC ,对象从老年代消失时,我们说 “Major GC” 或 “Full GC” 发生了
- 出现了 Major GC ,经常会伴随至少一次的 Minor GC (但非绝对的,在 Parallel Scavenge
收集器的收集策略里就有直接进行 Major GC 的策略选择过程) - 也就是在老年代空间不足时,会先尝试触发 Minor GC 。如果之后空间还不足,则触发 Major GC Major GC 的速度一般会比Minor GC 慢10倍以上, STW 的时间更长
- 如果 Major GC 后,内存还不足,就报 OOM 了
Full GC
触发 Full GC 执行的情况有如下五种:
- 调用 System.gc() 时,系统建议执行 Full GC ,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存
- 由 Eden 区、Survivor space0(From Space)区向 Survivor space1(To Space)区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
说明:Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些
GC 举例
我们编写一个 OOM 的异常,因为我们在不断的创建字符串,是存放在元空间的
public class GCTest {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "mogu blog";
while(true) {
list.add(a);
a = a + a;
i++;
}
}catch (Exception e) {
e.getStackTrace();
}
}
}
设置 JVM 启动参数
-Xms10m -Xmx10m -XX:+PrintGCDetails
打印出的日志
触发 OOM 的时候,一定是进行了一次 Full GC ,因为只有在老年代空间不足时候,才会爆出 OOM 异常
STW
STW ,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行