山西成宁做的网站,iis7安装wordpress,石家庄鹿泉网站建设,网站开发的项目开发计划在上一篇中#xff0c;介绍了JVM组件中的运行时数据区域#xff0c;这一篇主要介绍垃圾回收器
JVM架构图#xff1a; 1、垃圾回收概述 在第一篇中介绍JVM特点时#xff0c;有提到过内存管理#xff0c;即Java语言相对于C#xff0c;C进行的优化#xff0c;可以在适当的…在上一篇中介绍了JVM组件中的运行时数据区域这一篇主要介绍垃圾回收器
JVM架构图 1、垃圾回收概述 在第一篇中介绍JVM特点时有提到过内存管理即Java语言相对于CC进行的优化可以在适当的时机自动进行垃圾回收而不需要手动的编写回收逻辑。 而JVM中的垃圾回收主要体现在堆和方法区上。栈不存在垃圾回收的问题因为栈帧之间的线程独立并且方法在执行完后会弹出栈并且自动释放其中的内存。 1.1、方法区的垃圾回收 一般来说方法区的垃圾回收体现在以下几点 废弃常量的回收 方法区中存储的常量包括字符串常量、类的静态常量等。当这些常量没有被引用时它们就成为了废弃常量。垃圾回收器可以通过检查废弃常量并回收它们占用的内存空间。 无效的类的回收 在JVM中加载的类信息存储在方法区中。当一个类不再被使用例如类的实例已经全部被回收那么该类就成为无效的类。垃圾回收器可以通过检查无效的类并回收相关的内存空间。 而判定一个类是否能被卸载需要满足以下条件 实例是否可达 如果该类的所有实例都不再被任何活跃的引用链所持有即没有任何途径可以从根对象如静态变量、方法区中的常量等到达该类的实例那么该类的实例就成为了孤岛对象可以被回收。 静态变量和静态方法是否引用 如果该类的静态变量或静态方法仍然被其他地方引用那么该类本身的类信息也会保持可达状态无法被回收。因此如果某个类的静态资源仍然被引用即使该类的实例全部不可达该类也无法被回收。
public class GcDemo3 {public static void main(String[] args) {Student.study();}
}class Student{public static void study(){System.out.println(我是一个学习java时长达到两年半的练习生);}
} 没有unload 类加载器的生命周期 在Java中类的卸载与类加载器密切相关。当一个类加载器不再需要加载某个类时可以触发该类的卸载。只有当某个类的所有实例都不可达并且对应的类加载器也不再存活时才能够导致该类的卸载和回收。
案例
public class ClassUnload {public static void main(String[] args) throws InterruptedException {try {ArrayListClass? classes new ArrayList();ArrayListURLClassLoader loaders new ArrayList();ArrayListObject objs new ArrayList();for (int i 0; i 5; i) {URLClassLoader loader new URLClassLoader(new URL[]{new URL(file:D:\\Idea_workspace\\2024\\)});Class? clazz loader.loadClass(com.itheima.my.A);Object o clazz.newInstance();
// objs.add(o);
// loader null;
// classes.add(clazz);
// loaders.add(loader);System.gc();}} catch (Exception e) {e.printStackTrace();}}
}加入了两个JVM参数 -XX:TraceClassLoading 打印加载信息 -XX:TraceClassUnloading 打印卸载信息 每次循环都会触发回收 loader,class,a对应的实例都是强引用按道理强引用是宁愿OOM也不会垃圾回收案例中为什么每次循环都会触发回收 虽然每次循环中新创建了URLClassLoader对象、加载了类、实例化了对象但是在循环结束后这些对象都会丢失引用。因为这些对象都是在循环内部被创建的局部变量一旦循环结束它们就会超出作用域而无法再被访问到。 把案例中的代码注释打开则每次循环都将产生的对象放在对应的集合中产生了引用所以不会导致回收 1.2、堆区的垃圾回收 堆的垃圾回收取决于对象是否被引用 例如我现在有两个类A中引用了BB中引用了A
public class A {B b;
}public class B {A a;
}
public class Demo1 {public static void main(String[] args) {A a new A();B b new B();a.b b;b.a a;a null;b null;}
} 在JVM中是这样的结构 那么将a和b指向堆的地址设置成为null能否触发垃圾回收呢取决于不同的分析方法 1.2.1、引用计数法 每个对象都有一个与之关联的引用计数器当有新的引用指向该对象时计数器加1当引用不再指向该对象时计数器减1。当一个对象的引用计数器为0时即没有任何引用指向它可以判定该对象为垃圾可以被回收。 在上面的案例中a指向A的实例时引用计数器就1A的实例又指向B实例中的a时计数再次1b同理。 如果将a和b指向堆的地址设置成为null仍然保留了A的实例指向B中的aB的实例指向A中的b的引用即这种循环引用的情况下引用计数器无法归零这也是引用计数法的缺陷之一。 引用计数法的缺陷 无法处理循环引用 如果存在循环引用的情况即一组对象相互引用形成了一个环那么这些对象的引用计数器就永远不会为0导致它们无法被回收从而引发内存泄漏。 计数器更新开销较大 每次引用发生变化时需要对相关对象的引用计数器进行更新这会增加额外的开销并且会在多线程环境下引入并发访问的问题。 无法处理跨代引用 在分代垃圾回收算法中对象通常被分为不同的代而引用计数法无法处理跨代引用的情况可能导致一些对象被错误地回收或长时间无法回收。 1.2.2、可达性分析 该算法通过从一组根对象如线程栈、静态变量等出发递归地遍历所有可访问的对象标记它们为可达对象而未被标记的对象则被认为是不可达的可以被回收。 可达性分析主要包括以下几个步骤 确定根对象 首先确定一组根对象通常包括活跃线程的栈帧、静态变量以及常量池中的对象等。这些根对象是程序中已知的起始点是可以直接或间接访问到的对象。 遍历可达对象 从根对象开始对每一个对象进行遍历找出其引用的对象并标记这些对象为可达对象。然后对这些被标记的对象再次进行遍历重复这个过程直到无法找到新的可达对象为止。 标记未被访问到的对象 在完成可达对象的遍历后所有未被标记的对象都可以被认为是不可达的即无法从根对象出发访问到的对象。 回收不可达对象 将所有不可达对象进行回收释放它们占用的内存空间。 图中栈中的a,b即是根对象当将a和b指向堆的地址设置成为nullA和B的实例就已经被标记为不可达尽管内部依旧存在循环引用但是依旧会被回收。
2、四种引用 2.1、强引用 强引用是最常见的引用类型如果一个对象有强引用存在那么它就不会被垃圾回收器回收。即使出现内存不足的情况JVM也不会回收被强引用指向的对象。 什么时候一个对象会存在强引用 对象被赋值给一个变量例如通过Object obj new Object();将一个对象赋值给一个变量 obj 此时obj持有该对象的强引用。对象作为参数传递给方法例如调用方法时将对象作为参数传递给方法方法内部持有该对象的强引用。对象作为实例变量存储在其他对象中例如一个类中的实例变量引用了一个对象那么该对象就具有强引用只要该类的实例还存在。对象被设置为数组元素例如通过byte[] bytes1 new byte[1024 * 1024 * 100];将一个对象存储在数组中的某个元素位置该对象就具有强引用。 案例
public class Demo1 {public static void main(String[] args) {byte[] bytes1 new byte[1024 * 1024 * 100];byte[] bytes2 new byte[1024 * 1024 * 100];}
}将最大堆内存设置成200M上面的代码执行如果过程中没有发生GC就会内存溢出。 可以看到强引用即使是内存不足的情况下也不会进行回收 2.2、软引用 软引用是一种相对强引用弱化了一些的引用类型。当系统内存不足时JVM会尝试回收被软引用指向的对象但并不是必然回收在这样的情况下只有当对象已经没有强引用指向它时才会被回收。使用软引用可以有效地实现缓存功能。 同样将最大堆大小设置为200M。 byte数组对象被包装成了软引用这里为什么要在包装成软引用后把bytes设置为null呢可以思考一下。
public class SoftReferenceDemo2 {public static void main(String[] args) throws IOException {byte[] bytes new byte[1024 * 1024 * 100];SoftReferencebyte[] softReference new SoftReferencebyte[](bytes);bytes null;System.out.println(softReference.get());byte[] bytes2 new byte[1024 * 1024 * 100];System.out.println(softReference.get());}
} 当创建第二个byte数组时由于内存空间不足对被包装成软引用的byte数组对象进行了回收。 如果不将bytes设置为null则不会对被包装成软引用的byte数组对象进行了回收因为bytes有强引用指向它。 当被软引用SoftReference包装的实例对象被回收后其容器SoftReference对象也要被回收在SoftReference的构造方法中提供了一个ReferenceQueue队列。 当被软引用SoftReference包装的实例对象被回收后SoftReference对象会放入ReferenceQueue队列中 2.3、弱引用 弱引用比软引用的生命周期更短暂一旦GC运行无论内存是否充足被弱引用指向的对象就会被回收如被包装成弱引用的原有对象具有强引用依旧无法回收。弱引用通常用于实现规范映射或者监听器模式。
public class Demo2 {public static void main(String[] args) {WeakReferenceObject weakReference new WeakReference(new Object());System.out.println(weakReference.get());System.gc();System.out.println(weakReference.get());}
} 2.4、虚引用 虚引用是最弱的一种引用类型它几乎没有实际作用主要用于在对象被垃圾回收时收到一个系统通知。虚引用必须和引用队列ReferenceQueue一起使用。
PhantomReferenceObject phantomRef new PhantomReference(new Object(), referenceQueue); 此外还有一个finalize它代表的是终结器引用。当对象被回收时会将对象放入Finalizer类中的引用队列并且稍后由FinalizerThread线程从队列中获取对象执行finalize()方法用于对象在被垃圾回收之前进行清理和释放资源操作。但是具体的调用时间是不确定的而且垃圾回收器也不保证一定会调用每个对象的finalize()方法。 finalize()方法是Object类中的如果需要自定义逻辑只需要重写该方法。
案例 可以在重写的finalize()方法中再次将对象关联成强引用。
public class FinalizeReferenceDemo {public static FinalizeReferenceDemo reference null;public void alive() {System.out.println(当前对象还存活);}Overrideprotected void finalize() throws Throwable {try {System.out.println(finalize()执行了...);//设置强引用自救reference this;} finally {super.finalize();}}public static void main(String[] args) throws Throwable {reference new FinalizeReferenceDemo();test();test();}private static void test() throws InterruptedException {reference null;//回收对象System.gc();//执行finalize方法的优先级比较低休眠500ms等待一下Thread.sleep(500);if (reference ! null) {reference.alive();} else {System.out.println(对象已被回收);}}
} 由FinalizerThread线程从队列中获取对象 小结 日常程序开发中创建出的对象绝大部分都是强引用即使出现内存溢出也不会对被强引用的对象进行垃圾回收。弱引用和软引用区别在于前者是垃圾回收器只要发现了就会回收后者是内存不足时才会被优先回收但是共同点是如果被包装成软/弱引用的原目标对象被强引用则不会被回收。 3、垃圾回收算法 JVM常见的垃圾回收算法有标记-清除算法标记-整理算法复制算法分代回收算法。 无论哪一种算法其核心思想是先找到内存中存活的对象然后清除其他的对象并释放内存使得内存空间得以复用。 Java的垃圾回收是由单独负责GC的线程去执行的但是在执行的过程中都需要暂时停止用户线程(STW) 而评价一个垃圾回收算法的优劣通常会关注以下几点 内存利用效率垃圾回收算法应该尽可能地高效利用内存避免内存碎片化和内存泄漏问题。 垃圾回收的延迟垃圾回收算法执行时会导致程序停顿或延迟这会影响系统的响应速度。因此垃圾回收算法的评价中会考虑其对程序执行的影响包括停顿时间的长短、频率等。 垃圾回收的吞吐量垃圾回收算法应该尽可能地减少对程序的干扰以提高程序的整体吞吐量。垃圾回收的吞吐量指用户执行代码的时间/GC时间 碎片化处理能力垃圾回收算法应该有效地处理内存碎片化问题避免内存分配失败或者性能下降。 3.1、标记-清除算法 其核心思想在于利用可达性分析标记所有仍在使用中的对象然后清除所有不可达的孤岛对象。标记使用中的清除未使用的。 如图所示C对象为不可达会在下一次GC中被清除。 它的弊端在于在清理过程中可能会存在内存碎片。 首先明白一点内存是连续的而未被标记即将被清理的对象不会恰好都在一片连续的内存上如图所示红色部分代表被清理的对象。 由于内存碎片的存在JVM同时会维护一个空余内存的链表如果我要添加一个3字节的对象可能会遍历到链表的尾部才能实现如图所示这样的效率就会较低。 3.2、标记-整理算法 为了解决上述内存碎片导致的相关问题引入了标记-整理算法相当于对标记-清除算法的一种改进。 其核心思想在于通过可达性分析将所有使用中的对象进行标记然后对其进行整理使其存放在一块连续的内存上然后对所有未标记的对象进行清理 标记阶段 整理阶段 但是依旧存在一些缺陷 执行时间较长标记-整理算法在整理阶段需要移动存活对象这可能导致执行时间较长尤其在内存使用较大时整理操作对程序的停顿时间会更加明显。 需要额外空间标记-整理算法通常需要额外的空间来进行对象的移动操作这可能会增加算法的空间复杂度特别是当内存中存在大量存活对象时。 不适用于并发环境标记-整理算法通常需要在整理阶段移动对象这可能导致并发环境下的一些问题比如需要暂停所有线程的执行以保证整理操作的正确性。 3.3、复制算法 复制算法是另一种针对标记-清除算法的增强其核心思想在于将堆分为两个区域分别是From和To新创建的对象都在From中 当需要GC时将所有可达的对象放入To区域 然后清理From区域 最后将名称互换 可达的对象在GC时都是在To空间。 复制算法的主要弊端在于空间开销大复制算法需要额外的一块同等大小的内存空间来进行存活对象的复制这可能导致整体的内存利用率下降特别是在内存空间受限的情况下。 3.4、分代回收算法 其核心思想是将内存按代划分成新生代和老年代其中新生代又由伊甸园区和FromTo组成复制算法 新创建的对象会被分配在伊甸园区当伊甸园区空间充满后会触发一次Minor GC回收掉不可达的对象然后将剩下的对象放入To区并且将From和To的名称互换。 我们可以通过arthas工具查看一下这个过程JDK1.8版本需要添加JVM参数-XX:UseSerialGC 表示使用Serial 和 SerialOld垃圾回收器后面会说明
public class GcDemo0 {public static void main(String[] args) throws IOException {ListObject list new ArrayList();int count 0;while (true){System.in.read();System.out.println(count);//每次添加1m的数据list.add(new byte[1024 * 1024 * 1]);}}
} 然后通过arthas的memory命令查看从上到下分别是伊甸园区幸存者区老年区 同时我们也可以通过JVM命令自主设置这些区的大小 我们进行如下设置 -XX:UseSerialGC -Xms60m -Xmn20m -Xmx60m -XX:SurvivorRatio3 -XX:PrintGCDetail 初始状态下伊甸园区使用了8M 当我们添加了三次数据后触发了Minor GC: [GC (Allocation Failure) [DefNew: 15819K-4096K(16384K), 0.0084006 secs] 26354K-20135K(57344K), 0.0084792 secs] [Times: user0.00 sys0.00, real0.01 secs]: [GC (Allocation Failure) 表示这次垃圾回收是由于内存分配失败而触发的。[DefNew: 15819K-4096K(16384K)表示新生代Young Generation的垃圾回收情况。15819K是垃圾回收前新生代已使用的内存大小4096K是垃圾回收后新生代已使用的内存大小16384K是新生代总内存大小。26354K-20135K(57344K)表示整个堆内存的垃圾回收情况。26354K是垃圾回收前堆内存已使用的大小20135K是垃圾回收后堆内存已使用的大小57344K是堆内存总大小。0.0084006 secs:表示这次垃圾回收的实际耗时。[Times: user0.00 sys0.00, real0.01 secs]:表示垃圾回收过程中消耗的CPU时间和实际时间。 垃圾回收前后的内存情况 当某个对象在新生代存活过了最多15次垃圾回收存在一种特殊情况如果新生代的内存已满即使年龄没有到达15依旧会被放入老年代JVM会认为它很难被回收就会放入老年区中。 当老年代空间不足时会先触发一次Minor GC如果内存依旧不足则会触发一次Full GC对新生代和老年代所有的垃圾进行回收若Full GC后依旧无法回收老年代的垃圾就会触发OOM错误。 老年代空间不足并且触发了Minor GC依旧内存不足: 触发Full GC: 由于案例中的对象都是强引用所以无法回收再次向老年代放入则会触发OOM 4、垃圾回收器 垃圾回收器是负责管理堆内存中对象的回收和内存释放的组件。JVM的垃圾回收器有多种不同的实现通常新生代的垃圾回收器需要和老年代的垃圾回收器配合使用 4.1、SerialSerial Old 第一种组合方式是Serial新生代和Serial Old老年代 Serial、Serial Old是一个单线程的收集器有一个专门用于GC的线程进行垃圾回收过程中会暂停所有用户线程来进行垃圾回收。 区别在于Serial使用的是复制算法而Serial Old使用的是标记-整理算法。 如果需要使用该垃圾回收器需要配合JVM参数-XX:UseSerialGC 可以通过arthas的dashboard命令查看 4.2、ParNewCMSSerial Old 第二种组合方式是ParNew新生代配合CMS老年代或Serial Old老年代 ParNewSerial Old的组合实际上是针对SerialSerial Old的一种优化使用多线程进行垃圾回收ParNew使用的依旧是复制算法。 可通过JVM参数-XX:UseParNewGC 使用 CMS垃圾回收器在尽量减少停顿时间的同时进行垃圾回收。适用于对响应时间要求较高的应用。允许用户线程和垃圾回收线程在某些时刻同时运行。 使用的是标记-清除算法 执行主要分为四个阶段
初始标记阶段CMS会暂停所有应用线程来标记根对象。并发标记阶段CMS收集器会和应用程序线程并发地标记整个堆内存中的对象。重新标记阶段由于并发标记阶段没有暂停应用线程某些对象可能发生改变所以需要二次标记。暂停所有应用线程并发清理阶段与应用程序并发的执行清理所有未被标记的对象。 它存在的缺陷
因为使用的是标记-清除算法所以会产生内存碎片。在并发清理阶段因为应用程序没有暂停所以在清理的过程中也可能会存在新的垃圾。如果老年代内存不足无法分配对象CMS会退化为Serial Old单线程回收老年代。 4.3、Paraller ScavengeParaller Old 最后一种组合是Paraller Scavenge新生代配合Paraller Old老年代 Paraller ScavengeParaller Old组合通过多线程并行地进行垃圾收集将应用程序的暂停时间降到最低以此来提高系统的吞吐量。Paraller Scavenge使用的是复制算法Paraller Old使用的是标记-整理算法 在GC期间会暂停所有其他线程的执行。 在JDK1.8中默认就是使用该组合进行垃圾回收无需额外设置JVM参数。 同时这个组合允许自定义最大暂停时间、吞吐量以及自动调整内存大小
-XX:MaxGCPauseMillis 设置最大暂停时间-XX:GCTimeRatio 设置吞吐量-XX:UseAdaptiveSizePolic 自动调整内存大小 4.4、G1垃圾回收 G1垃圾回收器是在JDK 7中引入的一种全新的垃圾回收器并在JDK9中成为默认的垃圾回收器。 它具有以下的特点
区域化内存管理G1将堆内存划分为多个大小相等的区域每个区域既可以用作新生代也可以用作老年代这样就消除了新生代和老年代之间的严格划分。并发和并行G1垃圾回收器在标记、整理和清理阶段都可以利用并发和并行的方式进行垃圾回收以减少暂停时间并提高吞吐量。持续性垃圾回收G1会根据应用程序的动态情况来选择哪些区域进行垃圾回收以尽可能地减少垃圾回收的暂停时间。可预测的暂停G1通过一种叫做“垃圾优先Garbage-First”的算法来选择优先回收哪些区域从而实现可预测的垃圾回收暂停时间。 在分代回收算法中划分的新生代和老年代的内存空间是连续的 而在G1垃圾回收器中新生代与新生代老年代与老年代之间并非一定是一块完整的区域 而是将堆划分为大小相同的区域每个区域的大小可以通过堆大小/2048算得也可以自己通过JVM参数进行设置-XX:G1HeapRegionSize 但是参数的值必须是2的指数幂并且最大为32。 在垃圾回收时分为新生代回收和混合回收 4.4.1、新生代回收 当新生代的空间达到60%时会触发一次回收使用的是复制算法 标记伊甸园区和幸存者区放入另一个幸存者区但是为了兼顾最大暂停时间通常不会对所有的垃圾进行回收。当某个对象的年龄到达15时会被放入老年代。 如果某个对象的大小大于该区域的1/2例如某个区域分配的大小为4M某个对象的大小为3M这些对象会被集中存放入Homongous区中。 4.4.2、混合回收 当多次新生代回收后总堆占有率达到阈值时会触发混合回收使用的是复制算法 混合回收的阶段
初始标记阶段会暂停用户线程标记所有GC Root引用的对象不包含该对象的引用链。并发标记阶段会和用户线程并发执行将第一步中对象的引用链一并标记为存活。最终标记阶段会暂停用户线程主要是标记第二步中发生引用改变漏标的对象。并发清理阶段会和用户线程并发执行将存活的对象通过复制算法 复制到其他区域。 和CMS四个阶段的区别
CMS混合回收初始标记阶段标记所有GC Root引用的对象标记所有GC Root引用的对象并发标记阶段标记所有的对象标记第一步中对象的引用链最终重新标记阶段标记第二步中错标漏标的对象标记第二步中发生引用改变漏标的对象不管新创建和不再关联的对象并发清理阶段使用的是标记-清除算法将存活的对象通过复制算法 复制到其他区域。 G1进行老年代回收时也会优先清理存活度最低的区域但是在清理的过程中如果没有足够的区域去转移对象就会触发Full GC这时使用的是标记-整理算法。 总结 JVM基础篇完结