堆内存,最大的内存区域

 

如题,我觉得这样说一点都不为过。

给大家看一张生产环境的堆内存和非堆内存的监控图片。

大家观察下图中Heap Usage 设置的 最大内存,已经超过4GB,那么我们对比一下右边的non-Heap Usage,也就是300MB。可想而知,我们对于堆内存是多么的慷慨。

 

堆内存,最大的内存区域_数据

 

那么堆内存到底存放的是什么数据呢?

在运行时数据区域,很多说法都直接将其划分为 堆内存区域和非堆内存区域,也就是说,一个内存占用,要么是在堆内存中,要么就是在非堆内存中。可想而知,堆这个概念的重要性。

堆内存,最大的内存区域_实例化_02

 

那么,我们就花点时间,侧重分析一下,堆内存区域,到底做了什么?

 

 

这里还是强调一下:jvm堆内存是jvm虚拟机中所托管的内存占用中最大的一块区域。

在之前,我们分享过,Java虚拟机栈、程序计数器等区域,这些区域都是线程私有的,按照惯例,线程私有的数据通常来说都是占用偏小的,个人认为由于线程私有区域,那么线程的开启和关闭都是很频繁的动作,那么导致线程私有的数据会不断的弹出、释放,那么所占用的内存也不需要很大。

但是此处的堆内存不一样,是所有线程共享的内存区域,生命周期更是与线程私有的相关内存区域不一样,是随着虚拟机的创建而诞生。

那么堆内存中到底存放的是什么数据呢?我们推理一下?Java程序中,什么资源最多,最占用资源呢?常量?不是;临时变量?不是;对象结构元数据?不是;其实Java是通过不断的new对象,来构造对象的,不断的实例化新的对象,我们一个Object类,可以实例化无数个Object对象,是的,那么就是存放的是对象的实例,在Java虚拟机规范中描述:“所有的对象实例以及数组都应当在堆上分配”。那么就可以认定,基本上所有的对象实例都会在堆上进行实例化生成。

 

那么,堆内存中是不是应该也需要细分一下?我们才可以更了解其中划分的构造结构?

是的,我们经常会听到“新生代”,“老年代”等名词,在“新生代”中又会听到 伊甸园区(Eden)s1区(Survivor1) s2区(Survivor2)等等名词。那么其实这些就是堆内存区域的组成了。

我们通过一张模型图来直观的了解一下。

好早之前画过这么一张简图,用于分别:年轻代、老年代,永久代的位置

堆内存,最大的内存区域_数据_03

 

但是我们此文是为了更加清晰的认识堆内存中的结构,那么我们就将堆内存的结构再细化一下:

此次借用网络上的一张图片。

我们现在主要关心的是黄色框框中的内容,红色暂时不管,因为不属于堆内存。

其中黄色部分分为两大块:年轻代、老年代

那么呢,其中年轻代又划分为三个区域:伊甸园区(Eden)、S0、S1区域。结构很清晰,那么其中到底分别的作用的什么呢?其实我在之前的文章细细的分享过GC,

堆内存,最大的内存区域_堆内存_04

 

一次简单的“生代”过程如下:

堆内存,最大的内存区域_实例化_05

 

那么为啥会分为3个代呢?

年轻代:顾名思义,年轻的对象,刚刚new Object()出来的,会先在栈内存区域先使用,那么如果栈帧进行出栈操作后,如果当前对象还被引用,那么将会进入新生代中,那么如果新生代容量装不下,那么将直接进入老年代。如果新生代装下了,那么就会进行一系列的新生代内存回收,方式入上图,经过一定周期后,当前对象还没被回收,那么将进入老年代了。

老年代:即一直被使用的对象。

永久代:基本上可以认定为类似常量等数据了。

 

其实为啥会分为3个代?主要就是以前的内存回收算法都是基于分代进行设计的,堆内存中的关键又是垃圾回收,那么自然而然的,堆内存中就划分为各种代了。所以,堆内存的分代划分,并不是真实的物理内存结构就是如此,只不过,垃圾回收器是如此设计的回收算法,那么统一理解,就如此描述了。

但是在CMS之后的G1垃圾回收算法后开始,将没有分代设计这个思想了,便衍生出一种分区设计思想。所以分代设计也会随之过时,但是我们可以了解一下。

 

我们都知道,线程在构建的时候,都会申请出一个线程本地缓存区TLAB,至于为什么会有这个区域,就是当某个线程实例化一个对象的时候,就需要申请内存,那么这个时候,不同的垃圾回收算法,就会导致空闲区域的结构不同,同时,在多线程环境下,我们多个线程都在实例化新的实例,那么都会需要申请空闲的内存区域,这个时候就会出现一定概率的竞争情况,这是一种效率低下的设计,那么为了减少空闲区域的竞争,就会对线程提供一个私有的内存空间“线程本地缓冲区”,用于我们各个线程内部的对象构造,那么如果“线程本地缓冲区”不够用了怎么办呢?那这个时候就只能再去公共的堆内存区域,采用自旋方式去申请了。这是一种提升对象实例化内存分配的效率的设计。

我们还有一个硬性规则需要记住,堆内存中只会存放实例对象。

 

内存的区域分配,其实根据不同的回收算法特性,就会有不同的结果,假设,我们采用的标记清除算法,那么对于堆内存块上,那些空闲的区域,都是类似棋盘一样的结构,空闲的地方很杂乱无序,那么假设此时有一个实例对象需要使用两个空闲块,这个时候棋盘上又没有两个连续的空闲块,那么该对象就需要不连续方式进行存储了。这种方式,在我们的物理磁盘上也会有类似的设计,我们的物理磁盘,磁道结构,是不能够保证一个视频,就是连续的存储,但是我们在看视频的时候,他就是连续的播放的。这就是物理位置可能不是连续的,但是对于我们使用方来说,他就是连续的。

 

难道就没有连续的方式吗?有的,对于标记整理算法、拷贝算法等等,都会将空闲的区域进行整理,最大方式将所有空闲的区域归纳到一边,然后保证另一边都是被使用的,另外一边都是空闲的区域,那么对于我们实例化对象的时候,就方便申请内存了。从物理磁盘上就是连续的,对于我们在使用的时候,IO效率自然高了一些。

 

说到堆,自然而然需要联系到垃圾回收,那么垃圾回收是一个很大的范围的知识点,我觉得有必要单独讲讲,并且我们此篇文章只是单纯分享一下堆的组成。

 

堆内存在jvm虚拟机创建的时候创建,那么我们有能力去定义他的初始化分配。

对于线上的环境的,很多时候,我们都会将-Xmx设置的很大,比如一个虚拟机 我们就设置成4GB,因为对于现在,内存的成本不高,所以我们在初始化的时候就设置的很大,因为我们很害怕OOM,因为当堆内存不够的时候,就会出现OOM,并且OOM的出现的原因有很多,很多由于代码逻辑的错误,导致长时间无法是否对象,死循环构造对象都会直接导致内存溢出。

并且很多时候,我们设置-Xms的值和-Xmx是一样的,因为我们不考虑内存的压缩成本,且我们也想让初始化的堆内存大小一开始就达到最大的堆内存大小,这样好处就是减少了堆内存的来回在初始值和最大值间相互转换的资源浪费。

 

 

关于堆内存,此文说到这里,也不过多的赘述,后面我们会详细提到各种细节。