一、先认识下,你的jvm使用的是什么垃圾回收器
回收器 | 回收对象和算法 | 回收器类型 |
---|---|---|
Serial | 新生代,复制算法 | 单线程(串行) |
Parallel Scavenge | 新生代,复制算法 | 并行的多线程回收器 |
ParNew | 新生代,复制算法 | 并行的多线程收集器 |
Serial Old | 老年代,标记整理算法 | 单线程(串行) |
Parallel Old | 老年代,标记整理算法 | 并行的多线程回收器 |
CMS | 老年代,标记清除算法 | 并发的多线程回收器 |
G1 | 跨新生代和老年代:标记整理+化整为零 | 并发的多线程回收器 |
[root@VM-12-4-centos ~]# java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=30109632 -XX:MaxHeapSize=481754112 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_212"
Java(TM) SE Runtime Environment (build 1.8.0_212-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.212-b10, mixed mode)
[root@node ~]# jmap -heap 971
Attaching to process ID 971, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.212-b10
using thread-local object allocation.
Parallel GC with 2 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 989855744 (944.0MB)
NewSize = 20971520 (20.0MB)
MaxNewSize = 329777152 (314.5MB)
OldSize = 41943040 (40.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 125304832 (119.5MB)
used = 59405816 (56.65380096435547MB)
free = 65899016 (62.84619903564453MB)
47.40903846389579% used
From Space:
capacity = 10485760 (10.0MB)
used = 1290296 (1.2305221557617188MB)
free = 9195464 (8.769477844238281MB)
12.305221557617188% used
To Space:
capacity = 10485760 (10.0MB)
used = 0 (0.0MB)
free = 10485760 (10.0MB)
0.0% used
PS Old Generation
capacity = 41943040 (40.0MB)
used = 16824376 (16.04497528076172MB)
free = 25118664 (23.95502471923828MB)
40.1124382019043% used
14890 interned Strings occupying 1287896 bytes.
二、为什么以前会出现新生代和老年代使用不同的垃圾回收算法
主要原因就是因为新生代和老年代的对象生存特性决定的
年轻代
用来存放新生的对象。
目标就是尽可能快速的收集掉那些生命周期短的对象(比如我们有可能在一个for循环中创建了大量对象,但是循环结束后,这些对象引用就失效)。
老年代在年轻代中经历了多次(可设置,一般为15)垃圾回收后仍然存活的对象,就会被放到年老代中。
因此,可以认为年老代中存放的都是一些生命周期较长的对象。
三、垃圾回收算法
复制算法(Copying)
一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次MinorGC后,如果仍然存活,将会被移到Survivor区(一旦收集后,Eden就变成空的)。对象在Survivor区中每熬过一次MinorGC,年龄就会增加1岁,当他的年龄增加到一定程度时(默认是15岁),就会被移动到老年代中。因为年轻代中的对象基本都是朝生夕死,所以在年轻代的垃圾回收算法使用的是复制算法。
基本思想就是:将内存分为两块,每次只用其中一块,当这块内存用完,就将还活着的对象复制到另外一块上面。
优点:不会产生内存碎片
缺点:耗空间
如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。使用复制算法的前提是90%的对象会死,要是回收的对象没有这么多就麻烦了。
标记清除算法(Mark-Sweep)
扫描空间根据可达性分析标记可回收的对象,打上标记。
扫描空间根据标记可回收的对象,进行清除。
优点:不需要额外的空间
缺点:空间节约了,碎片出来了。同时两次扫描,耗时严重;
详细表述:
1,首先,缺点就是效率低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲。
2,其次,这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随机的出现在内存的各个角落的,清除后,内存的布局自然乱七八糟。为了应付这一点,JVM就不得不维持一个内存的空闲列表,也是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
也就有了标记-整理算法,对内存不连续的内存空间进行整理。
标记压缩算法(Mark-Compact)或者叫标记整理算法
比标记清除多了一步整理的过程。
理论上,耗时最长(在垃圾回收的时候,程序不能动)。
优点:没碎片
缺点:需要移动对象的成本
唯一的缺点就是效率不高,不仅要标记所有存活的对象,还要整理所有存活对象的引用地址。从效率上来说,标记整理算法要低于复制算法。
没有最好的算法,只有最合适的算法 ,因此也印证了我们为什么要进行分代垃圾收集。
思考:CMS为什么没有使用标记整理?
四、那怎么知道对象有没有死(未被引用)
引用计数法
为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。一旦某个对象的引用计数为0,则说明该对象已经死亡,便可以被回收。
具体实现方式:
如果有一个引用,被赋值为某一个对象,那么将该对象的引用计数加1。如果一个指向某衣蛾对象的引用,被赋值为其他值,那么将该对象引用计数减1。
对于引用计数法来说,除了需要额外的空间来存储计数器,以及频繁的更新计数器外,引用计数法无法处理循环引用。
所以目前JVM虚拟机主流垃圾回收器采用的是可达性分析算法。
根可达分析
将一系列被称为GC Roots的变量作为初始的存活对象合集,然后从该合集出发,所有能够被该合集引用到的对象,将其加入到该合集中,而不能被该合集所引用到的对象,宣告死亡。
哪些对象可以作为GCRoots?
1,虚拟机栈(栈桢中的局部变量区,也叫做局部变量表)中引用的对象。
2,方法区中的类静态属性引用的对象。
3,方法区中常量引用的对象。
4,本地方法栈中JNI(Native方法)引用的对象。
在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,但是可达性分析线程没有同步到新内容,容易漏报和误报。漏报顶多损失部分垃圾回收的机会,误报则会导致垃圾回收器回收了仍然被引用的对象。
STW(Stop the world),停止其他非垃圾回收线程,直到完成垃圾回收。
五、扩展
当然,JVM垃圾回收的内容也远远不止这些,这里算是入门引导。
推荐阅读: