天天看点

深入理解JVM虚拟机——Java内存模型结构之堆的概念

作者:一个即将被退役的码农

精彩预告: 下一篇我们会详细介绍“Java内存模型结构之方法区”的概念,记得关注哦!

思考一下

学习一项知识总该知道为什么要学习吧。有人会说,这些写代码好像又用不上,貌似所有的事情JVM都替我们做好了。那就,思考一下为什么要学习JVM虚拟机结构。

那你是否遇到这样的困惑:堆内存该设置多大?OutOfMemoryError异常到底是怎么引起的?如何进行JVM调优?JVM的垃圾回收是如何?甚至创建一个String对象,JVM都做了些什么?

这些疑问随着学习的深入都会慢慢得到解答,而要解决这些问题的第一步,就是先了解JVM的构成。

JVM内存结构

深入理解JVM虚拟机——Java内存模型结构之堆的概念

内存结构图

如果理解了上图,JVM的内存结构基本上掌握了一半。通过上图我们可以看到什么?

第一,JVM分为五个区域:虚拟机栈、本地方法栈、方法区、堆、程序计数器。

第二,JVM五个区中虚拟机栈、本地方法栈、程序计数器为线程私有,方法区和堆为线程共享区。图中已经用颜色区分,绿色表示“通行”,橘黄色表示停一停(需等待)。

第三,JVM不同区域的占用内存大小不同,一般情况下堆最大,程序计数器较小。那么最大的区域会放什么?当然就是Java中最多的“对象”了。

堆(Heap)

上面已经得出结论,堆内存最大,堆是被线程共享,堆的目的就是存放对象。几乎所有的对象实例都在此分配。当然,随着优化技术的更新,某些数据也会被放在栈上等。

因为堆占用内存空间最大,堆也是Java垃圾回收的主要区域(重点对象),因此也称作“GC堆”(Garbage Collected Heap)。

核心概念

1.Java堆区在JVM启动的时候即被创建。是JVM管理的最大一块内存空间,是GC执行垃圾回收的重点区域。

2.一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域,GC和OOM都存在。

《Java虚拟机规范》规定,堆可以处于物理上可以为不连续的内存空间中,但在逻辑上它应该被视为连续的。

3.所有的线程共享Java堆,但并不是共享堆的全部,内部同样存在每个线程私有的缓冲区(如:ThreadLocal Allocation Buffer,TLAB)。

4.《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。因为栈帧中保存的是引用,这个引用指向对象或者数组在堆中的位置。我要说的是:"几乎"所有的对象实例都在这里分配内存,有逃逸现象。

5. 在方法结束后,堆中的对象不会马上被移除,只有在垃圾收集的时候才会被移除。

堆空间大小的设置

1. -Xms:设置堆区的起始内存,等价于-XX:InitialHeapsize

2. -Xmx:设置堆区的最大内存,等价于-XX:MaxHeapSize

3. 一旦堆区中的内存大小超过-Xmx所指定的最大内存时,将会抛出OutOfMemoryError错误。

4. 通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新计算分配堆区的大小,从而提高性能。

5. 默认情况下,堆空间初始内存大小: 物理电脑内存大小 / 64,最大内存大小: 物理电脑内存大小 / 4

堆内存结构细分

在的垃圾收集器大部分都基于分代收集理论设计,JVM堆空间可细分为:

JDK7及以前:堆内存逻辑上分为:新生代 + 老年代 + 永久代、TLAB

Young Generation Space 新生代 Young/New:内部又划分为Eden、Survivor0、Survivor1三个区域

Tenure Generation Space 老年代 Old/Tenure

Permanent Space 永久代 Perm

TLAB

JDK8之后:堆内存逻辑上分为:新生代 + 老年代 + 元空间、TLAB

Young Generation Space 新生代 Young/New:内部又划分为Eden、Survivor0、Survivor1三个区域

Tenure Generation Space 老年代 Old/Tenure

Mete Space 元空间 Meta

TLAB

约定:

新生代 <–> 新生区 <–> 年轻区 --> 新生代

老年代 <–> 老年区 <–> 养老区

永久区 <–> 永久代

新生代与老年代

Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen),默认占比为:1:2

几乎所有的Java对象都是在Eden区被new出来的。GC对绝大部分的Java对象的回收也是在新生代进行的。

其中年轻代又可以划分为Eden空间、Survivor0空间和survivor1空间(有时也叫做from区、to区) ,默认占比为:8:1:1(官网上说是8:1:1,但实际查看是6:1:1,因为存在自适应内存分配策略)。

深入理解JVM虚拟机——Java内存模型结构之堆的概念

设置新生代与老年代在堆结构的占比(这个参数在开发中一般不会调)

-XX:NewRatio=X,默认为2,表示新生代占1,老年代占2,新生代占整个堆的1/3,可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5

XX:SurvivorRatio=X:设置新生代中Eden区和Survivor区的比例。

-XX:-UseAdaptiveSizePolicy: 关闭自适应内存分配策略,-XX:+UseAdaptiveSizePolicy:表示打开

-Xmn:新生代最大内存大小,这个设置的优先级大于设置比例。

对象分配的正常过程

为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑Gc执行完内存回收后是否会在内存空间中产生内存碎片。

大致过程如下:

1.new的对象先放Eden区。

2.当Eden的空间填满时,程序又需要创建对象,JVM才会将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。

3.然后将伊甸园中的经过Minor GC后幸存的对象移动到幸存者0区(S0),此时Eden区已经清空。

4.如果Eden区再满了,则再次触发Minor GC,对Eden区和不为空的Survivor区进行回收,上次幸存下来放到S0区的对象如果还没有被回收,就会移动到S1区。

5.如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区,如此反复,Survivor中总有一个是空的。

6.啥时候能去养老区呢?可以设置次数。默认是15次。可以设置参数:-XX:MaxTenuringThreshold=<N>进行设置。

注意

S0和S1区不能主动的触发垃圾回收,只有当Eden区满了之后被动的进行GC。

下一篇我们会单独介绍方法区的概念

继续阅读