Java的技术体系包括
- 支持Java程序运行的虚拟机(JVM)
- 提供接口支持的Java API
- Java 编程语言
- 第三方Java框架(如Spring等)
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人想出来。
Java虚拟机的自动内存管理,其实解决的是两个问题:给对象分配内存、回收不再使用的内存。内存的回收算法在前面的文章中已经阐述,我们来看一下虚拟机是如何给对象分配内存的。给对象分配内存,其实就是在堆上进行分配,通常情况下对象主要会分配在新生代的Eden区,如果启动了本地线程分配缓冲,将按线程优先分配在TLAB上。少数情况下也可能直接将对象分配在老年代中。
内存分配的规则是由垃圾收集器组合以及内存相关的参数设置两方面因素共同决定的。同时,内存分配也会共同遵守一些通用的策略,这些策略如下
对象优先分配在Eden区
大多数情况下,对象在新生代的Eden区中分配,当新生代没有足够空间时,虚拟机将发起一次MinorGC。
- MinorGC,新生代GC,指发生在新生代的垃圾回收行为。因为java中绝大多数对象都具备朝生夕死的特性,所以MonorGC非常频繁,一般回收速度也较快。
- MajorGC(或者成为FullGC),老年代GC,指发生在老年代的垃圾回收行为,出现了MajorGC经常会伴随至少一次的MinorGC。MajorGC的速度一般会比MinorGC的速度慢十倍以上。
大对象直接进入老年代
大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对于虚拟机而言是一个不友好的存在,因为经常出现大对象会导致内存还有不少空间时为了获取足够空间放置大对象而不得不提前触发垃圾回收行为。
虚拟机提供了一个参数 -XX:PretenureSizeThreshold,意思是当对象所需的内存空间大于这个值的时候,直接将对象分配在老年代。这样做可以避免在Eden区和两个Survivor区之间发生大量的内存复制,从而避免效率的降低。
长期存活的对象将进入老年代
虚拟机采用了分代收集的思想来管理内存,因此虚拟机给每个对象定义了对象年龄计数器,这部分内容存储在对象头(Header)中。对象年龄的定义是这样的:如果对象在Eden区出生,并且经过一次MinorGC之后仍然存活,之后被移动到Survivor区,此时对象年龄设置为1,当对象在Survivor区中每经历过一次MinorGC并且依然存活,则年龄加1。
当对象的年龄增加到一定程度,默认为15岁,将会被放到到老年代中。这个年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 设置。
动态对象年龄判定
为了更好地适应不同程序的内存状况,并不是只有对象年龄达到设置值才会进入老年代。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,那么年龄大于或等于该年龄的对象就可以直接进入老年代。
空间分配担保
在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果这个条件成立,那么MinorGC可以确保是安全的。如果不成立,虚拟机会查看 HandlePromotionFailur 的设置值是否允许担保失败,如果允许,那么会继续检查老年代的最大连续可用内存空间是否大于历次进入老年代对象容量的平均大小,如果大于,将尝试一次MinorGC,如果此时担保失败,会在失败后发起一次FullGC。如果小于或者设置值不允许进行冒险,这时也会进行一次FullGC。
HandlePromotionFailur设置值一般都会设置为允许担保失败,以避免频繁的FullGC。
总结
内存分配原则
- 对象优先分配在Eden区
- 大对象直接进入老年代
- 长期存活的对象,或者相同年龄对象之和大于Survivor空间一半的对象以及更老的对象,进入老年代
内存回收的时机(When 什么时候开始回收)
- 新生代没有足够空间时,进行MinorGC
- 担保失败,或者不允许担保失败,或者老年代剩余最大连续可用空间小于历次进入老年代对象大小的平均值,则进行FullGC
内存回收什么对象(What 回收些什么)
通过可达性分析算法找出的,到GCRoots之间没有任何引用链的对象
内存回收如何发起(怎么找到GCRoots开始回收)
以HosSpot虚拟机为例,通过OopMap-安全点-安全区域的一套机制来快速准确的完成对GC Roots的枚举
内存如何回收(How 怎么进行回收)
通过分代收集算法思想,将虚拟机内存分为
- 新生代,新生代采用复制算法进行回收,因此新生代又被分为
- Eden区,80%空间
- Survivor区,10%空间
- Survivor区,10%空间
- 老年代,采用标记复制的算法进行回收。