怎么开通公司网站,寻花问柳一家专注做男人喜爱的网站,棋牌网站开发搭建,成都软件开发工资一般多少钱目录JVM垃圾回收分代收集如何识别垃圾引用计数法可达性分析法引用关系四种类型#xff1a; 强、软、弱、虚强引用软引用 SoftReference弱引用 WeakReferenceWeakHashMap软引用与虚引用的使用场景虚引用与引用队列引用队列虚引用 PhantomReference垃圾回收算法引用计数复制 Cop…
目录JVM垃圾回收分代收集如何识别垃圾引用计数法可达性分析法引用关系四种类型 强、软、弱、虚强引用软引用 SoftReference弱引用 WeakReferenceWeakHashMap软引用与虚引用的使用场景虚引用与引用队列引用队列虚引用 PhantomReference垃圾回收算法引用计数复制 Copying标记 Mark-Sweep标整 Mark-Compact垃圾回收器种类串行回收器并行回收器并发回收器G1ZGC常用垃圾回收器实例新生代SerialParallel ScavengeParNew老年代Serial OldParallel OldCMSCMS四个阶段年轻代老年代G1JVM垃圾回收
为什么进行垃圾回收Java不像C语句那样使用时要自己通过代码开辟空间用过后在手动销毁。Java为我们程序提供虚拟机使得我们在写代码时更加关注业务逻辑至于内存空间的开辟释放这些交给JVM。代码运行在JVM上JVM会通过算法识别出哪一个是垃圾再通过垃圾回收算法自动的将程序中用不到的空间进行释放。C语言C好比手动挡Java好比自动挡。 GCGarbage Collection 垃圾收集年轻代的垃圾收集也叫GC老年代的垃圾收集称为Full GC。 什么是垃圾 程序在运行时需要在内存中开辟的空间存储数据。但此空间被使用一次后可能再也不会使用没有指向它的地址程序就无法使用这个地址空间的存储内容像内存中缥缈的幽灵。但这些数据是真真实实在内存存在的这就是垃圾。java程序在编写代码是只会开辟空间却不能释放空间就会使得内存中无用的数据占据的内存空间过多一点点的缩小着程序实际的可用内存最终导致内存溢出。 下面以一个单链表删除一个节点为例被删除的节点就是一个垃圾
public class Application {public static void main(String[] args) {// 创建三个节点Node n1 new Node(张三);Node n2 new Node(李四);Node n3 new Node(王五);// 将3个节点连接起来形成 n1-n2-n3n1.nextn2;n2.nextn3;// 遍历链表Node noden1;while (node!null){System.out.println(node.data);nodenode.next;}// 删除一个节点 将中间的节点删除n1.nextn3;n3.pren1;n2null;// 遍历链表noden1;while (node!null){System.out.println(node.data);nodenode.next;}}
}
Data
class Node{Node pre;Node next;Object data;public Node(Object data){this.datadata;}
}
思考n2这个节点是不是没有用了那么它还占用着空间呢 原链表
删除n2节点后我们已经知道n2就是个垃圾了但使用Java不像C不能够手动操作内存释放空间。但JVM会自动识别出垃圾帮我们释放。
这时JVM就会通过算法可达性分析识别出n2就是个垃圾最后再将它所在的内存空间回收
垃圾回收的作用区域与范围
俗话说栈管运行堆管存储堆占用的空间最多。GC负责对方法区和堆进行垃圾回收但主要是针对堆。其他地区空间空间占用极少且不会产生什么垃圾。
既然堆是垃圾回收的主要场所那么先忽视垃圾回收的一系列问题接下来先介绍堆的具体逻辑结构以及垃圾在堆中是怎样个转运过程
分代收集
GC垃圾回收极有可能能不止只回收一次而同一个内存区域也可能在第一次垃圾回收时还不是垃圾但在第二次回收就是垃圾。如何高效的扫描出垃圾高效的清除垃圾尽可能的不影响JVM正常运行GC将堆空间在逻辑上划分为三代物理上都是在内存中分别是新生代老年代元空间JDK7以前为永久代。JVM会将这三个区域的存储信息根据区域的特点采用针对它们的方式进行分代管理。例如在年轻代进行GCYoungGC老年代进行FullGC。 分代收集的思想主要有两个
绝大多数对象朝生夕死熬过越多次数的垃圾回收过程的对象就越不可能成为垃圾 接下来将描述这几个区域在实际的垃圾回收中是怎样协调运转以及为何这样划分 我们一开始创建的类对象在初始时都会放在伊甸园区Eden随着创建的对象越来越多如果新创建的对象在Eden没有空间存放这时会就会触发YoungGC。第一次触发YongGC会扫描Eden区所有的垃圾和幸存者from区此时from区为空然后将不是垃圾的复制到幸存区的to区然后清空Eden区和幸存者from区所有的数据同时将经历过一次GC扫描后判断不是垃圾的内容的年龄1。 经历过第一次GC后当前的堆空间状态Eden区为空幸存者to区有少量存活的数据这些数据的年龄都1将to区转为from区原幸存者from区转为to区此时to区为空。由于经历过一次GC后空间得到释放以后再新创建的对象接着放入Eden区还是随着程序的运行创建的对象越来越多当Eden再次满了这时会触发第二次GC注意和这一次幸存者区中有一为空即to区GC会扫描出Eden区和幸存者from区中不是垃圾的部分然后将这部分复制到另一个空的幸存者区to区同时也会将不是垃圾的数据年龄再次1。由于经历过一次GC后空间得到释放以后再新创建的对象接着放入Eden区循环往复。 直到某一次至少GC15次后由于每一次不被当为垃圾的数据年龄都会1直到有些数据的年龄达到15岁可通过配置JVM参数修改这时达到15岁的数据已经经历过15次GC这时系统认为这些数据在接下来会有很少的概率成为垃圾每次来回移动它们耗时费力于是将这些数据放入老年代。当触发YoungGC时不涉及老年代。 新生代的流程就是这样总结就是 复制1-清空-互换。复制 不是垃圾的数据到为空的幸存者区假设0区此时0区为to区同时这些数据年龄1然后清空Eden区和复制前不为空的幸存者区假设1区此时1区为from区然后将from区与to区互换。此时0区是from区1区就是to区。下次GC时再次互换。 接下来转到年龄已经达到15次在老年区的数据每一次发生在年轻代的GC都不会波及到这里但有可能GC后有新数据加入。随着频繁的GC不断的有新数据添加系统会提前预测老年代的空间是否会满每次新增数据的平均值老年代剩余空间当预测下次可能使得老年代空间满这时就会触发Full GC。扫描老年代的垃圾并将其清除释放老年代的内存空间。 这个图只是大致流程忽略很多细节 接下来介绍元空间 JDK7及以前元空间之前成为永久代是方法区的实现所以又称为非堆。但在逻辑上它是堆的一部分堆分为年轻代老年代永久代物理上使用的也是同一块内存。它和老年代绑定在一起无论谁满了都会触发清除老年代和永久代的垃圾。这样就不用单独为永久代编写对应的代码直接使用老年代的。永久代主要存储类信息普通常量静态常量编译器编译后的代码等。例如我们创建的对象根据哪个类创建的这个类的模板就在永久代中对象的头信息的中的类针就会指向永久代中它实例的类。在JDK7后将字符串常量池移动到了堆中。但仍有一个问题由于永久代存储的数据和堆中不同永久代到底设置为多大合适很难确定因此在JDK8以后将永久代移动到了直接内存中并改名为元空间Metaspace在逻辑上和物理上与老年代分离。 这样元空间使用的就是本地内存默认最大使用空间就是本地内存大小也可以配置上限可以自由的根据实际情况加载类的信息。有自己的垃圾回收频率而不用跟随老年代。 元空间是代替了老年代成为方法区规范的实现。 永久代与元空间最大的不同就是 元空间使用的是直接内存大小可以不必写死 注意
新创建的对象都会放入Eden区但在扫描垃圾时会将Eden区和不为空的幸存者区放在一起扫描。幸存者from区和幸存者to区大小永远一致并不是所有对象一定要年龄达到15才可以进入到老年区。如一些大对象或在Eden区和幸存者From区幸存者空间大小超过幸存者to区一半大小等这些都是直接放入老年区。触发老年区的Full GC的条件有很多 在代码中执行System.gc()在代码中极少使用老年代空间不足空间分配担保失败在GC前计算出平均每次从年轻代晋升到老年代的空间大小当大小超出老年代剩余空间就会进行FUll GC元空间超过阈值默认情况下元空间的大小与本地大小有关。当元空间使用总大小超过阈值就会进行Full GC。 Full GC造成的STW时间过长STW是GC的10倍以上所以GC的调优思想就是要么减少Full GC的次数要么减少Full GC的STW时间。
思考既然从Eden区和幸存者区中扫描出不是垃圾的内容要放入另一个幸存者区那这两个区的功能是固定好的吗 复制之后有交换谁空谁是to 在经历过第一次GC后谁空谁是to区。因为在年轻代垃圾清除采用的是复制算法。扫描出不为垃圾的部分将其转移到另一个目的地。谁做目的地是不固定的如果固定话假设幸存者1区永远为from区幸存者0区永远为to区第一次GC将不为垃圾部分复制到to区第二次垃圾回收就要扫描Eden区和to区要复制的地方就不能为to区了应该是from区。因为幸存者区也需要进行垃圾回收所以要有to区且from区与to区要能够实现逻辑互换。 思考为什么在新生区清除垃圾时是复制有用的而不是直接清除无用的这样不是就不需要幸存者区会更加节省空间了吗 根据统计在使用中98%的对象都是临时对象也就是说Eden区大部分都是垃圾。在垃圾堆里挑出有用的比一个个清除垃圾效率更高所以新生代采用复制有用的部分。但这种复制方式需要额外空间。老年代的数据经过15次GC在成为垃圾的概率很小所以只需要清除无用的部分即可。 思考什么时候会触发GCGC通常指YoungGC 当Eden区满了的时候就会触发GC。创建对象时是存储在Eden区的幸存者from区不参与直接存储新创建的对象。
上面已经简单介绍了垃圾回收过程那么从微观上来讲无论是年轻代还是老年代垃圾回收器是如何识别出哪些是垃圾的呢
如何识别垃圾
无论是那种算法核心就是找出在程序中没有引用关系的内存地址因为没有引用关系程序中就一定不会再使用到它了将其回收。
引用计数法
堆内存中的数据每被引用一次次数就1取消引用则次数减1如果堆内存中某块数据的引用次数为0则代表没有地方引用此数据则判定为垃圾。 例如有一个Node类由node1引用它引用次数为1-node1node2都引用它引用次数为2-node1取消引用它引用次数为1-node2取消引用它引用次数为0此地址的数据是垃圾。
public class Application {public static void main(String[] args) {Node node1 new Node(张三);Node node2node1;System.out.println(node2);node1null;System.out.println(node2);node2null;}
}但这种方式存在一个问题循环引用问题
public class Application {public static void main(String[] args) {Node node1 new Node(张三);Node node2 new Node(李四);node1.setData(node2);node2.setData(node1);node1null;node2null;}
}以name为张三的Node为例node1引用引用次数为1-node2中data引用引用次数为2-node1引用无效引用次数为1但此时由于Node2已经为null程序中已无法使用此Node此Node已经为垃圾但引用次数为1 不为0无法被识别为垃圾。这就是采用引用计数法带来的循环引用问题。 引用计数法的缺点
每个对象都要维护一个引用计数器有性能损耗拿以处理循环引用问题
由于引用计数的缺点突出现在已经基本上不会使用引用计数法了而是使用下面的可达性分析法。
可达性分析法
从一个可以作为GC roots的对象作为起点顺着它的引用关系遍历它的引用链。所有的GC roots对象都没有遍历到就是内存中的垃圾此地址的数据在程序中已经没有使用它的地方。就像葡萄串一样从根开始顺着藤蔓向下找能找到的葡萄粒一定是在串中的只有掉落的葡萄粒不会被遍历到这种就是垃圾。 还是上面采用技术法无法解决的例子
public class Application {public static void main(String[] args) {Node node1 new Node(张三);Node node2 new Node(李四);Node node3 new Node(王五);new Node(赵六);node1.setData(node2);node2.setData(node1);node1node3;node2null;}
}上面代码可以表示为
如果使用可达性分析法node1node2和node3都可以作为GC roots从它三开始遍历它的引用关系。最终也没有遍历到内存中name为张三李四赵六这三个节点因此判定这三个为垃圾会被回收
既然是从GC roots对象集合进行遍历引用关系那么那些对象可以作为GC roots呢GC roots是四类对象的集合
栈中局部变量引用的对象方法区中静态属性引用的对象方法区中常量引用的对象本地方法栈JNI引用的对象
在上面的例子中所有作为GC roots的对象都是第1种。
从GC roots遍历有引用关系的对象不是一定不会作为垃圾要分当时内存情况和那种引用关系。但没有引用关系的一定会被当做垃圾。 由遍历GC roots对象们的引用关系来确定哪些是垃圾这又牵扯到引用的关系类型。
引用关系四种类型 强、软、弱、虚
对象间的引用关系可以分为4种分别是强引用软引用弱引用虚引用。我们平时使用到的引用关系都是强引用剩余三种方式都是在特殊的功能下使用。 补充
JVM会在Eden区空间不足或老年代空间不足时触发GC我们在代码中也可以手动调用GC。使用 System.gc();这种方式不常见通常只在测试中使用。而且使用这种方式GC是Full GC。在默认的情况下堆内存最小分配占服务器总内存的六十四分之一最大内存占服务器总内存的四分之一 JVM会以最小内存启动当内存不够用它会调整直到达到最大内存如果仍不够用则OOM错误。 验证 本机内存16G 也可以通过代码的方式获取
public class Application {public static void main(String[] args) {OperatingSystemMXBean mem (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();// 获取内存总容量long totalMemorySize mem.getTotalPhysicalMemorySize();}
}查看JVM中最小堆内存和最大堆大小
public class Application {public static void main(String[] args) {System.out.println(Runtime.getRuntime().totalMemory()/1024/1024M);System.out.println(Runtime.getRuntime().maxMemory()/1024/1024M);}
}近似的满足 默认最大堆内存大小总内存大小/4默认最小堆内存大小总内存大小/64 这些默认的大小参数可以在Idea中调整服务器上可以在启动jar包时以命令行的方式调整通常为了防止内存波动将最大对堆内存空间与最小堆内存空间调成一致。 例如 -Xms5m 将堆内存最小空间设置为5M-Xmx5m 将堆内存最大空间设置为5M 在Idea中配置 代码验证结果 强引用
我们平常使用到的引用都是强引用其他引用需要特意用代码标出。例如
public class Application {public static void main(String[] args) {Node node1 new Node(张三);Node node2 new Node(李四);Node node3node1;}
}node1和node3引用name为张三的Nodenode2引用name为李四的Node共三个引用都是强引用。 强引用的特点就是在GC roots可达的情况下强引用永远不会发生垃圾回收。 如果内存不足也不会回收直接抛出OOM异常。
软引用 SoftReference
在内存不足的时候软引用的堆空间地址无效也会当做垃圾回收。 代码测试思路测试同一个代码在不同的环境下 内存充足和内存不足只有软引用下的堆空间是否会被当做垃圾回收
public class Application {public static void main(String[] args) {User u1 new User(张三); //强引用方式SoftReferenceUser softReferenceU1 new SoftReference(u1); // 软引用方式 与强引用都是引用同一块地址System.out.println(u1); // 以强引用的方式获取引用地址的数据User name“张三”System.out.println(softReferenceU1.get()); // 以软引用的方式获取引用地址的数据User name“张三”u1null; //此时 User“张三”失去了强引用 只有一个软引用System.gc(); // 经历了一次 Full GCSystem.out.println(softReferenceU1.get()); // 测试 User张三是否被回收掉}
}
Data
AllArgsConstructor
class User{private String username;
}内存充足的情况下
结果 由此可见在内存充足时由于堆内存中Username“张三”仍有一个软引用使得它没有被当做垃圾回收。
在内存不足的情况下 设置JVM参数将JVM堆的最大最小内存设置为5MB
由于内存不足会直接报错这里对上面代码进行修改使得内存不足的语句进行try包裹以便输出
public class Application {public static void main(String[] args) {User u1 new User(张三); //强引用方式SoftReferenceUser softReferenceU1 new SoftReference(u1); // 软引用方式 与强引用都是引用同一块地址System.out.println(u1); // 以强引用的方式获取引用地址的数据User name“张三”System.out.println(softReferenceU1.get()); // 以软引用的方式获取引用地址的数据User name“张三”u1null; //此时 User“张三”失去了强引用 只有一个软引用try {Byte[] load new Byte[1024 * 1024 * 10];// 直接开辟一个10M的内存空间 使得堆内存不足 这是检测是否会只有软引用是否会被当做垃圾回收}catch (Exception e){}finally {System.out.println(softReferenceU1.get()); // 测试 User张三是否被回收掉}}
}结果
弱引用 WeakReference
只要触发GC软引用就会失效只被软引用的空间会被当做垃圾处理 没有进行GC
public class Application {public static void main(String[] args) {User u1 new User(张三); //强引用方式WeakReferenceUser weakReferenceU1 new WeakReference(u1); // 弱引用方式 与强引用都是引用同一块地址System.out.println(u1); // 以强引用的方式获取引用地址的数据User name“张三”System.out.println(weakReferenceU1.get()); // 以弱引用的方式获取引用地址的数据User name“张三”u1null; //此时 User“张三”失去了强引用 只有一个弱引用System.out.println(weakReferenceU1.get());}
}
Data
AllArgsConstructor
class User{private String username;
}结果在没有GC之前若引用下的对象仍可以使用
进行GC 思考代码中为什么要将u1置为null 使用u1是强引用强引用的特点就是在任何情况下强引用指向的对象永远不会被当做垃圾处理。如果在测试软引用弱引用虚引用时不将强引用断开就无法看到结果。
WeakHashMap
当我们使用HashMap时Key是一个强引用关系。例如我创建了一个User u1Username“张三”想要给u1对象加个附属的值于是将u1作为key传递给HashMap。当u1对象使用完毕附属的值也应该消失想要释放时u1null但这时HashMap仍有一个强引用在指着User这个对象。这个对象就无法当做垃圾释放。 这样对象在垃圾回收时还会存在就会浪费内存引发OOM问题 使用WeakHashMap是一个与对象建立弱引用关系想要使用此对象又不想让成为对象不是垃圾的依据。当u1使用完毕断开强引用时在GC时就会将垃圾回收忽略WeakHashMap中的引用。
public class Application {public static void main(String[] args) {WeakHashMapUser, String weakHashMap new WeakHashMap();User u1 new User(张三);User u2 new User(李四);weakHashMap.put(u1,v1);weakHashMap.put(u2,v2);u1null; // User(张三)由于断开强引用只有一个弱引用的WeakHashMap与之相连在发生GC时会被回收System.out.println(weakHashMap);System.gc();System.out.println(weakHashMap); // 判断只有WeakHashMap引用下的对象是否被释放}
}结果 但当对象作为weakHashMap作为Value时User对象断开u1的强引用在GC后对象仍然存在所以WeakHashMap的value部分可能是强引用。如图 思考当key指向的对象被回收后在通过WeakHashMap的key去获取值时会返回什么
思考WeakHashMap可能由于Key是弱引用当引用对象没有强引用后被回收那么回收后WeakHashMap的value值去哪了 思考WeakHashMapObject,Object是否类似于HashMapReferenceObject,Object?
补充我在ThreadLocalMap源码中中就看到过这种使用弱引用的思路来解决ThreadLocal内存溢出问题 将ThreadLocal作为key当ThreadLocal销毁时Map中关于此ThreadLocal的数据也会释放。
软引用与虚引用的使用场景
软引用和弱引用通常配合着强引用来使用。软引用是当强引用断开时在内存允许的情况下还想使用该地址空间。软引用是当强引用断开自己也没必要去使用该地址空间通常配合着集合当强引用断开集合中的元素也没有意义使用弱引用能够就释放引用的堆地址集合移除的方式堆释放麻烦交给GC去释放。
例如利用软引用生成缓存 读取一个图片时如果每次使用都从磁盘读取会影响性能如果一次全部读取又有可能内存溢出。如何既能提升效率又不会使得内存溢出呢这时就要将内存利用最大化在内存空间不足快要发生OOM时将加载到内存的图片释放。如果运行期间内存充足则可以一直使用内存中的图片。 HashMapString, ReferenceByte hashMap new HashMap()
虚引用与引用队列
虚引用的使用要配合着引用队列没有引用队列使用虚引用将无意义。
引用队列
在引用的对象被销毁前会先放入引入队列中另一端监听这个队列可以进行一些关于即将销毁的对象的善后处理 软引用弱引用虚引用都可以在构建时指定引用队列这里的虚引用在构建时必须要使用指定引用队列。 下面虚引用结合引用队列的使用
public class Application {public static void main(String[] args) throws InterruptedException {ReferenceQueueObject referenceQueue new ReferenceQueue();User u1 new User(张三);WeakReferenceObject weakReference new WeakReference(u1, referenceQueue);u1null;new Thread(()-{while (referenceQueue.poll()null){}System.out.println(对象被销毁);}).start();System.gc();TimeUnit.SECONDS.sleep(1);}
}很少会使用到
虚引用 PhantomReference
phantom幽灵幻觉虚引用无法获取引用对象它唯一的用处就是和引用队列结合使得被引用的对象在销毁前能够加入引用队列这样在引入队列的另一头可以做一些善后工作。使用虚引用可以不对类产生任何额外的影响极少会用到。
public class Application {public static void main(String[] args) throws InterruptedException {User user new User(张三);ReferenceQueueObject referenceQueue new ReferenceQueue();PhantomReferenceUser reference new PhantomReference(user, referenceQueue); // 在创虚引用对象时就要指定消息队列System.out.println(reference.get()); //尝试获取虚引用引用的对象usernull;new Thread(()-{while (referenceQueue.poll()null){}System.out.println(虚引用引用的对象被销毁);}).start();System.gc();TimeUnit.SECONDS.sleep(1);}
}
结果
思考其他引用队列可以在销毁前可以加入引用队列吗 可以只是很少用到只是虚引用只有这个功能所以特别介绍 总结至于对象是否会被回收只需要查看引用此对象的个数和引用的方式。 上面已经介绍了如何识别垃圾接下来就是发现垃圾后如何处理呢
垃圾回收算法
引用计数
计数为0的就是垃圾有一次引用次数就1,有一个引用失效次数就-1。 这种算法是结合扫描垃圾的引用计数法但这种方法存在循环引用问题已经很少使用所以引用计数这种算法方法也不会使用。
复制 Copying
复制A区不是垃圾部分到B区然后清空A区。这种方式适用于堆的年轻代因为年轻代不是垃圾占少数移动量相对较少。将Eden区和幸存者from区不为垃圾的部分复制到幸存者to区然后清空Eden区和幸存者from区。 这种方式效率较高而且在复制是从to区全为空的地方开始复制清除是将Eden区和幸存者from区全部清除所以不会产生内存碎片。 这种方式的缺点
需要额外的空间如幸存者to区就需要和from区一样大小在任何时刻在幸存区中都有一半的内存被浪费如果遇到极端的情况如Eden区100%全不为垃圾这是复制有用部分耗时不说还会将幸存者to区撑爆。所以适合存活率低的地方在默认的情况下Eden区幸存者from区幸存者to去811
标记 Mark-Sweep
标记清除分为两部分 1. 遍历所有GC roots标记哪一部分是垃圾 2. 遍历整个堆清除垃圾。在标记清除垃圾和清除垃圾时都需要STWstop the world暂停整个应用。发生在老年代因为老年代绝大多数对象都是经历过15次GC的再次成为垃圾的概率较小。 它有两大缺点
效率低需要暂停整个应用清除使得内存不连续造成内存碎片过多。JVM就不得不维持一个内存的空闲列表这又是一种开销。而且在分配数组对象的时候大对象在老年区需要一大块连起来的空间寻找连续的内存空间会不太好找。
标整 Mark-Compact
标记整理分为三部分1. 遍历所有GC roots标记哪一部分是垃圾 2. 遍历整个堆清除垃圾。3.将内存进行整理减少碎片的产生。相较于标记算法多了整理一步这就使得内存碎片问题得到解决但也带来了新的问题整理时花费时间和耗费CPU。 标记清除和标记整理都发生在老年代在使用时常常会将二者结合在进行多次标记清除后进行一次整理。这样既避免了频繁标记整理耗费时间和性能又能使得内存碎片在合理的范围内。 这三个算法各有各的优势各有各的劣势。没有完美的算法要根据合适的长江选择合适的算法。 上面的是哪个算法是一种解决垃圾回收的思路具体的实现工具就是下面的垃圾回收器。
垃圾回收器种类
真正将根据算法实现的工具垃圾回收器总的来说回收的方式分为5类。
串行回收器
只有一个线程在进行回收在回收垃圾时会暂停所有用户线程。适合单线程的场景。就好比上课时只有一个保洁阿姨进来要打扫卫生。想要继续上课只能等到保洁阿姨打扫完卫生。所有用户线程都要暂停等待这一个线程GC完毕效率较低。 具体实现有 Serial用于年轻代Serial Old用于老年代
并行回收器
相较于串行回收器在回收时不再只有一个线程而是多个线程一起参与。这样就不再等待一个人干活任务量不变多个人一起干活相较于一个人干活能够减少用户线程的等待时间。 具体实现有 ParNew用于年轻代Parallel Scavenge用于年轻代Parallel Old用于老年代
并发回收器
用户线程和回收线程可以一起工作虽然有暂停但时间较短它的最大特点就是没有长时间暂停适合用于于用户对交互请求强的场景。因为用户在交互时肯定不希望突然长时间的暂停使用并发垃圾回收器可以减少响应时间。 具体的实现有 CMS用于老年代
G1
ZGC
思考什么是STW STWStop The Word是在进行垃圾回收时会暂停所有用户线程造成卡顿的现象。 思考复制算法也会到导致STW吗为什么要GC时要STW 所有垃圾回收器都会导致STW只是时间长短问题。因为在最终确认垃圾时一定要确保一致性否则程序不断运行引用关系不断变化分析的结果会不准确。如果在复制时没有暂停导致在这期间创建的对象就不会被标记为存活对象就会导致幸存者没被移动到幸存者区而被全部清除强引用被清除程序会出错。且在复制算法和标记整理算法中会导致原引用对象的地址会发生变化。防止引用混乱。
常用垃圾回收器实例
新生代
Serial
只有一个线程在年轻代中使用复制算法将伊甸园中与幸存者From区所有幸存的对象复制到幸存者To区。在GC时需要STW。可以与Serial Old垃圾回收器一起搭配工作。
Parallel Scavenge
开启多个线程在年轻代中使用复制算法将伊甸园中与幸存者From区所有幸存的对象复制到幸存者To区。在GC时需要STW由于采用多线程一起工作STW的时间可能会短些。 可以通过参数 XX:MaxGCPauseMillis及直接控制吞吐量的参数-XX:GCTimeRatio设置吞吐量。吞吐量程序运行时间/程序运行时间GC时间。提升吞吐量能够提升CPU的利用率但与响应速度无关并不一定能提升用户体验。
可以与CMS垃圾回收器Parallel Old垃圾回收器一起搭配工作。
ParNew
同样是多个线程一起GC与Parallel略有不同不能设置吞吐量。可以与CMS垃圾回收器一起工作
老年代
Serial Old
采用单一线程的方式回收垃圾在回收垃圾时会暂停所有的用户线程。与上面青年代的Serial类似但在老年代采用的是标记整理算法不会产生内存碎片。
Parallel Old
并发线程的回收与年轻代的Parallel 类似但在老年代采用的标记整理算法不会产生内存碎片。
CMS
CMS全称concurrent mark sweep翻译过来就是并发清除。
CMS四个阶段
初始标记在老年区将所有GC roots即根节点全部扫描出来。在这期间在暂停根节点的变化所以是STW的但由于GC roots相对较少所以STW时间相对较短并发标记由于在初始阶段已经将根节点全部扫描出来在这个阶段只需要顺着根节点扫描出无法引用到的对象并标记为垃圾。在这期间是与用户线程并行执行的。由于通常根节点下面的节点要比根节点要多所以是相对耗时的但由于是与用户线程并行执行并没有STW。缺点就是这节点比较耗费性能。重新标记由于用户线程一边执行一边扫描垃圾垃圾可能会新增。这是暂停所有用户线程来个最终的大扫除。将这一过程新增垃圾再次进行标记确保打扫彻底不能由于并发产生的垃圾不知道。虽然这个阶段是STW但由于新增的垃圾比较少所以时间很短。并发清除到这里就已经标记完所有的垃圾剩余的就是清除清除算法采用的是标记清除。清除的线程可以与用户线程一起执行。
CMS最大的特点就是STW较短在交互中系统卡顿的时间较短适用于与用户交互时使用。 但CMS有两大的缺点
在并发清除的过程中由于用户线程没有暂停这是可能有新的数据加入老年代像大对象直接放入老年代所以并不是满了再去清除而是预留一定的空间。比如10%然而如果在并发清除过程中新进入老年代的数据超过预留的10%则此时老年代没有空间存放会触发Concurrent Mode Failure然后CMS将退化为Serial Old垃圾回收器。暂停所有用户线程采用单线程的方式进行回收。造成严重卡顿。预留空间大会造成频繁Full GC预留空间小会造成CMS回收失败。CMS采用的算法是并发清除算法所以会产生内存碎片。碎片过多时就会触发Serial GC采用单线程标记整理算法进行清除和碎片整理。
思考CMS为什么不使用标记整理算法来解决碎片问题 如果使用 整理算法那么在并发清除时对象的地址就可能发生变化需要暂停所有用户线程。
年轻代老年代
G1
推荐阅读这篇博客 至于GC垃圾回收器的配置搭配使用以及JVM的一些参数、命令JVM结合Linux的调优等会在后续的博客中介绍。如果发现错误的地方欢迎一起探讨。