1.JVM 运行时内存
JVM 运行时内存可以分为:
-
新生代(约占1/3堆空间)
- Eden区
- From Survivor区
- To Survivor区
-
老年代(约占2/3堆空间)
-
永久代(位于方法区)
A.新生代(复制回收)
新创建的对象会存放在这里,一般占1/3堆空间,因为运行时不断会有对象被创建,所以新生代会频繁地触发垃圾回收。新生代分为三个区。
Eden区
新创建的对象会存放在这个区域,当Eden区满的时候会触发 Minor GC进行垃圾回收,仍然存活的对象会被移动到From Survivor区或者To Survivor区。
From Survivor区和To Survivor区
这两个区必须保证有一个区是空的,在每当触发Minor GC时,Eden区和Survivor区中非空的那块中存活的对象会移动到空的Survivor区中。From Survivor区和To Survivor区是可以互换的。
例如,此时Eden区和From Survivor区中有对象并触发了Minor GC,存活的对象被移动到To Survivor区,并清除Eden区和From Survivor区的内存。
B.老年代(标记回收)
老年代有两种产生的途径:
- 新生代的对象到了一定的年龄晋升为老年代
- 新的对象创建的时候新生代区域内存不足以存下这个对象,直接分配到老年代。
当老年代区域满了之后,会触发Full GC,回收整个堆内存。
C.永久代
主要存放虚拟机加载的类的信息。
2.垃圾回收
触发GC之后,有两件事要做:
- 判断哪些对象是垃圾
- 对垃圾进行回收
A.判断垃圾
判断垃圾有两种方法:
- 引用计数法
- 可达性分析
引用计数法
对象被引用的时候计数加一,对象引用失效的时候计数减一,当计数为零的时候,说明对象没有被引用,于是被视为垃圾。
但是这个方法不能解决两个无用对象相互引用的情况,因此需要第二个方法可达性分析。
可达性分析
为了解决循环引用,设计了可达性分析,即根据目标对象是否和一些特定对象有关联,来判断目标对象是否为活动对象。
我的理解是,这些特定对象需要维持活跃的状态,只要对象和这些保持活跃的对象有关联,就说明目标对象有可能会被使用,还不能当作垃圾进行回收。
举个例子:
void function(){
Object obj = new Object();
...
obj = null;
}
在function()方法中,定义了局部变量obj,在将obj引用赋值null之前,obj的引用一直存放在栈帧中,一直能被使用直到变为null或者方法结束被回收,这种对象在失效前就是活跃的。
这种特定的对象有几种:
- 方法中局部变量的对象的引用
- 常量池中的对象引用
- 本地方法中的对象引用
- 类的Class对象
只要能和这些特定的对象关联,就不会被视为垃圾,这个也解决了循环引用的问题。
B.回收算法
复制回收算法
新生代主要采取的回收算法。
将内存划为两块A和B,其中B块要保证为空,当A块的内存满了以后,将A内存中不是垃圾的对象复制到B中,并清除A的内存。
这种方法不容易产生内存碎片,但是要保证有一块内存是空闲的,在垃圾少,存活对象很多的情况下效率较低,所以适合垃圾较多,存活对象较少的新生代。新生代会将Eden区和其中一个Survivor区的存活对象复制到另一个空闲Survivor区,并清除Eden区和有垃圾的那个Survivor区。
标记回收算法(也叫标记整理算法)
老年代主要采用的回收算法
将内存中的垃圾进行标记,将存活的对象移动到内存的一端(减少产生内存碎片),之后清除存活对象外的内存。
这种方法因为要进行对象的转移,在回收大量对象的时候效率较低,所以适合垃圾较少的老年代。