JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。
在Java程序中,当一个对象O被创建时,它被放在Heap里。当GC运行的时候,如果发现没有任何引用指向O, 它就会被回收以腾出内存空间,或者换句话说, 一个对象被回收, 必须满足以下条件:
综上所述:
在Java程序中不能显式的分配和注销内存,因为这些事情JVM都帮我们做了,那就是GC。有些时候我们可以将相关的对象设置成null来试图显示的清除内存,但是并不是设置为null就会一定被标记为可回收,有可能会发生逃逸。将对象设置成null至少没有什么坏处,但是调用System.gc()会显著地影响系统性能,使用System.gc()时候并不是马上执行GC操作,而是会等待一段时间,甚至不执行,而且System.gc()如果被执行,会触发Full GC,这非常影响性能。
Eden区空间不够存放新对象的时候,执行Scavenge(Minro)GC。升到老年代的对象大于老年代剩余空间的时候执行Full(Major) GC。
(1)绝大多数刚刚被创建的对象会存放在伊甸园空间(Eden)。
(2)在伊甸园空间执行第一次GC(Scanvage GC)之后,存活的对象被移动到其中一个幸存者空间 (Survivor)。
(3)此后,每次伊甸园空间执行GC后,存活的对象会被堆积在同一个幸存者空间。
(4)当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。然后会清空已经饱和的那个幸存者空间。
(5)在以上步骤中重复N次(N=MaxTenuringThreshold(年龄阀值设定,默认15))依然存活的对象,就会被移动到老年代。
从上面的步骤可以发现,两个幸存者空间,必须有一个是保持空的。如果两个两个幸存者空间都有数据,或两个空间都是空的,那一定是你的系统出现了某种错误。我们需要重点记住的是,对象在刚刚被创建之后,是保存在伊甸园空间的(Eden)。那些长期存活的对象会经由幸存者空间(Survivor)转存到老年代空间。也有例外出现,对于一些比较大的对象(需要分配一块比较大的连续内存空间)则直接进入到老年代,一般在年轻代空间不足的情况下发生。
在学习Java GC 之前,我们需要记住一个单词:stop-the-world。它会在任何一种GC算法中发生。stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行。当stop-the-world发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成。
经过n次垃圾回收存活的对象(这个n被称为年龄阀值,默认是15次)。老年代空间的构成其实很简单,它不像新生代空间那样划分为几个区域,它只有一个区域,里面存储的对象并不像新生代空间绝大部分对象都是朝闻道,夕死矣。这里的对象几乎都是从Survivor空间中熬过来的,它们绝不会轻易的狗带。因此,Full GC发生的次数不会有Scanvage GC那么频繁,并且做一次Full GC的时间比Scanvage GC 要更长(约10倍)。
所以GC调优主要是减少Full GC的触发次数,可以通过NewRatio(新生代与老年代的占比)控制新生代转 老年代的比例,也可通过MaxTenuringThreshold(年龄阀值设定)设置对象进入老年代的年龄阀值。
默认的所占空间比例年轻代(Young generation) :老年代(Old generation) = 1 : 2
默认新生代空间的分配:Eden : SurvivorA : SurvivorB = 8 : 1 : 1
总结:
采用从根集合扫描,将存活的对象复制到空闲区间,当扫描完毕活动区间后,会将活动区间一次性全部回收。此时原本的空闲区间变成了活动区间。下次GC时候又会重复刚才的操作,以此循环。 复制算法在存活对象比较少的时候,极为高效,但是带来的成本是牺牲一半的内存空间用于进行对象的移动。
采用从根集合扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象进行直接回收, 将所有存活的对象往左端空闲空间移动,并更新对应的指针。虽然进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。
JVM为了优化内存的回收,使用了分代回收的方式,对于新生代内存的回收(Scavenge GC)主要采用 复制算法。而对于老年代的回收(Full GC),大多采用标记-整理算法。