图解带你掌握`JVM`运行时核心内存区
摘要:堆空间差不多是最大的内存空间,也是运行时数据区最重要的内存空间。堆可以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的。
堆的概述
一般来说:
一个Java程序的运行对应一个进程;
一个进程对应着一个JVM实例(JVM的启动由引导类加载器加载启动),同时也对应着多个线程;
一个JVM实例拥有一个运行时数据区(Runtime类,为饿汉式单例类);
一个运行时数据区中的堆和方法区是多线程共享的,而本地方法栈、虚拟机栈、程序计数器是线程私有的。
堆空间差不多是最大的内存空间,也是运行时数据区最重要的内存空间。堆可以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
堆内存大小设置
堆一旦被创建,它的大小也就确定了,初始内存默认为电脑物理内存大小的1/64,最大内存默认为电脑物理内存的1/4,但是堆空间的大小是可以调节,接下来我们来演示一下。
准备工具
JDK自带内存分析的工具:在已安装JDK的bin目录下找到jvisualvm.exe。打开该软件,下载插件Visual GC,一定要点击检查最新版本,否则会导致安装失败。

安装完重启jvisualvm

代码样例
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...");
}
}
IDEA设置


-Xms10m用于表示堆区的起始内存为10m,等价于-XX:InitialHeapSize;
-Xmx10m用于表示堆区的最大内存为10m,等价于-XX:MaxHeapSize;
其中-X是JVM的运行参数,ms是memory start
通常会将-Xms和-Xmx两个参数配置相同的值,其目的就是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
启动程序
启动程序之后去jvisualvm查看

一旦堆区中的内存大小超过-Xmx所指定的最大内存时,将会抛出OOM(Out Of MemoryError)异常。
堆的分代
存储在JVM中的java对象可以被划分为两类:
一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速;
另一类是生命周期非常长,在某些情况下还能与JVM的生命周期保持一致;
堆区分代
经研究表明70%-99%的对象属于临时对象,为了提高GC的性能,Hotspot虚拟机又将堆区进行了进一步划分。

如图所示,堆区又分为年轻代(YoungGen)和老年代(OldGen);其中年轻代又分为伊甸园区(Eden)和幸存者区(Survivor);幸存者区分为幸存者0区(Survivor0,S0)和幸存者1区(Survivor1,S1),有时也叫from区和to区。
分代完成之后,GC时主要检测新生代Eden区。
统一概念:
新生区<=>新生代<=>年轻代养老区<=>老年区<=>老年代
几乎所有的Java对象都是在Eden区被new出来的,有的大对象在该区存不下可直接进入老年代。绝大部分的Java对象都销毁在新生代了(IBM公司的专门研究表明,新生代80%的对象都是“朝生夕死”的)。
新生代与老年代在堆结构的占比
默认参数-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3;
可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5;
该参数在开发中一般不会调整,如果生命周期长的对象偏多时可以选择调整。
Eden与Survivor在堆结构的占比
在HotSpot中,Eden空间和另外两个Survivor空间所占的比例是8:1:1(测试的时候是6:1:1),开发人员可以通过选项-XX:SurvivorRatio调整空间比例,如-XX:SurvivorRatio=8
可以在cmd中通过jps 查询进程号-> jinfo -flag NewRatio(SurvivorRatio) + 进程号 查询配置信息
-Xmn设置新生代最大内存大小(默认就好),如果既设置了该参数,又设置了NewRatio的值,则以该参数设置为准。
查看设置的参数
以上边的代码为例:设置启动参数-XX:+PrintGCDetails;可在cmd窗口中输入jps查询进程号,然后通过jstat -gc 进程id指令查看进程的内存使用情况。

图解对象分配过程
对象分配过程

S0,S1满时不会触发YGC,但是YGC会回收S0,S1的对象。
总结
针对幸存者s0,s1区:复制之后有交换,谁空谁是to;
关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不再永久区/元空间收集。
对象特殊情况分配过程

内存分配策略
针对不同年龄段的对象分配原则如下:
数值变小原理
代码样例,设置参数:-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
明明设置的600M,怎么变成575M了呢?这是因为在堆内存存取数据时,新生代里边只有伊甸园和幸存者1区或者是幸存者2区存储对象,所以会少一个幸存者区的内存空间。
GC
JVM进行GC时,并非每次都对新生代、老年代、方法区(永久代、元空间)这三个区域一起回收,大部分回收是指新生代。
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
Partial GC
部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
新生代收集(Minor GC/Young GC):只是新生代的垃圾收集;
老年代收集(Major GC/Old GC):只是老年代的垃圾收集;
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集,只有G1 GC (按照region划分新生代和老年代的数据)会有这种行为。
目前,只有CMS GC会有单独收集老年代的行为;很多时候Major GC会和Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。
Full GC
整堆收集(Full GC):整个java堆和方法区的垃圾收集。
触发机制
年轻代GC(Minor GC)触发机制
老年代GC(Major GC/Full GC)触发机制
Full GC触发机制
触发Full GC执行的情况有以下五种:
Full GC是开发或调优中尽量要避免的,这样暂停时间会短一些。