垃圾收集(Garbage Collection)
1.哪些内存需要回收?
Java内存运行时区域的各个部分,其中程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不絮执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性。当方法结束或者线程结束时,内存就自然随着回收了。
Java堆和方法区则具备不确定性:一个接口的多个实现类需要等内存不一样,一个方法执行不同条件的分支所需要的内存也可能不一样。
2.对象已死?
2.1引用计数法
每个对象添加一个引用计数器,每被引用一次,计数器加1,失去引用,计数器减1,当计数器在一段时间内保持为0时,该对象就认为是可以被回收得了(在JDK1.2之前,使用的是该算法)。引用计数算法是将垃圾回收分摊到整个应用程序的运行当中了,而不是在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。因此,采用引用计数的垃圾收集不属于严格意义上的"Stop-The-World"的垃圾收集机制。
缺点:当两个对象A、B相互引用的时候,当其他所有的引用都消失之后,A和B还有一个相互引用,此时计数器各为1,而实际上这两个对象都已经没有额外的引用了,已经是垃圾了。但是却不会被回收
2.2可达性分析法
可达性分析算法(Reachability Analysis)的基本思路是,通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。
在java体系里面,固定可以作为GC Roots的对象包括以下几种:
a.在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用的参数,局部变量,临时变量等
b.在方法区中类静态属性引用的对象,譬如java类的引用类型静态变量
c.在方法区中常量引用的对象,譬如字符串常量池里的引用
d.在本地方法栈中JNI(即通常所说的Native方法)引用的对象
e.Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException)等,还有系统类加载器。
2.3回收方法区
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
回收废弃常量与回收java堆中的对象非常相似。假如“java”曾经进入常量池,但是当前系统又没有任何一个字符串对象的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,那么“java”就可能被回收。
判断一个类型不再使用,需要同时满足以下条件:
a.该类所有的实例都已经被回收
b.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器,如OSGi,JSP的重加载,否则很难达成。
c.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
3.垃圾收集算法
3.1标记-清除算法
标记清除算法分为“标记”和“清除”两个阶段,首先先标记出那些对象需要被回收,在标记完成后会对这些被标记了的对象进行回收。
这种算法的优点在于不需要对对象进行移动操作,仅对不存活的对象进行操作,所以在对象存活率较高的情况下效率非常高,但是从上图模拟的结果来看对象被回收后,可用的内存并不是连续的,而是断断续续,造成大量的内存碎片。 存储对象时要求内存空间时连续的,所以虚拟机在给新的内存较大的对象分配空间时,有可能找不到足够大的连续的空闲的空间来存放,从而引发一次垃圾回收动作,实际上里面是有大量的空闲空间的,只是不连续而已。
3.2复制算法
复制算法是将内存分为两块大小一样的区域,每次是使用其中的一块。当这块内存块用完了,就将这块内存中还存活的对象复制到另一块内存中,然后清空这块内存。这种算法在对象存活率较低的场景下效率很高,比如说新生代,只对整块内存区域的一半进行垃圾回收,在垃圾回收的过程也不会出现内存碎片的情况,不需要移动对象,只需要移动指针即可,实现简单,所以运行效率很高。运行效率是在建立在浪费空间的基础上的,这是典型的以空间换时间的方法,因为每次只能是使用内存的一半。
3.3标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
3.4分代收集算法
分代收集算法将heap区域划分为新生代和老年代,新生代的空间比老年代的空间要小。新生代又分为了Eden和两个survivor空间,它们的比例为8:1:1。对象被创建时,内存的分配是在新生代的Eden区发生的,大对象直接在老年代分配内存,IBM的研究表明,Eden区98%的对象都是很快消亡的。
为了提高gc效率,分代收集算法中新生代和老年代的gc是分开的,新生代发生的gc动作叫做minor gc 或 young gc,老年代发生的叫做major gc 或 full gc。
minor gc 的触发条件:当创建新对象时Eden区剩余空间小于对象的内存大小时发生minor gc;
major gc 触发条件:
1、显式调用System.gc()方法;
2、老年代空间不足;
3、方法区空间不足;
4、从新生代进入老年代的空间大于老年代空闲空间;
JVM每次只会使用eden和其中一块survivor来为对象服务,所以无论什么时候,都会有一块survivor空间,因此新生代实际可用空间只有90%。
Eden区对象的特点是生命周期短,存活率低,因此Eden区使用了复制算法来回收对象,上面也提到复制算法的特点是在存活率较低的情况下效率会高很多,因为需要复制的对象少。与一般的复制算法不同的是,一般的复制算法每次只能使用一半的空间,另一半则浪费掉了,Eden区的回收算法也叫做"停止-复制"算法。
当对象在eden(其中包括一个survivor,假如是from),当此对象经过一次minor gc后仍然存活,并且能够被另外一块survivor所容纳(这里survivor则是to了),则使用复制算法将这些仍然存活的对象复制到to survior区域中,然后清理掉eden和from survivor区域,并将这些存活的对象年龄+1,以后对象在survivor中每熬过一次gc则增加1,当年龄达到某个值时(默认15,通过设置参数-xx:maxtenuringThreshold来设置),这些对象就会成为老年代!但是也不一定,当一些较大的对象(需要分配连续的内存空间)则直接进入老年代。
老年代的对象基本是经过多次Minor gc后存活下来的,因此他们都是比较稳定的,存活率高,如果还是用复制算法显然是行不通的。所以老年代使用“标记-整理”算法来回收对象的,从而提高老年代回收效率。
4.垃圾收集日志
//VM参数: -Xms1m -Xmx1m -XX:+PrintGC public class Test { public static void main(String[] args) { List<Apple> list = new ArrayList(); while (true) { list.add(new Apple("red", 12f)); //System.out.println(list.size()); } } } [GC (Allocation Failure) 508K->512K(1536K), 0.0036330 secs] 1 ... 1345 1346[GC (Allocation Failure) 1024K->668K(1536K), 0.0011224 secs] ... 4030 4031 4032[GC (Allocation Failure) 1180K->756K(1536K), 0.0012094 secs] ... 6526 6527 6528 [GC (Allocation Failure) 1268K->912K(1536K), 0.0008448 secs] [Full GC (Ergonomics) 912K->825K(1536K), 0.0098845 secs] ... ... 17342 17343 [Full GC (Ergonomics) 1399K->1281K(1536K), 0.0172377 secs] ... 21077 21078 21079 [Full GC (Ergonomics) 1429K->1427K(1536K), 0.0209825 secs] [Full GC (Ergonomics) 1427K->1427K(1536K), 0.0223662 secs] [Full GC (Allocation Failure) 1427K->1427K(1536K), 0.0213661 secs] Exception in thread "main" [Full GC (Ergonomics) java.lang.OutOfMemoryError: Java heap space 1430K->523K(1536K), 0.0046020 secs] at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:261) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) at java.util.ArrayList.add(ArrayList.java:458) at com.ryj.dump.Test.main(Test.java:13)
5.内存分配与回收策略
5.1对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间时,虚拟机将发起一次MinorGC.
5.2大对象直接进入老年代
大对象是指需要大量连续内存空间的Java对象,比如很长的字符串或者元素数量很庞大的数组。在Java虚拟机中要避免大对象的原因是,在分配内存空间时,容易导致内存明明还有不少空间就提前碰触垃圾收集。而当复制对象时,大对象就意味着高额的内存复制开销。HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代中分配。
5.3长期存活的对象进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器。对象通常在Eden区里诞生,如果经过第一次MinorGC后可以存活,且能被Survivor容纳,该对象会被移动到Survivor空间,并将其对象年龄设置位1岁。对象在Survivor区每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认位15),就会被晋升到老年代。
5.4空间分配担保
在发生MinorGC之前,虚拟机必须检查老年代中最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立,那么MinorGC是安全的。如果不成立,虚拟机则会先查看是否允许担保失败(-XX:HandlePromotionFailure);如果允许,则会继续检查老年代最大可用的连续空间是否大于历次晋升老年代的平均对象大小,如果大于,将尝试一次MinorGC;如果小于,或者不允许担保失败,则进行Full GC.