Java中一个重要的概念就是引入了自动内存管理机制,不像C++那样需要开发人员手动的为对象分配内存和释放内存。而开发人员经常会忘记释放内存而造成内存泄露,而Java程序员虽然不必管这些,但是Java依然有可能发生内存泄露,如果我们对内存管理机制不理解的话,很难找出错误的所在
Java的自动内存管理做什么
- 为对象分配内存
- 回收已经不再存活的对象的内存
下面我们来看下具体是怎么做的
如何判断对象是否可回收
引用计数法:给对象添加一个引用计数器,当对象被引用时,计数器+1,当对象失去引用时,计数器-1,我们在垃圾回收的时候清除掉引用计数为0的对象。
优点:简单,高效,现在的Objective-C就是用这种算法
缺点:无法解决两个对象相互引用,在这种情况下,两个对象都无法再被访问,但是他们的引用计数都不为0,无法被回收从而造成内存泄露。
可达性分析
我们通过一些列被称为“GC-Roots”的对象作为起始点,从这些节点往下搜索,能被搜索到的节点确认为存活(也被称为在引用链上),不能被搜索到的节点可以被回收。
可以作为“GC-Roots”的对象有:- 虚拟机栈中引用的对象
- 方法区中常量引用的对象
- 方法区中静态属性引用的对象
- 本地方法区中JNI(即一般所说的Native方法)引用的对象
引用类型
垃圾回收的时间是不确定的,所以对象实际什么时候被回收也是不确定的。虽然垃圾回收是由JVM来决定的,但是开发人员能在一定程度上与GC交互,交互的桥梁就是引用类型
- 强引用类型:我们一般new出来的对象都是强引用类型,这种类型最常见。这种类型,只要有引用的存在就不会被回收(在引用链上)。例如有一个对象A,对象A中有对对象B的引用,那么只要A不主动把对象B的引用置为null,那么对象B的存活时间就比对象A要久,只有当对象A被回收之后,对象B才有可能回收
- 软引用(Soft Reference):在强度上弱于强引用,用来说明哪些对象是不那么重要的,在内存不足的时候会被释放。当JVM内存不足的时候,会释放掉软引用引用的对象,如果释放掉这些对象空间还是不足,才会报OOM错误。
- 弱引用(Weak Reference):在强度上弱于软引用,该引用的作用是指向一个对象,但是不组织对象的回收,也就是当发生GC的时候,无论内存空间是否足够都会被回收掉。
- 虚引用:最弱的引用,为一个对象设置虚引用的唯一目的就是能够在这个对象被回收的时候收到一个系统通知
垃圾回收算法
垃圾回收是一个复杂而且耗时的操作。如果JVM花费过多的时间在垃圾回收上,则势必会影响应用的运行性能。一般情况下,当垃圾回收器在进行回收操作的时候,整个应用的执行是被暂时中止(stop-the-world)的。这是因为垃圾回收器需要更新应用中所有对象引用的实际内存地址。不同的硬件平台所能支持的垃圾回收方式也不同。比如在多CPU的平台上,就可以通过并行的方式来回收垃圾。而单CPU平台则只能串行进行。不同的应用所期望的垃圾回收方式也会有所不同。服务器端应用可能希望在应用的整个运行时间中,花在垃圾回收上的时间总数越小越好。而对于与用户交互的应用来说,则可能希望所垃圾回收所带来的应用停顿的时间间隔越小越好。对于这种情况,JVM中提供了多种垃圾回收方法以及对应的性能调优参数,应用可以根据需要来进行定制。
标记清除回收
将内存中的需要回收的对象进行标记,然后进行被标记的对象的回收。
优点:简单,易实现
缺点:效率不高;容易造成内存碎片,之后的大对象没有位置存放
标记整理算法
将内存的需要回收的对象进行标记,然后将存活的对象移动到内存的一端,然后直接清除掉边界以外的内存
复制算法
将可用内存划分为大小相等的两块,每次只使用其中的一块。当一块的内存用完了,就将还存活的对象复制到另外一块上去,然后把已经使用过的这一块清除掉
优点:运行高效,实现简单
缺点:只能使用一半的内存
虚拟机一般都使用这种算法来回收新生代,但是我们不需要将空间化为1:1。而是将内存化为一个比较大的Eden和两个相对较小的survivor空间,每次只是用Eden空间和一个Survivor空间。当需要进行回收时,将Eden和Survivor中的存活的对象都复制到另外一个未使用的Survivor空间中去,然后清理掉Eden和之前使用过的Survivor空间。一般Eden和survivor的比例为8:1:1。这样我们只需要“浪费”10%的空间。
当然,并不是所有时候这10%的空间都能容纳存活的对象,这个时候我们就需要老年代的空间来进行担保。(老年代与新生代的比例一般为2:1)
分代回收
分代回收不能算是一个新算法,它只是根据新生代和老年代对象的存活情况不同采用不同的垃圾回收算法。新生代中对象朝生夕死,需要频繁的进行垃圾回收,需要我们使用复制算法来进行回收;在老年代中,对象一般不那么容易被回收,所以采用标记-清理算法或标记-整理算法。
内存分配与回收策略
之前我们如何GC如何回收,现在我们来谈一下GC如何给对象分配内存。
分配的规则不是100%的,其细节取决于当前使用的是哪一种收集器,还有相关的参数设置
优先在Eden上进行分配
大多数情况下,对象在新生代的Eden中分配。当Eden没有足够的空间时,发生一次MinorGC。关于MinorGC和FullGC
MinorGC:发生在新生代的垃圾回收,对象大多都是具备朝生夕死的特性,所以MinorGC发生的非常频繁
FullGC:值发生在老年代的GC,速度比MinorGC慢很多大对象直接进入老年代
为了避免大对象在新生代间的频繁复制,虚拟机提供了一个-XX:PretenureSizeThreshold参数,大于这个设置值的对象直接在老年代分配长期存活的对象进入老年代
如果Eden中的对象在一次MInorGC后还能存活并进入Survivor空间中的话,那么他的年龄设为1。对于Survivor中的对象没“熬过”一次MinorGC,那么它的年龄就会+1。当它的年龄大于设定值(默认是15)的时候,就会晋升到老年代中。
这个参数可以使用-XX:MaxTenuringThreshold来设置动态对象年龄判定
虚拟机并不是永远的要求对象的年龄必须达到MaxTenuringThreshold才会进入老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,那么年龄大于等于该年龄的对象就可以全部进入老年代中,而不需等待MaxTenuringThreshold设置的值空间分配担保
在发生一次MinorGC前,虚拟机会检查老年代中的最大连续空间是否大于当前新生代中全部对象的总和。如果条件成立的话,那么就会进行MinorGC。如果条件不成立的话,虚拟机会检查HandlePromotionFailure设置值是否允许担保失败。如果允许的话,就会检查老年代中的最大连续空间是否大于历次晋升到老年代的平均大小,如果大于,就会尝试进行一次MinorGc,尽管这次MinorGC是有风险的;如果小于,或者设置的不允许,那么就会改为进行一次FullGC。关于“冒险”:新生代采用复制算法,但是并不能保证一个survivor空间可以满足存活对象的需求,在这种情况下,就需要老年代进行担保,将survivor中无法容纳的对象直接进入老年代。但这种担保的前提是老年代中的空间也能容纳这些对象,一次垃圾回收之后,会有多少对象存活下来是不可知的,所以只好取之前每一次晋升到老年代的对象的平均大小作为预测值,与老年代的剩余空间进行比较,决定是否进行fullGC来让老年代腾出更多的空间。
取平均值只是一种经验手段,也有可能发生在某次MInorGC后,晋升到老年代的对象突增,也会导致担保失败。那么只好在失败后再进行一次FullGC。虽然担保失败时绕的圈子是最大的,但大部分情况下还是都会将HandlePromotionFailure开关打开,避免FullGC过于频繁。
参考文章:
说明:欢迎大家关注我的github,与我交流