本篇着重突击JVM非常之核心内存管理,以代码示例理解JVM内存分配和回收策略和判断对象是否可以回收,全面覆盖垃圾收集算法的方法论以及其实现常见垃圾收集器的应用 对象优先在Eden分配,如果Eden内存空间不足,就会发生Minor GC。虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。
VM option 增加-XX:+PrintGCDetails,触发GC,打印出年轻代和年老代的内存信息

大对象直接进入老年代。虚拟机提供了一个 -XX:PretenureSizeThreshold 参数 ,大于这个数量直接在老年代分配,缺省为0 ,表示绝不会直接分配在老年代。

长期存活的对象将进入老年代。默认15岁,-XX:MaxTenuringThreshold 参数可调整。
动态对象年龄判定。为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
空间分配担保。新生代中有大量的对象存活,survivor空间不够,当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代.只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小,就进行Minor GC,否则FullGC。所以,新生代一般不会内存溢出,因为有老年代做担保。

引用计数法 即给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1。当计数器为0时,就认为该对象就是不可能再被使用的。
可达性分析算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。而作为GC Roots的对象包括下面几种:



无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判断对象是否存活都与引用有关,那么就让我们再次来谈一谈引用。
package cn.itxs.entity;public class User { public int id = 0; public String name = ""; public User(int id, String name) { this.id = id; this.name = name; } @Override public String toString() { return "User [id=" + id + ", name=" + name + "]"; }}测试类,vm option 添加-Xms100m -Xmx100m -XX:+PrintGC,运行
package cn.itxs.garbage;import cn.itxs.entity.User;import java.lang.ref.SoftReference;import java.util.LinkedList;import java.util.List;public class SoftReferenceMain { public static void main(String[] args) { User u = new User(100,"IT小神"); //new是强引用 //软引用的使用示例: SoftReference<User> userSoft = new SoftReference<User>(u); u = null;//干掉强引用,确保这个实例只有userSoft的软引用 //--- 如果是 SoftReference<User> userSoft = new SoftReference<User>(new User()); 就没法干掉强引用 System.out.println(userSoft.get()); System.gc();//进行一次GC垃圾回收 System.out.println("After gc"); System.out.println(userSoft.get()); //往堆中填充数据,导致OOM List<byte[]> list = new LinkedList<>(); try { for(int i=0;i<100;i++) { System.out.println("*************"+userSoft.get()); list.add(new byte[1024*1024*50]); //1M的对象 } } catch (Throwable e) { //抛出了OOM异常时打印软引用对象 System.out.println("Exception*************"+userSoft.get()); } }}
package cn.itxs.garbage;import cn.itxs.entity.User;import java.lang.ref.WeakReference;public class WeakReferenceMain { public static void main(String[] args) { User u = new User(1,"小爽"); WeakReference<User> userWeak = new WeakReference<User>(u); u = null;//干掉强引用,确保这个实例只有userWeak的弱引用 System.out.println(userWeak.get()); System.gc();//进行一次GC垃圾回收 System.out.println("After gc"); System.out.println(userWeak.get()); }}
软引用 SoftReference和弱引用 WeakReference,可以用在内存资源紧张的情况下以及创建不是很重要的数据缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。实际运用如WeakHashMap、ThreadLocal
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”和直接回收。
第二次标记:如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,由一低优先级线程执行该队列对象中的finalize方法. 执行完毕后, GC会再次判断可达性(即只有一次自救的机会), 若不可达, 则直接进行回收, 否则对象“复活”
finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象这个时候,未被重新引用,那它基本上就真的被回收了。
package cn.itxs.entity;import cn.itxs.garbage.FinalizeMain;public class User { public int id = 0; public String name = ""; public User(int id, String name) { this.id = id; this.name = name; } @Override public String toString() { return "User [id=" + id + ", name=" + name + "]"; } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("触发finalize方法..."); // 进行拯救 FinalizeMain.user = this; } public void isAlive() { System.out.println("成功复活"); }}测试类
package cn.itxs.garbage;import cn.itxs.entity.User;public class FinalizeMain { public static User user = null; public static void main(String[] args) throws InterruptedException { user = new User(100,"IT小神"); // 对象被GC回收前执行finalize方法, 可以有一次自我拯救的机会 user = null; System.gc(); // finalize方法优先级低(JVM会调用一个优先级低的线程执行Queue-F队列中的finalize方法),sleep保证finalize方法已经执行完毕 Thread.sleep(1000); if (null != user) { user.isAlive(); } else { System.out.println("被回收了"); } // 尝试再次自救 user = null; System.gc(); // 因为finalize()方法优先级很低, 保证执行 Thread.sleep(1000); if (null != user) { user.isAlive(); } else { System.out.println("被回收了"); } }}
方法区主要回收的是类和常量
如何判断一个类是无用的类:同时满足一下3个条件,才能说一个类是无用的类。满足这3个条件便可以对无用类进行回收,但并非一定会回收。
如何判断一个常量是废弃常量:运行时常量池主要回收的是废弃的常量。
标记-清除算法(Mark-Sweep):分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可。新生代使用,有老年代空间担保。



垃圾收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实现。HotSpot虚拟机常见的垃圾收集器所处新生代和年老代、搭配使用关系如下图

Serial(串行)收集器是最基本、发展历史最悠久的收集器。用于Client模式;它是一个单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)。年轻代Serial收集器采用单个GC线程实现"复制"算法(包括扫描、复制),年老代Serial Old收集器采用单个GC线程实现"标记-整理"算法。Serial与Serial Old都会暂停所有用户线程(即STW),设置参数为

ParNew 收集器除了多线程外,其余的行为、特点和Serial收集器一样;但是此组合中的Serial Old又是一个单GC线程,所以该组合是一个比较尴尬的组合。
设置参数:

Parallel Scavenge收集器也是一个并行的多线程新生代收集器,它也使用复制算法。用于Server模式;Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。主要注重吞吐量(吞吐量越大,说明CPU利用率越高,所以主要用于处理很多的CPU计算任务而用户交互任务较少的情况),目标是达到一个可控制的吞吐量(Throughput)。 设置参数为

并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器。
特点
缺点
应用场景
运作过程:总体上说CMS收集器的内存回收过程与用户线程一起并发执行;分为一下四步:
设置参数
总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间; 但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Ye0vKYK-1644940079235)(http://www.itxiaoshen.com:3001/assets/1644899687176xbFaCsiJ.png)]
从G1与上面的CMS运作过程相比,仅在最后的"筛选回收"部分不同(CMS是并发清除)。
特点

应用场景
运作过程

设置参数
**本人博客网站 **IT小神 www.itxiaoshen.com