jvm虚拟机中的堆
- 一、JVM体系结构
- 二、Java堆简介
- 2.1 堆的特点
- 2.2 堆空间细分
- 2.3 堆空间的分代思想
- 2.4 堆的默认大小
- 三、JVM堆内存常用参数
- 四、垃圾回收算法(GC,Garbage Collection)
- 4.1 标记-清除(Mark-Sweep)
- 4.2 复制(Copy)
- 4.3 标记-整理(Mark-Compact)
- 五、常见错误
- 5.1 为什么会堆内存溢出?
一、JVM体系结构
我们接下来说的JVM中运行时数据区中的堆区(Heap Area)。
二、Java堆简介
对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块区域,在虚拟机启动时创建。此内存区域的唯一的目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配”。
2.1 堆的特点
- 一个JVM实例只存在一个堆内存,
堆也是Java内存管理的核心区域
。 - Java堆区在JVM启动的时候被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。但是
堆内存的大小是可以调节的
。 《Java虚拟机规范》
规定,堆可以处于**物理上不连续**
的内存空间中,但是**逻辑上**
它应该是被视为连续的
。- 堆针对一个 JVM 进程 来说是唯一的,也就是一个进程只有一个JVM,但是进程包含
多个线程,他们是共享同一堆空间的
。 - 所有的线程共享Java堆,在这里还可以划分线程私有的
缓冲区
(**Thread Local Allocation Buffer, TLAB**
)
2.2 堆空间细分
从上图中我们可以知道,堆的空间可以被细分为很多区:
年轻代(Young Gen)
老年代(Old Gen)
永久代(Perm Gen)
其中年轻代被分为 Eden 空间
、Survivor 1空间
(From区
)、Survivor 0空间
(To 区
),备注:谁空谁就是 To区
。
- 所有对象一出生都会在Eden区,并且对象年龄为0。Eden区满了之后会触发minor GC, minor GC只回收年轻代中需要回收的对象。每一次minor GC,若该对象没有被回收,那么对象的年龄就会 + 1。对象到达阈值以后该对象会进入老年代(android阈值为6)。
- 当老年代空间满了以后会触发full GC,full GC会同时回收年轻代和老年代中所有需要回收的对象。
2.3 堆空间的分代思想
年轻代、老年代、永久代
- 年轻代用于放置临时对象或者生命周期短的对象
- 老年代用于放置生命周期长的对象
- 永久代或者元空间,用于存放常量
- minor GC (
轻GC
)只管年轻代,Full GC(重GC
) 同时管理年轻代和老年代
为什么要分代?有什么好处?
- 经研究表明,不同对象的生命周期是不一致的,但是在具体的使用过程中,70%–90%的对象都是临时对象
- 分代唯一的理由就是优化GC的性能,如果没有分代,那么所有对象都处于一块空间,GC想要回收,则GC需要描述所有的对象,分代之后,长期持有的对象可以被挑出,短期持有的对象可以固定在一个位置回收,省掉很大一部分空间利用。
2.4 堆的默认大小
- 初始大小:电脑物理内存的 1/64
- 最大内存大小:电脑物理内存的 1/4
Runtime类:该类对应的JVM运行时数据区
long initialMemory = Runtime.getRuntime().totalMemory();
long maxMemory = Runtime.getRuntime().maxMemory();
long freeMemory = Runtime.getRuntime().freeMemory();
三、JVM堆内存常用参数
参数 | 说明 |
---|---|
-Xms | 堆内存初始大小,单位m(兆)、g(G) |
-Xmx(MaxHeapSize) | 堆内存最大允许大小,一般不要大于物理内存的80% |
-XX:PermSize | 非堆内存初始大小,一般应用设置初始化200m,最大1024m就够了 |
-XX:MaxPermSize | 非堆内存最大允许大小 |
-XX:NewSize(-Xns) | 年轻代内存初始大小 |
-XX:MaxNewSize(-Xmn) | 年轻代内存最大允许大小,也可以缩写 |
-XX:SurvivorRatio=8 | 年轻代中Eden区与Survivor区的容量比例值,默认为8,即8:1 |
-Xss | 堆栈内存大小 |
四、垃圾回收算法(GC,Garbage Collection)
以下几种垃圾回收算法只是抛砖引玉,具体详细的算法说明还需要自行搜索并加以学习,算法还有很多,所以学习的东西有很多哦。
红色是标记的非活动对象,绿色是活动对象。
4.1 标记-清除(Mark-Sweep)
GC分为两个阶段,标记和清除。首先标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。同时会产生不连续的内存碎片。碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得已再次触发GC。
4.2 复制(Copy)
将内存按容量划分为两块,每次只使用其中一块。当这一块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。这样使得每次都是对半个内存区回收,也不用考虑内存碎片问题,简单高效。缺点需要两倍的内存空间。
4.3 标记-整理(Mark-Compact)
也分为两个阶段,首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。此方法避免标记-清除算法的碎片问题,同时也避免了复制算法的空间问题。
一般年轻代中执行GC后,会有少量的对象存活,就会选用复制算法,只要付出少量的存活对象复制成本就可以完成收集。
而老年代中因为对象存活率高,没有额外过多内存空间分配,就需要使用标记-清理或者标记-整理算法来进行回收。
五、常见错误
5.1 为什么会堆内存溢出?
在年轻代中经过GC后还存活的对象会被复制到老年代中。当老年代空间不足时,JVM会对老年代进行完全的垃圾回收(Full GC)。如果GC后,还是无法存放从Survivor区复制过来的对象,就会出现OOM(Out of Memory)。
OOM(Out Of Memory)异常常见有以下几个原因:
老年代内存不足:java.lang.OutOfMemoryError:Javaheapspace
永久代内存不足:java.lang.OutOfMemoryError:PermGenspace
代码bug,占用内存无法及时回收
。
OOM在这几个内存区都有可能出现,实际遇到OOM时,能根据异常信息定位到哪个区的内存溢出。
可以通过添加个参数 -XX:+HeapDumpOnOutMemoryError
,让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。
熟悉了JAVA内存管理机制及配置参数,下面是对JAVA应用启动选项调优配置:
JAVA_OPTS=“-server -Xms512m -Xmx2g -XX:+UseG1GC -XX:SurvivorRatio=6 -XX:MaxGCPauseMillis=400 -XX:G1ReservePercent=15 -XX:ParallelGCThreads=4 -XX:
ConcGCThreads=1 -XX:InitiatingHeapOccupancyPercent=40 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:…/logs/gc.log”
设置堆内存最小和最大值,最大值参考历史利用率设置
设置GC垃圾收集器为G1
启用GC日志,方便后期分析
完结!