<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
  <channel>
    <title>赛博云记录</title>
    <link>http://123.57.101.202:8080</link>
    <description />
    <language>zh-CN</language>
    <item>
      <title>走走走游游游</title>
      <link>http://123.57.101.202:8080/archives/zou-zou-zou-you-you-you</link>
      <content:encoded>&lt;blockquote&gt; &lt;p&gt;走走走；游游游，甘为铜钱做马牛，做人哪有做妖好，不怕阎王命不休。&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;可能之前工作太顺了，也有可能之前的行情没那么糟，或者以前工作时间上不出现断层，每次离职后都能很快找到工作。 只是没想过，这次阻力这么大，可能年龄上去了，可能空窗期太久了，可能项目没亮点了，可能... ... 接受现实，这就是市场最真实的反馈，然后继续努力，继续前行吧。&lt;/p&gt;</content:encoded>
      <pubDate>Mon, 13 Apr 2026 03:57:00 GMT</pubDate>
    </item>
    <item>
      <title>深入理解JVM: (一)Java内存区域</title>
      <link>http://123.57.101.202:8080/archives/shen-ru-li-jie-jvm-yi-javanjavascripa-cun-qu-yu</link>
      <content:encoded>&lt;h3&gt;一、概述&lt;/h3&gt; &lt;p&gt;    本章讲解JAVA虚拟机内存的各个区域，介绍这些区域的作用、服务对象、以及其中可能产生的问题。&lt;/p&gt; &lt;h3&gt;二、运行时数据区域&lt;/h3&gt; &lt;h4&gt;2.1 运行时数据区域主要分为(图2.1)：&lt;/h4&gt; &lt;ol&gt; &lt;li&gt;方法区（线程共享）&lt;/li&gt; &lt;li&gt;堆（线程共享）&lt;/li&gt; &lt;li&gt;虚拟机栈&lt;/li&gt; &lt;li&gt;本地方法区&lt;/li&gt; &lt;li&gt;程序计数器&lt;/li&gt; &lt;/ol&gt; &lt;img src= "https://www.fiveseven.fun/upload/20210809_22215862.png"&gt; （图2.1） &lt;h4&gt;2.2 各个区域的定义与作用&lt;/h4&gt; &lt;h5&gt;2.2.1 程序计数器(PC计数器)&lt;/h5&gt; &lt;p&gt;    程序计数器是一块较小的内存空间，是当前线程锁执行字节码的型号指示器。字节码解释器工作时就是通过改变计数器的值，来获取下一次需要执行的字节码指令。     JAVA虚拟机的多线程是通过线程轮流且切换、分配处理器执行时间的方式来实现的(抢占式)。因此，为了线程切换后能恢复到正确的执行位置，每条线程都需要一个独立的程序计数器，各条线程之间计数器互不影响，独立存储，我们称这类内存区域为“线程私有”的内存。 如果线程正在执行的是一个JAVA方法，那么这个计数器记录的是正在执行的虚拟机字节码指令的地址值。&lt;/p&gt; &lt;h5&gt;2.2.2 JAVA虚拟机栈&lt;/h5&gt; &lt;p&gt;    JAVA虚拟机栈是线程私有的，它的生命周期与线程相同。每个方法被执行的时候，JAVA虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;(1)局部变量表&lt;/strong&gt;：     是一组变量值存储空间，用于存放局部变量(这里的局部变量指的是基本数据类型：boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不是对象本身，可能是一个指向对象起始地址值的引用指针，也可能是指向一个代表对象的句柄，目前HotSpot虚拟机，主要存储的是引用指针) 和 returnAddress类型（它指向了一条字节码指令的地址）。局部变量存储在局部变量表中，随着线程而生，线程而灭。并且线程间数据不共享。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;(2)操作数栈&lt;/strong&gt;：     每个独立的栈帧中除了包含局部变量表以外，还包含一个后进先出的操作数栈，也可以称为 表达式栈。操作数栈可以认为是，在一个方法中，用来暂时存放执行复制、交换、求和等操作的临时工作区，主要用于保存计算过程的中间结果，同时作为计算过程中变量临时的存储空间。当一个方法刚开始执行的时候，一个新的栈帧也会随之被创建出俩，这个方法的操作数栈是空的。如下图： &lt;img src= "https://www.fiveseven.fun/upload/20210828_2023495.png"&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;(3)动态连接&lt;/strong&gt;：     每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用，持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。Class文件的常量池中存有大量的符号引用，字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用，这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用，这部分称为动态连接。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Math math=new Math(); math.compute();//调用实例方法compute(); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;    以上面两行代码为例，解释一下动态连接：math.compute()调用时compute()叫符号，需要通过compute()这个符号去到常量池中去找到对应方法的符号引用，运行时将通过符号引用找到方法的字节码指令的内存地址。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;(4)方法出口&lt;/strong&gt;：     当一个方法开始执行后，只有两种方式可以退出这个方法。第一种方式是执行引擎遇任意一个方法返回的字节码指令，这时候可能会有返回值传递给上层的方法调用者（调用当前方法的方法称为调用者），这种退出方法的方式称为正常完成出口。另外一种退出方式是，在方法执行过程中遇到了异常，并且这个异常没有在方法体内得到处理，无论是Java虚拟机内部产生的异常，还是代码中使用athrow字节码指令产生的异常，只要在本方法的异常表中没有搜索到匹配的异常处理器，就会导致方法退出，这种退出方法的方式称为异常完成出口。一个方法使用异常完成出口的方式退出，是不会给它的上层调用者产生任何返回值的。&lt;/p&gt; &lt;p&gt;    无论采用何种退出方式，在方法退出之后，都需要返回到方法被调用的位置，程序才能继续执行，方法返回时可能需要在栈帧中保存一些信息，用来帮助恢复它的上层方法的执行状态。一般来说，方法正常退出时，调用者的PC计数器的值可以作为返回地址(即前一栈帧的地址值)，栈帧中很可能会保存这个计数器值。而方法异常退出时，返回地址是要通过异常处理器表来确定的，栈帧中一般不会保存这部分信息。方法退出的过程实际上就等同于把当前栈帧出栈，因此退出时可能执行的操作有：恢复上层方法的局部变量表和操作数栈，把返回值（如果有的话）压入调用者栈帧的操作数栈中，调整PC计数器的值以指向方法调用指令后面的一条指令等。&lt;/p&gt; &lt;p&gt;PS:如果线程请求的栈深度大于虚拟机所允许的深度，将抛出StackOverflowError异常。&lt;/p&gt; &lt;h5&gt;2.2.3 JAVA本地方法栈&lt;/h5&gt; &lt;p&gt;    本地方法栈与虚拟机栈所发挥的作用是非常相似的，其区别只是虚拟机栈为虚拟机执行JAVA方法服务，而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。&lt;/p&gt; &lt;h5&gt;2.2.4 JAVA堆&lt;/h5&gt; &lt;p&gt;    JAVA堆是虚拟机所管理的内存中最大的一块。JAVA堆是被所有线程共享的一块内存区域，在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例，JAVA世界里几乎所有的对象实例都在这里分配内存。JAVA堆是垃圾收集器管理的内存区域，从内存回收的角度看，由于现代垃圾收集器大部分都是基于分代收集理论设计的，所以JAVA堆经常会出现“新生代”、“老年代”、“永久代”、“Eden空间”、“From Survivor空间”、“To Survivor空间”等名词，这些区域划分仅仅是一部分垃圾收集器的共同特性或者设计风格而已，而非某个JAVA虚拟机具体实现的固有内存布局。&lt;/p&gt; &lt;p&gt;    如果从分配内存的角度来看，所有线程共享的JAVA堆中可以划分出多个线程私有的分配缓冲区，以提升对象分配时的效率，不过无论从什么角度，无论如何划分，都不会改变JAVA堆中存储内容的共性，无论是哪个区域，存储的都只能是对象的实例，将JAVA堆细分的目的只是为了更好地回收内存，或者更快地分配内存。&lt;/p&gt; &lt;p&gt;    JAVA堆既可以被实现成固定的大小，也可以设置成可扩展，不过当前主流的JAVA虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms设定)。如果在JAVA堆中没有内存完成实例分配，并且堆也无法扩展时，JAVA虚拟机将会抛出OOM(内存溢出)异常。&lt;/p&gt; &lt;h5&gt;2.2.5 方法区&lt;/h5&gt; &lt;p&gt;    方法区和JAVA堆一样，是各个线程共享的内存区域，它用于存储已被虚拟机加载的类型信息、常量、静态变量、即使编译器编译后的代码缓存等数据。方法区是堆的一个逻辑分区，本质上是属于堆内存，为了便于区分便称之为方法区，它还有个别名叫做“非堆”。&lt;/p&gt; &lt;p&gt;    在JDK8以前，许多人把“永久代”和方法区两者混为一谈，但本质上两者并不是等价的，因为“永久代”只是方法区的一种实现方式。JDK8以后，完全废弃了“永久代”的设计概念，取而代之的是通过本地内存实现的“元空间”，可以这么理解，JDK8之后的元空间，类似于之前的版本的“方法区”。为什么会放弃“永久代”？是因为通过永旧代来实现方法区，会使得JAVA应用更容易受到内存溢出的问题(永久代有-XX:MaxPermSize的上限)，即使不设置也有默认的大小，而通过本地内存来实现的元空间，只要没触碰到进程可使用的上限，就没有什么问题。所以在JDK7的时候就把原本放在永久代的字符串常量池、静态变量移出。而到了JDK8则完全废弃了“永久代”的概念，把JDK7中永久代剩余的内容(主要是类型信息)全部移到元空间中。 PS:     1.方法区如果无法满足新的内存分配需求是，也会报OOM异常。     **2.运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外，还有一项信息是常量池表，用于存放编译期生成的各种字面量和符号引用，以及符号引用被翻译后的直接引用。**这部分内容将在类加载后存放到方法区的运行时常量池中。&lt;/p&gt; &lt;h3&gt;三、虚拟机对象&lt;/h3&gt; &lt;h4&gt;3.1 概述&lt;/h4&gt; &lt;p&gt;    本节主要介绍虚拟机内存中数据是如何创建、布局、访问的。基于实用优先的原则，以最常用的内存区域JAVA堆为例，深入探讨一下HotSpot虚拟机在JAVA堆中对象分配，布局和访问的全过程。&lt;/p&gt; &lt;h4&gt;3.2 对象的创建&lt;/h4&gt; &lt;p&gt;    这边的对象的创建指的是通过new的方式创建对象，不包括复制、反序列化。当java虚拟机遇到一条字节码new指令的时候会有以下步骤：&lt;/p&gt; &lt;p&gt;    1、会根据这个指令的参数去运行时常量池里面找到对应的类的符号引用(运行时常量池存储类对应Class文件的信息，JDK8后把运行时常量池移到元空间里去)，如果找不到符号，说明没有这个类或对应的方法调用，在开发中，尤其是多人合作开发，本地代码更新引用了其他工程的类的方法或者字段，虽然在idea上并不会报错，但是当你本地工程打包的时候便会报找不到 xxxxxx.java 对应的符号引用，处理的方法需要你将应用的代码打包install到你的本地仓库才行。&lt;/p&gt; &lt;p&gt;    2、会检查这个符号引用对应的类是否已被加载、解析、初始化。如果没有那必须执行响应的类加载过程。类加载过程相对来说也比较复杂，类从被加载到JVM中开始，到卸载为止，整个生命周期包括：加载、验证、准备、解析、初始化、使用和卸载七个阶段，其中还涉及到双亲委派机制等等，此处不做展开，后续的章节会提到。&lt;/p&gt; &lt;p&gt;    3、通过类加载后，虚拟机会为新生对象分配内存，对象所需内存大小在类加载完后便可以完全确定(对象的大小包括：对象头、实例数据、对齐填充)。对象内存空间的分配实际上等同于把一块确定大小的内存从java堆里面划分出来**(有些情况不分配在堆里面，一般的对象，要求没有逃逸情况，而且对象大小不是很大，会优先在栈内存分配，因为在栈内存分配的对象GC效率很高，弹栈即GC)**。分配方式有“指针碰撞”方式、“空闲列表”方式，具体采用那种分配方式，取决于采用的垃圾收集器是否带有空间压缩的整理能力决定。&lt;/p&gt; &lt;p&gt;    4、内存分配完后，对分配到的内存空间(不包括对象头)初始化为零值&lt;/p&gt; &lt;p&gt;    5、设置对象头：对象属于哪个实例、对象哈希码值、对象GC分代年龄、是否启用偏向锁等。&lt;/p&gt; &lt;p&gt;    以上4个步骤从虚拟机角度来看，一个新对象已经产生，但从JAVA程序视角来看，对象的创建才刚刚开始，因为Class文件中的init方法还没执行，所有的字段都为默认的零值，new指令之后会接着init方法，按照程序员的医院对对象进行初始化，这样一个真正可用的对象才算完全被构造出来。 &lt;img src= "https://www.fiveseven.fun/upload/20210911_1643466.png"&gt;&lt;/p&gt; &lt;h4&gt;3.3 对象的内存的布局&lt;/h4&gt; &lt;p&gt;  对象对内存中的存储布局可以氛围三个部分：对象头、实例数据、对齐填充。&lt;/p&gt; &lt;h5&gt;3.3.1 对象头&lt;/h5&gt; &lt;p&gt;  对象头部分包括两类信息：   一、用于存储对象自身运行数据：哈希码值、GC分代年龄、锁信息(锁状态标志、线程持有的锁、偏向锁ID)等。   二、类型指针：即对象指向它的类型元数据的指针，JAVA虚拟机通过这个指针来确定该对象是哪个类的实例。&lt;/p&gt; &lt;h5&gt;3.3.2 实例数据&lt;/h5&gt; &lt;p&gt;  实例数据是对象真正存储的有效信息，即我们在程序代码里所定义的各种类型的字段内容，无论是从父类继承下来的，还是在子类中定义的字段都必须记录起来。&lt;/p&gt; &lt;h5&gt;3.3.3 对齐填充&lt;/h5&gt; &lt;p&gt;  没有特别的含义，JVM虚拟机的自动内存管理系统要求对象大小必须是8字节的整数倍，对象头已经被设计成8的倍数，如果对象实例数据不是8的倍数，便要通过对齐填充来补全。&lt;/p&gt; &lt;h5&gt;3.3.4 数组长度&lt;/h5&gt; &lt;p&gt;  如果对象是一个数组，那么对象头还必须有一块用于记录数组的长度的数据，因为JAVA虚拟机可以通过普通JAVA对象的元数据信息来确定JAVA对象的大小，但是如果数组的长度是不确定，将无法通过元数据中的信息来推断出数组的大小来分配所需要的内存。&lt;/p&gt; &lt;h4&gt;3.4 对象的访问定位&lt;/h4&gt; &lt;p&gt;  JAVA程序会通过栈里面的局部变量表存储的reference数据(对象引用),来操作堆上具体的对象，HotSpot虚拟机采取的对象引用主要通过直接指针访问的方式来实现(其他类型的虚拟机有的采用句柄的方式)，reference中存储的直接就是对象地址，它的好处就是访问速度块。   句柄指针： 如果使用句柄访访问的话，JAVA堆中将可能会划分出一块内存来作为句柄池，reference中存储的就是对象的句柄地址，而句柄中包含了对象实例数据与类型数据各自具体的地址信息。   直接指针： 如果使用直接指针的话，JAVA堆对象中的内存布局就必须考虑如何放置访问类型数据的相关信息，reference中存储的直接就是对象地址，如果只是访问对象本身的话，就不需要多一次简介访问的开销，如图2-3。   这两种访问各有优势，使用句柄来访问的足底啊好处是reference存储的是稳定的句柄，在对象被移动(垃圾回收，标记-复制，改变对象内存的地址值)时，只会改变句柄中的实例数据指针，而reference不需要修改。而使用直接指针访问的最大好处是访问速度更快。 &lt;img src= "https://www.fiveseven.fun/upload/20210911_16185031.png"&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 10:05:08 GMT</pubDate>
    </item>
    <item>
      <title>深入理解JVM: (二)垃圾收集器(1):GC Root与分代</title>
      <link>http://123.57.101.202:8080/archives/shen-ru-li-jie-jvm-ejavascriptr-la-ji-sjavascripthou-ji-qi-1gc-rootyu-fen-dai</link>
      <content:encoded>&lt;h3&gt;一、概述&lt;/h3&gt; &lt;p&gt;    垃圾收集器简称GC，如今内存回收技术已经相当成熟，去了解垃圾收集回收的目的很明确，当需要排查各种内存溢出、内存泄漏问题时，当垃圾收集成为系统达到更高并发量的瓶颈时，我们就必须对这些“自动化”的技术实施必要的监控和调节。&lt;/p&gt; &lt;p&gt;    JAVA运行时区域中的，程序计数器、虚拟机栈、本地方法栈，这几个区域随着方法结束或者线程结束时，内存自然而然就跟随着回收了，内存回收相对比较确定。而JAVA堆和方法区这两个区域则具有不确定性：一个接口的多个实现类的内存可能会不一样，一个方法所执行的不同条件分支所需要的内存也可能不一样，只有处于运行期间，我们才能知道程序创建了哪些对象，创建了多少个对象，这部分内存的分配和回收是动态的。垃圾收集器锁关注的正式这部分内存该如何管理。&lt;/p&gt; &lt;h3&gt;二、对象是否还被使用&lt;/h3&gt; &lt;h4&gt;2.1引用计数算法&lt;/h4&gt; &lt;p&gt;  在对象中添加一个计数器，每当有个地方引用地时候，计数器就+1，当引用失效地时候，计数器就-1。在任何时刻计数器为0地对象就是不可能再被使用地。   客观地说，引用计数器算法原理简单，判断效率也很高，但是在主流的JAVA虚拟机里面都没有采用引用计数器算法来管理虚拟机内存，因为这个看似简单的算法，必须要额外大量的处理才能保证正确的工作。譬如单纯的引用计数，就很难处理互相循环引用的问题。&lt;/p&gt; &lt;h4&gt;2.2可达性分析&lt;/h4&gt; &lt;p&gt;  这个算法通过一些列称为 &amp;quot;GC Root&amp;quot;的根对象作为起始节点集，从这些节点开始，根据引用关系向下搜索，搜索过程所走的路径被称为&amp;quot;引用链&amp;quot;，如果某个对象到GC Root间没有任何引用链相连，即从GC Root到这个对象不可达，则证明此对象是不可能再被使用的。   在JAVA技术体系里面，固定可以作为GC Root的对象包括以下：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;在虚拟机(栈帧中的本地变量表)中引用的对象，譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。&lt;/li&gt; &lt;li&gt;在方法区类静态属性引用(static修饰)的对象，譬如JAVA类的引用类型静态变量。&lt;/li&gt; &lt;li&gt;在方法区中常量引用(final修饰)的对象，譬如字符床常量池里的引用。&lt;/li&gt; &lt;li&gt;在本地方法栈中的JNI(即通常所说的Native方法)引用的对象。&lt;/li&gt; &lt;li&gt;JAVA虚拟机内部的引用，如基本数据类型对应的class对象，一些常驻的异常对象，类加载器等。&lt;/li&gt; &lt;li&gt;所有被同步锁(synchronized关键字)持有的对象。&lt;/li&gt; &lt;li&gt;反应JAVA虚拟机内部情况的JMXBean等。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;  除了这些固定的GC Root集合外，根据用户选用的垃圾收集器，及其当前回收的内存区域不同，还有可能其他对象&amp;quot;临时性&amp;quot;地加入。&lt;strong&gt;这边&amp;quot;临时性&amp;quot;加入的原因，是因为某些对象即便是GC Root不可达，但是确确实实是有被引用的，而这些对象不能被GC，之所以会有这种情况，起始和选用的垃圾收集器有关，G1之前的垃圾收集器一般采用分代收集，G1后采用局部收集，即分区收集物理上不再有分代的概念，所以就必须考虑到一个问题，某个区域里面的对象完全有可能被堆中的其他区域对象所引用，这时候就需要将这些关联区域的对象也一并加入到GC Root集合里面去，才能保证可达性分析的正确性。&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;三、引用的类型&lt;/h3&gt; &lt;p&gt;  在JDK1.2及之前引用是很传统的定义：如果reference类型的数据存储的数值代表的是另外一块内存的起始地址，就称该reference数据是代表某块内存、某个对象的引用。**这种要么被引用和未被引用的状态过于狭隘，很难符合某些应用场景，譬如，当内存充足的时候，这个对象可以被保留，但是如果内存在进行垃圾收集后仍然十分紧张，那就可以抛弃这些对象。**所以在JDK1.2之后，JAVA对引用进行了扩充，将引用的概念进行了扩充，引用被分为四种：&lt;strong&gt;强引用、软引用、弱引用、虚引用。四种引用强度逐级递减。&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;强引用&lt;/strong&gt;： 程序中的赋值引用，即类&amp;quot;Object obj = new Object()&amp;quot;,在这种情况下，无论任何情况下，只要强引用关系还存在，垃圾收集器就永远不会回收掉这类引用对象。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;软引用(SoftReference)&lt;/strong&gt;： 软引用是用来描述一些还有用，但非必需的对象。只被软引用关联着的对象，在系统将要发生内存溢出异常之前，会把这些对象列进回收范围之中进行第二次回收，如果这次回收还没有足够的内存，才会抛出内存溢出异常。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;弱引用(WeakReference)&lt;/strong&gt;： 若引用也是用来描述那些非必须的对象，但是它的强度比软引用更弱一些，被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作，无论当前内存是否足够，都会回收掉只被弱引用关联的对象。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;虚引用(PhantomReference)&lt;/strong&gt;： 虚引用对对象而言是无感知的，对象有虚引用跟没有是完全一样的，使用虚引用的目的就是为了得知对象被GC的时机，所以可以利用虚引用来进行销毁前的一些操作，比如说资源释放等。虚引用不会影响对象的生命周期。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;四、堆的回收&lt;/h3&gt; &lt;img src = "https://www.fiveseven.fun/upload/20210914_00003293.png"&gt; #### 4.1年轻代: #####   4.1.1 **Eden、Survivor**   **Eden**:  大多数情况下，对象再新生代Eden区中分配。当Eden区没有足够空间进行分配的时候，虚拟机将发起一次Minor GC。Eden区默认占年轻代80%的空间。   &lt;p&gt;  &lt;strong&gt;Survivor&lt;/strong&gt;：Survivor细分为S0和S1，二者没有本质上的区别，作用：当Eden区发生第一次轻GC(Minor GC)的时候会将Eden区还存活并符合条件的对象复制到S0区。当发生第二次轻GC的时候，会把Eden区和S0区还存活并符合条件的对象都复制到S1区。同理，当再次发生轻GC的时候，会把Eden区和S1区的复制到S0区，并以此类推下去。以上是一种比较简单和理想化的状态，真实场景中的GC更为复杂。&lt;/p&gt; &lt;h5&gt;  4.1.2 &lt;strong&gt;Minor GC&lt;/strong&gt;&lt;/h5&gt; &lt;p&gt;  Minor GC又称为轻GC，只针对Eden区的垃圾收集，即Eden区间满了，或连续空间不足以分配新的对象时，会出发Minor GC，而Survivor区满不会触发Minot GC。&lt;/p&gt; &lt;h5&gt;  4.1.3 对象存活的年龄&lt;/h5&gt; &lt;p&gt;  虚拟机为了决策那些存活对象应当放在新生代，那些存活对象放在老年代中，虚拟机给每个对象定义了一个对象年龄计数器，存储在对象头里面。对象通常在Eden区诞生，如果经过一次轻GC后，仍然存活并且能被Survivor所容纳的话，对象便会被移动到Survivor空间中，并且将其对象年龄设定为1岁。对象在Survivor没熬过一次轻GC，年龄就增加1岁，当年龄达到一定程度(默认15岁)就会被移动到老年代中。年龄的阈值可以通过 -XX:MaxTenuringThreshold = xx 来设置。&lt;/p&gt; &lt;h5&gt;  4.1.4 空间分配担保&lt;/h5&gt; &lt;p&gt;  在Minor GC的时候，Survivor没有足够的空间存放上一次新生代收集下来的对存活对象，这些对象将会通过分配担保机制进入到老年代。   所以在发生Minor GC之前，虚拟机必须检查老年代最大可用连续空间是否大于新生代所有对象的总空间，如果条件成立，那么这次的Minor GC是安全的。如果不成立，虚拟机会先查看 -XX:HandlePromotionFailure参数是否允许担保失败；如果允许，那么会继续检查老年代最大连续可用空间是否大于历次晋升到老年代对象总和的平均大小，如果大于，则尝试进行一次Minor GC，尽管这次GC是有风险的(实际晋升到老年代的对象可能超出老年代最大连续可用空间)；如果小于，或者-XX:HandlePromotionFailure不允许则这次GC改为Full GC, Full GC 清理的区域为年轻代，老年代，永久代。&lt;br /&gt;   上文提到的风险指的是：新生代使用复制算法，但是为了内存的利用率，只使用了一个Survivor空间来作为轮换备份，因此当出现大量对象在Minor GC后仍然存活，这时候会把Survivor无法容纳的对象放入老年代，因此出现一种极端情况，即Minor GC后，年轻代的所有对象都还存活，老年代要担保本身还有容纳这些对象的剩余空间。当最大连续可用空间小于年轻代对象，就需要取之前晋升老年代对象的平均值作为经验值，并且结合配置来决定是否需要Full GC。取历史平均值其实也是一种概率事件，当某次Minor GC后实际存活的对象远大于历史平均值超出老年代所能容纳范围，出现老年代担保分配失败，只能重新发起一次Full GC。但是，通常情况下我们还是会把-XX:HandlePromotionFailure配置打开，避免Full GC过于频繁。&lt;/p&gt; &lt;h4&gt;4.2老年代:&lt;/h4&gt; &lt;h5&gt;  4.2.1 Magor GC/Full GC&lt;/h5&gt; &lt;p&gt;  CMS收集器中，当老年代满时会触发 Major GC。目前，只有CMS收集器会有单独收集老年代的行为。其他收集器均无此行为，只能通过触发Full GC收集老年代(JDK8之前)。   Full GC 对收集整堆（新生代、老年代）和方法区的垃圾收集。一下几种情景会导致Full GC:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;老年代空间不足&lt;/li&gt; &lt;li&gt;方法区空间不足&lt;/li&gt; &lt;li&gt;-XX:HandlePromotionFailure配置为不开启，即不开启空间分配担保配置，当在发生Minor GC之前，如果老年代最大可用连续空间小于新生代所有对象总空间，那么会触发Full GC；第二种情况是，如果开启空间分配担保，如果发老年代最大连续可用空间大于历次晋升到老年代对象的平均值，便会尝试冒险Minor GC，极端情况下，Minor GC后发现，存活对象远高于历史平均，并且老年代已无法容纳，这时候便会出现担保失败的情况，在这种情况下只能重新发起一次Full GC。最后一种情况是，如果开启空间分配担保，并且老年代最大连续可用空间小于历次晋升到老年代对象的平均值，那么就直接Full GC。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;五、方法区的回收&lt;/h3&gt; &lt;h4&gt; 5.1 概论&lt;/h4&gt; &lt;p&gt;  首先《JAVA虚拟机规范》中并不要求虚拟机在方法区实现垃圾回收，其次，方法区垃圾收集的性价比通常比较低：在JAVA堆中，尤其是在新生代中 ，对常规应用的一次垃圾回收能达到70%~99%的内存空间，相比之下，方法区回收囿于苛刻的判断条件，其区域的垃圾回收成果往往远低于此。&lt;/p&gt; &lt;h4&gt; 5.2 方法区什么时候被回收？&lt;/h4&gt; &lt;p&gt;  方法区回收，即永久代回收，&lt;strong&gt;只有发生Full GC的时候才会回收永久代&lt;/strong&gt;，但是在回收永久代的时候，能被视为垃圾回收的对象却十分苛刻，具体条件见5.3。&lt;br /&gt;   在JDK8之后起，就越来越淡化分代的思想，取而代之的是分区，因此如果说的是JDK8及其以上的垃圾回收，再用分代垃圾回收便不太合适了。&lt;/p&gt; &lt;h4&gt; 5.3 方法区的垃圾回收主要分两个部分:&lt;/h4&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;废弃的常量： 通过可达性分析，某个常量(final修饰)不再被应用，这时候发生垃圾回收，而且垃圾收集器判断确实有必要回收，这时候该对象就会被当成垃圾处理掉。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;不再使用的类型： 被判定为&amp;quot;不再使用&amp;quot;的条件比较苛刻，需要同时满足以下3个条件：&lt;br /&gt; 1.该类所有的实例都已经被回收。&lt;br /&gt; 2.加载该类的类加载器已经被回收，这个条件除非是经过精心设计的可替换类加载器的场景，否则很难达成。&lt;br /&gt; 3.该类对应的Class对象没有在任何地方被引用，无法在任何地方通过反射访问该类的方法。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;  那么什么时候建议需要对永久代进行垃圾回收呢？&lt;br /&gt;   在大量使用反射、动态代理、CGLIB等字节码框架，动态生成JSP的场景中，通常都需要JAVA虚拟机具备类型卸载的能力，以保证不会对方法区造成过大的内存压力。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;PS:&lt;/strong&gt;&lt;br /&gt;   方法区的实现方式在JDK8之前是通过永久代来实现，JDK8之后是通过元空间。一般垃圾回收指的是堆里面的垃圾回收，堆里面又分为&amp;quot;年轻代&amp;quot;、&amp;quot;老年代&amp;quot;，&lt;strong&gt;这里的&amp;quot;XX代&amp;quot;区域划分仅仅是一部分垃圾收集器的共同特性或者是设计风格，而非某个JAVA虚拟机具体是新的固有内存布局，更不是JAVA堆的进一步划分。&lt;/strong&gt;&lt;br /&gt;   在G1垃圾收集器出现之前的JVM虚拟机，内部的垃圾收集器全部基于经典的分代来设计，因此需要新生代、老年代收集器搭配才能工作。但是到了今天，尤其是JDK9中的G1垃圾收集器的出现，就已经取消了分代设计的思想，采用分区(Region)的方式来进行垃圾收集。&lt;/p&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 10:00:00 GMT</pubDate>
    </item>
    <item>
      <title>深入理解JVM: (二)垃圾收集器(2):分代假说与GC算法</title>
      <link>http://123.57.101.202:8080/archives/shen-ru-li-jie-jvm-er-la-ji-shou-ji-qi-2fen-dai-jia-shuo-yu-gcsuan-fa</link>
      <content:encoded>&lt;h3&gt;一、垃圾收集算法&lt;/h3&gt; &lt;h4&gt;  1.1分代收集理论&lt;/h4&gt; &lt;p&gt;  分代收集理论建立在三个分代假说上：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;弱分代假说：绝大多数的对象都是朝生夕灭&lt;/li&gt; &lt;li&gt;强分代假说：熬过越多次垃圾收集过程的对象就越难以消亡&lt;/li&gt; &lt;li&gt;跨代引用假说：跨代引用相对于同代引用来说仅占极少数 ，因为存在相互引用的对象更倾向于同时生存或者同时消亡，比如说，某个新对象存在跨代引用，由于老年代难以消亡，该引用会使得新生代再收集的时候同样得以存活，进而年龄增长后进入老年代，这时跨代引用的关系随之被消除。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;  弱分代和强分代假说，共同奠定了多款垃圾收集器设计的一致的原则：收集器应该将JAVA堆分出不同的区域，然后将其回收的对象，按照垃圾回收过程中熬过的次数来划分年龄，并且分配到对应的区域中存储管理。比如说，堆里面的新生代，里面存储的对象基本都是朝生夕灭的对象，基本上new出来的对象都会放在新生代这里(如果创建的对象没有方法逃逸的话，亦或者不是特别大)。把它们几种放在一起，每次回收只关注如何保留少量的存活对象，而不是去标记那些大量将要被回收的对象，就能以较低代价回收到大量的空间。所以新生代大多数的垃圾收集器一般都是采用“标记-复制”的算法，简单来说就是在Monir GC的时候将Eden和S0区还存活的对象标记，并复制到S1区间，当然也有其他情况，在此不做展开。而对于那些难以消亡的对象，也把它们集中存放在一起，虚拟机便可以使用较低的频率来回收这个区域，这就同时兼顾了垃圾回收的时间开销和内存的空间有效利用，比如说老年代，熬过几次垃圾回收的对象会进入到老年代，当然这只是其中一种情况，具体不做展开了，老年代采用的垃圾回收算法一般是“标记-清除”。&lt;br /&gt;   把分代收集理论放到JVM堆里面，就是所谓“年轻代”、“老年代”。但是分代收集并非只是简单的划分一下内存区域那么简单，它至少存在一个明显的困难:对象不是孤立的，对象之间会存在跨代引用。&lt;br /&gt;   假如为了做一次年轻代的垃圾回收，但新生代里面完全有可能被老年代所引用的对象，为了找出该区域的对象，不得不在固定的GC Root之外，再额遍历整个老年代中所有的对象来确保可达性分析结果是正确，反过来老年代GC，也要遍历整个年轻代。这样无疑会给内存回收带来很大的性能负担，为了解决这个问题，便有了跨代引用的假说。&lt;br /&gt;   依据跨代假说，我们就不必为了少量的跨代对象 而去扫描整个老年代，只需要在新生代上建立一个全局的数据结构(记忆集)，这个结构把老年代划分成若干小块，表示出老年代的那一块内存会存在跨代引用。此后当发生Minor GC的时候，只有包含了跨代引用的小块内存里面的对象才会被加入到GC Root里面。虽然这种方法需要在对象改变引用关系的时候，去维护记录数据的正确性，会增加运行时的开销，但比起收集时扫描整个老年代来说，仍然比较划算。&lt;/p&gt; &lt;h4&gt;  1.2标记-清除算法&lt;/h4&gt; &lt;p&gt;  &lt;strong&gt;定义&lt;/strong&gt;：&lt;br /&gt;   标记-清除算法分为两个阶段：首先，标记存活对象，即遍历所有GC Root,将所有GC Roots可达的对象标记为存活的对象;其次，统一回收所有未标记的对象。标记的过程属于垃圾判定的过程。&lt;br /&gt;   &lt;strong&gt;优点&lt;/strong&gt;：执行速度快&lt;br /&gt; &lt;strong&gt;缺点&lt;/strong&gt;：容易产生较多的内存碎片，使得当需要分配较大的对象时，又不得不触发一次GC。 &lt;img src = "https://www.fiveseven.fun/upload/20210920_11004950.png"&gt;&lt;/p&gt; &lt;h4&gt;  1.3标记-复制算法&lt;/h4&gt; &lt;p&gt;  &lt;strong&gt;定义&lt;/strong&gt;：&lt;br /&gt;   为了解决标记-清除算法会产生内存碎片的问题，标记-复制算法，把可用内存按容量划分为大小相等的两块，每次只是用其中一块，当这一块的内存用完后，就将还存活的对象复制到另外一块上面，然后再将已使用过的内存一次性全部清理掉。如果内存中大多数对象都是存活着的话，这种算法会产生大量的内存间复制的开销，因此这种算法不适用于老年代的回收，更适合年轻代的垃圾回收。而将对象复制到另外一半内存的时候，按顺序分配即可，因此也不会有内存碎片化的问题。&lt;br /&gt;   &lt;strong&gt;优点&lt;/strong&gt;：不会产生内存碎片化。&lt;br /&gt;   &lt;strong&gt;缺点&lt;/strong&gt;：可用内存缩小为原来的一般。 &lt;img src = "https://www.fiveseven.fun/upload/20210920_11011552.png"&gt;&lt;/p&gt; &lt;h4&gt;  1.4分代-整理算法&lt;/h4&gt; &lt;p&gt;  &lt;strong&gt;定义&lt;/strong&gt;：&lt;br /&gt;   当对象的存活率比较高的话，如果继续使用标记-复制算法的话，效率将会降低。针对老年代对象存活的特征，提出了另外一种有针对性的“标记-整理”算法，其中标记过程和“标记-清除”算法一样，但是后续的步骤不是直接对可回收对象进行清理，而让所有存活的对象都想内存空间一端移动，按照内存地址依此排列，而未被标记的内存会被清理掉。如此一来，当我们给内存对象分配内存时，JVM只需要持有一个对象的内存地址即可，这比维护一个空闲列表显然少了很多开销，接着直接清理掉边界意外的内存。“标记-复制”于“标记-整理”本质差异在于前者是一种非移动式的回收算法，而后者是移动式的，是否移动回收后的存活对象是一项优缺点并存的风险决策：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;如果移动存活对象，尤其是老年代这种每次回收都有大量对象的区域，移动对象并更新所有引用这些对象的地方，将会是一种极为负重的操作，而且这种对象移动操作必须全程咱暂停用户引用程序才能进行，像这种停顿的操作被称为&amp;quot;Stop The World&amp;quot;，简称STW。&lt;/li&gt; &lt;li&gt;如果跟“标记-清除”一样，完全不考虑内存碎片的话，那便只能依赖更为复杂的内存分配器和内存访问器来解决。内存的访问是用户程序最频繁的操作，假如在这个环节上增加额外的负担，势必会直接影响应用程序的吞吐量。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;  基于以上两点，是否移动对象，都存在弊端，移动内存，则回收时会更复杂，不移动则内存分配会更复杂。从垃圾回收停顿的角度上来看，不移动对象停顿的时间更短，但是从整体程序的吞吐量来看，移动对象&lt;/p&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;吞吐量 = CPU在用户应用程序运行的时间 / （CPU在用户应用程序运行的时间 + CPU垃圾回收的时间） &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;会更划算。虽然移动对象，会增加内存回收的时长，吞吐量的下降，但是和不移动对象相比，因内存分配和访问要比垃圾收集频率高很多，所以导致吞吐量收到的影响会比移动对象更大。&lt;br /&gt;   另外还有一种兼顾的方法，可以不在内存分配和访问上增加太大额外负担，做法是让虚拟机平时大多数时间采用“标记就-清除”算法，暂时容忍内存碎片化的存在，直到内存空间的碎片化程度已经大到影响对象分配时，再采用“标记-整理”算法收集一次，以获得规整的内存空间。CMS收集器面临空间碎片化过多的时候，采用的就是这种处理办法。&lt;br /&gt;   &lt;strong&gt;优点&lt;/strong&gt;：1.消除了复制算法当中，内存减半的高额代价。2.消除了标记-清除算法当中，内存区域分散的缺点，我们需要给新对象分配内存时，JVM只需要持有一个内存的起始地址即可，即分配内存很快，只需要将指针移动到下一块连续的空闲区域地址。如果是分散的内存，可能还需要，一张列表用来记录被占用的地址值。&lt;br /&gt;   &lt;strong&gt;缺点&lt;/strong&gt;：1.从效率上来说，标记-整理算法要低于复制算法；2.移动对象的同时，如果对象被其他对象引用，则还需要调整引用的地址。3.移动过程中，需要全程暂停用户应用程序，即：STW。 &lt;img src = "https://www.fiveseven.fun/upload/20210920_11014295.png"&gt;&lt;/p&gt; &lt;h3&gt;二、JVM算法细节实现 //TODO&lt;/h3&gt; &lt;h4&gt;  2.1 根节点枚举&lt;/h4&gt; &lt;h4&gt;  2.2 安全点&lt;/h4&gt; &lt;h4&gt;  2.3 安全区域&lt;/h4&gt; &lt;h4&gt;  2.4 记忆集与卡表&lt;/h4&gt; &lt;h4&gt;  2.5 写屏障&lt;/h4&gt; &lt;h4&gt;  2.6 并发的可达性分析&lt;/h4&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 09:52:58 GMT</pubDate>
    </item>
    <item>
      <title>深入理解JVM: (二)垃圾收集器(3):经典垃圾收集器</title>
      <link>http://123.57.101.202:8080/archives/shen-ru-li-jie-jvm-er-la-ji-shou-ji-qi-3jing-dian-la-ji-shou-ji-qi</link>
      <content:encoded>&lt;h3&gt;一、经典垃圾收集器&lt;/h3&gt; &lt;h4&gt;  概念:&lt;/h4&gt; &lt;p&gt;  如果说收集算法是回收内存的方法论，那么垃圾收集器就是内存回收的实践者。不同的虚拟机一般都会提供各种参数供用户根据自己应用特点和要求组合出各个内存分代所使用的收集器。各款收集器之间的关系如&lt;strong&gt;图3-6&lt;/strong&gt;所示，young generation标识年轻代的垃圾收集器，tenured generation标识老年代的垃圾收集器，G1表示年轻代和老年代都适用，而JDK9表示，Serial+CMS和ParNew+Serial Old组合到了JDK9之后不再被支持。 &lt;img src = "https://www.fiveseven.fun/upload/20210920_11283688.png"&gt;&lt;/p&gt; &lt;h4&gt;  1.1 Serial收集器&lt;/h4&gt; &lt;p&gt;  Serial收集器是最基础，历史最悠久的收集器，它是一个单线程工作的收集器，但是它的单线程的意义并不是仅仅说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作，更重要的在于强调它进行垃圾收集时，必须暂停其他所有工作线程，直到它收集结束，这个过程被称之为“Stop The World”，即STW。在服务端现在虚拟机正常情况下基本不再使用Serial垃圾收集器回收内存了，但是对于某些运行在&lt;strong&gt;客户端的虚拟机&lt;/strong&gt;，Serial收集器依然是新生代默认的收集器。主要原因在于和其他垃圾收集器比起来，它简单而高效。它是所有收集器里额外内存占用最小的；对于单核处理器或者处理器核心数较少的环境来说，Serial收集器由于没有线程交互开销，专心做垃圾收集自然可以获得最高的单线程收集效率。在一些用户桌面应用，分配给虚拟机管理的内存并会特别大，收集几十兆甚至几百兆，Serial垃圾收集器完全可以控制在即几十到一百多毫秒之内，只要不是频繁垃圾收集，这个停顿对用户来说完全是可以接受的，因此Serial垃圾收集器对于运行在客户端模式下的虚拟机是一个不错的选择。 &lt;img src = "https://www.fiveseven.fun/upload/20210920_11404099.png"&gt;&lt;/p&gt; &lt;h4&gt;  1.2 ParNew收集器&lt;/h4&gt; &lt;p&gt;  ParNew收集器实质上是Serial收集器的多线程并行版本，除了同时使用多条线程进行垃圾收集之外，其余的行为包括Serial收集器可用的所有控制参数、收集算法、STW、对象分配规则、回收策略等都与Serial收集器完全一致，在实现上这两种收集器也公用了相当多的代码。如图3-8所示。&lt;br /&gt;   ParNew的诞生有一个与功能、性能无关但其实很重要的原因是：除了Seriral收集器之外，目前只有它能与CMS收集器配合工作。在ParNew还没诞生之前，CMS只能与Serial进行配合工作。在JDK5中，当使用CMS来作为老年代的收集器时，新生代默认采用的收集器是ParNew。而到了JDK9，ParNew+CMS不再是官方推荐的服务端模式下的收集器解决方案，官方希望它能被G1代替。&lt;br /&gt;   ParNew收集器在单核心处理器的环境中绝对不会比Serial收集器有更好的效果，主要是因为存在线程交互的开销。但是随着处理器核心线程数的增加，ParNew对垃圾收集时系统资源的高效利用还是很有好处的，它默认开启的收集线程数与处理器核心数量相同。 &lt;img src = "https://www.fiveseven.fun/upload/20210920_12131636.png"&gt;&lt;/p&gt; &lt;h4&gt;  1.3 Parallel Scavenge收集器&lt;/h4&gt; &lt;p&gt;  Parallel Scavenge收集器也是一块新生代收集器，采用“标记-复制”算法，诸多特性从表面上看和ParNew非常相似。Parallel Scavenge(以下简称PS)的特点和关注点与其它啊收集器不同，CMS等手机其关注点是尽可能缩短垃圾收集时用户线程的停顿时间，而PS收集器的目标更侧重吞吐量的控制，并提供了对用的配置参数。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间) &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;  PS提供了两个参数用来靳准控制吞吐量，分别是控制最大垃圾收集停顿时间：-XX:MaxGCPauseMillis参数，和直接设置吞吐量大小的-XX:GCTimeRatio参数。&lt;br /&gt;   -XX:MaxGCPauseMillis参数允许值是一个大于0的毫秒数，收集器将尽量保证在这个时间段完成垃圾收集，但不是这个数字越小越好，垃圾收集停顿时间是以牺牲吞吐量和新生代空间为代价换取的：系统把新生代调小，收集300M肯定比500M块，但这样导致垃圾收集频率更加频繁，原来10秒收集一次停顿100毫秒，现在5秒收集一次，停顿70毫秒。频率增加这样总的垃圾收集时间也在增加，导致整体的吞吐量其实是在下降的。&lt;br /&gt;   -XX:GCTimeRatio的参数值应当是一个大于0小于100的整数，表示希望在GC花费不超过应用程序执行时间的1/(1+n)。换句话说，此参数的值表示运行用户代码时间是GC运行时间的n倍。默认值是99，即允许最大1%的垃圾收集时间。&lt;br /&gt;   PS还提供了一个参数-XX:+UseAdaptiveSizePolicy,这是一个开关参数，开启后，就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SuvivorRatio)、晋升老年代对象的大小(-XX:PretenureSizeThreshold)等细节参数，虚拟机会根据当前系统的运行情况收集性能监控信息，动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种自动调节称为垃圾收集的自适应的调节策略，这也是PS收集器区别于ParNew收集器的一个重要特性。&lt;/p&gt; &lt;h4&gt;  1.4 Serial Old 收集器&lt;/h4&gt; &lt;p&gt;  Serial Old是Serial收集器的老年代版本，同样是一个单线程收集器，采用“标记-整理”算法。这个收集器也是供客户端模式先JVM使用的。如果服务端，它一般是两种用途：JDK5及以前与PS收集器配合使用，另外一种是CMS收集器发生失败后的备案，当CMS收集失败后，采用Serial Old备选。 &lt;img src = "https://www.fiveseven.fun/upload/20210920_23295862.png"&gt;&lt;/p&gt; &lt;h4&gt;  1.5 Parallel Old 收集器&lt;/h4&gt; &lt;p&gt;  Parallel Old是Parallel Scavenge收集器的老年代版本，支持多线程收集，基于“标记-整理”实现。Parallel Old的出现，其实主要也是为了弥补搭配Parallel Scavenge老年代上收集器的不足，在JDK6之前，如果选择了Parallel Scavenge，那么老年代只有Serial Old一种选择，由于单线程的老年代收集器无法充分利用服务器多处理器的并行处理能力，在老年代内存空间很大而且硬件规格比较高级的运行环境中，Parallel+Serial Old的总吞吐量还不及ParNew+CMS组合来得优秀。&lt;br /&gt;   在注重吞吐量或者处理器资源较为稀缺的场合，可以优先考虑使用Parallel+Parallel Old收集器组合。 &lt;img src = "https://www.fiveseven.fun/upload/20210920_23470122.png"&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 09:51:14 GMT</pubDate>
    </item>
    <item>
      <title>深入理解JVM: (二)垃圾收集器(4):低延迟垃圾收集器CMS、G1</title>
      <link>http://123.57.101.202:8080/archives/shen-ru-li-jie-jvm-er-la-ji-shou-ji-qi-4di-yan-chi-la-ji-shou-ji-qi-cmsg</link>
      <content:encoded>&lt;h3&gt;一、低延迟垃圾收集器&lt;/h3&gt; &lt;h4&gt;概念：&lt;/h4&gt; &lt;p&gt;  衡量垃圾收集器的三项最重要的指标是：内存占用、吞吐量、延迟，三者共同构成了一个“不可能三角”。一款优秀的收集器最多可以同时达到其中的两项。在内存、吞吐量、延迟这三项指标中，目前最关键的是延迟，因为随着加算计硬件的发展、性能的提升，我们可以允许收集器多占一点内存；硬件性能的增长，处理器核心数的增加，收集器运行的对程序的影响会降低，换句话说，就是吞吐量会更高。但是对延迟则不是这样，系统内存增大，对延迟反而会带来负面影响，，比如虚拟机要回收1TB的内内存和回收1GB的内存，在时间的耗费上是不一样的，因此低延迟会成为垃圾收集器最被重视的性能指标了。&lt;/p&gt; &lt;h4&gt;  1.1 CMS 收集器&lt;/h4&gt; &lt;p&gt;  CMS是一种一获取最短停顿时间为目标的收集器，CMS比较符合部署在服务端上的互联网网站或者基于浏览器的B/S系统的JAVA应用。CMS收集器是基于“标记-清除”算法实现的，整个运作过程分为4部分:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;初始标记&lt;/strong&gt;：仅仅只是标记一下GC Root能直接关联到的对象，速度很快，这个过程会导致STW。&lt;/li&gt; &lt;li&gt;&lt;strong&gt;并发标记&lt;/strong&gt;：根据初始标记关联到的GC Root对象开始遍历整个对象图的过程，这个过程耗时比较长，但是不需要停顿用户线程，可以与垃圾收集线程一起并发运行。&lt;/li&gt; &lt;li&gt;&lt;strong&gt;重新标记&lt;/strong&gt;：重新标记是为了修正并发标记的过程当中，因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录（并发的可达性分析，三色标记），这个阶段会比初始标记稍长一点，但远比并发标记时间短，这个过程会导致STW。&lt;/li&gt; &lt;li&gt;&lt;strong&gt;并发清除&lt;/strong&gt;： 清理删除标记阶段判断已经死亡的对象，由于不存在移动存活的对象，因此这个阶段也是可以与用户线程同时并发。&lt;br /&gt;   CMS是一款优秀的收集器，并发收集，低停顿，但是有三个明显的缺点：&lt;br /&gt;   1. CMS收集器对处理器资源非常敏感，虽然垃圾收集线程可以和用户线程并发执行，但是有个前提是，系统的处理器核心数不能太低，否则会因为占用处理器计算能力而导致应用程序变慢，减低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量+3)/4，也就是说如果处理器核心数在4或以上，并发回收垃圾线程只占用25%不到的处理器运算资源，并且会随着核心数增加而下降。但是当处理器核心数不足4个时，CMS对用户线程的影响就可能变得很大。&lt;br /&gt;   2. CMS收集器无法处理“浮动垃圾”，有可能因为“浮动垃圾”而触发另外一次Full GC。在CMS并发标记和并发清除的同时，用户线程也在运行，这样会伴随着新的垃圾对象生成，但是这部分垃圾对象是出现在标记之后，CMS无法在当此垃圾回收就清理掉，只好等待下一次垃圾收集，这一部分垃圾就称为“浮动垃圾”，这样就要求还需要预留足够内存空间提供给用户线程使用，因此CMS收集器不能像传统的垃圾收集器等到老念叨几乎完全被填满之后再进行收集，必须预留一部分空间供并发收集时的程序使用。在JDK6，默认设置下，当老年代被使用了92%的空间后，会激活Major GC，当然这个阈值可以通过-XX:CMSInitiatingOccu-pancyFraction参数来设置，但是阈值设置得太高会出现一定的风险：CMS运行期间，预留的内存空间无法满足程序分配新对象的需求，就会出现一次“并发失败”(Concurrent Mode Failure)，这时候虚拟机不得不启动后备预案：冻结程序运行，临时启用Serial Old收集器来重新进行老年代的垃圾收集，但这样的停顿时间就更长了。&lt;br /&gt;   3. 在没有“标记-整理”之前会产生内存碎片，CMS是基于“标记-清除”来实现的一块垃圾收集器，因此垃圾收集结束后，会产生大量的空间碎片，所以往往会出现，老年代明明内存空间还很多，但遇到需要给较大的对象分配空间的时候却触发Full GC的情况。为了解决这个问题CMS提供了-XX:+UseCMSCompactFullCollection开关参数，这个参数默认是开启的，用在与当CMS不得不进行Full GC的时候开启内存碎片合并整理，由于这个过程需要移动存活的对象，因此是无法并发执行的，会产生STW。因为并不是每次Full GC都需要进行内存整理的，这样会导致停顿时间边长，因此CMS又提供了一个参数-XX:CMSFullGCsBeforeCompaction，这个参数的作用是要求CMS进行若干次Full GC垃圾收集不整理空间的情况之后，下一次Full GC前会先进行碎片整理(默认值为0，标识每次进入Full GC都会碎片整理)。&lt;/li&gt; &lt;/ul&gt; &lt;img src="https://www.fiveseven.fun/upload/20210921_09540545.png"&gt; #### 1.2 Garbage First 收集器   ##### 1.2.1 简介   Garbage First(简称G1)开启了收集收集从分代收集面向局部收集得设计思路和基于Region的内存布局形式。     G1是一款面向服务端应用的垃圾收集器，在JDK9版本，G1宣告取代Parallel Scavenge + Paralle Old组合成为服务端下默认的垃圾收集器，而CMS则被声明为不推荐使用的收集器，如果启用CMS的话，只能和ParNew来进行搭配使用。     G1的Mixed GC模式:在G1收集器出现之前的所有收集器，包括CMS在内，垃圾收集的目标范围要么是整个新生代(Minor GC)，要么就是整个老年代(Major GC),再要么是整个堆(Full GC)，而G1跳出了这个樊笼它可以面向对内存任何部分来组成回收集进行回收，它可以面向堆内存任何部来组成回收集(Collection Set，简称CSet),衡量标准不再是它属于哪个分代，而是哪块内存中存放的垃圾数量最多，回收收益最大，这就是G1收集器的Mixed GC模式。  ##### 1.2.2 Region分区 **区域Region的意义:**     G1里面的Region区域的概念是不同于其他垃圾收集的一个重要区别，也是思维的方式不同，后续垃圾收集的单位都是以Region为单位的，但仍然属于分代收集器。正是由于内存被划分为小块，避免扫描整个空间堆， 要想更好的控制GC停顿时间就得有灵活扫描堆内存和回收堆内存的选择权利，而Region让这一切成为了可能 。Region的出现不必让线程每次都必须全部扫描堆文件并且一次回收全部的垃圾，让GC时间可控，并且还保留了之前垃圾收集的分代思想，可以说G1进行了取长补短，在保留之前的垃圾收集的优势后又做了一个优化升级。   &lt;p&gt;&lt;strong&gt;Region区的特点:&lt;/strong&gt;&lt;br /&gt;   G1逻辑上虽然区分老年代和新生代，但它们一系列区域的动态集合，会发生变化。G1收集的最小单元是Region，因此可以有计划的避免在整个JAVA堆中进行全区域的垃圾收集。更具体的思路是让G1收集器去统计各个Region里面的空间大小以及回收所需要的时间，然后维护在一个优先级列表里，每次根据用户设定的收集停顿时间(-XX:MaxGCPauseMillis指定，默认200毫秒)，优先处理回收价值收益最大那些Region,以保证有限的时间内，尽可能获取最高的收集效率，通过Region分区，使得可预测的gc停顿时间成为可能。默认分为： 2048个分区，每个区块大小范围为：1～32M。 &lt;img src="https://www.fiveseven.fun/upload/20210921_12072679.png"&gt; &lt;strong&gt;Region中有类特殊的区域：&lt;/strong&gt;&lt;br /&gt;   Humongous区域，专门用来存储大对象。只要G1任务对象的大小超过Region区域大小的一半，即被认为大对象，每个Region的大小可以通过-XX:G1HeapRegionSize来设置，取值范围1MB~32MB之间，且应为2的N次幂。对于那些超过了整个Region的超级大对象，则会被存放在N个连续的Humongous Region之中，G1的大多数行为都把Humongous Region作为老年代的一部分来进行看代。&lt;br /&gt;   Humongous区有三个特点:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;每个大对象都被连续分配在老年代。对象的起始总是被分配在序列中第一个区块的起始，区块中剩下的空间将不再使用，除非该对象被回收(容易产生内存碎片)。&lt;/li&gt; &lt;li&gt;通常来说， 大对象只有在标记清理的最后阶段或者Full gc中才会被清除，假如已经不再被引用。但是有个例外便是，对于基本类型的数组对象G1在垃圾回收的过程当中发现，引用的数量并不是很多，那么也会对其进行回收，可以通过XX:G1EagerReclaimHumongousObjects来进行关闭。&lt;/li&gt; &lt;li&gt;大对象的分配可能会导致gc过早发生，每次一有大对象分配，G1就会检查老年代占用是否超出阈值，如果超出，会导致标记过程的发生。&lt;/li&gt; &lt;li&gt;大对象在gc过程中是不会移动的（因为这个复制消耗是不可接受的，这也是老年代为什么不采用复制算法的原因），会产生大量的内存空间碎片，这会导致缓慢的Full GC甚至内存溢出。&lt;/li&gt; &lt;/ul&gt; &lt;h5&gt;1.2.3 G1垃圾收集的两个周期&lt;/h5&gt; &lt;img src="https://www.fiveseven.fun/upload/20210925_13172981.png"&gt; - **年轻阶段（The young-only phase ）**     这个阶段以普通Minor GC为始(小蓝圆圈，一个小蓝圆圈代表一次年轻代的Minot GC)，并且把符合条件的存活对象放入老年代。     该阶段随着一些年轻代的对象逐步晋升到老年代而开始，当老年代的内存占用率达到一定阈值(-XX: InitiatingHeapOccupancyPercent 默认值时45)便触发三个步骤：  1. **Concurrent Start:**     这个阶段除了有一般年轻代的收集功能外，还会去并发标记老年代的存活对象(提前为下一阶段老年代回收垃圾做准备)，而这个标记过程是和年轻代的垃圾回收并发经进行的，  2. **Remark:**     这个阶段会产生STW，并且完成所有标记。并且在从Remark结束到Cleanup阶段这过程，G1会并发得计算出所选的老年代区域能够释放的空间大小。  3. **Cleanup:**     判断是否需要进行空间回收，如果需要进行空间回收，则进行一次 Prepare Mixed GC。   - **空间回收阶段（The space-reclamation phase）**     老年代超过阈值，经过Ramark和Cleanup，会进入空间收集阶段，在空间收集阶段，会进行多次混合收集，除了回收年轻代Region的垃圾对象，还会移动老年代Region的对象。随着老年代移动回收的进行，当G1觉得移动更多的老年代对象不会释放出更多的内存时，空间回收阶段结束。 &lt;p&gt;  &lt;strong&gt;G1收集器至少有一下这些关键细节问题需要处理://TODO&lt;/strong&gt;&lt;/p&gt; &lt;ol&gt; &lt;li&gt;将JAVA堆分成若干个Region后，存在着跨Region引用的问题如何处理?&lt;br /&gt;   使用记忆集(RSet)避免全堆作为GC Root扫描，每个Region上都维护着自己的记忆集，这些记忆集会记录别的Region指向自己的指针，并记录这些指针分别在那些卡页的范围之内。&lt;/li&gt; &lt;li&gt;在并发标记阶段如何保证收集线程和用户线程互不干扰的运行？&lt;br /&gt;   通过SATB、TAMS&lt;/li&gt; &lt;li&gt;怎么建立可靠的停顿预测模型？&lt;br /&gt;   通过衰减均值理论基础来实现。&lt;/li&gt; &lt;li&gt;G1清除-复制，将存活的对象复制到另外的Region，发现没有空余空间的时候，一定会触发Full GC吗?&lt;br /&gt;   不会，前提是发生在垃圾回收快结束的时候，也就是说，大部分的存活对象已经复制到其他的Region，并且剩余的内存空间足够程序继续去运行。在这个前提下G1会取消复制剩余得不到内存分配的对象，并且使程序能够正常的运行。&lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;调优：&lt;/h3&gt; &lt;p&gt;尽量减少FullGC, FullGC 一般采用标记-整理算法，效率低 STW时间长&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;//打印JVM初始配置的值大小 java -XX:+PrintCommandLineFlags -version &lt;/code&gt;&lt;/pre&gt; &lt;img src="https://www.fiveseven.fun/upload/20210911_15501840.png"&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 09:47:00 GMT</pubDate>
    </item>
    <item>
      <title>深入理解JVM: (三)JVM性能监控与处理</title>
      <link>http://123.57.101.202:8080/archives/shen-ru-li-jie-jvm-san-jvmxing-nen-jian-kong-yu-chu-li</link>
      <content:encoded>&lt;p&gt;写了好一段时间的JVM收集器，尤其是G1，看着网上各种复制来复制去的资料，甚至有一些还给出了错误的结论，真的有些心力交瘁，最终还是参考了Oracle官网的结论，打算过段时间再回到JVM分区及垃圾回收的主线上。&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;时隔两年多才再回来填这个坑。&lt;br /&gt; &lt;strong&gt;jps: 虚拟机进程状况工具&lt;/strong&gt;:&lt;br /&gt; jps [opetions]&lt;br /&gt; 查询正在运行的虚拟机进程。并显示虚拟机执行的主类(Main Class，main()函数所在的类)名称一级这些进程的本地虚拟机唯一ID。 &lt;img src="https://www.fiveseven.fun/upload/20230716_1732036.jpg"&gt; &lt;strong&gt;jstat: 虚拟机统计信息监视工具&lt;/strong&gt;:&lt;br /&gt; 用于监视虚拟机各种运行状态信息的命令执行工具。他可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾手机、即时编译等运行时数据。&lt;br /&gt; jstat [options 进程id interval s/ms count] interval和count分别表查询间隔和次数，如果省略这2个参数，说明只查询一次。&lt;br /&gt; eg: jstat -gcutil 10594 250 20&lt;br /&gt; &lt;img src="https://www.fiveseven.fun/upload/20230716_17454999.png"&gt; E：伊甸园区使用比例，S0、S1分别表示S0：幸存0区当前使用比例和幸存1区当前使用比例,O：老年代使用比例，M：元数据区使用比例，CCS：压缩使用比例，YGC：年轻代垃圾回收次数，FGC：老年代垃圾回收次数，FGCT：老年代垃圾回收消耗时间，GCT：垃圾回收消耗总时间。 &lt;img src="https://www.fiveseven.fun/upload/20230716_17374679.jpg"&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;jinfo: java配置信息工具&lt;/strong&gt;:&lt;br /&gt; 实时查看和调整虚拟机各项参数&lt;br /&gt; jinfo [option] pid&lt;/p&gt; &lt;p&gt;&lt;strong&gt;jmap: 内存映像工具&lt;/strong&gt;:&lt;br /&gt; 用于生成堆转储快照(dump文件),当然也可以通过设置 -XX:+HeadDumpOnOutOfMemoryError参数，来让虚拟机在发生内存溢出的时候生成快照。jmap也可以用来查询堆或者方法区使用的垃圾收集器。&lt;br /&gt; jmap [option] pid &lt;img src="https://www.fiveseven.fun/upload/20230716_18132861.jpg"&gt; eg: jmap -dump:format=b,file=/dump.bin 10594 &lt;img src="https://www.fiveseven.fun/upload/20230716_18185423.png"&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;jstack: java堆栈跟踪工具&lt;/strong&gt;: 用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合，生成线程快照的目的通常是定位线程出现长时间停顿的原因，如线程间思索、死循环、请求外部资源导致的长时间挂起，都是导致线程长时间停顿的常见原因。 jstack [optoini] pid &lt;img src="https://www.fiveseven.fun/upload/20230716_18352222.jpg"&gt; &lt;strong&gt;常用命令汇总:&lt;/strong&gt;: &lt;img src="https://www.fiveseven.fun/upload/20230716_1727370.png"&gt; &lt;img src="https://www.fiveseven.fun/upload/20230716_17281142.png"&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 08:38:32 GMT</pubDate>
    </item>
    <item>
      <title>深入理解JVM: (五)虚拟机类加载机制(1):加载过程</title>
      <link>http://123.57.101.202:8080/archives/shen-ru-li-jie-jvm-wu-xu-ni-ji-lei-jia-zai-ji-zhi-1jia-zai-guo-cheng</link>
      <content:encoded>&lt;h3&gt;一、概述&lt;/h3&gt; &lt;p&gt;  JAVA虚拟机把描述类的数据从Class文件加载到内存，并对数据进行校验、转换解析和初始化，最终形成可以被虚拟机直接使用的JAVA类型，这个过程被称作虚拟机的类加载机制。每个Class文件都代表着一个类或者接口的可能，而Class文件只是用来承载最终转化成机器能够理解的二进制流的载体而已，“Class文件”可以是以，磁盘文件、网络、数据库、内存或者动态产生都可以。&lt;/p&gt; &lt;h3&gt;二、类加载的时机&lt;/h3&gt; &lt;p&gt;  一个类从被加载到虚拟机内存到 卸载出内存，它的整个生命周期如一下：加载、验证、准备、解析、初始化、使用、卸载。其中验证、准备、解析统称为连接。《JAVA虚拟机规范》中严格强调有且只有以下6种情况必须立即对类进行“初始化”：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;遇到new、getstatic、putstatic、invokestatic这四条指令时，如果类型没有进行过初始化，则需要先触发其初始化阶段，能够生成这四条指令的典型代码场景有：&lt;br /&gt;    a. 使用new关键字创建对象    b. 读取或设置一个类型的静态字段    c. 调用一个类型的静态方法&lt;/li&gt; &lt;li&gt;使用java.lang.reflect包的方法对类型进行反射调用的时候，如果类型没有进行过初始化，则要先触发初始化&lt;/li&gt; &lt;li&gt;当初始化类的时候，如果发现其父类还没有进行过初始化，则需要先初始化其父类&lt;/li&gt; &lt;li&gt;当虚拟机启动时，用户需要指定一个要执行的主类(包含main()方法的那个类)，虚拟机会先初始化这个主类&lt;/li&gt; &lt;li&gt;当使用JDK7新加入的动态语言支持时(这个我也没有弄懂 - - !!!)&lt;/li&gt; &lt;li&gt;当一个接口中定义了JDK8新加入的默认方法，如果这个接口的实现类发生了初始化，那么该接口要在其之前被初始化&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;  以上的这6种行为称之为对类型的主动引用，除此之外，所有的引用类型的方式都不会触发初始化，称之为被动引用。举三个例子：&lt;br /&gt;   &lt;strong&gt;例子 一：&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * 父类  * 通过子类引用父类的静态字段，不会导致子类初始化  */ public class SuperClass {     public static int a = 10;     static {         System.out.println(&amp;quot;Super class printed&amp;quot;);     } } &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * 子类  */ public class SubClass extends SuperClass{     static {         System.out.println(&amp;quot;Sub class printed&amp;quot;);     } } &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * 测试类  */ public class Test {     public static void main(String[] args) {         System.out.println(SubClass.a);     } } //最终打印出来的结果为： Super class printed 10 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;上述代码运行后，只会打印出来“Super class printed”，及其对应的变量数值，是因为，对于静态字段，只有直接定义这个字段的类才会被初始化，因此通过其子类来引用父类的静态字段，只会触发父类的初始化，而不会触发子类的初始化。&lt;br /&gt;   &lt;strong&gt;例子二：&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * 通过数组定义来引用类，不会触发此类的初始化  */ public class ArrClass {     public static void main(String[] args) {         SuperClass[] arr = new SuperClass[10];     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;运行后并没有打印出数据，说明没有触发SuperClass的初始化，但并不能说明没有类被初始化，虚拟机会自动生成一个直接继承Object的子类(Lorg.xxx.xxx.xxx.SuperClass)，创建动作由newarray字节码指令触发，这个子类代表了一个类型元素为org.xxx.xxx.xxx.SuperClass的一堆数组，数组中应有的属性和方法的实现都在这个类里面。&lt;br /&gt;   &lt;strong&gt;例子三：&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * 常量在编译阶段会存入类的常量池中，  * 本质上没有直接引用到定义常量的类，因此不会触发定义常量的类的初始化  */ public class ConstClass {     static {         System.out.println(&amp;quot;ConstClass printed&amp;quot;);     }     public static final String str =&amp;quot;Hello、World&amp;quot;; } &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * 测试类  */ public class Test {     public static void main(String[] args) {         System.out.println(ConstClass.str);     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;上述代码运行后，并没有输出“Hello、World”，这是因为放在编译阶段通过常量传入的“Hello World”，已经直接被存储在Test这个类的常量池里面了，此后Test对常量ConstClass.str的引用，实际都被转化为Test类对自身常量池的引用了，也就是说，实际上Test的Class文件中并没有ConstClass类的符号引用入口，在两个类编译完成Class文件之后就不存在任何联系了。&lt;br /&gt;   接口的加载过程和类的加载过程稍有不同，真正的区别在于：类的初始化时，要求其父类全部都已经被初始化过了，但是一个接口在初始化的过程当中，并不要求其父类接口全部都要完成初始化。&lt;/p&gt; &lt;h3&gt;三、类加载的过程&lt;/h3&gt; &lt;h4&gt;3.1 加载&lt;/h4&gt; &lt;p&gt;  加载阶段，虚拟机会完成以下三件事：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;通过一个类的全限定名来获取定义此类的二进制字节流&lt;/li&gt; &lt;li&gt;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构&lt;/li&gt; &lt;li&gt;在内存中生成一个代表这个类的java.lang.Class对象，作为方法区这个类的各种数据的访问入口&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;针对第一点《JAVA虚拟机规范》并没指明二进制流必须从某个Class文件里面获取，因此仅仅通过这一个空隙，后续边诞生了多种不同读取字节流的方式：   a.从zip压缩包中读取，最终成为日后的jar包、war包等格式&lt;br /&gt;   b.运行时计算生成，这种场景使用的最多的就是动态代理技术，在java.lang.reflect.Proxy中，就是用了ProxyGenerator.generateProxyClass()来为特定的接口生成形式为“*$Proxy”的代理类的二进制字节流。&lt;br /&gt;   c.从加密的文件中获取，这是典型防Class文件被反编译的保护措施，通过加载时解密Class文件来保证程序运行逻辑不被偷窥。&lt;/p&gt; &lt;h4&gt;3.2 验证&lt;/h4&gt; &lt;h5&gt;3.2.1 概念：&lt;/h5&gt; &lt;p&gt;  验证阶段的目的是确保Class文件的字节流中包含的信息符合《JAVA虚拟机规范》的全部约束要求，保证这些信息被当作代码运行后不会危害虚拟机自身的安全。&lt;br /&gt;   这边的校验并非指编译器对java文件编译成class文件的校验(虽然能一定程度阻止有问题的代码，比如说引用不存在的类等等)。但是虚拟机实际读取的是字节码文件，现在有很多手段绕过编译器生成字节码，因此JAVA虚拟机需要检查输入的字节流，否则如果载入了有错误或者恶意的字节码流后果将不堪设想。&lt;br /&gt;   验证阶段是一个非常重要的，但并不是一个必须执行的阶段，因为它只有通过或者不通过的差别。如果程序&lt;strong&gt;运行的全部代码&lt;/strong&gt;已经被反复使用和验证过了，其实就可以考虑关闭大部分类的类验证措施，以缩短虚拟机类加载的时间，可以通过 -Xverify:none 参数设置。&lt;br /&gt;   &lt;strong&gt;目前虚拟机字节码验证分为以下四个阶段：&lt;/strong&gt;&lt;/p&gt; &lt;ol&gt; &lt;li&gt;文件格式验证：&lt;br /&gt;   校验字节流是否符合Class文件格式的规范，并且能否被当前虚拟机理解，只有通过这个阶段，字节流才允许进入JAVA虚拟机内存的方法区中存储。&lt;/li&gt; &lt;li&gt;元数据验证：&lt;br /&gt;   对字节流描述的信息进行语义分析，确保符合《JAVA语言规范》的要求，比如：1.这个类是否有父类(至少有个Obejct父类)；2.如果这个类implement了某个接口，那么该类是否有实现其接口方法。&lt;/li&gt; &lt;li&gt;字节码验证&lt;br /&gt;   这个阶段是整个过程最复杂的，&lt;strong&gt;确定程序语义上合法的、符合逻辑的。这个阶段主要是对方法体进行校验分析&lt;/strong&gt;，保证校验类的方法在运行中不会出现危害虚拟机安全的行为，只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。例如：JVM 会对代码组成的数据流和控制流进行校验，确保 JVM 运行该字节码文件后不会出现致命错误。例如一个方法要求传入 int 类型的参数，但是使用它的时候却传入了一个 String 类型的参数。一个方法要求返回 String 类型的结果，但是最后却没有返回结果。代码中引用了一个名为 Apple 的类，但是你实际上却没有定义 Apple 类(当然这些如果是通过IDEA编辑的话，在编译器肯定是会报错的，这边讨论的是生成后的字节码阶段，已经绕过了编译期)。&lt;/li&gt; &lt;li&gt;符号引用验证：&lt;br /&gt;   可以看作是对类自身以外的各类信息进行匹配校验，通俗来说就是，该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源，通常需要校验的有以下：1.符号引用中通过字符串描述的全限定类名是否能够找到对应的类；2.在指定的类中是否存在对应的方法或者字段；3.符号引用中的类、字段、方法是否能够被访问到(private、protected、ppublic)，如果无法通过符号引用一般会抛出诸如：IllegalAccessError、NoSuchFieldError、NoSuchMethodError异常。&lt;br /&gt;   符号引用校验其实是为后续的解析做准备，真正将符号引用转化为直接引用的这个阶段发生在“解析阶段”。&lt;/li&gt; &lt;/ol&gt; &lt;h4&gt;3.3 准备&lt;/h4&gt; &lt;p&gt;  为类中的类变量(被static修饰过的为类变量，否则为实例变量)，分配内存并设置类变量的初始值，JDK7及以前是分配在方法区里，JDK8及之后则分配在堆里面。准备阶段内存分配不包括实例变量，实例变量会在对象实例化随着对象一起分配在堆内存中。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class Test {     private static int a = 12;   //类变量     private static final int b = 13; //类变量  private int c = 14;     //实例变量 } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;  例如类变量a在准备阶段仅仅是被初始化为0值(如果是引用类型的话，则是null值)，而把a复制为12的动作需要等到初始化阶段才会被执行，但是如果类变量被final修饰过，例如类变量b，那么准备阶段就会直接给赋值成13。&lt;/p&gt; &lt;h4&gt;3.4 解析&lt;/h4&gt; &lt;p&gt;  该阶段是将符号引用替换为直接引用的过程，以下是对二者的定义：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;符号引用&lt;/strong&gt;:用一组符号来描述所引用的目标，作用是准确地定位到引用地目标。符号引用和虚拟机实现的内存布局无关，引用的目标不一定是已经加载到虚拟内存当中的内容。在Java中，一个java类将会编译成一个class文件。在编译时，java类并不知道所引用的类的实际地址，因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类，在编译时People类并不知道Language类的实际内存地址，因此只能使用符号org.simple.Language**（假设是这个，当然实际中是由常量池中的表CONSTANT_Class_info来表示的,CONSTANT_Class_info存放的是该类下需要引用到其他类的全限定名常量）**来表示Language类的地址。&lt;/li&gt; &lt;li&gt;&lt;strong&gt;直接引用&lt;/strong&gt;:直接引用可以是一个直接指向目标的指针，它和虚拟机实现的内存布局直接相关。如果有了直接引用，那引用的目标必须已经在虚拟机内存中存在的。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符这7类符号引用进行。&lt;br /&gt;   &lt;strong&gt;针对前四种进行说明下：&lt;/strong&gt;&lt;br /&gt; 1.类或接口解析&lt;br /&gt;   设当前类型为D，如果要把一个从未解析过的符号引用N解析成一个类或接口C的的直接引用，虚拟机完成整个解析过程需要三个步骤：&lt;br /&gt;   a.如果C不是一个数组类型，虚拟机会通过符号引用中的信息，将全限定类名传递给D的类加载器去加载类C，加载过程当中，又会触发其他相关类的加载动作，例如类C有又会去加载父类，或者实现的接口。一旦这个过程当中出现任何异常，解析过程就宣告失败。&lt;br /&gt;   b.如果C类是一个数组，并且元素为引用类型，那么会先按照第a点的规则加载数组元素类型，接着由虚拟机生成一个代表该数组的Class对象。(一般是在全限定类名前面加L，例如Integer[]，由虚拟机生成的对象为Ljava/lang/Integer)。&lt;br /&gt;   c.如果上面两部已没有任何异常，继续对其访问权限的校验，比如对当前类D是否有对类C的访问权限(如果一个类不用public来修饰，那么这个类仅能被同包的其他类引用)，如果发现不具备访问权限，就会抛出IllegalAccessError异常。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;  2.字段解析&lt;br /&gt;   如果该字段未被解析过，首先会在常量池中的表CONSTANT_FIELDREF_info里面找到对应的CONSTANT_Class_info符号引用进行解析，也就是该字段所示的类或接口的符号引用。如果在解析这个类或接口符号引用过程中出现问题，都会导致字段符号引用解析的失败。解析完成后，还需要按照以下步骤对C进行后续的字段搜索：&lt;br /&gt;   a.如果C本身包含的字段能与目标相匹配，则返回这个字段的直接引用。&lt;br /&gt;   b.否则，在C中实现的接口中，由下而上递归搜素各个接口，如果在接口中能找到与之相匹配的字段，则返回这个字段的直接引用。&lt;br /&gt;   c.否则 ，如果C不是Obejc的话，按照继承关系，由下而上递归搜索其父类，如果能找到与之匹配的字段，则返回这个字段的直接引用。&lt;br /&gt;   d.否则，抛出NoSuchFieldError异常&lt;/p&gt; &lt;p&gt;  3.类方法解析&lt;br /&gt;   方法解析第一个步骤和字段解析一样，先解析常量池里面的方法表CONSTANT_Methodref_info所属的类或接口的符号引用，如果解析成功，那么会按照以下步骤对方法进行搜索，我们依旧用C表示所属方法的类：&lt;br /&gt;   a.Class文件中类的方法和接口的方法符号引用的常量类型定义是分开的，如果在类的方法表中发现对应C类方法是个接口的话，那就抛出IncompatibleClassChangeError异常。&lt;br /&gt;   b.如果通过第一步，则在C类中查找是否有方法名与之相匹配的方法，如果有则直接返回这个方法的直接引用。&lt;br /&gt;   c.否则，在C类的父类中递归查找所有能够匹配的方法，如果有则直接返回这个方法的直接引用。&lt;br /&gt;   d.否则，在C类实现的接口及其继承的接口中递归寻找与之匹配的方法，如果匹配说明C类是一个抽象类，并抛出AbstractMethodError。&lt;br /&gt;   e.否则，宣告查找方法失败，抛出NoSuchMethodError。&lt;/p&gt; &lt;p&gt;  4.接口方法解析&lt;br /&gt;   接口方法解析和类方法解析类似，不再赘述。&lt;/p&gt; &lt;h4&gt;3.5 初始化&lt;/h4&gt; &lt;h5&gt;3.5.1 概念&lt;/h5&gt; &lt;p&gt;  类的初始化阶段是类加载过程的最后一个阶段，直到这个阶段，JAVA虚拟机才真正开始执行类中编写的JAVA代码程序，将主导权移交给应用程序。&lt;br /&gt;   在准备阶段，变量已经进行过一次初始零值的赋值操作，在初始化阶段，会根据程序员的主观计划去初始化类变量和其他资源。而初始化阶段是执行类构造器&lt;clinit&gt;()方法的过程**(而实例化一个对象，是执行实例构造器&lt;init&gt;()方法，同样也是这个阶段才对new出来的对象赋予主观的程序变量)**。&lt;/p&gt; &lt;h5&gt;3.5.2 clinit()方法定义：&lt;/h5&gt; &lt;ol&gt; &lt;li&gt;clinit方法是由编译器自动收集类中所有的类变量的赋值动作和静态代码块中的语句合并产生的。也就是说如果一个类中，没有对类变量进行赋值操作或者没有静态代码块，那么编译器可以不为这个类生成&lt;clinit&gt;()方法。&lt;/li&gt; &lt;li&gt;编译器收集的顺序是由语句在源文件中出现的顺序决定的，静态代码块只能访问到定义在它之前的，而定义在之后的变量不能访问，但可以赋值。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class Parent {      static {         a =2;                   //可以赋值         System.out.println(a);  //不能访问     }     public static int a =1; } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;类构造器&lt;clinit&gt;()和实例构造器&lt;init&gt;()之间的区别&lt;br /&gt;   a.出现的时间顺序不用，clinit方法一定会比init方法先执行，因为类的加载一定优先于类的实例化，可以这么认为类的加载完后会生成一个对应的class对象放入到共享的方法区，后续的new出来的实例对象，其实都是拿着这个class对象模板来创建的。&lt;br /&gt;   b.因为构造器加载的顺序不同，导致文件里的资源访问的顺序不同，即：实例变量或者是实例方法可以访问类变量或类方法，反之则编译报错 ，其根本原因是两者的初始化顺序不同，类变量和类方法在类加载的时候就已经初始化好了，而实例变量和实例方法得等到创建实例的时候才初始化好。一个已初始化好的类变量怎么能够去访问还未初始化的实例变量呢？&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class StaticClass {          private static int a = 100;          private int b = a;          //实例变量可以访问类变量          private static int x = b;   //类变量不能访问实例变量，编译报错          private static void staticMethod(){         instanceMethod();       //类方法不能访问实例方法，编译报错     }          private void instanceMethod(){         staticMethod();         //实例方法可以访问类方法     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;  c.init方法会显示的调用父类构造器，而clinit不用，这是因为在初始化之前，在&lt;strong&gt;解析阶段&lt;/strong&gt;就会将继承的父类加载好，而init方法执行的时候，假如有继承的父类，那么会先去执行父类的init方法。具体如下图：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;//父类 public class Parent {     static {         System.out.println(&amp;quot;父类类加载完成&amp;quot;);     }     public Parent() {         System.out.println(&amp;quot;父类构造器执行&amp;quot;);     }     public Parent(int a) {         System.out.println(&amp;quot;父类构造器执行&amp;quot;);     } } //子类 public class Son extends Parent{     static {         System.out.println(&amp;quot;子类类加载完成&amp;quot;);     }     public Son() { //        super();  //如果父类有默认的无参构造函数，则这边可不写         super(1);//如果父类无默认的构造函数，                     // 那么必须声明调用父类构造函数，并且是放在方法函数的第一行         System.out.println(&amp;quot;子类构造器执行&amp;quot;);     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;以上的打印顺序为：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;父类类加载完成 子类类加载完成 父类构造器执行 子类构造器执行 &lt;/code&gt;&lt;/pre&gt; &lt;ol start="4"&gt; &lt;li&gt;接口中不能使用静态代码块，但仍然有变量初始化的赋值操作，因此接口也可以生成clinit方法，但是接口与类不同的是，执行clinit方法的时候不需要去先执行父接口的clinit方法，除非是需要加载父接口，因为只有当父接口中定义的变量被使用时，父类接口才会被初始化。此外接口的实现类在初始化的时候，也不会去执行接口的clinit方法，即不会去加载接口，除非是接口的变量在子类中有被使用到。&lt;/li&gt; &lt;li&gt;JAVA虚拟机在加载类的时候，必须保证，只有一个线程执行clinit方法，即，在多线程环境中，clinit方法会被加锁同步，如果有多个线程同时去初始化这个类，那么只会有一个线程去执行clinit方法，其他线程都需要阻塞等待，知道活动线程执行完毕&lt;clinit&gt;方法。如果有一个类的clinit方法耗时很长的操作，那就可能造成多个线程阻塞，这也就是为什么很多大型项目在上线前，为什么都需要预先做一下热启动的方案，这是因为在热启动阶段通过较少的线程去将整个运行程序所需要的类都加载完毕，避免在生产环境并发量很高的时候，导致线程阻塞，将服务压垮。&lt;br /&gt; PS：需要注意，其他线程虽然会阻塞，但如果执行clinit方法的那条线程退出clinit方法后，其他线程唤醒后，不会再次进入clinit方法。同个类加载器下，一个类型只会被初始化一次。&lt;/li&gt; &lt;/ol&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 08:35:58 GMT</pubDate>
    </item>
    <item>
      <title>深入理解JVM: (五)虚拟机类加载机制(2):类加载器</title>
      <link>http://123.57.101.202:8080/archives/shen-ru-li-jie-jvm-wu-xu-ni-ji-lei-jia-zai-ji-zhi-2lei-jia-zai-qi</link>
      <content:encoded>&lt;h3&gt;一、类加载器&lt;/h3&gt; &lt;h4&gt;概念：&lt;/h4&gt; &lt;p&gt;  通过一个类的全限定类名来获取描述该类的二进制字节流，将这个动作放到JAVA虚拟机外部去实现，以便应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为**“类加载器”**。&lt;/p&gt; &lt;h4&gt;1.1 类与类加载器&lt;/h4&gt; &lt;p&gt;  对于任意一个类，都必须由加载它的类加载器和这个类本身一起共同确立其在JAVA虚拟机的唯一性，每一个类加载器，都拥有一个独立的类名称空间。这句话的意思是，比较两个类是否相等，只有在这两个类都是由同一个类加载器加载的前提下才有意义，否则，即使这两个类来源于同个class文件，被同一个JAVA虚拟机加载，只要加载他们的类加载器不同，那这两个类就必定不相等。这里的相等指的是Class对象的equals()方法、instanceof 关键字做对象所属关系判定等各种情况。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class DemoClassLoader {     public static void main(String[] args) throws Exception {         ClassLoader classLoader = new ClassLoader() {   //这个方法如果被覆盖掉了，说明双亲委派机制失效，双亲委派机制，是通过loadClass来连接父加载器的             public Class&amp;lt;?&amp;gt; loadClass(String name) throws ClassNotFoundException {                 try {                     String fileName = name.substring(name.lastIndexOf(&amp;quot;.&amp;quot;) + 1) + &amp;quot;.class&amp;quot;;                     InputStream is = getClass().getResourceAsStream(fileName);                     if (is == null) {                         return super.loadClass(name);                     }                     byte[] b = new byte[is.available()];                     is.read(b);                     return defineClass(name, b, 0, b.length);                 } catch (IOException e) {                     throw new ClassNotFoundException();                 }             }         };         Object o = classLoader.loadClass(&amp;quot;com.example.demo.classLoader.DemoClassLoader&amp;quot;).newInstance();         System.out.println(o.getClass());         //自定义的类加载器实例化的对象         System.out.println(o instanceof DemoClassLoader);         DemoClassLoader a = new DemoClassLoader();         DemoClassLoader b = new DemoClassLoader();         System.out.println(o.getClass()==a.getClass());//false         System.out.println(a.getClass()==b.getClass());//true     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;以上的输出结果为:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;class com.example.demo.classLoader.DemoClassLoader false false true &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;从结果不难得知，我们自定义了一个类加载去加载这个类，并实例化出了对象，但是从上面个的输出结果可以看出，这个类确实是com.example.demo.classLoader.DemoClassLoader，但是在给对象做所属类型检验是返回了false，说明JAVA虚拟机中同时存在两个DemoClassLoader类，一个是由虚拟机的应用程序加载，一个是由我们自定义的加载器加载，虽然他们都是来自于同个class文件，但是在虚拟机中仍然是连个相互独立的类。&lt;/p&gt; &lt;h4&gt;1.3 三层类加载器&lt;/h4&gt; &lt;p&gt;&lt;img src="https://www.fiveseven.fun/upload/20211106_1444409.png" alt="" /&gt;   对于JAVA虚拟机来说，只存在两种不同的类加载器：一种是启动类加载器，这个类加载器使用C++实现，是虚拟机自身的一部分；另外一种就是其他所有的类加载器，这些类加载器都由java语言实现，独立于虚拟机外部，并且全部继承自抽象类java.lang.ClassLoader。&lt;br /&gt;   站在开发者角度，则分为三层类加载器：启动类加载器、扩展类加载器、应用程序类加载器。&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;启动类加载器:&lt;/strong&gt;&lt;br /&gt;   这个类加载器负责加载&amp;lt;JAVA_HOME&amp;gt;\lib目录，或者被-Xbootclasspath参数所指定的路径中存放的，而且是JAVA虚拟机能够识别的类库加载到虚拟机的内存中。启动类加载器无法被JAVA程序直接引用，所以当获取启动类加载器的时候，会返回一个null值。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;扩展类加载器:&lt;/strong&gt;&lt;br /&gt;   这个类加载器由ExtClassLoader实现，负责加载&amp;lt;JAVA_HOME&amp;gt;/lib/ext目录中，或者被java.ext.dirs系统变量所指定的路径中所有的类库。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;应用程序类加载器:&lt;/strong&gt;&lt;br /&gt;   这个类加载器由AppClassLoader实现，负责加载用户类路径(classPath)上所有的类库，一般情况下，如果程序中没有自定义类加载器，AppClassLoader为应用程序的默认的类加载器。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;1.4 双亲委派机制&lt;/h4&gt; &lt;p&gt;  双亲委派模型的工作过程是：如果一个类加载器收到了类的加载请求，它首先不会自己去加载这个类，而是把这个请求委派给父类加载器去完成，每一个层次的类加载器都是如此，因此所有类的加载请求，最终都应该传送到最顶层的启动类加载器中，只有当父加载器反馈自己无法完成这个加载请求的时候(在搜索范围内搜索不到对应的类)，子加载器才会尝试自己去完成加载。&lt;br /&gt;   双亲委派机制的一个好处就是，它设置了类加载器的优先级，例如类java.lang.Object，它存在rt.jar包中，无论哪一个类加载器要加载这个类，最终都委派给处于模型最顶端的启动类加载器，这样就能确保程序里面的Object类在各种类加载器中都是同一个。以下代码为双亲委派机制的核心代码在loadClass方法里面。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;双亲委派机制的好处：&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;可以避免重复加载，当父类加载器已经加载到该类时，就没必要子加载器再加载一次。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;考虑到安全因素，JAVA核心api中定义类型不会被随意替换，假设通过网络传递一个名为java.lang.Object的类，通过双亲委派机制模型传递启动类加载器，而启动类加载器在核心java api中发现这个名称的类已经被加载过了，并不会重新加载网络传递过来的java.lang.Object，而直接返回已经加载过的Object，从而保证相同的类名的字节码文件，在虚拟机内存只保存一份。这样就可以避免核心api没恶意篡改，不过这个机制的前提是，这个传递过来的Object类加载请求必须是得到启动类加载器才可以，因为同一个类在同一个加载器里，只会被加载一次，但是如果是不同的类加载器的话，还是可以被加载的。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;二、自定义类加载器&lt;/h3&gt; &lt;p&gt;  在讲解热部署之前，需要先连接ClassLoader这个类里面几个比较核心的方法：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;findClass:&lt;/strong&gt; 在自定义类加载时，一般是覆盖这个方法，且ClassLoader中给出了一个抛错的实现。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    protected Class&amp;lt;?&amp;gt; findClass(String name) throws ClassNotFoundException {         throw new ClassNotFoundException(name);     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;loadClass:&lt;/strong&gt; 这个方法是双亲委派机制的代码实现，只有父类加载器加载不到类的时候，才会调用自身findClass方法进行类的查找，所以在定义自己的类加载器时，不要覆盖掉该方法，而是应该覆盖掉findClass方法。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;protected Class&amp;lt;?&amp;gt; loadClass(String name, boolean resolve)         throws ClassNotFoundException     {         synchronized (getClassLoadingLock(name)) {             // 首先检查类是否已经被加载过             Class&amp;lt;?&amp;gt; c = findLoadedClass(name);             if (c == null) {                 long t0 = System.nanoTime();                 try {   //查看是否有父加载器，没有的话默认使用启动类加载器                     if (parent != null) {                         c = parent.loadClass(name, false);                     } else {                         c = findBootstrapClassOrNull(name);                     }                 } catch (ClassNotFoundException e) {                     //父加载器抛ClassNotFoundException异常说明父加载器，加载不到                 }                  if (c == null) {                     // 父加载器无法加载时，调用本身的findClass方法来进行加载                     long t1 = System.nanoTime();                     c = findClass(name);                      // this is the defining class loader; record the stats                     sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);                     sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);                     sun.misc.PerfCounter.getFindClasses().increment();                 }             }             if (resolve) {                 resolveClass(c);             }             return c;         }     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;defineClass:&lt;/strong&gt; 用来将class字节解析成虚拟机能够识别的对象，defineClass方法一般和findClass一起结合使用，在自定义类加载器的时候，一般会覆盖ClassLoader的findClass方法，然后再调用defineClass方法生成Class对象。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    protected final Class&amp;lt;?&amp;gt; defineClass(String name, byte[] b, int off, int len,                                          ProtectionDomain protectionDomain)         throws ClassFormatError     {         protectionDomain = preDefineClass(name, protectionDomain);         String source = defineClassSourceLocation(protectionDomain);         Class&amp;lt;?&amp;gt; c = defineClass1(name, b, off, len, protectionDomain, source);         postDefineClass(c, protectionDomain);         return c;     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;resolveClass:&lt;/strong&gt; 连接指定的类。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;三、通过类加载器来实现热部署&lt;/h3&gt; &lt;p&gt;绕开双亲委派机制核心方法loadClass，直接调用自身重写的findClass方法&lt;/p&gt; &lt;h3&gt;四、JAVA模块化系统（JDK9）&lt;/h3&gt; &lt;p&gt;//TODO&lt;/p&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 08:34:59 GMT</pubDate>
    </item>
    <item>
      <title>深入理解JVM: (六)JMM与线程(1):JAVA内存模型</title>
      <link>http://123.57.101.202:8080/archives/shen-ru-li-jie-jvm-liu-jmmyu-xian-cheng-1javana-cun-mo-xing</link>
      <content:encoded>&lt;h3&gt;一、JAVA内存模型&lt;/h3&gt; &lt;h4&gt;概念：&lt;/h4&gt; &lt;p&gt;  “JAVA内存模型”(Java Memory Model 即:JMM)，定义了一套规则，用来屏蔽JAVA程序在不同平台下运行可能出现的硬件和操作系统的内存访问差异，达到内存一致的访问效果。JMM到JDK5之后才算是真正的完善起来。&lt;/p&gt; &lt;h4&gt;  1.1主内存与工作内存&lt;/h4&gt; &lt;p&gt;  JAVA内存模型的主要目的是定义程序中的各种变量的访问规则，即关注在虚拟机中变量存储到内存和从内存中取出变量值这样的底层细节。此处的变量包括实力字段、静态字段、和构成数组对象的元素。但是不包括局部变量与方法参数，因为后者是线程私有的，不会被共享，自然也就不会存在线程安全问题。&lt;br /&gt;   Java内存模型规定了所有变量都存储在主内存(虚拟机内存的一部分)。每条线程都还有自己的工作内存，线程中的内存保存了被该线程使用的变量的主内存副本，线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行，不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量，线程间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者之间交互关系如下： &lt;img src="httsp://www.fiveseven.fun/upload/20211114_11125668.png"&gt;   这里所讲的主内存、工作内存和JAVA内存区域中的堆、栈、方法区并不是同一个层次的内存的划分，两者基本上没有任何关系，如果勉强对应，那么变量、主内存、工作内存的定义上来看，主内存主要对应JAVA堆中的对象实例数据、静态变量部分，而工作内存则对应虚拟机栈中的部分区域。&lt;/p&gt; &lt;h4&gt;  1.2内存间交互操作&lt;/h4&gt; &lt;p&gt;  关于主内存和工作内存之间具体的交互协议，即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存这一类的实现细节，JAVA内存模型通过定义了8个原子操作来完成。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;lock(锁定)：作用于主内存，它把一个变量标记为一条线程独占状态；&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;&lt;strong&gt;unlock(解锁)：作用于主内存，它将一个处于锁定状态的变量释放出来，释放后的变量才能够被其他线程锁定；&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;&lt;strong&gt;read(读取)：作用于主内存，它把变量值从主内存传送到线程的工作内存中，以便随后的load动作使用；&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;&lt;strong&gt;load(载入)：作用于工作内存，它把read操作的值放入工作内存中的变量副本中；&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;&lt;strong&gt;use(使用)：作用于工作内存，它把工作内存中的值传递给执行引擎，每当虚拟机遇到一个需要使用这个变量的指令时候，将会执行这个动作；&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;&lt;strong&gt;assign(赋值)：作用于工作内存，它把从执行引擎获取的值赋值给工作内存中的变量，每当虚拟机遇到一个给变量赋值的指令时候，执行该操作；&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;&lt;strong&gt;store(存储)：作用于工作内存，它把工作内存中的一个变量传送给主内存中，以备随后的write操作使用；&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;&lt;strong&gt;write(写入)：作用于主内存，它把store传送值放到主内存中的变量中。&lt;/strong&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;  如果要把一个变量从主内存拷贝到工作内存，那就要按顺序执行read和load操作，如果要把变量从工作内存同步到主内存，就要按顺序执行store和write操作。JAVA内存模型只要求上述两个操作必须按顺序执行，但是不要求是连续执行，中间也可能穿插了其他得原子操作。&lt;/p&gt; &lt;p&gt;  除此之外，JAVA内存模型还规定了执行上述8种原子操作时，必须满足以下几种规则：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;不允许read和load、store和write操作之一单独出现（即不允许一个变量从主存读取了但是工作内存不接受，或者从工作内存发起会写了但是主存不接受的情况），以上两个操作必须按顺序执行，但没有保证必须连续执行，也就是说，read与load之间、store与write之间是可插入其他指令的。&lt;/li&gt; &lt;li&gt;不允许一个线程丢弃它的最近的assign操作，即变量在工作内存中改变了之后必须把该变化同步回主内存。&lt;/li&gt; &lt;li&gt;不允许一个线程无原因地（没有发生过任何assign操作）把数据从线程的工作内存同步回主内存中。&lt;/li&gt; &lt;li&gt;一个新的变量只能从主内存中“诞生”，不允许在工作内存中直接使用一个未被初始化（load或assign）的变量，换句话说就是对一个变量实施use和store操作之前，必须先执行过了assign和load操作。&lt;/li&gt; &lt;li&gt;一个变量在同一个时刻只允许一条线程对其执行lock操作，但lock操作可以被同一个条线程重复执行多次，多次执行lock后，只有执行相同次数的unlock操作，变量才会被解锁。&lt;/li&gt; &lt;li&gt;如果对一个变量执行lock操作，将会清空工作内存中此变量的值，在执行引擎使用这个变量前，需要重新执行load或assign操作初始化变量的值。&lt;/li&gt; &lt;li&gt;如果一个变量实现没有被lock操作锁定，则不允许对它执行unlock操作，也不允许去unlock一个被其他线程锁定的变量。&lt;/li&gt; &lt;li&gt;对一个变量执行unlock操作之前，必须先把此变量同步回主内存（执行store和write操作）。&lt;/li&gt; &lt;/ol&gt; &lt;h4&gt;  1.3原子性、可见性、有序性&lt;/h4&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;原子性：&lt;/strong&gt;&lt;br /&gt;   是指一个操作或多个操作要么全部执行，且执行的过程不会被任何因素打断，要么就都不执行。&lt;br /&gt;   首先需要说明，处理器会自动保证基本的内存操作是原子性的。处理器保证从系统内存中读取或写入一个字节是原子的。意思是，当一个处理器读取一个字节时，其他处理器不能访问这个字节的内存地址。&lt;br /&gt;   而JAVA内存模型直接保证原子性变量操作包括&lt;strong&gt;read、load、assign、use、store、write&lt;/strong&gt;这六个，我们可以大致认为，基本数量类型的访问读写都是具备原子性的，如果应用场景需要一个更大范围的原子性保证，JAVA内存模型还提供了，&lt;strong&gt;lock、unlock&lt;/strong&gt;操作来满足这种需求，通过字节码指令monitorenter和monitorexit来隐式使用这两个操作，这两个字节码指令反映到JAVA代码中就是同步代码块--sysnchronized关键字，因此synchronized块之间的操作也具备原子性。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;可见性：&lt;/strong&gt;&lt;br /&gt;   可见性就是指当一个线程修改了共享变量的值时，其他线程能够立即得知这个修改。JAVA内存模型是通过在变量修改后将新值同步回主内存，在变量读取前从主内存刷新变量值这种通过依赖主内存作为传递媒介的方式来实现可见性的，这点对于无论式普通变量或者是volatile修饰过的变量都是一样。但是普通变量和volatile变量的区别是，volatile的特殊规则，能使变量被修改后立即同步到主内存（即volatile变量的store和write的原子操作必须是连续且一起出现的），以及每次使用前从主内存刷新取值(即每次执行use的原子操作，都必须刷新工作内存中的变量值)。因此我们可以说volatile保证了多线程操作时的可见性。   除了volatile能够保证可见性外，synchronized和final关键字也能保证可见性，synchronized可见性是由“对一个变量执行unlock之前，必须先把此变量同步到主内存中(store、write)”这条规则获得的。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;有序性：&lt;/strong&gt;&lt;br /&gt;   在了解有序性前，先来了解一下什么是重排序，重排序是对内存访问操作的一种优化，他可以在不影响单线程程序正确性的前提下进行一定的调整，进而提高程序的性能，但是对于多线程场景下，就可能产生一定的问题，但是重排序导致的问题，不是必然出现的。重排序表现在两种形式：1.编译器编译后，程序顺序和源代码顺序不一致。2.处理器执行顺序和程序不一致。&lt;br /&gt;   那么什么时候需要禁止指令的重排序呢？当存在多个线程同时访问一块内存时，并且重排序可能会导致逻辑错误的时候，需要禁止部分代码重排序。如以下代码所示：&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class DemoConfig {     private int count = 1;     private boolean flag = false;     private volatile boolean sync = false;      public void write1(){         count = 10;         flag = true;//没有volatile修饰，实际执行顺序可能是flag = true; 先执行     }      public void read1(){         if(flag){             //有些JVM打印1，有些JVM打印10，不确定             System.out.println(count);         }     }      public void write2(){         count = 11;         sync = true;//volatile修饰，禁止指令重排序，前面的指令不会跑到后面去，即count=11;一定会比sync=true;先执行     }      public void read2(){         if(sync){             //只打印11             System.out.println(count);         }     }      public static void main(String[] args) {         DemoConfig config = new DemoConfig();         new Thread(() -&amp;gt; config.write1()).start();         new Thread(() -&amp;gt; config.read1()).start();         new Thread(() -&amp;gt; config.write2()).start();         new Thread(() -&amp;gt; config.read2()).start();     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;JAVA语言提供了，volatile和synchronized两个关键字来保证线程之间操作的有序性。volatile关键字本身就包含了禁止指令重排序的语义，使用volatile关键字，它不允许后面的指令重排序到volatile之前，也不允许前面的指令重排序到volatile之后，这意味着volatille之前的指令操作都已经执行完成，这样便形成了“指令重排序无法越过内存屏障”的效果。而synchronized则是由“一个变量同时一个时刻只允许一条线程对其进行lock操作”这条规则来实现线程间的有序性，因为在这个规则下，线程对对象的访问是串行访问的，如以下代码所示：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class DemoSync {      private int count = 1;     private boolean flag = false;          public synchronized void write1(){         count = 10;         flag = true;     }      public synchronized void read1(){         if(flag){             System.out.println(count);         }     }      public static void main(String[] args) {         //通过设置synchronized 关键字，锁住整个方法，这样即便是count =10与flag=true 进行了重排序，         //也不会影响线程间操作的有序性，因为经过synchronized修饰后，线程只能串行访问         new Thread(()-&amp;gt;new DemoSync().write1());         new Thread(()-&amp;gt;new DemoSync().read1());     } } &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;  1.4对于volatile型变量的特殊规则&lt;/h4&gt; &lt;p&gt;  关键字volatile是JAVA虚拟机提供的最轻量级的同步机制，当一个变量被定义成volatile之后，它将具备两个特性：&lt;br /&gt;   &lt;strong&gt;第一项是保证此变量对所有线程的可见性，&lt;/strong&gt; 可见性是指当一条线程修改了这个变量的值，新值对于其他线程是来说是可以立即得知的，即：X=1，线程A和线程B先后读取X，此时在线程A和B的工作内存里面读取的X=1，然后A线程原子性操作，先将X=1修改为2，虽然此时B工作内存的依然为1，但是假如X是经过volatile关键字修饰过的话，B线程在使用(use)X值的时候，会去刷新X值，从主内存获取最新的值，而对于没有经过volatile关键字修饰的变量，那么就无法感知到X是否被修改过了，依旧为1。&lt;br /&gt;   对于普通变量而言，普通变量的值在线程间传递时均需要通过主内存来完成。比如，线程A修改一个普通变量的值，然后向主内存回写，另外一条线程B在线程A回写完成后再对主内存进行读取的操作，新变量值才会对B线程可见。&lt;br /&gt;   但是可见性并不能保证线程一定是安全的，因为volatile修饰的变量只保证了获取值(read、load、use)或者修改值(assign、store、write)的原子性，即经过volatile修饰过后的变量原子性从最小的原子操作，上升到了3个原子操作，但是并不能保证获取值和修改值这两个操作是在同一个原子里面(即非原子性操作)，导致当出现基于volatile变量的运算在并发的情况下，是线程不安全的。&lt;br /&gt;   由于volatile变量只能保证可见性，在不符合以下两个规则的运算场景中，我们仍然要通过加锁来保证原子性：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;运算结果并不依赖变量的当前值，或者能够确保只有单一的线程修改变量的值。&lt;/li&gt; &lt;li&gt;变量不需要与其他的状态变量共同参与不变约束。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;  &lt;strong&gt;第二项是语义禁止指令重排序，&lt;/strong&gt; 普通的变量仅保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果，而不能保证赋值操作的顺序与程序代码的执行顺序一样。而经过volatile修饰的变量，不允许后面的指令重排序到volatile之前，也不允许前面的指令重排序到volatile之后。&lt;/p&gt; &lt;p&gt;  以下为JAVA内存模型对volatile变量定义的特殊规则，定义变量V、W被volatile修饰，线程T会操作变量V和W，那么现在进行read、load、use、assign、store、write操作时需要满足以下规则：&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;只有当线程T 对变量V 执行的前一个动作是load ,线程T 方能对变量V 执行use;并且，只有当线程T 对变量V 执行的后一个动作是use,线程T才能对变量V执行load.线程T 对变量V 的use可认为是和线程T对变量V的load,read相关联，必须连续一起出现 &lt;strong&gt;(这条规则要求在工作内存中，每次使用V前都必须先从主内存刷新最新的值语,用于保证能看见其他线程对变量V所做的修改后的值)&lt;/strong&gt;&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;只有当线程T 对变量V 执行的前一个动作是  assign ，线程T才能对变量V 执行store,并且，只有当线程T对变量V执行的后一个动作是store ,线程T才能对变量V执行assign，线程T对变量V的assign可以认为是和线程T对变量V的store,write相关联，必须连续一起出现 &lt;strong&gt;(这条规则要求在工作内存中，每次修改V 后都必须立刻同步回主内存中，用于保证其他线程可以看到自己对变量V所做的修改)&lt;/strong&gt;&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;假定动作A 是线程T 对变量V实施的use或assign,假定动作F 是和动作A 相关联的load或store,假定动作P 是和动作F 相应的对变量V 的read 或write,类似的，假定动作B 是线程T 对变量W 实施的use或assign 动作，假定动作G是和动作B 相关联的load或store,假定动作Q 是和动作G 相应的对变量W的read或write，如果A 先于B，那么P先于Q &lt;strong&gt;(这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)&lt;/strong&gt;&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;1.5先行发生原则&lt;/h4&gt; &lt;h5&gt;概念:&lt;/h5&gt; &lt;p&gt;  先行发生原则(Happens-Before)是用来判断数据是否存在竞争，线程是否安全的非常重要手段，是用来定义JAVA内存模型中两项操作之间的偏序关系。举个例子来说明下什么是先行发生原则：&lt;br /&gt;   操作A先行发生于操作B，那么在B操作发生之前，A操作产生的“影响”都会被操作B感知到。这里的影响是指修改了内存中的共享变量、发送了消息、调用了方法等。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;//以下操作在线程A执行 i = 1; //以下操作在线程B执行 j = i; //以下操作在线程C执行 i = 2; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;  假设线程A中的操作“i=1”先行发生于线程B的操作“j=i”，那么可以确定在线程B的操作执行后，变量j的值一定等于1，得出这个结论的依据有两个：一是根据先行发生原则，“i=1”的结果可以被观察到；二是线程C还没“登场”，线程A操作结束之后没有其他线程会修改变量i的值。现在再来考虑线程C，我们依然保持线程A和线程B之间的先行发生关系，而线程C出现在线程A和线程B的操作之间，但是线程C与线程B没有先行发生关系，那j的值会是多少呢？答案是不确定！1和2都有可能，因为线程C对变量i的影响可能会被线程B观察到，也可能不会，这时候线程B就存在读取到过期数据的风险，不具备多线程安全性。&lt;/p&gt; &lt;p&gt;  下面是Java内存模型下一些“天然的”先行发生关系，这些先行发生关系无须任何同步器协助就已经存在，可以在编码中直接使用。如果两个操作之间的关系不在此列，并且无法从下列规则推导出来的话，它们就没有顺序性保障，虚拟机可以对它们随意地进行重排序：&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;程序次序规则（Program Order Rule）：&lt;strong&gt;在一个线程内&lt;/strong&gt; ，按照程序代码顺序，书写在前面的操作先行发生于书写在后面的操作。准确地说，应该是控制流顺序而不是程序代码顺序，因为要考虑分支、循环等结构。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;管程锁定规则（Monitor Lock Rule）：&lt;strong&gt;一个unlock操作&lt;/strong&gt; 先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁，而“后面”是指时间上的先后顺序。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;volatile变量规则（Volatile Variable Rule）：对一个volatile变量的写操作先行发生于后面对这个变量的读操作，这里的“后面”同样是指时间上的先后顺序。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;线程启动规则（Thread Start Rule）：Thread对象的start()方法先行发生于此线程的每一个动作。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;线程终止规则（Thread Termination Rule）：线程中的所有操作都先行发生于对此线程的终止检测，我们可以通过Thread.join()方法结束、Thread.isAlive（）的返回值等手段检测到线程已经终止执行。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;线程中断规则（Thread Interruption Rule）：对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生，可以通过Thread.interrupted()方法检测到是否有中断发生。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;对象终结规则（Finalizer Rule）：一个对象的初始化完成（构造函数执行结束）先行发生于它的finalize()方法的开始。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;传递性（Transitivity）：如果操作A先行发生于操作B，操作B先行发生于操作C，那就可以得出操作A先行发生于操作C的结论。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 08:32:57 GMT</pubDate>
    </item>
    <item>
      <title>深入理解JVM: (六)JMM与线程(2):JAVA与线程</title>
      <link>http://123.57.101.202:8080/archives/shen-ru-li-jie-jvm-liu-jmmyu-xian-cheng-2javayu-xian-cheng</link>
      <content:encoded>&lt;h4&gt;线程&lt;/h4&gt; &lt;h5&gt; 概念&lt;/h5&gt; &lt;p&gt;  线程是比进程更轻量级的调度单位，是JAVA里面进行处理器资源调度的最基本单位，实现线程主要有三种方式：&lt;strong&gt;使用内核线程实现(1:1实现)、使用用户线程实现(1:N实现)、使用用户线程加轻量级进程混合实现(N:M实现)。&lt;/strong&gt;&lt;/p&gt; &lt;h5&gt; 一、线程实现：&lt;/h5&gt; &lt;h5&gt;  1.1 内核线程实现&lt;/h5&gt; &lt;p&gt;  内核线程（Kernel-Level Thread,KLT）就是直接由操作系统内核支持的线程，这种线程由 &lt;strong&gt;内核来完成线程切换&lt;/strong&gt; ，内核通过操纵 &lt;strong&gt;调度器&lt;/strong&gt; 对线程进行调度，并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身，这种操作系统就有能力同时处理多件事情，支持多线程的内核就叫做多线程内核。&lt;br /&gt;   程序一般不会直接去使用内核线程，而是去使用内核线程的一种高级接口：轻量级进程（Light Weight Process ，LWP），轻量级进程就是我们通常意义上所讲的线程，由于每个轻量级进程都由一个内核线程支持，因此只有先支持内核线程，才能有轻量级进程。这种轻量级进程与内核线程之间1：1的关系成为一对一的线程模型。&lt;br /&gt;    由于内核线程的支持，每个轻量级进程都成为一个独立的调度单元，即使有一个轻量级进程在系统调用中阻塞了，也不会影响整个进程继续工作，但是轻量级进程具有它的局限性：&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;首先，由于是基于内核线程实现的，所以各种线程操作，如创建，析构和同步，都需要进行系统调用。而系统调用的代价相对较高，需要在用户态和内核态中来回切换。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;其次，每个轻量级进程都需要有一个内核线程的支持，因此轻量级进程要消耗一定的内核资源（如内核线程的栈空间），因此一个系统支持轻量级进程的数量是有限的。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;img src="https://www.fiveseven.fun/upload/20211120_19292912.png"&gt; #####   1.2 用户线程实现     从广义上讲，一个线程只要不是内核线程，就可以认为是用户线程（User Thread，UT），因此，从这个定义上来讲， **轻量级进程也属于用户线程** ，但轻量级进程的实现始终是建立在内核之上的，许多操作都要进行系统调用，效率会受到限制。     而狭义上的 **用户线程指的是完全建立在用户空间的线程库上，系统内核不能感知线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成，不需要内核的帮助。** 如果程序实现得当，这种线程不需要切换到内核态，因此操作可以是非常快速且低消耗的，也可以支持规模更大的线程数量，部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间1:N的关系称为一对多的线程模型。 &lt;img src="https://www.fiveseven.fun/upload/20211120_19463947.png"&gt; #####   1.3 混合实现   &lt;img src="https://www.fiveseven.fun/upload/20211120_20140371.png"&gt; #####  二、JAVA线程实现： #####   2.1 实现    //TODO #####   2.2 调度 //TODO</content:encoded>
      <pubDate>Thu, 09 Apr 2026 08:31:28 GMT</pubDate>
    </item>
    <item>
      <title>深入理解JVM: (八)锁优化(完结)</title>
      <link>http://123.57.101.202:8080/archives/shen-ru-li-jie-jvm-ba-suo-you-hua-wan-jie</link>
      <content:encoded>&lt;h3&gt;一、锁优化&lt;/h3&gt; &lt;h4&gt; 概念&lt;/h4&gt; &lt;h4&gt; 1.1自旋锁&lt;/h4&gt; &lt;p&gt;  互斥同步对性能最大的影响是阻塞的实现，挂起线程和恢复线程的操作都不可避免地需要在用户态和内核态进行切换，这些操作给JAVA虚拟地并发性能带来很大压力。一般情况下，共享数据的锁定状态只会持续很短的一段时间，为了这段时间去挂起和恢复线程并不值得。在多核处理器并行的情况下，我们可以让请求锁的那个线程稍微等待一下，不放弃处理器的执行时间，看看持有锁的线程是否很快就会释放。为了让线程等待，我们只需要让线程执行忙循环(自旋)，这就是所谓的自旋锁。&lt;/p&gt; &lt;h4&gt; 1.2自适应自旋锁&lt;/h4&gt; &lt;p&gt;  JDK1.6以后自旋锁默认开启，自旋锁等待虽然可以避免线程切换带来的开销，但是要占用处理器的执行时间，如果占用时间很短，那么自旋等待的效果会很好，反之如果锁占用时间很长，那么自旋的线程只会白白消耗处理器资源，自旋锁默认超过10次如果还没获取到锁，那么便会挂起线程，可以通过 -XX:PreBlockSpin 参数设置自旋的次数。JDK1.6后对自旋锁进行了优化，可以根据上次自旋获取到锁的概率，来决定这次自旋的次数，或者是否有必要进行自旋。有了自适应自旋，随着程序运行时间的增长，及性能监控信息的不断完善，虚拟机对程序锁的状况预测就会越来越精准。&lt;/p&gt; &lt;h4&gt; 1.3锁消除&lt;/h4&gt; &lt;p&gt;  锁消除是指虚拟机即时编译器在运行时，对一些代码要求同步，但是对被检测到不可能存在共享数据竞争的锁进行消除。如果判断到一段代码中，在堆上所有的数据都不会逃逸出去被其他线程锁访问到，那就可以把它们当成栈上的数据对待，认为它们是线程私有的，同步枷锁自然也无须再进行，因此在经过服务端编译器的即时编译后，这段代码就会忽略所有的同步措施而直接执行。如一下代码:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public void metod(){         DemoX demoX = new DemoX();         synchronized (demoX){             //.....         }     } &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt; 1.4锁粗化(没复现，有待验证)&lt;/h4&gt; &lt;p&gt;  一般写代码的时候，尽量只把同步代码块的作用范围缩小到共享数据的实际作用域，这样是为了使得同步的操作尽可能变少，即使存在锁竞争，等待锁的线程也能尽快地释放和获取。&lt;br /&gt;   但是如果一系列的连续操作都是对同一个对象反复加锁解锁，甚至加锁操作是出现在一个循环体中，那即使没有线程竞争，频繁地进行互斥同步操作也会导致不必要地性能损耗。&lt;br /&gt;   对于这种类似的情况，虚拟机会把加锁同步的范围扩展到整个操作序列的外部，如一下代码 &lt;strong&gt;(理论上是这样，但是实际操作并没有被粗化)&lt;/strong&gt;：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;//锁粗化前     public void method(String str){         for (int i = 0; i &amp;lt; 10; i++) {             synchronized (&amp;quot;lock&amp;quot;){                 System.out.println(str);             }         }     } &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-java"&gt;//锁粗化后     public void method(String str) {         synchronized (&amp;quot;lock&amp;quot;) {             for (int i = 0; i &amp;lt; 10; i++) {                 System.out.println(str);             }         }     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;PS:对于锁的粗化，目前只在StringBuffer验证出来过，至于自己写的demo，通过打印出来的结果来看，并没有锁粗化，目前还未找到具体的问题，猜测锁的粗化并不作用于所有的类。&lt;/strong&gt; &lt;img src="https://www.fiveseven.fun/upload/20211226_1644255.png"&gt;&lt;/p&gt; &lt;h4&gt; 1.5轻量级锁&lt;/h4&gt; &lt;p&gt;   &lt;strong&gt;锁采用 CAS 操作，将锁对象的标记字段替换为一个指针，指向当前线程栈上的一块空间，存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。&lt;/strong&gt;&lt;br /&gt;   当锁是偏向锁的时候，被另外的线程所访问，偏向锁就会升级为轻量级锁，其他线程会通过自旋的形式尝试获取锁，不会阻塞，从而提高性能。&lt;br /&gt; 当锁是偏向锁的时候，被另外的线程所访问，偏向锁就会升级为轻量级锁，其他线程会通过自旋的形式尝试获取锁，不会阻塞，从而提高性能。&lt;br /&gt;   在代码进入同步块的时候，如果同步对象锁状态为无锁状态（锁标志位为“01”状态，是否为偏向锁为“0”），虚拟机首先将在当前线程的栈帧中建立一个名为锁记录（Lock Record）的空间，用于存储锁对象目前的Mark Word的拷贝，然后拷贝对象头中的Mark Word复制到锁记录中。&lt;br /&gt;   拷贝成功后，虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针，并将Lock Record里的owner指针指向对象的Mark Word。&lt;br /&gt;   如果这个更新动作成功了，那么这个线程就拥有了该对象的锁，并且对象Mark Word的锁标志位设置为“00”，表示此对象处于轻量级锁定状态。&lt;br /&gt;   如果轻量级锁的更新操作失败了，虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧，如果是就说明当前线程已经拥有了这个对象的锁，那就可以直接进入同步块继续执行，否则说明多个线程竞争锁。&lt;br /&gt;   若当前只有一个等待线程，则该线程通过自旋进行等待。但是当自旋超过一定的次数，或者一个线程在持有锁，一个在自旋，又有第三个来访时，轻量级锁升级为重量级锁。&lt;br /&gt;   多个线程在不同的时间段请求同一把锁，也就是说没有锁竞争。针对这种情形，Java 虚拟机采用了轻量级锁，来避免重量级锁的阻塞以及唤醒。&lt;/p&gt; &lt;h4&gt; 1.6偏向锁&lt;/h4&gt; &lt;p&gt;   &lt;strong&gt;只会在第一次请求时采用 CAS 操作，在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中，持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。&lt;/strong&gt;&lt;br /&gt; 偏向锁是指一段同步代码一直被一个线程所访问，那么该线程会自动获取锁，降低获取锁的代价。&lt;br /&gt;   在大多数情况下，锁总是由同一线程多次获得，不存在多线程竞争，所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。&lt;br /&gt;   当一个线程访问同步代码块并获取锁时，会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁，而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径，因为轻量级锁的获取及释放依赖多次CAS原子指令，而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。&lt;br /&gt;   偏向锁只有遇到其他线程尝试竞争偏向锁时，持有偏向锁的线程才会释放锁，线程不会主动释放偏向锁。&lt;br /&gt;   偏向锁在JDK 6及以后的JVM里是默认启用的。一般偏向锁适用于同步但无竞争的程序，如果程序中大多数的锁都总是被多个不同的线程访问，那么偏向锁模式就是多余的，可以通过JVM参数关闭偏向锁：-XX:-UseBiasedLocking=false，关闭之后程序默认会进入轻量级锁状态。&lt;/p&gt; &lt;h4&gt; 1.7重量级锁&lt;/h4&gt; &lt;p&gt;   &lt;strong&gt;锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。Java 虚拟机采取了自适应自旋，来避免线程在面对非常小的 synchronized 代码块时，仍会被阻塞、唤醒的情况。&lt;/strong&gt;&lt;/p&gt; &lt;h4&gt; 2 锁膨胀的过程:&lt;/h4&gt; &lt;img src="https://fiveseven.fun/upload/20211226_16395979.png"&gt; &lt;h2&gt;参考文献&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;[1] &lt;a href="https://www.cnblogs.com/yuhangwang/p/11295940.html"&gt;java并发笔记四之synchronized 锁的膨胀过程（锁的升级过程）深入剖析&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 08:29:38 GMT</pubDate>
    </item>
    <item>
      <title>记录一个非常低级的bug</title>
      <link>http://123.57.101.202:8080/archives/ji-lu-yi-ge-fei-chang-di-ji-de-bu</link>
      <content:encoded>&lt;p&gt;凌晨接到测试的一个电话，说灰度上有我一个bug，我立马打开电脑，查看线上灰度日志，居然是一个空指针，最近在导出加了一个需求，之前提测的时候，导出也报过一个空指针的异常，但是在测试环境已经处理过了呀？难道我的代码没有上到灰度环境？ &amp;lt;img src=&amp;quot;https://www.fiveseven.fun/upload/20220305_02242367.png:&amp;gt; 接着连上灰度环境，通过arthas jad反编译我的代码，发现我的代码是有上到灰度环境的，看着代码是50行报的错误，肯定是我的代码有问题没跑了。 &lt;img src="https://www.fiveseven.fun/upload/20220305_02281670.png"&gt; 以下是写的源码，其实就是一个简简单单的枚举类和取对应类型的方法，但是错就错在，在遍历自身枚举的时候，忘记对枚举类的属性做判空了，这是一个非常低级的错误。哎，以后的路还有很长要走.... &lt;img src="https://www.fiveseven.fun/upload/20220305_02314161.png"&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 08:23:40 GMT</pubDate>
    </item>
    <item>
      <title>手写分布式事务(2PC)</title>
      <link>http://123.57.101.202:8080/archives/shou-xie-fen-bu-shi-shi-wu-2p</link>
      <content:encoded>&lt;h1&gt;tx-distribute&lt;/h1&gt; &lt;p&gt;项目地址: https://gitee.com/back1968/tx-distribute&lt;/p&gt; &lt;h4&gt;介绍&lt;/h4&gt; &lt;p&gt;为了更好的了解分布式事务底层的原理，参考了别人造的轮子，整理了思路，自己也造了个轮子 倒不是说特意去造轮子，而是只有自己在参与并敲击的过程中，才能对分布式事务有更深入的理解 出于学习的目的，造的轮子是很粗糙的，从而也感知到了，实际生产中，分布式事务需要考虑的点是非常多的&lt;/p&gt; &lt;h4&gt;感悟&lt;/h4&gt; &lt;p&gt;这次手撸的分布式事务主要涉及到这几个主键：&lt;br /&gt; TC：事务协调器(独立服务部署)&lt;br /&gt; TM：事务管理器(嵌入到具体应用服务)&lt;/p&gt; &lt;p&gt;TM嵌入具体应用服务，通过切面获取到数据库connection的控制权，并且在提交事务之前将本地事务情况汇报给远程TC， 然后重开一条线程进入等待状态，等待远程TC唤醒线程。 TC通过全局事务id进行分组，当最后一个事务提交汇报后， 判断该分组下的所有事务状态是否都是成功，如果有其中一个失败，那么便下发全部回滚的状态，应用服务通过监听，接收 到回滚状态，唤醒事务线程执行回滚。&lt;/p&gt; &lt;p&gt;在这过程中有几个点需要注意：&lt;/p&gt; &lt;p&gt;1.如何判断所有事务都已经提交，这里的处理方式是，人为自己定义哪个微服务接口是最后一个提交的事务，这样在实际开发中 肯定是行不通的。&lt;/p&gt; &lt;p&gt;2.如果TC挂了话怎么办？ 如果TC挂了，服务应用得不到唤醒，那么所有事务都将阻塞得不到执行，当然是可以通过设置失效时间， 但是这样也是有风险的，高并发的情况下，事务都阻塞几秒，直接影响到整个数据库的吞吐量，IOPS吃满整个服务会变得巨卡， 这个在生产环境也不是没有遇到过，最后处理的方式是杀死线程，但是这样又可能产生脏数据&lt;/p&gt; &lt;p&gt;3.如果某个业务链很长，分支很杂，通过注解方式实现的分布式事务，其实是很被动的，因为这样会导致整个事务非常大并且不可控制， 其实可以考虑通过面向编程的方式，将事务的颗粒度细腻到具体的执行sql，根据具体的业务，来判断是否需要独立执行职务， 因为有些特殊的业务，它是不需要回滚的&lt;/p&gt; &lt;p&gt;综上：2PC 是一种尽量保证强一致性的分布式事务，因此它是同步阻塞的，而同步阻塞就导致长久的资源锁定问题，总体而言效率低，并且存在单点故障问题，在极端条件下存在数据不一致的风险。&lt;/p&gt; &lt;h4&gt;流程图&lt;/h4&gt; &lt;img src="https://www.fiveseven.fun/upload/20220320_17220947.png"&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 08:19:00 GMT</pubDate>
    </item>
    <item>
      <title>浅析工作中使用的分布式事务</title>
      <link>http://123.57.101.202:8080/archives/qian-xi-gong-zuo-zhong-shi-yong-de-fen-bu-shi-shi-wu</link>
      <content:encoded>&lt;h3&gt;前言：&lt;/h3&gt; &lt;p&gt;  在Spring中，事务有两种实现方式：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;编程式事务管理&lt;/strong&gt;：编程式事务管理使用底层源码可实现更细粒度的事务控制。即事务只攘括需要执行sql的那部分逻辑，这样做的优点是，在业务复杂的情况下，可以避免事务过于庞大，控制事务的自由度会更高。&lt;/li&gt; &lt;li&gt;&lt;strong&gt;申明式事务管理&lt;/strong&gt;：添加@Transactional注解，并定义传播机制+回滚策略。基于Spring AOP实现，本质式对方法前后进行拦截，然后目标方法开始之前创建或者加入一个事务，在执行完目标方法之后根据执行情况进行提交或者回滚事务。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;为什么采用编程事务管理？&lt;/h3&gt; &lt;p&gt;  采用什么技术偏向取决于当下及其未来业务发展的趋势，电商erp业务庞大且复杂，需要更加细粒度的事务管理机制，加上现在微服务采取的都是分布式，为了能够和自研的分布式事务框架做更好的融合，编程式事务管理是最佳的选择。&lt;/p&gt; &lt;p&gt;//TODO&lt;/p&gt; &lt;h3&gt;如何实现编程式事务管理？&lt;/h3&gt; &lt;p&gt;1.使用MethodInvocation 获取目标函数 2.继承AbstractPlatformTransactionManager 重写方法&lt;/p&gt; &lt;h3&gt;Spring如何管理事务&lt;/h3&gt; &lt;h4&gt;一、注解配置：&lt;/h4&gt; &lt;p&gt;1.定义事务管理器 2.注册事务驱动，将事务托管与Spring管理&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Configuration public class Config implements TransactionManagementConfigurer {      /**      * mybatis 自行配置生成的数据源      */     @Autowired     private DataSource dataSource;      /**      * 注册事务驱动，将事务管理器编入Spring事务使用      * @return      */     public TransactionManager annotationDrivenTransactionManager() {         System.out.println(&amp;quot;设置事务驱动&amp;quot;);         return transactionManager();     }      /**      * 自定义定义事务管理器，本质上是继承DataSourceTransactionManager      * @return      */     @Bean     public PlatformTransactionManager transactionManager() {         System.out.println(&amp;quot;加载事务管理器&amp;quot;);         return new TarsTransactionalManager(dataSource);     } } &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;二、事务关键类：&lt;/h4&gt; &lt;p&gt;&lt;strong&gt;1.TransactionManager&lt;/strong&gt;&lt;br /&gt; 事务管理器顶层基类，没有定义任务接口，纯粹用在驱动注入事务管理器的时候声明指定类型&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Configuration public abstract class AbstractTransactionManagementConfiguration implements ImportAware {   //声明事务管理器  protected TransactionManager txManager;   @Autowired(required = false)  void setConfigurers(Collection&amp;lt;TransactionManagementConfigurer&amp;gt; configurers) { //注入事务管理器 configurer.annotationDrivenTransactionManager();  } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;2.PlatformTransactionManager&lt;/strong&gt;&lt;br /&gt; 定义了三个抽象方法，1.获取事务；2.提交事务；回滚事务&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface PlatformTransactionManager extends TransactionManager {   TransactionStatus getTransaction(@Nullable TransactionDefinition definition)    throws TransactionException;   void commit(TransactionStatus status) throws TransactionException;   void rollback(TransactionStatus status) throws TransactionException;  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;3.AbstractPlatformTransactionManager&lt;/strong&gt;&lt;br /&gt; 抽象类，实现了PlatformTransactionManager接口，并定义了大量关于事务的方法，包括事务的提交、回滚、挂起、恢复、超时时间设置等等，并提供了四个抽象方法供子类实现，&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    protected Object doGetTransaction() throws TransactionException;      protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException;      protected void doCommit(DefaultTransactionStatus status) throws TransactionException;      protected void doRollback(DefaultTransactionStatus status) throws TransactionException; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;4.DataSourceTransactionManager&lt;/strong&gt; 继承AbstractPlatformTransactionManager，并实现了其父类的抽象方法，真正实现获取事务的连接、事务的提交、事务的回滚&lt;/p&gt; &lt;p&gt;&lt;strong&gt;5.TransactionInterceptor&lt;/strong&gt; 开启注解@Transactional扫描后，对标记有Transactional方法的增强，TransactionInterceptor的顶层接口为MethodInterceptor，需要额外了解MethodInterceptor的作用&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt; public Object invoke(MethodInvocation invocation) throws Throwable { //实际增加方法写在父类TransactionAspectSupport里面  invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;6.TransactionAspectSupport&lt;/strong&gt; TransactionInterceptor的父类，实际对标有Transactional进行方法增强的类&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt; protected Object invokeWithinTransaction(Method method, @Nullable Class&amp;lt;?&amp;gt; targetClass,  final InvocationCallback invocation) throws Throwable {  //对标注有@Transactional注解方法的增强  ......  } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;自研分布式事务&lt;/h3&gt; &lt;h4&gt;采用技术&lt;/h4&gt; &lt;h4&gt;流程图&lt;/h4&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 08:16:30 GMT</pubDate>
    </item>
    <item>
      <title>金额计算中如何处理浮点类型的精度问题</title>
      <link>http://123.57.101.202:8080/archives/jin-e-ji-suan-zhong-ru-he-chu-li-fu-dian-lei-xing-de-jing-du-wen-ti</link>
      <content:encoded>&lt;p&gt;  在大多数的商业计算中，一般采用java.math.BigDecimal 类来进行精确计算。&lt;br /&gt;   使用步骤:&lt;br /&gt;   1.用 float 或者 double 变量构建 BigDecimal 对象。通常使用 BigDecimal 的构造方法或者静态方法的 valueOf() 方法把基本类型的变量构建成 BigDecimal 对象。&lt;br /&gt;   2.通过调用 BigDecimal 的加，减，乘，除等相应的方法进行算术运算。&lt;br /&gt;   3.把 BigDecimal 对象转换成 float，double，int 等类型。&lt;/p&gt; &lt;p&gt;  由于我们的计算机是二进制的。浮点数没有办法使用二进制进行精确表示十进制的位数。(二进制系统中无法精确的表示分数1/10),这就好比十进制无法精确地表示分数1/3一样。&lt;/p&gt; &lt;p&gt;  解决措施：&lt;br /&gt; 采用java.math.BigDecimal 类来进行精确计算。 &lt;img src="http://www.fiveseven.fun/upload/20230713_15062063.png"&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 08:12:00 GMT</pubDate>
    </item>
    <item>
      <title>sql层面如何保证库存的准确性</title>
      <link>http://123.57.101.202:8080/archives/sqlceng-mian-ru-he-bao-zheng-ku-cun-de-zhun-que-xing</link>
      <content:encoded>&lt;p&gt;  一般库存的操作主要涉及到两个点，一个是并发下库存的扣减，保证业务上不会发生超卖导致库存负数问题(某些业务场景下是允许超卖的)，另外一个是，新增库存，也就是说更新库存数量的时候，如何保证在多人操作的场景下不会出现数据的相互覆盖。&lt;/p&gt; &lt;p&gt;第一个问题: 为了防止库存超卖，那么只要在sql层面控制即可，通过更新行数据的原子性，来保证库存数量不会被抵扣成负数。类似以下sql,只要在每次抵扣库存的时候通过where判断以下 (account_balance - #{num}) &amp;gt;=0 是否大于等于0，就不会出现超卖的场景。&lt;/p&gt; &lt;p&gt;eg:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;update account_info set account_balance = account_balance - #{num} where id = #{id} and (account_balance - #{num}) &amp;gt;=0 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;第二个问题：库存的变动除了来自买家的下单，还来自于卖家的后台管理，比如说采购或者是库存盘点，可能都会导致库存的变动。一般情况下库存上层的不同业务模块可能不是由同一个人管理的，因此可能出现在同个时间段多个人进行修改库存的场景，导致数据的相互覆盖问题。针对这个问题，可以采用CAS的一个思想，即：在表增加一个版本号字段，每次行数据更新成功，那么我的版本号就递增，而每次去操作更新的时候，都会拿着当前的版本号和表里面的版本号做对比，如果版本号一样，说明行数据没有修改，如果不一样，那么就是有其他人更新过数据，此时便不会更新成功，可以给与前端弹窗提醒：数据已被更新，请刷新。 eg:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;update account_info set account_balance = #{num} where id = #{id} and version = #{version} &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Thu, 09 Apr 2026 08:10:02 GMT</pubDate>
    </item>
    <item>
      <title>手写分布式锁</title>
      <link>http://123.57.101.202:8080/archives/shou-xie-fen-bu-shi-suo</link>
      <content:encoded>&lt;p&gt;实现分布式锁的方式有很多，可以通过redis、mysql、zk等方式去实现一个分布式锁。什么情况下需要用到分布式锁呢？一种是多个微服务实例，另外一种是在多线程的情况下可能也需要用到。&lt;br /&gt; 今天主要介绍用redis怎么去实现分布式锁。&lt;br /&gt; 实现分布式锁，需要考虑3个要素：&lt;br /&gt; 1.锁过期时间&lt;br /&gt; 2.锁的释放&lt;br /&gt; 3.锁的续期&lt;/p&gt; &lt;p&gt;1.锁过期时间：之所以要设置锁的过期时间是因为在服务宕机的场景下，没办法去主动释放锁，因此只能基于redis的缓存过期时间来辅助释放。&lt;/p&gt; &lt;p&gt;2.锁的释放：锁的释放一般是在try{}catch(){}finally{}中的finally里面去释放，这样可以保证就算业务发生报错，也能即使把锁释放掉&lt;/p&gt; &lt;p&gt;3.锁的续期：有一种场景是当前的业务执行时间比锁的过期时间还要长，就会出现，当前业务还没执行完，锁就被释放掉了，为了避免这种情况，需要单独为这个锁重启一个线程，专门用来观察业务是否执行完毕，如果在锁快到期之前，业务还没执行完，那么该线程会更新这个锁的过期时间。当然如果业务时间很短，能保证每次执行的业务时间在过期时间内，那么就没必要去增加一个锁的续期&lt;/p&gt; &lt;p&gt;以下为代码实现:&lt;br /&gt; 1.加锁，加锁成功后再触发watchDog，一般redis加锁可以直接通过setnx或者hsetnx的方式，也可以用lua表达式，一般推荐用lua表达式，因为这样可以写出更丰富的逻辑，并且能保证一条lua脚本是在一个原子操作里面执行的。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public boolean lock(String id, int keyExpireTime, int watchTime) throws InterruptedException {         for (; ; ) {             //HSET命令返回OK ，则证明获取锁成功             String script = &amp;quot;if (redis.call('exists', KEYS[1]) ~= 1) then &amp;quot;                     + &amp;quot;return redis.call('hsetnx', KEYS[1], KEYS[2],ARGV[1]) &amp;quot;                     + &amp;quot;else &amp;quot;                     + &amp;quot;return 0 &amp;quot;                     + &amp;quot;end&amp;quot;;             List&amp;lt;String&amp;gt; keys = new ArrayList&amp;lt;&amp;gt;();             keys.add(LOCK_KEY);             keys.add(id);             List&amp;lt;String&amp;gt; arg = new ArrayList&amp;lt;&amp;gt;();             arg.add(&amp;quot;&amp;quot;+keyExpireTime);             String result = null;             Jedis jedis = null;             try {                 jedis = jedisPool.getResource();                 result = jedis.eval(script, keys, arg).toString();             } catch (Exception e) {                 e.getStackTrace();             }finally {                 jedis.close();             }             if (&amp;quot;1&amp;quot;.equals(result)) {                  Runnable r = ()-&amp;gt;{                     try {                         watchDog(id, keyExpireTime,watchTime);                     } catch (Exception e) {                         e.getStackTrace();                         throw new RuntimeException(e);                     }                 };                 pool.execute(r);                 System.out.println(id+&amp;quot;获取到锁&amp;quot;);                 return true;             }         }     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;2.解锁：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public boolean unlock(String id) {         Jedis jedis = jedisPool.getResource();         List&amp;lt;String&amp;gt; keys = new ArrayList&amp;lt;&amp;gt;();         keys.add(LOCK_KEY);         keys.add(id);         //LUA表达式         String script =                 &amp;quot;if (redis.call('hexists', KEYS[1], KEYS[2]) == 1) then &amp;quot; +                         &amp;quot;return redis.call('del',KEYS[1]) &amp;quot; +                         &amp;quot;else &amp;quot; +                         &amp;quot;return 0 &amp;quot; +                         &amp;quot;end&amp;quot;;         String result = jedis.eval(script, keys, Collections.singletonList(id)).toString();         jedis.close();         boolean flag = &amp;quot;1&amp;quot;.equals(result) ? true : false;         if (flag){             System.out.println(id+&amp;quot;解锁&amp;quot;);         }         return flag;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;3.续期(watchDog)&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public boolean watchDog(String uuid, int keyExpireTime,int time) {         //每time秒去检查一下线程任务是否执行完毕，如果没有执行完毕，那么需要对锁进行续签         String script = &amp;quot;if (redis.call('hexists', KEYS[1],KEYS[2]) == 1) then &amp;quot;                 + &amp;quot;return redis.call('expire', KEYS[1], ARGV[1]) &amp;quot;                 + &amp;quot;else  &amp;quot;                 + &amp;quot;return 0 &amp;quot;                 + &amp;quot;end &amp;quot;;          List&amp;lt;String&amp;gt; arg = new ArrayList&amp;lt;&amp;gt;();         List&amp;lt;String&amp;gt; keys = new ArrayList&amp;lt;&amp;gt;();         keys.add(LOCK_KEY);         keys.add(uuid);         arg.add(keyExpireTime+&amp;quot;&amp;quot;);          String result = null;         Jedis jedis = jedisPool.getResource();         try {             result = jedis.eval(script, keys, arg).toString();         } catch (Exception e) {             e.getStackTrace();             throw new RuntimeException(e);         }finally {             jedis.close();         }         boolean flag = &amp;quot;1&amp;quot;.equals(result) ? true : false;         if (flag){             try {                 System.out.println(uuid+&amp;quot;续期&amp;quot;);                 Thread.sleep(time*1000);             } catch (InterruptedException e) {                 throw new RuntimeException(e);             }             return watchDog(uuid,keyExpireTime,time);         }else {             System.out.println(&amp;quot;续期结束&amp;quot;);             return false;         }     } &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Mon, 16 Mar 2026 11:38:12 GMT</pubDate>
    </item>
    <item>
      <title>什么是不可变对象</title>
      <link>http://123.57.101.202:8080/archives/shi-me-shi-bu-ke-bian-dui-xiang</link>
      <content:encoded>&lt;p&gt;  什么是不可变对象，个人理解，比较简单的来说，就是这个引用所关联对象的信息不会发生变化。众所周知，String对象是不可变的，它是如何做到不可变的呢？如下图所示：&lt;br /&gt; &lt;img src="http://123.57.101.202:8080/upload/20220102_19375925.png"&gt;   我们发现当给String对象重新赋值的时候，地址值也跟着发生了变化，个人理解当通过&amp;quot;&amp;quot;双引号赋值的时候，其实是等同于new 一个对象在字符串常量池里面，只不过如果池里面已经存在对应值的时候，那么直接返回对应的引用，如果没有才会去创建。&lt;br /&gt;   翻看String源码:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public final class String     implements java.io.Serializable, Comparable&amp;lt;String&amp;gt;, CharSequence {     /** The value is used for character storage. */     private final char value[]; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;我们可以发现String其实就是对char做得一层高级的封装，对类声明了final不可继承，保证了String的安全不被篡改，并且对核心变量value也是做了私有和final修饰，保证value不可改变，至少是引用是不会发生改变，但是作为一个数组，里面的值是可以被替换或者发生改变的。而String的不可变巧妙就巧妙在，String类里面的的方法，都很小心没有去动value变量里面的元素。所以String的不可变不单单是将类和value设为final，还在于变量的私有化，还有里面提供的api方法都是安全的，不会去修改核心变量value里面的元素。类似的subString();replaceFirst();等方法，都是返回一个新的String对象，而不会去修改原有的对象的信息。&lt;/p&gt; &lt;h2&gt;参考资料&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;[1] &lt;a href="httpss://www.zhihu.com/question/20618891"&gt;如何理解 String 类型值的不可变？&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Mon, 16 Mar 2026 11:34:24 GMT</pubDate>
    </item>
    <item>
      <title>Saas平台如何实现文件的导出？</title>
      <link>http://123.57.101.202:8080/archives/saasping-tai-ru-he-shi-xian-wen-jian-de-dao-chu</link>
      <content:encoded>&lt;p&gt;  最近在重构以往文件导出的一部分代码，在重构之前我先摸索了了下，文件导出的整体链路以及实现方式，惊讶地发现，对于Saas平台系统而言，文件导出其实是一个普遍但考虑点偏多的功能。&lt;/p&gt; &lt;p&gt;  我们的业务属于电商ERP系统，平时用户存储的商品、订单、流水数量都是万级别单位，如何保证多数用户在导出数据文件的时候，系统内存不吃满，程序还能稳定运行，其实是一件不那么容易的事。其中实现，需要涉及到：&lt;strong&gt;对用户需求上的限制、导出文件的技术选型、分页数据查询、队列分组优先级、文件的上传和下载&lt;/strong&gt;&lt;/p&gt; &lt;h5&gt;一、需求限制：&lt;/h5&gt; &lt;p&gt;  对于数据的导出，首先在源头上对用户导出的数据量做了限制，设置最大导出数量，禁止用户无限制导出，避免单个用户的导出操作一直占用服务资源，一般情况下，导出的最大数量因业务而异，比如订单最大的导出数量为10W条。&lt;/p&gt; &lt;h5&gt;二、技术选型：&lt;/h5&gt; &lt;p&gt;  目前采用的导出框架为阿里系的EasyExcel，在采用EasyEcel之前，用的是EasyPOI技术读写文件，但是考虑POI在读写文件的时候比较吃内存，假如服务并发量很小，其实使用POI是没什么问题的，但是对于Saas系统，并发上来后一定会OOM或者频繁的full gc 导致CPU飙高，给用户最直接的反馈就是页面点起来巨卡，并且目前POI还存一些在并发情况下未能修复的bug，综合考虑弃用EasyExcel投向EasyExcel，EasyExcel对于上述的问题都做了比较友好的处理，具体内容官网也给出了详细介绍:httpss://alibaba-easyexcel.github.io/support/about.html&lt;/p&gt; &lt;h5&gt;三、分页查询：&lt;/h5&gt; &lt;p&gt;  分页查询的目的其实非常明确，就是为了避免大数据查询占用过多内存导致OOM或者频繁FullGC，分页的大小建议范围为200-1000。在每次查询完一页数据，会将数据冲内存缓冲区flush到磁盘上。writer.flush();作用除了清楚内存Buffer之外，更重要的是将当前线程临时标记为空闲，允许调用程序在CPU资源分配给其他可能需要处理的线程。防止系统中几个大的导出任务阻塞住其他导出任务的现象。&lt;/p&gt; &lt;h5&gt;四、队列分组优先级：&lt;/h5&gt; &lt;p&gt;  虽然分页查询可以降低单个用户查询数据所占用的内存，但是当同时存在多个用户在导出的情况下，分页导出带来的效果其实是不大的，尤其是一些用户每到月末，财务需要对账，会大批量去导出数据，虽然分页上是降低了单个用户内存的占用，但是用户数量一多，内存还是容易出现吃满的情况，一般情况就是导出超时，最后失败。为了解决这个问题，需要把用户提交的导出任务丢到线程池队列里面，然后对队列进行分组区分出优先级别，对于一些VIP大户，我们会优先选择任务较少的队列。这边采用的队列为有序阻塞队列 LinkedBlockingQueue。通过线程池队列的形式去执行导出任务，可以避免大量的导出请求把服务压垮，简单来说就是对流量的一个削峰。&lt;/p&gt; &lt;h5&gt;五、文件的上传和下载&lt;/h5&gt; &lt;p&gt;  需要下载的文件一开始会以临时文件的形式存储在服务的本地磁盘上，当文件成功生成后，那么会将临时文件上传到远程的阿里OSS对象存储服务器，接着返回给前端OSS服务的文件下载链接，并删除临时文件。&lt;/p&gt;</content:encoded>
      <pubDate>Mon, 16 Mar 2026 11:30:31 GMT</pubDate>
    </item>
  </channel>
</rss>

