1、内存模型
jvm会使用ClassLoader类加载器加载生成的XXX.class文件。将类信息、方法、对象、常量等进行保存在内存里。
如此,jvm的内存模型变分为:
方法区 | Java堆 | Java栈 | 本地方法区 |
同时,GC垃圾回收器就是针对这个内存进行工作的。
方法区:保存类信息、各种类型的常量、类的字段、方法等。通常与永久区(Perm)关联在一起。
Java堆内存:与程序开发最为密切吧,系统中生成的对象对保存在Java堆中,所有线程共享此资源。
同时,也是GC的主要工作区间,对于GC来说又对此块内存进行了分代。
eden | s0 | s1 | tenured |
eden区:也叫伊甸园。所有的对象都在这里出生,如果对象过大会直接放到老年代(tenured)。
s0、s1:统称幸存区(survivor)。s0、s1是两块一模一样大小的内存块,与eden区三者统称新生代。
tenured区:就是老年代。接受新生代无法放置的大对象、长久被引用的对象、系统中存活很久的对象。存活很久意思就是在GC一定次数之后都能存活的对象。
Java栈:也叫线程栈。每一个线程都有一个私有的内存,里面保存线程的私有变量、局部变量、对外的引用等。
本地方法区:本地方法???此处有疑问,待填补。
2、GC算法
GC:Garbage Collection,垃圾回收器。主要对象是JVM中的堆内存和永久区。
2.1 引用计数法
以跟对象开始,对于一个对象K,如果别的对象引用了K,那么K的引用计数器就+1。当引用取消时,则-1;那么对象的引用计数器为0,则对象就可以被回收啦。
缺点:对象引用伴随着引用计数器的加减,影响性能。并且对于循环引用无法回收吧。
2.2 标记-清除
标记清除算算是现代回收算法的基础,到JDK1.8都在使用此思想。此算法分为两步,先以根节点进行标记可达对象,未被标记的对象就属于可回收对象。第二步便是将所有未被标记的对象清除。
标记清除算法比引用计数法直观看起来就爽的飞起来。但是也能看出来,他清理完毕之后,内存是零散的。万一后面生成的对象很大或者很小,那么零散的内存就没办法最大化的利用了。所以又有升级版,标记压缩。
2.3 标记-压缩
标记压缩算法是对标记清除算法的升级,解决了内存碎片化的问题。
2.4 复制算法
复制算法就是将原有的内存一分为二,大小相等的内存A、内存B。每次只使用其中的一块内存,垃圾回收时,内存A将标记存活的对象复制到内存B中,然后清除内存A。下次垃圾回收时,将内存B的标记对象复制到内存A中,清空内存B。如此反复完成GC操作。
这个算法就是新生代最常用的GC方式。
在堆内存中,分为新生代与老年代。新生代中又分为eden、s0、s1。对象都在eden区出生,当eden区内存达到阈值(比如80%)时,就会触发GC操作,eden标记的对象会复制到s0,然后清空eden区。等eden区又满了,GC再次触发,会标记eden、s0区的存活对象,然后复制到s1上,清空eden、s0。之后执行的GC步骤就循环往复了。
如果eden区直接产生一个对象很大,eden剩余的内存不够使用了,就会直接放在老年代;GC一定次数之后,如果每次都标记复制了一个对象,那么也会将此对象放到老年代里。
此算法针对的场景:在内存中产生的对象数量很多、但是生命周期很短。并且这种现象比较频繁,就比较适合使用此策略。
比如新生代,eden中产生的对象生命周期短、数量大、存活的对象少。每次GC时,只需要少量存活对象复制到新内存上,清空其他内存块。这样处理十分干净、速度也快。所以在新生代中,我们设置内存的占比时,将eden设置的高一些,survivor区少一点。占比几乎是eden:s0:s1 = 8:1:1
2.5 分代思想
新生代和老年代是由对象的生命周期长短而论的。新生代产生的对象多、寿命短,老年代一般寿命都比较长。所以新生代比较适合复制算法,老年代则更适合标记-压缩。