为什么要分成年轻代和老年代?
因为在Java程序里,对象可以分成短期存活的对象和长期存在的对象,不同存活的时间的对象如果都放在同一个地方,会影响垃圾回收算法的效率。
大部分的正常对象,都是优先在新生代分配内存的。
public class Person{
private static Student stu = new Student();
public static void main(String[] args){
loadData();
while(true){
fetchFromRemote();
Thread.sleep(1000);
}
}
private static void loadData(){
Manager manager = new Manager();
manager.load();
}
private static void fetchFromRemote(){
stu.read();
}
}
什么是永久代?
JVM的永久代就是之前所说的方法区,存放一些类的信息。
方法区(永久代)会不会进行垃圾回收呢?
在以下几种情况下,方法区里的类会被回收:
- 首先该类的所有实例对象都已经从Java堆内存被回收了。
- 其次加载这个类的ClassLoader已经被回收
- 最后,对该类的Class对象没有任何引用。
什么时候才会触发Minr GC(Young GC)呢?
在分配新的对象的时候,发现新生代的内存不足,就会触发一次垃圾回收,然后就会把所有存在新生代的垃圾对象全部给干掉,腾出大量的内存空间。
长期存放的对象会躲过多次垃圾回收
上述代码中,Student对象的实例长期被一个“Person”类的静态变量student所引用,那么就会长期存活,当触发多次young GC的时候,Student对象的实例就会被转移到老年代中。
跟JVM内存相关的几个核心差数
- -Xms:Java堆内存的大小
- -Xmx:Java堆内存的最大大小
- -Xmn:java新生代的大小,堆-新=老年代的内存大小
- -XX:PermSize:永久代大小
- -XX:MaxPermSize:永久代最大大小
- -Xss:每个线程的栈内存大小
被那些变量引用的对象是不能回收的?
JVM中使用了一种可达性分析算法来判断那些对象是可以被回收的,那些是不可以的。
这个算法的意思是,就是说对每个对象,都分析一下有谁在引用他,然后一层一层的判断,看是否有一个GC Roots。在JVM规范中,局部变量可以作为GC Roots的。例如replicaManager。
一句话总结:只要你的对象被方法的局部变量、类的静态变量给引用了,就不会回收他们。
Java对象的不同引用类型
-
强引用:只要是强引用,垃圾回收不会回收这个对象
-
软引用:正常情况下垃圾回收是不会回收软引用对象的,但是如果内存快要溢出的时候,此时就会把这些软引用对象给回收掉,哪怕他被变量引用了,但是因为是软引用,所以还是要回收。
3. 弱引用
弱跟没引用没什么区别,发生垃圾回收就会清理掉。
- 虚引用 忽略 很少用
总结:没有 GC Roots引用的对象可以回收,有的话,要分是不是强、弱、软引用来分析,强不能回收,弱可以回收,软分情况,内存满回收,不满不回收
finalize()方法的使用
假设没有GC Roots引用的对象一定立马被回收吗?不是的,要看finalize()方法,
假设有个ReplicaManager对象要被垃圾回收了,加入这个对象重写了Object的finalize()方法,此时在回收之前会调用一下这个方法,看这个方法立马是否把这个实例对象重新引用,如果引用了就不会垃圾回收。
这个很少用到实际代码立马。
对于新生代垃圾回收算法的变化
一开始是把新生代的内存分为2个区域,然后内存满的时候就会触发Minor GC,把要清理的对象标记一下,然后保留存放的对象,这会导致在内存区域东一个对象西一个对象,造成内存碎片。
内存碎片造成内存浪费,碎片是离散型的,导致没有一块足够大的内存存放新的对象,这种方法是不可取的。
一个合理的垃圾回收思路 先对要存放的对象标记一下,把这些对象移到新生代的另一个内存区域,存放的时候是顺序的,这样就不会导致内存碎片的产生。
另外一块内存就变成空白的了,就可以继续使用。
这就是复制算法。这样有什么缺点呢?如果新生代的内存为1G,那么只有512MB的内存空间是可以使用的,另外一块空着,造成内存浪费,内存利用率太低。
复制算法的优化:Eden区和survivor区
把新生代的内存区域化为3部分,1个eden区,2个survivor区,默认比例为8:1:1。
平时可以使用就是一个Eden加上Survivor1区=900MB。
当eden区满的时候,就会触发垃圾回收,就把存活的对象放在survivor区中,因为每次存放的对象比较少,survivor可能存放的下。当下一次eden区满的时候,就会把eden存活的对象和上次存放的survivor区存活的对象一起转移到另一个survivor区。循环着用,内存利用率达到90%. 如果存放的对象大于survivor区的怎么办?这时候就会把这些对象转移到老年代中去。
躲过15次GC对象会进入到老年代
默认15次,参数可以配置“-XX:MaxTenuringThreshold”
大对象直接进入老年代
有一个参数"-XX:PretenureSizeThreshold"可以设置为字节数,比如“1048676”字节,就是1MB。
之所以这样做,避免躲过多次GC,造成新生代的内存浪费。来回复制也需要时间。
触发Minor GC之前会如何检查老年代可用内存大小和新生代内存大小?
首先在执行Minor GC之前,JVM会先检查一下老年代的可用空间大小,是否大于新生代所有对象的大小。大于的话,就放心的进行Minor GC,就算新生代所有对象都存放,那么老年代也是可用放的下的。
假如在执行Minor GC之前,发现老年代的可用内存小于新生代的所有对象,那么这个时候执行Minor GC之后,
新生代的所有对象都存放,那么老年代的可用内存放不下了,那么怎么办呢?就会看一个“-XX:-HandlePromotionFailure"的参数是否设置了,如果设置了进行下一个判断
老年代垃圾回收算法
标记整理算法,可达性分析活着的对象,让活的对象在内存进行移动,存活的紧靠的在一起,避免回收垃圾产生内存碎片,然后再一下子回收垃圾对象。
这个标记整理算法比新生代的回收算法慢10倍,所以full Gc代价很大。