Java运行时内存区主要分为 运行时栈(虚拟机栈)、本地方法栈、程序计数器、堆空间、方法区(JDK1.8之后是元空间),今天来聊一聊我们的堆空间.
一个对象或者数组的创建是在堆空间中完成的,堆的大小是有限的(固定的),所以,必不可少的我们要考虑一下堆的空间分配问题和对象的分配问题.
堆空间默认的初始化内存最小值为 系统内存/64,最大值为系统内存/4;我们可以通过命令 -Xms666m -Xmx666m,来将堆空间的初始化大小最小值和最大值都设置为666m;
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");
}
}
输出结果如下:
因为我的系统内存是16G,所以对应的 243M 和3602M.
设置堆空间的最小和最大内存之后,输出结果如下:
看到这里大家也许会有疑问,为什么这里输出的数据会跟我们的实际分配的数据有一定的差别呢? 来,我们来详细探究一下.运行起来刚才的程序,然后我们通过命令行来进行查看
这里 可以看到具体的堆空间的每个空间分配的大小情况.大家用电脑中的计算机进行加一下,可以发现.
S0C(S0区)+S1C(S1区)+EC(Eden区)+OC(老年代区) = 681984/1024 = 666M~
咦,这里怎么会又一样了呢, 那我们再做一下试验,S0C(S0区)+EC(Eden区)+OC(老年代区) = 653824/1024=638M,这个结果跟我们输出的结果是一样的.
这是因为在JVM规定当中,新生代区分为Eden区(伊甸园区)、S0区(Survivor0区,幸存者0区)S1区(Survivor1区,幸存者1区) ,而在两个幸存者区中只有一个空间会存放数据,另外一个空间是空的,所以在计算堆空间大小的时候,我们只计算一个空间大小.这也就是为什么会有一定偏差的原因
通过上面的实验结果,我们也可以发现新生代区和老年代区的默认分配大小比例为1:2(222M:444M).我们可以通过参数-XX:NewRatio=a,来设置新生代和老年代的大小比例为1:a,
我们在通过随便跑一个没有设置过的分配比例的代码(如图,我随便用了一个EdenSurvivorTest方法),
代码如下:
public class EdenSurvivorTest {
public static void main(String[] args) {
System.out.println("进行测试,进行测试");
try {
//睡一会,方便我们查看命令
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
然后再cmd中打入命令jsp,查看当前运行的方法对应的pid,然后用命令 jinfo -flag NewRatio pid 来查看,得出结论,默认的新生区和老年区的大小比例为1:2.
对象最一开始创建的时候是分配在在Eden区中的,当我们不断的创建对象,Eden区不可避免的会存放满,这个时候就会触发YGC,而此时根据算法得出那些对象是还有引用的,这个时候就会将这些对象放入到Survivor区中的S0区(随机放入S0和S1,此处我们用S0举例子),此时S0区就是FROM区,S1区就是TO区. 这些放入到Survivor区中的对象会有一个age(年龄计数器),记录这个对象在Survivor区中存放的迭代次数.每次迭代之后,age+1.
随后在不断迭代这个过程中,如果Survivor区中有兑现给的age到达了 我们设置的-XX:MaxTenuringThreshold 次数(默认是15,但是不同的JVM和不同的GC ,此数值都不相同),此时我们就会将Survivor区中的对象放到到Old(老年代区).
通过代码来验证一下:
public class HeapInstanceTest {
//创建一个随机大小的字节数组
byte[] buffer = new byte[new Random().nextInt(1024 * 200)];
public static void main(String[] args) {
ArrayList list = new ArrayList();
while (true) {
list.add(new HeapInstanceTest());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
程序最终会以OOM异常结束.在结束前,我们可以使用JDK自带的Java VisualVM工具来查看此时内存的情况:
由图中可以看出.Eden区的大小是不断增多到达满值之后触发GC一下降低到0,然后继续增多....而对应的,Eden区GC之后,有引用的对象放入到了S0区,然后迭代次数多了之后Old区的对象开始多了起来.
当我们创建的新对象太大,超出了Eden区大小或者是本来Eden区就已经存放了一些数据的时候,此时新对象创建,判断Eden区放不下,这时候就会触发YGC,如果此时Eden区放得下,那么我们就会将之放入Eden区,这也是我们上面说的一般情况中的.
而如果是因为创建的新对象太大,YGC之后,Eden区依然放不下的话,我们就判断Old区是否能放下,如果能放下的话,直接分配到Old区,而如果放不下的话,我们就会触发FGC,FGC之后如果能放下,我们就会放入到Old区;FGC之后Old区依然放不下的话,JVM就会直接报出OOM异常,退出程序.
当我们对象放入Eden区之后,Eden区满,触发YGC,此时会将还有引用的对象放入到Survivor区,如果此时Survivor区放不下的话,就会直接晋升到老年代.
还有一种情况是,如果在Survivor区中相同年龄的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到-XX:MaxTenuringThreshold中要求的年龄。