目录
概述
堆空间内部结构
JDK7版本
JDK8版本
堆空间的内存划分
堆空间大小设置参数
概述
Java堆是虚拟机所管理的内存中最大的一块,其在JVM启动时即被创建,并且空间大小也被确定(这里是不考虑Java8之后以本地内存来实现的元空间,在Java8之前可以很确切的这么说,个人认为如果将其与元空间比较意义并不大),同时也是所有线程共享的一块内存区域,此内存区域的唯一目的就是存放对象实例,”几乎“所有的对象实例都在这里分配(注意这里的“几乎”二字,除了堆上分配,还可以栈上分配,这依赖于逃逸分析技术的提高)。
堆内存被分为了:新生代(Young)、老年代(Old),新生代又划分为:伊甸区(Eden),幸存者0区(Survivor 0)和幸存者1区(Survivor 1)(有的资料也写作:from 区和 to 区)。
从Java堆的内存划分便可以体现出Java堆是内存回收的重点地区,这种划分形式也是为了更高效的实现垃圾回收。
堆空间内部结构
我们平常讨论的JVM如果没有特别指明类型的话,默认都是以HotSpot虚拟机而言,下文也是入此。
说到堆空间的内部结构,需要根据JDK的版本而定,在JDK7及之前,HotSpot虚拟机选择将方法区和堆空间合并实现,称作永久代,其好处就是省略了专门为方法区编写代码,可以复用堆空间的代码。但是以现在的眼光来看,还是存在诸多弊端。所以在JDK8改为用本地内存来实现方法区,称作元空间。
JDK7版本
方法区被叫做永久代,并且和堆空间合并一起实现。其实在JDK7时开发人员就已经意识到了缺陷并且在开始解决了,JDK7时将字符串常量池、静态变量从永久代中存放到堆空间中。
JDK8版本
移除永久代,用本地内存实现方法区改称元空间。其中原本存放在永久代中的类型信息、字段、方法、常量保存在本地内存的元空间,字符串常量池和静态变量依旧保存在堆中(堆是内存回收的重点区域,而字符串是开发中经常用到的,所以也需要频繁的清理,所以字符串常量池放在堆空间是合理的)。
堆空间的内存划分
堆空间被分为了新生代、老年代,新生代被划分为伊甸区、幸存者0区、幸存者1区。
堆内存默认结构占比:
新生代:老年代 = 1:2
伊甸区:幸存者0区:幸存者1区 = 8:1:1
堆空间大小设置参数
Java堆区用于存储Java对象实例,堆的大小在JVM启动时就已经设定好了,可以在启动时设置启动参数来指定堆空间的大小。
- “-Xms"用于表示堆区的起始内存,等价于
-XX:InitialHeapSize
- “-Xmx"则用于表示堆区的最大内存,等价于
-XX:MaxHeapSize
一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常。
通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在ava垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
堆空间分代思想
上文已经提到了默认情况下堆空间的分代结构以及具体的内存大小划分。这种分代形式主要跟垃圾回收机制有关,垃圾回收机制大多数都遵循了分代收集的原则:
- 弱分代假说:绝大多数对象都是朝生熄灭的。
- 强分代假说:熬过越多次垃圾收集的对象就越难以消亡。
- 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。
可以粗略的理解为:存放在堆空间的对象需要经常清理,但是对象与对象之间的存活周期也是不同的。那些存活周期比较长的对象存放在老年代中,朝生熄灭的对象存放在新生代中。可以通过设置对两个代的垃圾扫描频率来达到更高的回收效率,新生代的对象朝生熄灭那么就需要经常清理,老年代的存活周期长可以降低频率提高效率。
分代内存设置
设置新生代内存大小
“-XX:NewSize” 新生代的最小值
”-XX:MaxNewSize“ 新生代的最大值
“-XX:NewRatio” 设置新生代与老年代在堆空间的大小
"-XX:SurvivorRatio" 设置幸存者区和伊甸区的大小比值(注意:幸存者区有两个)
例1:-XX:NewSize=10M -XX:MaxNewSize=20M 设置新生代最小值为10M,最大值为20M。
例2:-XX:NewRatio=4 设置新生代和老年代的比值为4,即 新生代 :老年代 = 1 :4
例3:-XX:SurvivorRatio=8 表示 幸存者区和伊甸区的大小比例为 1 :8。
对象在堆中的分配过程
上面的流程图表示的是对象在堆中内存分配的过程,内存分配和内存回收密切相关。
- 创建的对象先放伊甸园区,此区有大小限制。
- 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区。(值得注意的是,幸存者区空间填满时并不会触发垃圾回收,对幸存者区的回收是在MinorGC中顺便做的事情)
- 然后将伊甸园中的剩余对象移动到幸存者0区。
- 如果再次触发垃圾回收,此时上次幸存下来的被放到幸存者0区的对象如果没有被回收,就会放到幸存者1区。(幸存者两个区之间是通过标记复制算法进行垃圾回收的)
- 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
- 对象什么时候能够去到老年代?JVM默认是经历了15次垃圾回收之后。当然也可以设置参数来控制:设置
-Xx:MaxTenuringThreshold=N
- 当老年代内存不足时,再次触发GC:Major GC,进行养老区的内存清理。
- 若老年代执行了Major GC之后,发现依然无法进行对象的保存,就会产生OOM异常。
下图主要描述的是幸存者区之间的关系。
上面的描述只是从宏观的角度描述了对象在堆中的内存分配,接下来仔细考虑以下几个问题:
具体的分配方式是什么?
对于第一个问题,首先需要清楚的是,对象所需的内存在类加载阶段完成后便可确定下来。为对象分配内存实际上就是将一块内存从java堆中划分出来存放这个对象。如果java堆中的内存是绝对规整的,即所有用过的内存放置在一边,所有没用过的内存放置在另一边。那么我们可以很方便的使用一个中间指针来分配内存,新的对象到来了中间指针就往空闲内存方向移动新的对象的内存大小的距离即可。这种分配方式称为“指针碰撞”(Bump The Pointer)。但是如果java堆中的内存并不是规整的,已使用过的和未使用过的内存交织在一起,就不能使用指针碰撞的方式分配内存了,虚拟机就需要维护一个列表,记录哪些内存块是可用的,分配时从列表中找到一块足够大的空间划分给对象即可,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。选择哪种分配方式取决于java堆是否规整,而java堆是否规整又取决于所采用的垃圾收集器是否带有空间压缩整理的能力决定。
对象的分配是很频繁的操作,并发情况下是线程安全的吗?
对于第二个问题,对象创建时并发情况下可能出现正准备分配给A对象的内存在指针还没来得及分配时,B对象又使用了这一块内存区域的情况。解决这个问题有两种可选方案:第一种就是对分配内存空间的动作进行同步处理,虚拟机采用的是CAS配上失败重试的方式来保证更新操作的原子性。另一种方式是把内存分配的动作按照线程划分在不同的空间中进行,给每一个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲,线程要在堆中分配内存,首先在各自的本地缓冲区中分配,本地缓冲区用完了,分配新的缓冲区时才需要同步锁定。