参考:《深入理解Java虚拟机》
《宋红康JVM教程》
堆是Java虚拟机所管理的最大的内存区域,由上图也可见,堆是线程共享的一个区域,在虚拟机启动时创建,堆的唯一作用便是存放对象实例
几乎所有的对象都在堆上分配内存=,这里的描述是“几乎”,因为随着栈上分配,标量替换这些优化技术,所有对象的分配在堆上不是那么“绝对”。
堆,是垃圾收集器管理的主要区域,虚拟机栈上不存在垃圾回收。
堆是运行时数据区的线程共享区域,但是在这里还可以划分线程私有的缓冲区(TLAB:Thread Local Allocation Buffer)
堆的内部结构在jdk7之后发生了比较的变化。
在jdk7以前:新生区+养老区+永久区
在jdk7之后:新生区+养老区+元空间:
由上图可以知道,堆区进一步细分可以划分为新生区、老年区,元空间其实是用来存储class的信息的,是方法区的实现,新生区又可以划分为Eden(伊甸园区)、Survivor0和Survivor1区(也可以叫做from、to区)。
通过下面的程序来解释对象在内存的具体分配过程:
/**
* @author 四五又十
* @version 1.0
* @date 2020/7/26 20:35
*/
public class HeapInstanceTest {
//创建一个数组 0-200k之间
byte[] buffer = new byte[new Random().nextInt(1024 * 200)];
public static void main(String[] args) {
//创建一个list集合
ArrayList<HeapInstanceTest> list = new ArrayList<HeapInstanceTest>();
while (true) {
//list集合里面一直new HeapInstanceTest
list.add(new HeapInstanceTest());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
以上程序执行,很显然会报一个异常OOM(这里说的异常时广义上的异常):
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.java.HeapInstanceTest.<init>(HeapInstanceTest.java:12)
at com.java.HeapInstanceTest.main(HeapInstanceTest.java:17)
堆空间分配对象的过程:
上面7个过程是一般的对象分配过程,其中可能会因为对象的大小不同会有一些其他的问题,例如:如果对象太大,Eden区一开始就放不下呢?如果出现Minor GC之后,将Eden区的剩余对象放入Survivor区时,Survivor区放不下?等等这些会通过下面流程图来详细解答
新对象申请时,首先判断Eden是否放得下,如果放得下那么该对象分配至Eden中,如果放不下需要进行Minor GC,Minor GC会将Eden中的对象进行清理,将不用的对象回收,需要使用的对象防止在Survivor区中,如果进行Minor GC后,Eden放得下,那么分配对象,如果还放不下,那么判断老年代是否放的下,如果放得下,那么分配对象,如果放不下,进行FGC,如果在FGC之后还是放不下,则报出OOM
在YGC的过程中,需要将Eden区中的可用对象放到Survivor区,如果放不下,则将这些对象直接晋升老年代,不需要年龄计数器达到15,如果放得下,那么检查该对象的年龄计数器,如果大于165,将该对象晋升老年代。
在默认情况下,堆的初始空间大小是:物理内存大小/64 ,最大内存大小是:物理内存大小/4,
/**
* @author 四五又十
* @version 1.0
* @date 2020/7/16 16:21
*/
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 : 123M
System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 1796M
System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");//系统内存大小为:7.6875G
System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");//系统内存大小为:7.015625G
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
通过上面程序计算出来的系统空间相差比较大,原因是:通过分配对象的过程可以知道,Survivor有两个区域,但是每次只能使用其中一个区域,所以最大可以使用空间是没有将另外一个Survivor计算上去的。
可以在cmd窗口上使用jstat -gc 进程id 查看各个区域的内存大小
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZxxmfgYe-1596377988932)(C:\Users\VSUS\Desktop\笔记\JVM\img\30.png)]
后面有个U的代表以及使用,那么123m是如下计算出来的:(S0C(或者S1C) + EC + OC)/1024 = 123MB
这里也证实了S区中只能使用其中一个区域
参数:
注意:一般会将-Xms和-Xmx两个参数配置相同的值,其目的就是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能
堆内部有新生区,养老区;新生区内有Eden区,S0,S1区,那么这些结构分别占堆整个空间大小的多少呢?
新生区与老年区占比
新生区与老年区的大小占比,可以使用参数:-XX:NewRatio,查看官方文档:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html可知,
-XX:NewRatio=ratio
Sets the ratio between young and old generation sizes. By default, this option is set to 2. The following example shows how to set the young/old ratio to 1:
-XX:NewRatio=1
-XX:NewRatio参数的默认值是2,也就是新生区 : 老年区 = 2,可以使用该参数来改变这个占比值
新生区内结构占比
参数: -XX:SurvivorRatio
默认值是8,参考官方文档
-XX:SurvivorRatio=ratio
Sets the ratio between eden space size and survivor space size. By default, this option is set to 8. The following example shows how to set the eden/survivor space ratio to 4:
-XX:SurvivorRatio=4
也就是Eden : S0 : S1 = 8 : 1 :1
例如:
/**
* -Xms30m -Xmx30m
*/
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...");
}
}
使用VisualVM工具查看各个空间占比
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GYBr1cVV-1596377988934)(C:\Users\VSUS\Desktop\笔记\JVM\img\31.png)]
就是和上面两个参数的默认值一致
为什么要把Java堆分代?不分代就不能正常工作了么
“几乎”所有的对象都在堆上分配,但是不是所有的对象都在堆上分配,如果一个对象经过逃逸分析后发现这个对象没有逃逸出方法的话,很有可能这个对象会被优化为“栈上分配”,那么对象所占用的空间就会随栈帧的出栈而销毁。
当一个对象在方法中定义之后,只能在方法内部使用,则认为没有发生逃逸;否则认为是发生了逃逸。
public void method(){
V v = new V();
//use V
//......
v = null;
}
上述例子中,对象V只能在方法内部使用,则认为对象V是没有发生逃逸的
通过逃逸分析,编译器可以做以下优化:
栈上分配:将堆分配转化为栈分配。如果一个对象在子线程中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以北方问道,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
下面只说明栈上分配,例如如下代码:
/**
* @author 四五又十
* @version 1.0
* @date 2020/7/23 21:54
*/
public class StackAllocation {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
// 查看执行时间
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start) + " ms");
// 为了方便查看堆内存中对象个数,线程sleep
try {
Thread.sleep(1000000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
User user = new User();//未发生逃逸
}
static class User {
}
}
由于使用的是jdk1.8版本,那么默认开启逃逸分析,运行代码
显式关闭逃逸分析,使用-XX:-DoEscapeAnalysis参数,则代码运行的结果为
由此可见,通过逃逸分析后栈上分配,代码运行速度提高不小!