旅游网站的功能结构图,业之峰,赣州高端网站开发,网站中备案与不备案的区别这里是Themberfue 在上节课对内存区域划分以及类加载的过程有了简单的了解后#xff0c;我们再了解其他两个较为重要的机制#xff0c;这些都是面试中常考的知识点#xff0c;有必要的话建议背出来#xff0c;当然不是死记硬背#xff0c;而是要有理解的背~~~如果对 JVM … 这里是Themberfue 在上节课对内存区域划分以及类加载的过程有了简单的了解后我们再了解其他两个较为重要的机制这些都是面试中常考的知识点有必要的话建议背出来当然不是死记硬背而是要有理解的背~~~如果对 JVM 的其他机制感兴趣的话建议去阅读一些书籍我的建议是失眠的时候看看可以有效改善睡眠bushi 类加载器 类加载器从 JDK 1.0 就出现了最初只是为了满足 Java Applet已经被淘汰 的需要。后来慢慢成为 Java 程序中的一个重要组成部分赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。我们先看官方 API 文档的介绍A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a class file of that name from a file system. Every Class object contains a reference to the ClassLoader that defined it. Class objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.简单来说在 Java 中类加载器ClassLoader是负责加载类文件.class 文件的组件。类加载器的主要作用是将字节码加载到 JVM 中并使其成为JVM 可识别并执行的类。类加载器是Java 运行时环境的一部分它控制了类的加载过程并且是 JVM 的重要组成部分之一。类加载器是一个负责加载类的对象用于实现类加载过程中的加载这一步。每个类都是通过 类加载器加载的。类加载器将字节码文件加载到内存并将其转化为 JVM 内部的 Class 对象从而使得程序能够使用该类。每个 Java 类都有一个引用指向加载它的 ClassLoader。数组类不是通过 ClassLoader 创建的数组类没有对应的二进制字节流是由 JVM 直接生成的。 class ClassT {...private final ClassLoader classLoader;CallerSensitivepublic ClassLoader getClassLoader() {//...}...
} 在前边多线程的学习中我们学习了懒汉模式Java 类加载的规则也是采用懒汉模式来加载类的。JVM 启动时并不会一次性加载所有类而是根据需要动态加载类。也就是说大部分类在具体用到的时候才会去加载这样对内存更加友好。这便是懒汉模式一个应用~~~对于已经加载的类会被放在 ClassLoader 中。在类加载的时候系统会首先判断当前类是否被加载过。已经被加载的类会直接返回否则才会尝试加载。也就是说对于一个类加载器来说相同二进制名称的类只会被加载一次。 public abstract class ClassLoader {...private final ClassLoader parent;// 由这个类加载器加载的类。private final VectorClass? classes new Vector();// 由VM调用用此类加载器记录每个已加载类。void addClass(Class? c) {classes.addElement(c);}...
} 分类 Bootstrap ClassLoader引导类加载器 作用负责加载 JDK 核心类库也就是 Java标准库如 java.lang.*、java.util.* 等。加载路径它从 JRE 的 lib 目录下加载类如 rt.jar、resources.jar 等。特点Bootstrap 类加载器是 JVM 自带的用 C 实现不能被 Java 代码继承。它加载的类在 JVM 启动时就被加载到内存。 Extension ClassLoader扩展类加载器 作用负责加载 JDK 扩展类库即 JRE/lib/ext/ 目录下的类或者 java.ext.dirs 指定的路径中的类。加载路径默认加载路径是 JRE/lib/ext/ 目录下的 JAR 文件和类文件。特点Extension ClassLoader 是由 ClassLoader 继承而来用 Java 实现。 Application ClassLoader应用类加载器 作用负责加载 应用程序的类库即 classpath 下的类。加载路径从 classpath 环境变量指定的路径加载类文件例如.class 文件或 .jar 包。特点是最常见的类加载器它负责加载我们自己编写的 Java 类文件通常由 java.lang.ClassLoader 类提供。Java 的第三方库例如使用 Maven 管理的库 自定义类加载器Custom ClassLoader 作用开发者可以继承 ClassLoader 类重新实现 findClass() 方法来创建自己特定的类加载器。用途自定义类加载器通常用于 插件化、动态加载、热部署 等场景。 类加载器的父子关系 Java 中的类加载器是按照父子层级结构组织的。每个类加载器都有一个父类加载器类加载的过程遵循 父加载器优先加载的原则。类加载器的父子关系如下 Bootstrap ClassLoader 是根加载器它不继承任何类加载器。Extension ClassLoader 是 Bootstrap ClassLoader 的子类。Application ClassLoader 是 Extension ClassLoader 的子类。 双亲委派模型 Java 的类加载机制采用了 双亲委派模型。该模型的核心思想是每个类加载器都会首先委托给父类加载器进行加载只有父类加载器无法加载时子类加载器才会尝试加载。流程当一个类加载器需要加载一个类时它会 首先委托给父加载器。如果父加载器不能加载该类即类不存在子加载器才会 自己尝试加载该类。如果子加载器依然不能加载该类则 返回错误。简单来说就是在进行类加载时也就是通过全限定类名找 .class 文件时会先从 Application ClassLoader 作为入口开始随后把 加载类 这样的任务委托给其父类也就是 Extension ClassLoader 来进行。Extension ClassLoader 也不会立刻开始 加载类 的任务而是继续委托给其父类也就是 Bootstrap ClassLoader 来进行。Bootstrap Loader 作为顶层类加载器自然没有父类了便会从 Bootstrap Loader 这一层开始查找也就是先从 Java标准库 开始查找。如果 Bootstrap Loader 没有找到则交给其子类 Extension ClassLoader 查找若 Extension ClassLoader 没有找到则交给其子类 Application ClassLoader 查找若依旧没有找到且没有子类时则抛出异常。 ⚠️双亲委派模型并不是一种强制性的约束只是 JDK 官方推荐的一种方式。其实这个双亲翻译的容易让别人误解我们一般理解的双亲都是父母这里的双亲更多地表达的是“父母这一辈”的人而已并不是说真的有一个 MotherClassLoader 和一个FatherClassLoader 。个人觉得翻译成单亲委派模型更好一些不过国内既然翻译成了双亲委派模型并流传了按照这个来也没问题不要被误解了就好。 双亲委派模型保证了 Java 程序的稳定运行可以避免类的重复加载JVM 区分不同类的方式不仅仅根据类名相同的类文件被不同的类加载器加载产生的是两个不同的类也保证了 Java 的核心 API如 java.lang.String不被篡改。 类加载器的应用 自定义类加载器 在一些特殊的应用场景下如 热部署、插件机制、动态代理 等可以通过 自定义类加载器 来控制类的加载方式。例如JSP 热部署、Spring 容器的 Bean 加载等都需要自定义类加载器。 Java 的反射机制 反射机制如 Class.forName()通过类加载器动态加载类并获取类的构造方法、字段、方法等信息支持 运行时动态创建对象、调用方法 等操作。 隔离加载 Java 类加载器的父子层次结构使得不同应用程序可以 隔离类的加载避免不同程序之间的类冲突。例如Tomcat、Jetty 等应用服务器支持 Web 应用程序的隔离它们为每个应用创建不同的类加载器防止不同应用之间共享类。 垃圾回收(GC) 在过去 C语言时我们在申请了一块内存空间后要去手动释放这块内存也就是调用 free() 函数若不释放掉很容易造成内存泄漏和内存溢出等问题。就算代码块里确实调用了 free() 函数有时候程序也可能因为 bug 没有走到那里从而导致内存泄漏等。手动释放内存太麻烦了所以 Java 为了开发的便捷性便引入 GCGarbage Collection垃圾回收 机制JVM 会自动识别出哪块内存是不需要使用的并释放掉这些内存不再需要我们过度关注。相比较于 CJava 的代码执行时间要短一些就是因为 JVM 的这些机制。不过随着 JDK 版本的不断迭代目前 Java 的执行时间以及和 C 较为接近了STW 时间大部分情况下都在 1ms 以下。STWStop The World在 JVM GC 时就会因为需要GC其他业务的代码不得不停止运行必须等待 GC 执行完后才能继续运行这个时间间隔就被称为 STW Java 的 GC 主要是针对对象内存的回收和对象内存的分配。所以Java GC 最核心的功能是 堆 内存中对象的分配与回收。堆 是 GC 主要管理的区域所以 堆 也被称作 GC 堆Garbage Collected Heap。在 Java 代码执行的过程中必然伴随着新对象的创建和旧对象的消亡所以回收内存 的本质就是 回收对象。在 Java 中对象的生命周期通常由 JVM 管理当对象不再被引用时它就变成了 垃圾这些对象占用了内存资源。没有垃圾回收机制程序员需要手动清理这些不再使用的对象这可能导致内存泄漏程序员没有正确地释放不再使用的对象导致内存不能被回收。内存溢出程序分配的内存资源不能及时释放导致系统内存不足程序崩溃。 GC 的工作过程大致可以分为两步 找到 垃圾即为不再使用的对象死亡对象释放 垃圾该对象所申请的内存释放掉 死亡对象的判断 引用计数法 在 new 对象时给对象单独添加一个引用计数器 每当有一个地方引用它计数器就加 1当引用失效置为 null 计数器就减 1任何时候计数器为 0 的对象就是不可能再被使用的即为死亡对象。 这便是 Python 与 PHP 使用的方法这个方法实现简单效率高但是目前主流的虚拟机中并没有选择这个算法来管理内存是因为其存在某些致命的缺点。 内存消耗需要为每个对象都额外开辟一块空间添加计数器如果一个对象的大小为8字节计数器的空间通常为4字节这就比原来多了 50% 的内存空间。对象之间循环引用所谓对象之间的相互引用问题如下面代码所示除了对象 objA 和 objB 相互引用着对方之外这两个对象之间再无任何引用。但是他们因为互相引用对方导致它们的引用计数器都不为 0于是引用计数算法无法通知 GC 回收器回收他们。 public class Test {Object instance null;public static void main(String[] args) {Test objA new Test();Test objB new Test();objA.instance objB;objB.instance objA;objA null;objB null;}
} 可达性分析 引用计数法在空间上的开销较大而可达性分析则是用时间换空间的思想。 以代码中一些特定的对象作为 GCRoots也就是遍历的起点从 GCRoots 开始向下搜索节点所走过的路径称为引用链所经过的对象被标记为 “可达”当一个对象到 GC Roots 没有任何引用链相连的话该对象则被标记为 “不可达”则证明此对象是不可用的需要被回收。下图为一些对象的引用关系其中Obj5 - Obj7 之间虽然有引用关系但是从 GCRoots开始并不能遍历到 Obj5 - Obj7被认为不可达然后回收。可达性分析 会尽可能的遍历每一个对象遍历完之后将那些 “不可达” 的对象进行释放。但是即使在可达性分析法中不可达的对象也并非是 “非死不可” 的这时候它们暂时处于 “缓刑阶段”要真正宣告一个对象的死亡至少要经历两次标记过程可达性分析法中不可达的对象被第一次标记并且进行一次筛选筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法或 finalize 方法已经被虚拟机调用过时虚拟机将这两种情况视为没有必要执行。被判定为需要执行的对象将会被放在一个队列中进行第二次标记除非这个对象与引用链上的任何一个对象建立关联否则就会被真的回收。Object 类中的 finalize 方法一直被认为是一个糟糕的设计成为了 Java 语言的负担影响了 Java 语言的安全和 GC 的性能。JDK9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。忘掉它的存在吧~~~ ❓哪些对象可以作为 GCRoots 虚拟机栈(栈帧中的局部变量表)中引用的对象本地方法栈(Native 方法)中引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象所有被同步锁持有的对象JNIJava Native Interface引用的对象 可达性分析便是 Java 采取的方案这种方案的弊端也很明显在每次可达性分析时都会尽可能的遍历所有对象如果对象过多其所消耗的时间也会相应的增长STW 的时间就会变大。 垃圾收集算法 标记-清除算法 找到可以清除的垃圾对象后我们还需要对其进行释放。标记-清除算法Mark and Sweep是最基础的垃圾回收算法。后续的算法都是根据这个算法的缺点进行优化得来的。 它分为两个阶段标记阶段遍历所有对象标记出哪些对象是可以访问的即有引用指向的对象。清除阶段回收所有没有标记的对象释放它们占用的内存。 这个算法的缺点也十分明显。首先体现在效率上标记和清除这两个过程的效率都不高。再者从空间上看标记清除后会产生大量不连续的内存碎片。 我们申请内存时要求这块内存必须是连续的不能是分散的。假设你需要申请 1GB 的内存空间目前空闲内存空间为 4GB但依然申请失败这是因为 4GB 的内存大部分都是零零散散分布的并非连续的。 复制算法 复制算法Copying将内存分为两部分一部分用来存储活动对象另一部分为空闲区。每次垃圾回收时将存活的对象复制到另一部分内存上从而回收掉已经不再使用的内存。 该算法虽然解决了标记-清除算法带来的内存碎片问题。但需要额外的内存空间通常是原内存空间的两倍大小这对于内存的空间利用率是非常低的。如果存活的对象过多复制所消耗的时间和资源也是巨大的。 标记-整理算法 标记-整理算法Mark-Compact是标记-清除算法的改进版标记阶段仍然和标记-清除算法相同但清除阶段不再是直接删除不可达对象而是将存活对象整理到内存的一端最后释放出内存的连续空间。 虽然解决了内存碎片的问题以及空间利用率低的问题。但是相较于标记-清除算法标记-整理算法的实现更加复杂。且由于多了整理这一步因此效率也不高适合这种垃圾回收频率不是很高的场景。 分代收集算法 分代收集算法Generational Garbage Collection基于这样一个假设大部分对象都很快会变成垃圾。因此将堆内存划分为多个区域如新生代伊甸区、幸存区、老年代通过将新生代的对象频繁回收来提高回收效率。 堆内存通常被分为以下三个部分 新生代内存(Young Generation)老生代(Old Generation)永久代(Permanent Generation) 下图所示的 Eden伊甸 区、两个 Survivor幸存 区 S0 和 S1 都属于新生代中间一层属于老年代最下面一层属于永久代。JDK 8 版本之后 PermGen(永久) 已被 Metaspace(元空间) 取代元空间使用的是直接内存用于存储类元信息、静态变量等。 分代垃圾回收集具体过程 分代收集算法根据不同 “代” 次区分对象的大小每进行 GC 一次每个对象的 “代” 次就会 1。大多数对象最开始都是 0 代也就是放在 Eden 区。⚠️每次 Minor GC 后代数才会 1。 新生代垃圾回收Minor GC 当 Eden 区没有足够空间进行分配时虚拟机将发起一次 Minor GC这是一个特殊的 GC。Minor GC 只扫描新生代而不会扫描整个堆。由于大部分对象都存活不到 Minor GC 发生所以此时所剩的对象也不多此时 Minor GC 会回收 Eden 中的垃圾对象存活对象会被复制到 Survivor 区S0 或 S1。因为存活对象不多只需要付出少量对象的复制成本就可以完成每次垃圾收集所以使用复制算法并不会产生过大的开销。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活并且能被 Survivor 容纳的话将被移动到 Survivor 空间s0 或者 s1中并将对象代设为 1(Eden 区 - Survivor 区后对象的初始代数变为 1)。对于 Survivor 区的对象也会进行 GC 回收伴随着每次 Minor GCSurvivor 区的对象的 “代” 次也会相应的增加Survivor 区的对象会在 S0 和 S1 区反复横跳。多次 GC 仍存活的对象达到一定阈值默认为 15 代后晋升到 老年代。⚠️如果 Survivor 区满存活对象直接晋升到 老年代。 老年代垃圾回收Major GC / Full GC 当老年代内存不足时如 Survivor 区晋升过来的对象太多、显式调用 System.gc() 时、Minor GC 失败即新生代对象无法晋升到老年代而老年代已满时会触发 Major GC。由于老年代的对象可能较少且生命周期都很长所以使用标记-清除算法标记存活对象并清除不可达对象且使用标记-整理算法整理存活对象避免内存碎片。 Full GC Full GC 代表的是 全堆垃圾回收即 同时回收新生代和老年代。当老年代满了如 Minor GC 失败后需要腾出老年代空间、元空间Metaspace满了、调用 System.gc() 时会触发 Full GC。由于 Full GC 影响较大因为它会暂停所有线程通常需要尽量避免。 Survivor 区溢出如果 Survivor 区装不下存活对象则直接晋升老年代。大对象直接进入老年代大对象就是需要大量连续内存空间的对象比如字符串、数组。会直接分配到老年代避免在新生代频繁移动。大对象直接进入老年代的行为是由虚拟机动态决定的它与具体使用的垃圾回收器和相关参数有关。大对象直接进入老年代是一种优化策略旨在避免将大对象放入新生代从而减少新生代的垃圾回收频率和成本。 JVM的垃圾回收器 Java 提供了多种垃圾回收器常见的有 Serial GC 特点单线程工作所有的垃圾回收工作都由一个线程完成。适用场景适合内存较小、单核 CPU 的环境。 Parallel GC并行回收 特点多个线程并行进行垃圾回收适合多核 CPU 的环境。适用场景适合 CPU 核心数多的环境能显著提高 GC 的吞吐量。 CMSConcurrent Mark-Sweep 特点旨在最小化垃圾回收停顿时间。它通过并行标记阶段和并发清除阶段来减少 GC 停顿时间。适用场景适合对响应时间有严格要求的应用如 Web 应用。 G1 GCGarbage-First GC 特点G1 是一种新型的垃圾回收器它通过将堆划分为多个区域进行回收以最小化停顿时间并保持较高的吞吐量。G1 GC 可以动态调节回收的停顿时间具有较好的可预测性。适用场景适用于大内存和对延迟有较高要求的系统。 JVM 机制的介绍到此就结束了之后更新什么内容之后再说吧~~~毕竟不知后事如何且听下回分解 ❤️❤️❤️❤️❤️❤️❤️⚠️⚠️⚠️本文章部分文案参考——JavaGuide