1.堆的核心概述
1.1 认识堆内存
1.1.1 堆与进程
- 一个java程序运行起来对应一个JVM进程
- 一个JVM进程对应一个JVM实例,一个JVM实例中只有一个运行时数据区;
- 一个运行时数据区只有一个方法区和堆,而一个进程中有多个线程,这就意味着一个进程中的多个线程就要共享方法区和堆;每个线程独享程序计数器、本地方法栈、虚拟机栈
1.1.2 对堆的认识
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
- Java堆区在
JVM启动
的时候即被创建,JVM启动后其空间大小也就确定了【类比数组】。
堆是JVM管理的最大一块内存空间;
堆内存的大小是可以调节的,在启动java程序前对jvm堆内存大小进行配置;
jvm什么时候启动?
java程序启动起来的时候,jvm实例就会通过引导类加载器帮我们启动起来
-
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
-
一个进程中的所有的线程共享Java堆,但是在这里堆内存还可以划分线程私有的缓冲区[Thread Local Allocation Buffer,TLAB]
面试问题:堆空间一定是所有线程共享的么?不是,TLAB缓冲区在堆中,是线程独有的) -
《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated)
老师说:从实际使用角度看的,“几乎”所有的对象实例都在这里分配内存。因为还有一些对象是在栈上分配的(逃逸分析,标量替换) -
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
public class SimpleHeap {
private int id;//属性、成员变量
public SimpleHeap(int id) {
this.id = id;
}
public void show() {
System.out.println("My ID is " + id);
}
public static void main(String[] args) {
SimpleHeap sl = new SimpleHeap(1);
SimpleHeap s2 = new SimpleHeap(2);
int[] arr = new int[10];
Object[] arr1 = new Object[10];
}
}
-
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
(1)也就是触发了GC的时候,才会进行回收
(2)如果堆中对象马上被回收,那么用户线程就会收到影响,因为有stop the word
如上面的代码示例中,main方法执行结束了,但是堆内存中的s1和s2实例不会被立马回收;
因为方法是用来被调用的,如果方法中存在对实例的引用,方法弹出栈就回收,那么GC的频率太高了,会影响使用体验 -
堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
1.2堆内存结构细分
- Java 7及之前堆内存逻辑上分为三部分:新生区 + 养老区 + 永久区
- Young/New Generation Space 新生区(Young/new),又被细划分为Eden区和Survivor区
- Old/Tenure generation space 养老区(Old)
- Permanent Space永久区 (Perm)
- Java 8及之后堆内存逻辑上分为三部分:新生区 + 养老区 + 元空间
- Young/New Generation Space 新生区,又被划分为Eden区和Survivor区
- Old/Tenure generation space 养老区
- Meta Space 元空间 Meta
- 约定:新生区= 新生代=年轻代 、 养老区=老年区 =老年代、 永久区 = 永久代
- 堆空间内部结构,JDK1.8开始从永久代 替换成 元空间
2.堆内存大小的设置
- 由于一个java程序运行起来对应一个JVM进程,也就是一个jvm实例,因此可以针对每一个java程序来设置其jvm参数
- Jvm参数的设置是在java运行环节加入进来的,和编译没关系,因此可以先将java代码编译成字节码文件,在执行java指令准备运行代码的时候,将jvm参数加入进来
2.1 堆内存的 JVM参数
-Xms和-Xmx
-
参数影响的内存范围
逻辑上分为新生区+养老区的总大小(即Xms/Xmx分配的内存物理上没有涉及方法区) -
-Xms:用于表示堆区的起始内存,等价于 -XX:InitialHeapSize
-X:jvm 的运行参数
ms:memory start -
-Xmx:用于表示堆区的最大内存,等价于 -XX:MaxHeapSize
一旦堆区中的内存大小超过 -Xmx 所指定的最大内存时,将会抛出 OutOfMemoryError 异常。 -
生产环境中,通常会将 -Xms 和 -Xmx 两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能【避免频繁扩容、释放,避免GC,让系统承担额外压力】
-
配置大小的单位默认是byte
2.2 IDEA中设置堆内存大小示例
1.程序demo
package chapter08;
/**
* -Xms10m
* -Xmx10m
*/
public class HeapDemo {
public static void main(String[] args) {
System.out.println("start.....");
try{
Thread.sleep(1000000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("end....");
}
}
2.编译
3.配置jvm参数
4.运行java程序
2.2 堆内存的默认大小
- 默认情况下:
初始内存大小:物理内存大小 / 64
最大内存大小:物理内存大小 / 4
package chapter08;
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"); //-Xms: 243M
System.out.println("-Xmx: " + maxMemory + "M"); //-Xmx: 3604M
System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
//系统内存大小为:15.1875G
System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");
//系统内存大小为:14.078125G
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.查看java程序对应的jvm内存的四种方式
3.1 代码打印
- 手动设置下面的程序的JVM参数:-Xms600m -Xmx600m
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");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
打印结果为:
-Xms: 575M
-Xmx: 575M
这里减去了一个s区,所以没有600m
3.2 命令行方式
通过CMD进入终端jps + jstat -gc 进程id
- jps 指令可以查看当前进行的java进程
- jstat -gc 进程id: 查看进程内存使用情况
各个参数说明:
- 单位是Kbyte;
- S0和S1为幸存者区,EC为Eden区;
- OC为老年代;
- 新生代总内存 = S0C+S1C+EC (25m+25m+150m = 200m)
- 新生代已用内存 = S0U + S1U + EU
- 老年代总内存 = OC (400m)
- 老年代已用内存 = OU
堆总内存 = S0C + S1C + OC +EC
设置堆大小为600m,为什么打印结果为575m?
- Runtime.getRuntime().totalMemory() 和 Runtime.getRuntime().maxMemory()的打印结果为575M
- 因为幸存者区S0和S1各占据了25m,但是他们始终有一个是空的; 新生代存放对象的是伊甸园区和一个幸存者区。
- 通过Runtime获取的Memory的大小为: S0(或者S1) + Eden区 + old区;少了一个S区,所以计算结果比实际的堆内存要小
3.3 VM参数 控制台打印详细
在VM Options 添加 -XX:+PrintGCDetails
注意:这里新生代总大小179200k 仍然是只计算了eden+一个S区
3.4 Java VisualVM
- Java VisualVM是jdk自带的工具,在 jdk目录下的bin目录下的
jvisualvm.exe
- 需要安装额外的Visual GC插件来看堆内存明细
4.OOM
OOM原因
在对老年代进行GC之后,GC不意味着清空,再往老年代添加对象,放不下了,C了,就
OOM Demo代码
/**
* -Xms600m -Xmx600m
* @author shkstart shkstart@126.com
* @create 2020 21:12
*/
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<>();
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024 * 1024)));
}
}
}
在内存中创建一个list,不断地添加元素;执行一段时间爆OOM
minor gc后,如果s区放不下了,会放进老年代,因此每次gc会导致老年代增加
5.年轻代与老年代的内存分配设置
- 存储在堆内存中的java对象可以被分为两类:
- 生命周期短暂,这类对象的创建和消亡都非常迅速
- 另一类对象生命周期长,极端情况下还能与JVM生命周期保持一致。
- 对应的Java堆区进一步细分可以分为年轻代(YoungGen)和老年代(OldGen)
- 其中年轻代可以分为Eden空间、Survivor0空间和Survivor1空间(有时也叫frmo区,to区)
5.1 配置新生代与老年代在堆结构的占比
Demo代码
/**
* -Xms600m -Xmx600m
*
* -XX:NewRatio : 设置新生代与老年代的比例。默认值是2.
* -XX:SurvivorRatio :设置新生代中Eden区与Survivor区的比例。默认值是8
* -XX:-UseAdaptiveSizePolicy :关闭自适应的内存分配策略 (暂时用不到)
* -Xmn:设置新生代的空间的大小。 (一般不设置)
*
* @author shkstart shkstart@126.com
* @create 2020 17:23
*/
public class EdenSurvivorTest {
public static void main(String[] args) {
System.out.println("我只是来打个酱油~");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
-
配置参数 -XX:NewRatio
-
一般不会调这个比例,当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优
-
默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
5.2 配置Eden和S在新生代中的占比
- 配置参数
-XX:SurvivorRatio
-XX:SurvivorRatio= 8 即 8:1:1 - 在hotSpot中,默认的设置为:Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1
- 开发人员可以通过选项 -XX:SurvivorRatio 调整空间比例,如-XX:SurvivorRatio=4
配置演示
(1) 设置堆内存为600M:
(2) 此时不设置-XX:SurvivorRatio= 8 查看堆内存情况:
会发现Eden只有150M,S区25M【eden和s区不是按照8:1:1来的,而是6:1:1】
(3) 要想实现真正的8:1:1 必须显示的设置:
如下所示 设置虚拟机参数:
可以看出实现了8:1:1
5.3 -XX:-UseAdaptiveSizePolicy
- -XX:-UseAdaptiveSizePolicy:关闭自适应的内存分配策略
- -XX:+UseAdaptiveSizePolicy:开启自适应的内存分配策略
内存分配策略即使关闭了,Eden:S仍然是6:1:1,所以必须显式设置Eden和S的比例才能设置为8:1:1
5.4 -Xmn 设置新生代最大内存
- -Xmn设置新生代最大内存,如果和-XX:NewRatio 冲突的时候,以-Xmn为准。
- 这个参数一般使用默认值
5.5 通过命令行查看各种比例
-
查看新生代与老年代的比例
jps
jinfo -flag NewRatios 进程id -
查看新生区中伊甸园区与幸存者区的比例
jps
jinfo -flag SurvivorRatio 进程id