java内存回收机制(笔记)
java整个GC的过程是虚拟机自动完成的,就是因为虚拟机把这部分工作都做掉了,导致我们对内存分配和回收了解的很少,所以有必要就深入的看下虚拟机是如何完成内存分配和回收的。
主流的虚拟机包括JRockit、HotSpot及其他,在JDK7中,默认的虚拟机仍然是HotSpot,所以这里说的GC是指HotSpot的GC机制。
一、内存区域划分
1.程序计数器(Program Counter Register):程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。
2.虚拟机栈(JVM Stack):一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。
3.本地方法栈(Native Method Statck):虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的
4.堆区(Heap):对象创建所需要的内存是在堆上分配的(不是绝对的,栈上也能直接分配内存),GC的针对的也是堆区内存。
5.方法区(Method Area):方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。
二、对象访问方式
通常一个Java的引用访问涉及到3个内存区域:JVM栈,堆,方法区。
举例:Object obj = new Object(),Object obj表示一个本地引用,存储在JVM栈的本地变量表中,通过一个reference类型数据指向堆中obj实际的内存区域;堆中还记录了Object类的类型信息(接口、方法、field、对象类型等)的地址,这些地址所执行的数据存储在方法区中;
三、内存分配规则
Java内存分配和回收的机制概括的说,就是:分代分配,分代回收。
对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation)
如图:内存在年轻代中分配,满足条件可以升级进入老年代。
1.年轻代(Young Generation)
对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,于是被年轻代的GC机制清理掉,这个GC机制被称为Minor GC或Young GC。Minor GC并不代表年轻代内存不足,仅仅表示在Eden区上的GC。
年轻代可以分为3个区域:Eden区(内存首次分配的区域)和两个存活区(Survivor 0 、Survivor 1)。
GC后
规则如下:
1.绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;
2.最初一次,当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);
3.下次Eden区满了,再执行一次Minor GC,将消亡的对象清理掉,将存活的对象复制到Survivor1中,然后清空Eden区;
4.将Survivor0中消亡的对象清理掉,将其中可以晋级的对象晋级到Old区,将存活的对象也复制到Survivor1区,然后清空Survivor0区;
5.当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代,但这只是个最大值,并不代表一定是这个值)之后,仍然存活的对象,将被复制到老年代。
从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。这种回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中)。
2.年老代(Old Generation)
对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。
如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上。
老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-整理算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。
四、垃圾收集器
新生代采用的停止复制算法中,“停 止(Stop-the-world)”的意义是在回收内存时,需要暂停其他所有线程的执行。这个是很低效的,现在的各种新生代收集器越来越优化这一点,但仍然只是将停止的时间变短,并未彻底取消停止。
1.Serial收集器(串行):新生代收集器,使用停止复制算法,使用一个线程进行GC,串行,其它工作线程暂停。
2.ParNew收集器(并行):新生代收集器,使用停止复制算法,Serial收集器的多线程版,用多个线程进行GC,并行,其它工作线程暂停,关注缩短垃圾收集时间。
3.CMS(Concurrent Mark Sweep)收集器(并发):老年代收集器,致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。
注意并发和并行的区别:
并发是指用户线程与GC线程同时执行(不一定是并行,可能交替,但总体上是在同时执行的),不需要停顿用户线程(其实在CMS中用户线程还是需要停顿的,只是非常短,GC线程在另一个CPU上执行);
并行收集是指多个GC线程并行工作,但此时用户线程是暂停的;
所以,Serial是串行的,Parallel收集器是并行的,而CMS收集器是并发的.
五、心得
触发年轻代GC后虚拟机的策略是停止复制,把存活的对象在2个存活区域来回拷贝,满足条件的直接拷贝至老年代,这个过程所有线程是处于停止状态的!
所有应该尽量避免触发GC操作,也就是尽量避免频繁创建对象
1.使用对象池
2.避免在for循环中创建对象,引发内存抖动
3.避免自动装箱问题
…