在JVM内存模型中会将堆内存划分新生代、老年代两个区域,两块区域的主要区别在于新生代存放存活时间较短的对象,老年代存放存活时间较久的对象,除了存活时间不同外,还有垃圾回收策略的不同,在JVM中中有以下回收算法:
有了垃圾回收算法,那JVM是如果确定对象是垃圾对象的呢?判断对象是否存活JVM也会有几套自己判断算法了:
有了垃圾回收和判断对象存在这两个概念后,再来逐步分析它们。
要是让开发人员来判断一个对象是否有用是很简单的,简单的说就是: 对象没有任何引用
就认为该对象可以被回收了。假设有如下程序代码:
public class App { public static void main(){ checkFile("/"); } public static boolean checkFile(String path ){ File file = new File(path); return file.exists(); } } 复制代码
程序执行起来在调用 checkFile
的时候JVM图大概像这样:
到 checkFile
方法执行完成之后,它里面的局部变量 file
就会随着栈帧一起被清理,这个时候还存活在JVM堆中的File对象也是无用的了:
要是人为来判断非常清晰的就发现File对象已经无用了,那换成JVM它又是如何来判断对象是否能存活的呢?
引用记数算法原理比较简单,想象下有个对象它有一个 count
属性,每次引用该对象都会使 count
加1,假设JVM在判断该对象是否存活的时候去检查这个 count
属性,发现这个属性不为0说明还有其他对象在引用该对象。
等到 checkFile
方法执行完之后count就会减1变成0:
这样一来JVM就很容易判断一个对象是否存活了。
但是引用记数有一个明显的缺点,就是无法解决循环引用的问题比如:A --> B --> A 这样的对象关系它是没有办法来判断对象是否该不该回收的。
为什么会被称为 可达性分析
算法呢?可以这样理解如果通过 GC Root
能到达一个对象那么这个对象就是存活的。那什么样的对象才是 GC Root
呢?
在Java语言中,可作为GC Roots的对象包括下面几种:
还是用上面的例子,在 checkFile
方法执行时,因为 栈帧变量file
可做为 GC Root
所以在执行期间JVM是绝对不会回收掉这个File对象:
但是等到 checkFile
执行完成之后,这个栈帧会被弹出,其中的变量也会被释放,相应的没有 GC Root
能到达堆中的File对象,这个时候就可以判断这个对象是一个无用的对象了,然后安全回收。
这种算法分两分:标记、清除两个阶段, 标记阶段是从根集合(GC Root)开始扫描,每到达一个对象就会标记该对象为存活状态,清除阶段在扫描完成之后将没有标记的对象给清除掉。
用一张图说明:
这个算法有个缺陷就是会产生内存碎片,如上图B被清除掉后会留下一块内存区域,如果后面需要分配大的对象就会导致没有连续的内存可供使用。
标记整理就没有 内存碎片
的问题了,也是从根集合(GC Root)开始扫描进行标记然后清除无用的对象,清除完成后它会整理内存。
这样内存就是连续的了,但是产生的另外一个问题是:每次都得移动对象,因此成本很高。
复制算法会将JVM推分成二等分,如果堆设置的是1g,那使用复制算法的时候堆就会有被划分为两块区域各512m。给对象分配内存的时候总是使用其中的一块来分配,分配满了以后,GC就会进行标记,然后将存活的对象移动到另外一块空白的区域,然后清除掉所有没有存活的对象,这样重复的处理,始终就会有一块空白的区域没有被合理的利用到。
两块区域交替使用,最大问题就是会导致空间的浪费,现在堆内存的使用率只有50%。
JVM的堆分为新生代和老年代,两种类型有不同的特性,根据它们的特性来选择不同的回收算法,这种算法会将新生代划分为一块 Eden
和二个 Survivor
区:
如上面的图有三块区域它们会按照8:1:1的比例进行分配,如1000m的堆 Eden
是800m,二个 Survivor
各占100m,那它们是如何运行的呢?
Survivor
是空着的,内存使用率是90% Eden
和其中一块 Survivor 1
中分配内存 Minor gc
,会将存活下来的对象移动到空着的 Survivor 2
中 Eden
和 Survivor 2
中继续分配内存, Survivor 1
空着等着下次使用 这样就能使内存使用率达到90%,也不会产生内存碎片。
老年代对象即使进行了垃圾回收,对象的存活率也高,所以采用标记清除或标记整理算法都是不错的选择,这里就不做阐述。
欢迎大家关注公众号:Java派一起交流学习Java、Android知识。