文章目录
- 堆
- 堆的概述
- 堆的细分内存结构
- 设置堆内存大小和OOM
- 年轻代与老年代
- 图解对象分配和回收过程
- Minor GC、Major GC、Full GC的对比
- 堆空间分代思想
- 内存分配策略
- 为对象分配内存:TLAB
- 堆是分配对象的唯一选择吗
堆
堆的概述
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域;
- Java堆在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间;
- 堆内存的大小是可以调节的;
- 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但是在逻辑上,它应该被视为连续的;
- 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer、TLAB);
package org.westos.heap;
/**
* -Xms10m -Xmx10m
* @author lwj
* @date 2020/9/14 13:15
*/
public class HeapDemo1 {
public static void main(String[] args) {
System.out.println("start");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end");
}
}
package org.westos.heap;
/**
* -Xms20m -Xmx20m
* @author lwj
* @date 2020/9/14 13:16
*/
public class HeadDemo2 {
public static void main(String[] args) {
System.out.println("start");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end");
}
}
-Xms10m # 初始堆大小
-Xmx10m # 最大堆大小
跑两个程序,启动两个main方法(两个进程,两个JVM实例),分别设置堆内存大小,用下面的工具看。
双击打开JDK安装目录/bin目录下的jvisualvm.exe
程序。
给jdk自带的jvisualvm安装Visual GC插件。
选择Visual GC,下载。
在上面那张图片中的工具—>插件,点击添加插件,
选择安装,重启jvisualvm。即可看到Visual GC。
- 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上
- 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或数组在堆中的位置;
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除;
- 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
public class SimpleHeap {
private int id;
public SimpleHeap(int id) {
this.id = id;
}
public static void main(String[] args) {
SimpleHeap simpleHeap1 = new SimpleHeap(1);
SimpleHeap simpleHeap2 = new SimpleHeap(2);
int[] arr = new int[10];
Object[] objs = new Object[20];
}
}
0 new #3 <org/westos/heap/SimpleHeap>
3 dup
4 iconst_1
5 invokespecial #4 <org/westos/heap/SimpleHeap.<init>>
8 astore_1
9 new #3 <org/westos/heap/SimpleHeap>
12 dup
13 iconst_2
14 invokespecial #4 <org/westos/heap/SimpleHeap.<init>>
17 astore_2
18 bipush 10
20 newarray 10 (int)
22 astore_3
23 bipush 20
25 anewarray #5 <java/lang/Object>
28 astore 4
30 return
堆的细分内存结构
1、Java7及之前堆内存逻辑上分为三部分:新生区 + 养老区 + 永久区;
新生区
- Eden区(伊甸园) + Survivor区(幸存区)
养老区
永久区(Permanent Space)
2、Java8及之后堆内存逻辑上分为三部分:新生区 + 养老区 + 元空间;
新生区
- Eden区 + Survivor区
养老区
元空间(Meta Space)
注意:s0和s1在同一个时刻,只有一个区存在对象。
设置堆内存大小和OOM
Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,可以通过选项-Xms
和-Xmx
来进行设置。
-
-Xms
用于表示堆区的起始内存,等价于-XX:InitialHeapSpace
; -
-Xmx
则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
;
一旦堆区中的内存大小超过-Xmx
所指定的最大内存时,将会抛出OutOfMemoryError
异常。
通常会将-Xms和-Xmx两个参数配置相同的值,其目的在于为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,提高性能。
默认情况下,初始内存:物理电脑内存大小 / 64;
最大内存大小:物理电脑内存大小 / 4。
package org.westos.heap;
/**
* -Xms:用来设置堆空间(年轻代 + 老年代)的初始内存大小
* -X:JVM运行参数
* ms:memory start
* -Xmx:用来设置堆空间能获取到的最大内存
* @author lwj
* @date 2020/9/14 18:14
*/
public class HeapSpaceInitial {
public static void main(String[] args) {
//返回Java虚拟机中的堆内存总量
long totalMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms:" + totalMemory + "M");
System.out.println("-Xmx:" + maxMemory + "M");
//默认情况下
//-Xms:123M 物理内存1/64 1/8G
//-Xmx:1804M 物理内存1/4 2G
}
}
查看堆空间大小:
方式一:
jps
# 查看进程id
jstat -gc 进程id
# 查看对应进程的堆空间大小
以HeapDemo1为例,设置-Xms10m -Xmx10m。
方式二:加参数
-XX:+PrintGCDetails
HeapSpaceInitial的例子(参数:-Xms10m -Xmx10m -XX:+PrintGCDetails)
public class HeapSpaceInitial {
public static void main(String[] args) {
//返回Java虚拟机中的堆内存总量
double totalMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024.0;
double maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024.0;
System.out.println("-Xms:" + totalMemory + "M");
System.out.println("-Xmx:" + maxMemory + "M");
//当设置参数-Xms10m -Xmx10m
//-Xms:9.5M
//-Xmx:9.5M
//为什么是9.5M呢?因为survivor0区和survivor1区同时只能有一个区域存储对象
}
}
-Xms:9M
-Xmx:9M
Heap
PSYoungGen total 2560K, used 1591K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 77% used [0x00000000ffd00000,0x00000000ffe8dc48,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
Metaspace used 3076K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 334K, capacity 388K, committed 512K, reserved 1048576K
年轻代2560 = Eden space 2048 + from区 / to区
2048 + 512 + 512 + 7168 = 10240K
2048 + 512 + 7168 = 9.5m
OOM
Out Of Memory内存溢出。
在堆中new很多对象。
package org.westos.heap;
import java.util.ArrayList;
/**
* -Xms30m -Xmx30m
* @author lwj
* @date 2020/9/14 19:07
*/
public class OOMTest {
public static void main(String[] args) {
ArrayList<RandomTest> list = new ArrayList<>();
for (int i = 1; ; i++) {
try {
Thread.sleep(20);
//为了能看到过程
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new RandomTest(i));
}
}
}
class RandomTest {
private byte[] b;
public RandomTest(int length) {
this.b = new byte[length];
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at org.westos.heap.RandomTest.<init>(OOMTest.java:29)
at org.westos.heap.OOMTest.main(OOMTest.java:20)
当老年代已经满了时,已经不能GC了,就爆了。
Eden space —> s0 / s1 —> Old space。当使用Visual GC时,可以观察到细节。
年轻代与老年代
Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(OldGen),其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫作from区、to区)。
默认新生代与老年代在堆结构的占比:
- 默认
-XX:NewRatio=2
,表示新生代占1,老年代占2,新生代占整个堆的1/3; - 可以修改
-XX:NewRatio=4
,表示新生代占1,老年代占4,新生代占整个堆的1/5。
NewRatio:设置新生代与老年代的比例情况。
用指令查看某个进程的NewRatio比例。
jinfo -flag NewRatio 进程id
在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1。
当然开发人员可以通过选项-XX:SurvivorRatio
调整这个空间比例。
比如-XX:SurvivorRatio=8
。
几乎所有的Java对象都是在Eden区被new出来的。
绝大部分的Java对象的销毁都在新生代进行了;
- IBM公司的研究表明,新生代中80%的对象都是朝生夕死的;
可以使用选项-Xmn
设置新生代最大内存大小。
- 这个参数一般使用默认值就可以了。
事实上使用时,并不是8:1:1。
-XX:-UseAdaptiveSizePolicy
:关闭自适应的内存分配策略。
图解对象分配和回收过程
可达性分析算法(Eden、Minor GC)
为每一个对象分配一个年龄计数器,从Eden区到幸存者区,赋值为1。
第二次Minor GC。(From S0 —> To S1)
第三次Minor GC。(From S1 —> To S0)
触发Minor GC的条件:Eden区满的时候。
当Eden区满的时候,触发Minor GC(Young GC),会将Eden区和幸存者区一起进行回收。
关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久代/元空间收集。
对象分配的特殊情况
常用调优工具
JDK命令行
- jinfo
- jstat
- javap
Jconsole
VisualVM
Jprofiler(性能监控与调优)
Java Flight Recorder
GCViewer
Minor GC、Major GC、Full GC的对比
JVM在进行GC时,大部分时候回收的都是指新生代。
针对HotSpot VM的实现,它里面的GC按照回收区域分为两大种类型,一种是部分收集(Partial GC),一种是整堆收集(Full GC)。
- 部分收集:不是完整收集整个Java堆的垃圾,其中又分为:
- 新生代收集(Minor GC / Young GC):只是新生代(Eden、S0/S1)的垃圾收集;
- 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。
- 目前只有CMS GC会有单独收集老年代的行为;
- 很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收;
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
年轻代GC(Minor GC)触发机制:
- 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC;
- 因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快;
- Minor GC会引发STW,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。
老年代GC(Major GC / Full GC)触发机制:
- 指发生在老年代的GC,对象从老年代消失时,我们说Major GC或Full GC发生了;
- 出现了Major GC,经常会伴随至少一次的Minor GC;
- Major GC的速度一般会比Minor GC满10倍以上,STW的时间更长;
- 如果Major GC后,内存还不足,就报OOM。
Full GC触发机制:
- 调用System.gc(),系统建议执行Full GC,但是不必然执行;
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
- 由Eden区、S0区向S1区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
Full GC是开发或调优中尽量要避免的。
堆空间分代思想
为什么需要把Java堆分代?
- 不同对象的生命周期不同,70%-99%的对象是临时对象。
其实不分代完全可以,分代的唯一理由就是优化GC性能。
内存分配策略
为对象分配内存:TLAB
为什么要有TLAB(Thread Local Allocation Buffer)?
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据;
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的;
- 为了避免多个线程操作同一地址,需要使用加锁等机制。进而影响分配速度。
线程本地分配缓存区,这是一个线程专用的内存分配区域。
- 对Eden区域继续进行划分,JVM为每个线程分配了一个私有的缓存区域,它包含在Eden空间;
- JVM是将TLAB作为内存分配的首选;
- 在程序中,开发人员可以通过选项
-XX:UseTLAB
设置是否开启TLAB空间,默认情况下是开启的; - 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%;
- 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
堆是分配对象的唯一选择吗
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无需进行垃圾回收了。这也是最常见的堆外存储技术。
逃逸分析的基本行为就是分析对象动态作用域:
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸;
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生了逃逸。例如作为调用参数传递到其他地方中。
public void method() {
V v = new V();
//use v
v = null;
}
没有发生逃逸的对象,可以在栈上分配,随着方法执行的结束,栈空间就被移除。
同步省略:锁消除。
多个线程必须是同一把锁,同一个对象。
标量替换
标量是指一个无法再分解成更小数据的数据。Java中的原始数据类型就是标量。
相对的,那些还可以分解的数据叫做聚合量,Java中的对象就是聚合量。
如果经过逃逸分析,发现一个对象不会被外界访问,那么经过JIT优化,就会把这个对象拆解成若干个成员变量来代替,这个过程就是标量替换。