<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
<title><![CDATA[与猪搏斗的日子]]></title> 
<atom:link href="https://www.chen0adapter.top/rss.php" rel="self" type="application/rss+xml" />
<description><![CDATA[]]></description>
<link>https://www.chen0adapter.top/</link>
<language>zh-cn</language>
<generator>www.emlog.net</generator>
<item>
    <title>本站架构简记</title>
    <link>https://www.chen0adapter.top/?post=22</link>
    <description><![CDATA[<p>&emsp;&emsp;本站（确切地说是本域名）注册的时间是在2020年3月份，一直是使用的静态发布，在今年（2023）某日某夜突发奇想，容器化不是很流行嘛？要不咱也把站点给它容器化一下？<br />
&emsp;&emsp;一夜思索，下定决心：搞！把web服务器、站点、静态资源和后台笔记都一起搬进容器。各种工具选择比对之后，选定了1Panel来做容器化管理工具，openresty（Nginx）替代caddy来做web服务器，主站切换到emlog，后台笔记采用思源笔记，之前发布的书籍依然采用静态形式，但是存储在容器化后的openresty站点目录下。<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202403/10101711422256.png"><img src="https://www.chen0adapter.top/content/uploadfile/202403/thum-10101711422256.png" alt="" /></a><br />
&emsp;&emsp;用openresty来反代主站和book：<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202403/fd101711423239.png"><img src="https://www.chen0adapter.top/content/uploadfile/202403/thum-fd101711423239.png" alt="" /></a><br />
&emsp;&emsp;用户访问网络简图如下：<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202403/de021711422239.jpg"><img src="https://www.chen0adapter.top/content/uploadfile/202403/thum-de021711422239.jpg" alt="" /></a></p>
<p>&emsp;&emsp;不得不说，1panel十分清爽简洁，发布简单，当前用1Panel管理的容器如下：<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202403/a4de1711423398.png"><img src="https://www.chen0adapter.top/content/uploadfile/202403/thum-a4de1711423398.png" alt="" /></a></p>]]></description>
    <pubDate>Sun, 01 Oct 2023 10:27:00 +0800</pubDate>
    <dc:creator>林立</dc:creator>
    <guid>https://www.chen0adapter.top/?post=22</guid>
</item>
<item>
    <title>IO读写基本原理</title>
    <link>https://www.chen0adapter.top/?post=29</link>
    <description><![CDATA[<h2>用户态与内核态</h2>
<p>&emsp;&emsp;为了避免用户进程直接操作内核，保证内核安全，操作系统将内存（虚拟内存）划分为两部分：一部分是内核空间（Kernel-Space），另一部分是用户空间（User-Space）。在Linux系统中，内核模块运行在内核空间，对应的进程处于内核态；用户程序运行在用户空间，对应的进程处于用户态。<br />
&emsp;&emsp;操作系统的核心是内核程序，它独立于普通的应用程序，既有权限访问受保护的内核空间，也有权限访问硬件设备，而普通的应用程序并没有这样的权限。内核空间总是驻留在内存中，是为操作系统的内核保留的。应用程序不允许直接在内核空间区域进行读写，也不允许直接调用内核代码定义的函数。每个应用程序进程都有一个单独的用户空间，对应的进程处于用户态，用户态进程不能访问内核空间中的数据，也不能直接调用内核函数，因此需要将进程切换到内核态才能进行系统调用。<br />
&emsp;&emsp;内核态进程可以执行任意命令，调用系统的一切资源，而用户态进程只能执行简单的运算，不能直接调用系统资源，那么问题来了：用户态进程如何执行系统调用呢？答案是：用户态进程必须通过系统调用（System Call）向内核发出指令，完成调用系统资源之类的操作。<br />
&emsp;&emsp;用户程序进行IO的读写依赖于底层的IO读写，基本上会用到底层的read和write两大系统调用。虽然在不同的操作系统中read和write两大系统调用的名称和形式可能不完全一样，但是它们的基本功能是一样的。<br />
&emsp;&emsp;操作系统层面的read系统调用并不是直接从物理设备把数据读取到应用的内存中，write系统调用也不是直接把数据写入物理设备。上层应用无论是调用操作系统的read还是调用操作系统的write，都会涉及缓冲区。具体来说，上层应用通过操作系统的read系统调用把数据从内核缓冲区复制到应用程序的进程缓冲区，通过操作系统的write系统调用把数据从应用程序的进程缓冲区复制到操作系统的内核缓冲区。<br />
&emsp;&emsp;简单来说，应用程序的IO操作实际上不是物理设备级别的读写，而是缓存的复制。read和write两大系统调用都不负责数据在内核缓冲区和物理设备（如磁盘、网卡等）之间的交换。这个底层的读写交换操作是由操作系统内核（Kernel）来完成的。所以，在应用程序中，无论是对socket的IO操作还是对文件的IO操作，都属于上层应用的开发，它们在输入（Input）和输出（Output）维度上的执行流程是类似的，都是在内核缓冲区和进程缓冲区之间进行数据交换。</p>
<h2>内核缓冲区与进程缓冲区</h2>
<p>&emsp;&emsp;缓冲区的目的是减少与设备之间的频繁物理交换。计算机的外部物理设备与内存和CPU相比，有着非常大的差距，外部设备的直接读写涉及操作系统的中断。发生系统中断时，需要保存之前的进程数据和状态等信息，结束中断之后，还需要恢复之前的进程数据和状态等信息。为了减少底层系统的频繁中断所导致的时间损耗、性能损耗，出现了内核缓冲区。<br />
&emsp;&emsp;操作系统会对内核缓冲区进行监控，等待缓冲区达到一定数量的时候，再进行IO设备的中断处理，集中执行物理设备的实际IO操作，通过这种机制来提升系统的性能。至于具体什么时候执行系统中断（包括读中断、写中断）则由操作系统的内核来决定，应用程序不需要关心。<br />
&emsp;&emsp;上层应用使用read系统调用时，仅仅把数据从内核缓冲区复制到应用的缓冲区（进程缓冲区）；上层应用使用write系统调用时，仅仅把数据从应用的缓冲区复制到内核缓冲区。<br />
&emsp;&emsp;内核缓冲区与应用缓冲区在数量上也不同。在Linux系统中，操作系统内核只有一个内核缓冲区。每个用户程序（进程）都有自己独立的缓冲区，叫作用户缓冲区或者进程缓冲区。在大多数情况下，Linux系统中用户程序的IO读写程序并没有进行实际的IO操作，而是在用户缓冲区和内核缓冲区之间直接进行数据的交换。<br />
&emsp;&emsp;用户程序所使用的系统调用read和write并不是使数据在内核缓冲区和物理设备之间交换：read调用把数据从内核缓冲区复制到应用的用户缓冲区，write调用把数据从应用的用户缓冲区复制到内核缓冲区。<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202404/082e1712469658.jpg"><img src="https://www.chen0adapter.top/content/uploadfile/202404/thum-082e1712469658.jpg" alt="" /></a><br />
&emsp;&emsp;以read系统调用为例，看一下一个完整输入流程的两个阶段：1.应用程序等待数据准备好。2.从内核缓冲区向用户缓冲区复制数据。<br />
&emsp;&emsp;如果是读取一个socket（套接字），那么以上两个阶段的具体处理流程：1.第一个阶段，应用程序等待数据通过网络到达网卡，当所等待的分组到达时，数据被操作系统复制到内核缓冲区中。这个工作由操作系统自动完成，用户程序无感知。2.第二个阶段，内核将数据从内核缓冲区复制到应用的用户缓冲区。<br />
&emsp;&emsp;再具体一点，如果是在Java客户端和服务端之间完成一次socket请求和响应（包括read和write）的数据交换，其完整的流程如下：<br />
&emsp;&emsp;1.客户端发送请求：Java客户端程序通过write系统调用将数据复制到内核缓冲区，Linux将内核缓冲区的请求数据通过客户端机器的网卡发送出去。在服务端，这份请求数据会从接收网卡中读取到服务端机器的内核缓冲区。<br />
&emsp;&emsp;2.服务端获取请求：Java服务端程序通过read系统调用从Linux内核缓冲区读取数据，再送入Java进程缓冲区。<br />
&emsp;&emsp;3.服务端业务处理：Java服务器在自己的用户空间中完成客户端的请求所对应的业务处理。<br />
&emsp;&emsp;4.服务端返回数据：Java服务器完成处理后，构建好的响应数据将从用户缓冲区写入内核缓冲区，这里用到的是write系统调用，操作系统会负责将内核缓冲区的数据发送出去。<br />
&emsp;&emsp;5.发送给客户端：服务端Linux系统将内核缓冲区中的数据写入网卡，网卡通过底层的通信协议将数据发送给目标客户端。</p>]]></description>
    <pubDate>Mon, 01 May 2023 13:53:00 +0800</pubDate>
    <dc:creator>林立</dc:creator>
    <guid>https://www.chen0adapter.top/?post=29</guid>
</item>
<item>
    <title>JVM探索-7.JVM虚拟机架构</title>
    <link>https://www.chen0adapter.top/?post=30</link>
    <description><![CDATA[<h2>Java 虚拟机是怎么运行的</h2>
<ul>
<li>首先 jvm 在类中查找main方法。如果存在，它将从该处开始执行。</li>
<li>在 Java 虚拟机内部，线程有两种形式：守护程序和非守护程序。 守护程序线程通常是虚拟机本身使用的线程，例如执行垃圾收集的线程。 但是，该应用可以将其创建的任何线程标记为守护程序线程。 应用的初始线程（从main()开始的线程）是非守护程序线程。</li>
<li>只要任何非守护程序线程仍在运行，Java 应用就会继续执行（虚拟机实例继续运行）。 Java 应用的所有非守护程序线程都终止时，虚拟机实例将退出。 如果得到安全管理器允许，应用程序还可以通过调用Runtime或System类的exit()方法来使其自身灭亡。</li>
</ul>
<h2>Java 虚拟机架构</h2>
<ul>
<li>所有的 JVM 都包含两个组成部分：</li>
<li>1.类加载器子系统：一种用于加载给定完全限定名称的类型（类和接口）的机制。</li>
<li>2.执行引擎：负责执行已加载类的方法中包含的指令的机制。</li>
<li>当 Java 虚拟机运行程序时，它需要内存来存储许多内容，包括字节码、来自加载的类文件的信息、程序实例化的对象、方法的参数、返回值、局部变量和计算的中间结果。</li>
<li>虚拟机的不同实现可能具有非常不同的内存限制。 一些实现可能需要大量内存才能工作，而其他实现可能很少。 一些实现可能能够利用虚拟内存，而其他一些则不能。 运行时数据区规范的抽象性质有助于简化在各种计算机和设备上实现 Java 虚拟机的过程。</li>
<li>不同的 Java 程序具有不同的 jvm 实例。 如果执行一个类，则一个单独的 jvm 实例将处理该类。</li>
<li>当虚拟机加载类文件时，它会从类文件中包含的二进制数据中解析有关类型的信息。 它将类型信息放入方法区域，将程序实例化的所有对象放入堆。</li>
<li>每个新线程在产生时都会获得自己的 PC 寄存器（程序计数器）和 Java 堆栈。如果线程正在执行 Java 方法（不是 native 方法），则 PC 寄存器的值指示要执行的下一条指令。线程的 Java 堆栈存储线程的 Java（非本机）方法调用的状态。</li>
<li>Java 堆栈由堆栈帧（或帧）组成。堆栈帧包含一个 Java 方法调用的状态。当线程调用方法时，Java 虚拟机会将新帧推送到该线程的 Java 堆栈上。当该方法完成时，虚拟机会弹出并丢弃该方法的帧。</li>
<li>Java虚拟机实际上包含了两大类加载器：bootstrap类加载器和用户自定义类加载器。</li>
<li>PC 寄存器的大小为1个字，因此它既可以保存本机指针，也可以保存返回地址， 当线程执行 Java 方法时，PC 寄存器包含该线程正在执行的当前指令的地址。这个地址可以是本机指针，也可以是方法字节码开头的偏移量。如果线程正在执行 native 方法，则 PC 寄存器的值未定义。</li>
<li>此处有两个堆栈：</li>
<li>1.Java 堆栈：JVM所持有并维护。</li>
<li>2.Native 堆栈： 看情况而定，主要依赖于外部实现能力。</li>
</ul>]]></description>
    <pubDate>Tue, 08 Nov 2022 10:30:00 +0800</pubDate>
    <dc:creator>林立</dc:creator>
    <guid>https://www.chen0adapter.top/?post=30</guid>
</item>
<item>
    <title>JVM探索-6.JVM日志与G1简要参数</title>
    <link>https://www.chen0adapter.top/?post=28</link>
    <description><![CDATA[<p>&emsp;&emsp;如果启动JVM的时候我们没有指定参数，则可以通过设置Java -XX:+Print CommandLineFlags这个参数让JVM打印出那些已经被用户或者JVM设置过的详细的XX参数的名称和值。例如我们可以得到JVM使用的默认垃圾收集器，如下：</p>
<pre><code class="language-shell">-XX:InitialHeapSize=266930688 -XX:MaxHeapSize=4270891008 
-XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers 
-XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation 
-XX:+UseParallelGC</code></pre>
<p>&emsp;&emsp;如果指定G1作为垃圾回收，但是没有指定堆空间的参数，当发生GC的时候，我们可以看到：</p>
<pre><code class="language-shell">-Xmx256M -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions 
-XX:G1LogLevel=finest -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 
-XX:+UseAdaptiveSizePolicy
garbage-first heap   total 131072K, used 37569K [0x00000000f8000000, 
  0x00000000f8100400, 0x0000000100000000)
  region size 1024K, 24 young (24576K), 0 survivors (0K)</code></pre>
<p>&emsp;&emsp;Eden开始之前是24MB，主要来自于预测值，且24个分区，即每个分区都是1MB。<br />
&emsp;&emsp;第一次GC后的堆空间信息如下所示：</p>
<pre><code class="language-shell">[Eden: 24.0M(24.0M)-&gt;0.0B(13.0M) Survivors: 0.0B-&gt;3072.0K Heap: 
  24.0M(128.0M)-&gt;12.9M(128.0M)]</code></pre>
<p>&emsp;&emsp;GC之后Eden设置为13M，来自于256M×5%=12.8MB，取整后就是13MB，并且满足预测时间。其中，256M是堆的大小，5%是G1 NewSizePercent指定的默认值。<br />
&emsp;&emsp;上面已经详细介绍了G1中堆大小和新生代大小的计算、分区设置、G1的停顿预测模型以及停顿预测模型中的几个参数。这里给出使用中的一些注意事项：<br />
&emsp;&emsp; - 参数G1HeapRegionSize指定堆分区大小。分区大小可以指定，也可以不指定；不指定时，由内存管理器启发式推断分区大小。<br />
&emsp;&emsp; - 参数xms/xmx指定堆空间的最小值/最大值。一定要正确设置xms/xmx，否则将使用默认配置，将影响分区大小推断。<br />
&emsp;&emsp; - 在以前的内存管理器中（非G1），为了防止新生代因为内存不断地重新分配导致性能变低，通常设置Xmn或者NewRatio。但是G1中不要设置MaxNewSize、NewSize、Xmn和NewRatio。原因有两个，第一G1对内存的管理不是连续的，所以即使重新分配一个堆分区代价也不高，第二也是最重要的，G1的目标满足垃圾收集停顿，这需要G1根据停顿时间动态调整收集的分区，如果设置了固定的分区数，即G1不能调整新生代的大小，那么G1可能不能满足停顿时间的要求。<br />
&emsp;&emsp; - 参数GCTimeRatio指的是GC与应用程序之间的时间占比，G1中默认值为9，表示GC与应用程序时间占比为10%。增大该值将减少GC占用的时间，带来的后果就是动态扩展内存更容易发生；在很多情况下10%已经很大，例如可以将该值设置为19，则表示GC时间不超过5%。<br />
&emsp;&emsp; - 根据业务请求变化的情况，设置合适的扩展G1ExpandByPercentOfAvailable速率，保持效率。<br />
&emsp;&emsp; - JVM在对新生代内存分配管理时，还有一个参数就是保留内存G1ReservePercent（默认值是10），即在初始化，或者内存扩展/收缩的时候会计算更新有多少个分区是保留的，在新生代分区初始化的时候，在空闲列表中保留一定比例的分区不使用，那么在对象晋升的时候就可以使用了，所以能有效地减小晋升失败的概率。这个值最大不超过50，即最多保留50%的空间，但是保留过多会导致新生代可用空间少，过少可能会增加新生代晋升失败，那将会导致更为复杂的串行回收。<br />
&emsp;&emsp; - G1NewSizePercent是一个实验参数，需要使用-XX:+UnlockExperimentalVMOptions才能改变选项。有实验表明G1在回收Eden分区的时候，大概每GB需要100ms，所以可以根据停顿时间，相应地调整。这个值在内存比较大的时候需要减少，例如32G可以设置-XX:G1NewSizePercent=3， 这样Eden至少保留大约1GB的空间，从而保证收集效率。<br />
&emsp;&emsp; - 参数MaxGCPauseMillis指期望停顿时间，可根据系统配置和业务动态调整。因为G1在垃圾收集的时候一定会收集新生代，所以需要配合新生代大小的设置来确定，如果该值太小，连新生代都不能收集完成，则没有任何意义，每次除了新生代之外只能多收集一个额外老生代分区。<br />
&emsp;&emsp; - 参数GCPauseIntervalMillisGC指GC间隔时间，默认值为0，GC启发式推断为MaxGCPauseMillis+1，设置该值必须要大于MaxGCPauseMillis。<br />
&emsp;&emsp; - 参数G1ConfidencePercent指GC预测置信度，该值越小说明基于过去历史数据的预测越准确，例如设置为0则表示收集的分区基本和过去的衰减均值相关，无波动，所以可以根据过去的衰减均值直接预测下一次预测的时间。反之该值越大，说明波动越大，越不准确，需要加上衰减方差来补偿。<br />
&emsp;&emsp; - JVM中提供了一个对象对齐的值ObjectAlignmentInBytes，默认值为8，需要明白该值对内存使用的影响，这个影响不仅仅是在JVM对对象的分配上面，正如上面看到的它也会影响对象在分配时的标记情况。注意这个值最少要和操作系统支持的位数一致才能提高对象分配的效率。所以32位系统最少是4，64位最少是8。一般不用修改该值。</p>]]></description>
    <pubDate>Sat, 05 Nov 2022 14:29:00 +0800</pubDate>
    <dc:creator>林立</dc:creator>
    <guid>https://www.chen0adapter.top/?post=28</guid>
</item>
<item>
    <title>JVM探索-5.内存分配与管理</title>
    <link>https://www.chen0adapter.top/?post=27</link>
    <description><![CDATA[<p>&emsp;&emsp;JVM作为内存分配的管理器，一定涉及如何与内存交互。那么JVM是如何管理内存的？实际上内存管理的算法很多，简单来说JVM从操作系统申请一块内存，然后根据不同的GC算法进行管理。<br />
&emsp;&emsp;首先JVM先通过操作系统的系统调用（system call）进行内存的申请，典型的就是mmap。众所周知glibc提供了我们常用的内存管理函数如malloc/free/realloc/memcopy/memset等。为什么JVM不直接使用这些函数？glibc里面的malloc也是通过mmap等系统调用来完成内存的分配，之后glibc再对已经分配到的内存进行管理。GC算法实现了一套自己的管理方式，所以再基于malloc/free实现效率肯定不高。mmap必须以PAGE_SIZE为单位进行映射，而内存也只能以页为单位进行映射，若要映射非PAGE_SIZE整数倍的地址范围，要先进行内存对齐，强行以PAGE_SIZE的倍数大小进行映射。还要注意一点，操作系统对内存的分配管理典型地分为两个阶段：保留（reserve）和提交（commit）。保留阶段告知系统从某一地址开始到后面的dwSize大小的连续虚拟内存需要供程序使用，进程其他分配内存的操作不得使用这段内存；提交阶段将虚拟地址映射到对应的真实物理内存中，这样这块内存就可以正常使用。<br />
&emsp;&emsp;对于保留和提交，Windows在使用VirtualAlloc分配内存时传递不同的参数MEM_RESERVE/MEM_COMMIT，Linux在mmap保留内存时使用MAP_PRIVATE|MAP_NORESERVE|MAP_ANONYMOUS，提交内存时使用MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS。其中MAP_NORESERVE指不要为这个映射保留交换空间，MAP_FIXED使用指定的映射起始地址。<br />
&emsp;&emsp;JVM中常见的对象类型有以下6种：<br />
&emsp;&emsp;ResourceObj：线程有一个资源空间（Resource Area），一般ResourceObj都位于这里。定义资源空间的目的是对JVM其他功能的支持，如CFG、在C1/C2优化时可能需要访问运行时信息（这些信息可以保存在线程的资源区）。<br />
&emsp;&emsp;StackObj：栈对象，声明的对象使用栈管理。其实栈对象并不提供任何功能，且禁止New/Delete操作。对象分配在线程栈中，或者使用自定义的栈容器进行管理。<br />
&emsp;&emsp;ValueObj：值对象，该对象在堆对象需要进行嵌套时使用，简单地说就是对象分配的位置和宿主对象（即拥有这个ValueObj对象的对象）是一样的。<br />
&emsp;&emsp;AllStatic：静态对象，全局对象，只有一个。值得一提的是C++中静态对象的初始化并没有通过规范保证，可能会有一个问题，就是两个静态对象相互依赖，那么在初始化的时候可能出错。JVM中的很多静态对象的初始化，都是显式调用静态初始化函数。<br />
&emsp;&emsp;MetaspaceObj：元对象，比如InstanceKlass这样的元数据就是元对象。<br />
&emsp;&emsp;CHeapObj：这是堆空间的对象，由new/delete/free/malloc管理。其包含的内容很多，比如Java对象、InstanceOop（G1对象分配出来的对象）。除了Java对象，还有其他的对象也在堆中。<br />
&emsp;&emsp;JVM中为了准确描述这些堆中的对象，以方便对JVM进行优化，所以又定义了更具体的子类型，代码如下所示：</p>
<pre><code class="language-c++">hotspot/src/share/vm/memory/allocation.hpp
// JVM中使用的内存类型
  mtJavaHeap          = 0x00,  // Java堆
  mtClass             = 0x01,  // JVM中Java类
  mtThread            = 0x02,  // JVM中线程对象
  mtThreadStack       = 0x03,
  mtCode              = 0x04,  // JVM中生成的编译代码
  mtGC                = 0x05,  // GC的内存
  mtCompiler          = 0x06,  // 编译器使用的内存
  mtInternal          = 0x07,  // JVM中内部使用的类型，不属于上述任何类型
  mtOther             = 0x08,  // 不是由JVM使用的内存
  mtSymbol            = 0x09,  // 符号表使用的内存
  mtNMT               = 0x0A,  // NMT使用的内存
  mtClassShared       = 0x0B,  // 共享类数据
  mtChunk             = 0x0C,  // Chunk用于缓存
  mtTest              = 0x0D,
  mtTracing           = 0x0E,
  mtNone              = 0x0F,</code></pre>
<p>&emsp;&emsp;这些信息描述了JVM使用内存的情况，这一部分信息能够帮助定位JVM本身运行时出现的问题。</p>]]></description>
    <pubDate>Mon, 31 Oct 2022 13:12:00 +0800</pubDate>
    <dc:creator>林立</dc:creator>
    <guid>https://www.chen0adapter.top/?post=27</guid>
</item>
<item>
    <title>JVM探索-4.对象头</title>
    <link>https://www.chen0adapter.top/?post=26</link>
    <description><![CDATA[<p>&emsp;&emsp;C++语言本身支持多态调用，众所周知，C++完成多态依赖于虚指针，这个指针指向一个虚表，这个虚表里面存储的是虚函数的地址，而这些函数的地址是在C++代码编译时确定的，通常虚表位于程序的数据段中。<br />
&emsp;&emsp;因为Java代码首先被翻译成字节码（bytecode），在JVM执行时才能确定要执行函数的地址，如何实现Java的多态调用，最直观的想法是把Java对象映射成C++对象或者封装成C++对象，比如增加一个额外的对象头，里面指向一个对象，而这个对象存储了Java代码的地址。所以JVM设计了对象的数据结构来描述Java对象，这个结构分为三块区域：对象头（Header）、实例数据（Instance Data）和对齐填充（Padding）。而刚才提到的类似虚指针的东西就可以放在对象头中，而JVM设计者还利用对象头来描述更多信息，对象的锁信息、GC标记信息等。<br />
&emsp;&emsp;JVM中对象头分为两部分：标记信息、元数据信息，代码如下所示：</p>
<pre><code class="language-c++">hotspot/src/share/vm/oops/oop.hpp
class oopDesc {
private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
  // 静态变量用于快速访问BarrierSet
  static BarrierSet* _bs;
……
}</code></pre>
<h2>1.标记信息</h2>
<p>&emsp;&emsp;第一部分标记信息位于MarkOop。<br />
&emsp;&emsp;根据JVM源码的注释，针对标记信息在32位JVM用32位来描述，可以总结出这32位的组合情况。<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202404/49511711986710.jpg"><img src="https://www.chen0adapter.top/content/uploadfile/202404/thum-49511711986710.jpg" alt="" /></a><br />
&emsp;&emsp;另外在源代码中我们还看到一个Promoted的状态，Promoted指的是对象从新生代晋升到老生代时，正常的情况需要对这个对象头进行保存，主要的原因是如果发生晋升失败，需要重新恢复对象头。如果晋升成功这个保存的对象头就没有意义。所以为了提高晋升失败时对象头的恢复效率，设计了promo_bits，这个其实是重用了加锁位（包括偏向锁），实际上只需要在以下三种情况时才需要保存对象头：<br />
&emsp;&emsp; - 使用了偏向锁，并且偏向锁被设置了。<br />
&emsp;&emsp; - 对象被加锁了。<br />
&emsp;&emsp; - 对象设置了hash_code。<br />
&emsp;&emsp;这里和GC直接相关的就是标记位11，前面的30位指针是非常有用的。在GC垃圾回收时，当对象被设置为marked（11）时，ptr指向什么位置？简单来说这个ptr是为了配合对象晋升时发生的对象复制（copy）。在对象复制时，先分配空间，再把原来对象的所有数据都复制过去，再修改对象引用的指针，就完成了。但是我们要思考这样一个问题，当有多个引用对象的字段指向同一个被引用对象时，我们完成一个被引用对象的复制之后，其他引用对象还没有被遍历（即还指向被引用对象老的地址），如何处理这种情况？这个时候简单设置状态为marked，表示被引用对象已经被标记且被复制了，ptr就是指向新的复制的地址。当遍历其他引用对象的时候，发现被引用对象已经完成标记，则不再需要复制对象，直接完成对象引用更新就可以了。</p>
<h2>2.元数据信息</h2>
<p>&emsp;&emsp;第二部分元数据信息字段指向的是Klass对象（Klass对象是元数据对象，如Instance Klass描述Java对象的类结构），这个字段也和垃圾回收有关系。<br />
&emsp;&emsp;就是在垃圾回收的时候如何区别一个立即数和指针地址？比如从Java的根集合中发现有一个值（如：0X12345678），那么这个数到底是一个整数还是一个Java对象的地址？实际上垃圾回收器不能区别，但是为了准确地回收垃圾，必须区别出来。一个简单的办法就是，把0X12345678先看成一个地址，即强制转换成OOP结构，再判定这个OOP是否是含有Klass指针，如果有的话即认为是一个指针，如果是NULL的话则认为是一个立即数。那么这里会有一个误判，即把一个立即数识别成一个OOP，当这个立即数刚好和一个OOP的地址相同的时候。所以JVM维护了一个全局的OOpMap，用于标记栈里面的数是立即数还是值。每一个InstanceKlass都维护了一个Map（OopMapBlock）用于标记Java类里面的字段到底是OOP还是int这样的立即数类型。这里面的字段Klass很多时候用于再次确认。<br />
&emsp;&emsp;由此可见，可以从根集合出发开始标记，通过外部的数据结构来标识是否为OOP对象。但是我们在JVM源码中还是看到了很多地方会根据对象头里面的Klass指针是否为NULL来判断是不是OOP对象，这似乎是多此一举。理论上根据额外的数据结构已经不需要再次判断，但是在垃圾回收的时候，通常是对整个区域的一块内存进行完全遍历，在对象分配时都是连续分配，当堆的尾部有尚未分配对象的时候，比如在新生代一个字通常初始化为0x20202020，需要对这些空白地址进行转换以判断是否为OOP，是否需要垃圾回收。在这里即使误判影响也不大，因为会根据RSet来判定是否为活跃对象（live object），如果是的话继续，即使误判之后也没关系，这相当于是浮动垃圾，在下一次回收的时候仍然可能被回收。</p>]]></description>
    <pubDate>Sat, 29 Oct 2022 23:43:00 +0800</pubDate>
    <dc:creator>林立</dc:creator>
    <guid>https://www.chen0adapter.top/?post=26</guid>
</item>
<item>
    <title>JVM探索-3.卡表和位图</title>
    <link>https://www.chen0adapter.top/?post=25</link>
    <description><![CDATA[<p>&emsp;&emsp;卡表（CardTable）在CMS中是最常见的概念之一，G1中不仅保留了这个概念，还引入了RSet。卡表到底是一个什么东西？<br />
&emsp;&emsp;GC最早引入卡表的目的是为了对内存的引用关系做标记，从而根据引用关系快速遍历活跃对象。举个简单的例子，有两个分区，假设分区大小都为1MB，分别为A和B。如果A中有一个对象objA，B中有一个对象objB，且objA.field=objB，那么这两个分区就有引用关系了，但是如果我们想找到分区A，要如何引用分区B？做法有两种：<br />
&emsp;&emsp;遍历整个分区A，一个字一个字的移动（为什么以字为单位？原因是JVM中对象会对齐，所以不需要按字节移动），然后查看内存里面的值到底是不是指向B，这种方法效率太低，可以优化为一个对象一个对象地移动（这里涉及JVM如何识别对象，以及如何区分指针和立即数），但效率还是太低。<br />
&emsp;&emsp;借助额外的数据结构描述这种引用关系，例如使用类似位图（bitmap）的方法，记录A和B的内存块之间的引用关系，用一个位来描述一个字，假设在32位机器上（一个字为32位），需要32KB（32KB×32=1M）的空间来描述一个分区。那么我们就可以在这个对象ObjA所在分区A里面添加一个额外的指针，这个指针指向另外一个分区B的位图，如果我们可以把对象ObjA和指针关系进行映射，那么当访问ObjA的时候，顺便访问这个额外的指针，从这个指针指向的位图就能找到被ObjA引用的分区B对应的内存块。通常我们只需要判定位图里面对应的位是否有1，有的话则认为发生了引用。<br />
&emsp;&emsp;以位为粒度的位图能准确描述每一个字的引用关系，但是一个位通常包含的信息太少，只能描述2个状态：引用还是未引用。实际应用中JVM在垃圾回收的时候需要更多的状态，如果增加至一个字节来描述状态，则位图需要256KB的空间，这个数字太大，开销占了25%。所以一个可能的做法位图不再描述一个字，而是一个区域，JVM选择512字节为单位，即用一个字节描述512字节的引用关系。选择一个区域除了空间利用率的问题之外，实际上还有现实的意义。我们知道Java对象实际上不是一个字能描述的（有一个参数可以控制对象最小对齐的大小，默认是8字节，实际上Java在JVM中还有一些附加信息，所以对齐后最小的Java对象是16字节），很多Java对象可能是几十个字节或者几百个字节，所以用一个字节描述一个区域是有意义的。但是我没有找到512的来源，为什么512效果最好？没有相应的数据来支持这个数字，而且这个值不可以配置，不能修改，但是有理由相信512字节的区域是为了节约内存额外开销。按照这个值，1MB的内存只需要2KB的额外空间就能描述引用关系。这又带来另一个问题，就是512字节里面的内存可能被引用多次，所以这是一个粗略的关系描述，那么在使用的时候需要遍历这512字节。<br />
&emsp;&emsp;再举一个例子，假设有两个对象B、C都在这512字节的区域内。为了方便处理，记录对象引用关系的时候，都使用对象的起始位置，然后用这个地址和512对齐，因此B和C对象的卡表指针都指向这一个卡表的位置。那么对于引用处理也有可有两种处理方法：<br />
&emsp;&emsp;处理的时候会以堆分区为处理单位，遍历整个堆分区，在遍历的时候，每次都会以对象大小为步长，结合卡表，如果该卡表中对应的位置被设置，则说明对象和其他分区的对象发生了引用。<br />
&emsp;&emsp;在G1除了512字节粒度的卡表之外，还有bitMap，例如使用bitMap可以描述一个分区对另外一个分区的引用情况。在JVM中bitMap使用非常多，例如还可以描述内存的分配情况。<br />
&emsp;&emsp;G1在混合收集算法中用到了并发标记。在并发标记的时候使用了bitMap来描述对象的分配情况。例如1MB的分区可以用16KB（16KB×ObjectAlignmentInBytes×8=1MB）来描述，即16KB额外的空间。其中ObjectAlignmentInBytes是8字节，指的是对象对齐，第二个8是指一个字节有8位。即每一个位可以描述64位。例如一个对象长度对齐之后为24字节，理论上它占用3个位来描述这个24字节已被使用了，实际上并不需要，在标记的时候只需要标记这3个位中的第一个位，再结合堆分区对象的大小信息就能准确找出。其最主要的目的是为了效率，标记一个位和标记3个位相比能节约不少时间，如果对象很大，则更划算。</p>]]></description>
    <pubDate>Fri, 28 Oct 2022 22:10:00 +0800</pubDate>
    <dc:creator>林立</dc:creator>
    <guid>https://www.chen0adapter.top/?post=25</guid>
</item>
<item>
    <title>JVM探索-2.G1垃圾回收器基础概念</title>
    <link>https://www.chen0adapter.top/?post=23</link>
    <description><![CDATA[<p>&emsp;&emsp;通常我们所说的GC是指垃圾回收，但是在JVM的实现中GC更为准确的意思是指内存管理器，它有两个职能，第一是内存的分配管理，第二是垃圾回收。这两者是一个事物的两个方面，每一种垃圾回收策略都和内存的分配策略息息相关，脱离内存的分配去谈垃圾回收是没有任何意义的。</p>
<h1>分区</h1>
<p>&emsp;&emsp;分区（Heap Region，HR）或称堆分区，是G1堆和操作系统交互的最小管理单位。G1的分区类型（HeapRegionType）大致可以分为四类：<br />
&emsp;&emsp;- 自由分区（Free Heap Region，FHR）<br />
&emsp;&emsp;- 新生代分区（Young Heap Region，YHR）<br />
&emsp;&emsp;- 大对象分区（Humongous Heap Region，HHR）<br />
&emsp;&emsp;- 老生代分区（Old Heap Region，OHR）<br />
&emsp;&emsp;其中新生代分区又可以分为Eden和Survivor；大对象分区又可以分为：大对象头分区和大对象连续分区。<br />
&emsp;&emsp;每一个分区都对应一个分区类型，在代码中常见的is_young、is_old、is_houmongous等判断分区类型的函数都是基于上述的分区类型实现，关于分区类型代码如下所示：</p>
<pre><code class="language-c++">hotspot/src/share/vm/gc_implementation/g1/heapRegionType.hpp
// 0000 0 [ 0] Free
// 
// 0001 0      Young Mask
// 0001 0 [ 2] Eden
// 0001 1 [ 3] Survivor
// 
// 0010 0      Humongous Mask
// 0010 0 [ 4] Humongous Starts
// 0010 1 [ 5] Humongous Continues
// 
// 01000 [ 8] Old</code></pre>
<p>&emsp;&emsp;在G1中每个分区的大小都是相同的。该如何设置HR的大小？设置HR的大小有哪些考虑？<br />
在G1中每个分区的大小都是相同的。该如何设置HR的大小？设置HR的大小有哪些考虑？</p>
<p>&emsp;&emsp;HR的大小直接影响分配和垃圾回收效率。如果过大，一个HR可以存放多个对象，分配效率高，但是回收的时候花费时间过长；如果太小则导致分配效率低下。为了达到分配效率和清理效率的平衡，HR有一个上限值和下限值，目前上限是32MB，下限是1MB（为了适应更小的内存分配，下限可能会被修改，在目前的版本中HR的大小只能为1MB、2MB、4MB、8MB、16MB和32MB），默认情况下，整个堆空间分为2048个HR（该值可以自动根据最小的堆分区大小计算得出）。HR大小可由以下方式确定：<br />
&emsp;&emsp;- 可以通过参数G1HeapRegionSize来指定大小，这个参数的默认值为0。<br />
&emsp;&emsp;- 启发式推断，即在不指定HR大小的时候，由G1启发式地推断HR大小。<br />
&emsp;&emsp;HR启发式推断根据堆空间的最大值和最小值以及HR个数进行推断，设置Initial HeapSize（默认为0）等价于设置Xms，设置MaxHeapSize（默认为96MB）等价于设置Xmx。堆分区默认大小的计算方式在HeapRegion.cpp中的setup_heap_region_size()，-代码如下所示：</p>
<pre><code class="language-c++">hotspot/src/share/vm/gc_implementation/g1/heapRegion.cpp
void HeapRegion::setup_heap_region_size(...) {
  /*判断是否是设置过堆分区大小，如果有则使用；没有，则根据初始内存和最大分配内存，获得平均值，并根据HR的个数得到分区的大小，和分区的下限比较，取两者的最大值。*/
  uintx region_size = G1HeapRegionSize;
  if (FLAG_IS_DEFAULT(G1HeapRegionSize)) {
    size_t average_heap_size = (initial_heap_size + max_heap_size) / 2;
    region_size = MAX2(average_heap_size / HeapRegionBounds::target_number(),
                      (uintx) HeapRegionBounds::min_size());
  }
  // 对region_size按2的幂次对齐，并且保证其落在上下限范围内
  int region_size_log = log2_long((jlong) region_size);
  region_size = ((uintx)1 &lt;&lt; region_size_log);
  // 确保region_size落在[1MB，32MB]之间
  if (region_size &lt; HeapRegionBounds::min_size()) {
    region_size = HeapRegionBounds::min_size();
  } else if (region_size &gt; HeapRegionBounds::max_size()) {
    region_size = HeapRegionBounds::max_size();
  }
  // 根据region_size计算一些变量，如卡表大小
  region_size_log = log2_long((jlong) region_size);
  LogOfHRGrainBytes = region_size_log;
  LogOfHRGrainWords = LogOfHRGrainBytes - LogHeapWordSize;
  GrainBytes = (size_t)region_size;
  GrainWords = GrainBytes &gt;&gt; LogHeapWordSize;
  CardsPerRegion = GrainBytes &gt;&gt; CardTableModRefBS::card_shift;</code></pre>
<p>&emsp;&emsp;按照默认值计算，G1可以管理的最大内存为2048×32MB=64GB。假设设置xms=32G，xmx=128G，则每个堆分区的大小为32M，分区个数动态变化范围从1024到4096个。<br />
&emsp;&emsp;G1中大对象不使用新生代空间，直接进入老生代，那么多大的对象能称为大对象？简单来说是region_size的一半。</p>
<h2>新生代大小</h2>
<p>&emsp;&emsp;新生代大小指的是新生代内存空间的大小，前面提到G1中新生代大小按分区组织，即首先计算整个新生代的大小，然后根据上一节中的计算方法计算得到分区大小，两者相除得到需要多少个分区。G1中与新生代大小相关的参数设置和其他GC算法类似，G1中还增加了两个参数G1MaxNewSizePercent和G1NewSizePercent用于控制新生代的大小，整体逻辑如下：<br />
&emsp;&emsp; - 如果设置新生代最大值（MaxNewSize）和最小值（NewSize），可以根据这些值计算新生代包含的最大的分区和最小的分区；注意Xmn等价于设置了MaxNewSize和NewSize，且NewSize=MaxNewSize。<br />
&emsp;&emsp; - 如果既设置了最大值或者最小值，又设置了NewRatio，则忽略NewRatio。<br />
&emsp;&emsp; - 如果没有设置新生代最大值和最小值，但是设置了NewRatio，则新生代的最大值和最小值是相同的，都是整个堆空间/（NewRatio+1）。<br />
&emsp;&emsp; - 如果没有设置新生代最大值和最小值，或者只设置了最大值和最小值中的一个，那么G1将根据参数G1MaxNewSizePercent（默认值为60）和G1NewSizePercent（默认值为5）占整个堆空间的比例来计算最大值和最小值。</p>
<p>&emsp;&emsp;如果G1推断出最大值和最小值相等，则说明新生代不会动态变化。不会动态变化意味着G1在后续对新生代垃圾回收的时候可能不能满足期望停顿的时间。</p>
<p>&emsp;&emsp;新生代大小相关的代码如下所示：</p>
<pre><code class="language-c++">hotspot/src/share/vm/gc_implementation/g1/g1CollectorPolicy.cpp
// 初始化新生代大小参数，根据不同的JVM参数判断计算新生代大小，供后续使用
G1YoungGenSizer::G1YoungGenSizer() : _sizer_kind(SizerDefaults), _adaptive_
  size(true),  _min_desired_young_length(0), _max_desired_young_length(0) {
  // 如果设置NewRatio且同时设置NewSize或MaxNewSize的情况下，则NewRatio被忽略
  if (FLAG_IS_CMDLINE(NewRatio)) {
    if (FLAG_IS_CMDLINE(NewSize) || FLAG_IS_CMDLINE(MaxNewSize)) {
      warning("-XX:NewSize and -XX:MaxNewSize override -XX:NewRatio");
    } else {
      _sizer_kind = SizerNewRatio;
      _adaptive_size = false;
      return;
    }
  }
  // 参数传递有问题，最小值大于最大值
  if (NewSize &gt; MaxNewSize) {
    if (FLAG_IS_CMDLINE(MaxNewSize)) {
      warning("…”);
    }
    MaxNewSize = NewSize;
  }
  // 根据参数计算分区的个数
  if (FLAG_IS_CMDLINE(NewSize)) {
    _min_desired_young_length = MAX2((uint) (NewSize / HeapRegion:: 
      GrainBytes), 1U);
    if (FLAG_IS_CMDLINE(MaxNewSize)) {
      _max_desired_young_length = MAX2((uint) (MaxNewSize / HeapRegion:: 
        GrainBytes), 1U);
      _sizer_kind = SizerMaxAndNewSize;
      _adaptive_size = _min_desired_young_length == _max_desired_young_length;
    } else {
      _sizer_kind = SizerNewSizeOnly;
    }
  } else if (FLAG_IS_CMDLINE(MaxNewSize)) {
    _max_desired_young_length =  MAX2((uint) (MaxNewSize / HeapRegion:: 
      GrainBytes), 1U);
    _sizer_kind = SizerMaxNewSizeOnly;
  }
}
// 使用G1NewSizePercent来计算新生代的最小值
uint G1YoungGenSizer::calculate_default_min_length(uint new_number_of_heap_
  regions) {
  uint default_value = (new_number_of_heap_regions * G1NewSizePercent) / 100;
  return MAX2(1U, default_value);
}
// 使用G1MaxNewSizePercent来计算新生代的最大值
uint G1YoungGenSizer::calculate_default_max_length(uint new_number_of_heap_
  regions) {
  uint default_value = (new_number_of_heap_regions * G1MaxNewSizePercent) / 100;
  return MAX2(1U, default_value);
}
/*这里根据不同的参数输入来计算大小。recalculate_min_max_young_length在初始化时被调用，在堆空间改变时也会被调用。*/
void G1YoungGenSizer::recalculate_min_max_young_length(uint number_of_heap_
  regions, uint* min_young_length, uint* max_young_length) {
  assert(number_of_heap_regions &gt; 0, "Heap must be initialized");
  switch (_sizer_kind) {
    case SizerDefaults:
      *min_young_length = calculate_default_min_length(number_of_heap_regions);
      *max_young_length = calculate_default_max_length(number_of_heap_regions);
      break;
    case SizerNewSizeOnly:
      *max_young_length = calculate_default_max_length(number_of_heap_regions);
      *max_young_length = MAX2(*min_young_length, *max_young_length);
      break;
    case SizerMaxNewSizeOnly:
      *min_young_length = calculate_default_min_length(number_of_heap_regions);
      *min_young_length = MIN2(*min_young_length, *max_young_length);
      break;
    case SizerMaxAndNewSize:
      // Do nothing. Values set on the command line, don't update them at runtime.
      break;
    case SizerNewRatio:
      *min_young_length = number_of_heap_regions / (NewRatio + 1);
      *max_young_length = *min_young_length;
      break;
    default:
      ShouldNotReachHere();
  }
}</code></pre>
<p>&emsp;&emsp;如果G1是启发式推断新生代的大小，那么当新生代变化时该如何实现？简单地说，使用一个分区列表，扩张时如果有空闲的分区列表则可以直接把空闲分区加入到新生代分区列表中，如果没有的话则分配新的分区然后把它加入到新生代分区列表中。G1有一个线程专门抽样处理预测新生代列表的长度应该多大，并动态调整。</p>
<p>&emsp;&emsp;另外还有一个问题，就是分配新的分区时，何时扩展？一次扩展多少内存？</p>
<p>&emsp;&emsp;G1是自适应扩展内存空间的。参数 -XX:GCTimeRatio 表示GC与应用的耗费时间比，G1中默认为9，计算方式为_gc_overhead_perc=100.0×(1.0/(1.0+GCTimeRatio))，即G1 GC时间与应用时间占比不超过10%时不需要动态扩展，当GC时间超过这个阈值的10%，可以动态扩展。扩展时有一个参数G1ExpandByPercentOfAvailable（默认值是20）来控制一次扩展的比例，即每次都至少从未提交的内存中申请20%，有下限要求（一次申请的内存不能少于1M，最多是当前已分配的一倍），代码如下所示：</p>
<pre><code class="language-c++">size_t G1CollectorPolicy::expansion_amount() {
  // 先根据历史信息获取平均GC时间
  double recent_gc_overhead = recent_avg_pause_time_ratio() * 100.0;
  double threshold = _gc_overhead_perc;
  /* G1 GC时间与应用时间占比超过阈值才需要动态扩展，这个阈值的值为_gc_overhead_perc = 100.0 × (1.0 / (1.0 + GCTimeRatio))，上文提到GCTimeRatio=9，即超过10%才会扩张内存*/
  if (recent_gc_overhead &gt; threshold) {
    const size_t min_expand_bytes = 1*M;
    size_t reserved_bytes = _g1-&gt;max_capacity();
    size_t committed_bytes = _g1-&gt;capacity();
    size_t uncommitted_bytes = reserved_bytes - committed_bytes;
    size_t expand_bytes;
    size_t expand_bytes_via_pct = uncommitted_bytes * G1ExpandByPercentOfAvailable / 100;
    expand_bytes = MIN2(expand_bytes_via_pct, committed_bytes);
    expand_bytes = MAX2(expand_bytes, min_expand_bytes);
    expand_bytes = MIN2(expand_bytes, uncommitted_bytes);
    ……
    return expand_bytes;
  } else {
    return 0;
  }
}</code></pre>
<h1>G1停顿预测模型</h1>
<p>&emsp;&emsp;G1是一个响应时间优先的GC算法，用户可以设定整个GC过程的期望停顿时间，由参数MaxGCPauseMillis控制，默认值200ms。不过它不是硬性条件，只是期望值，G1会努力在这个目标停顿时间内完成垃圾回收的工作，但是它不能保证，即也可能完不成（比如我们设置了太小的停顿时间，新生代太大等）。<br />
&emsp;&emsp;那么G1怎么满足用户的期望呢？就需要停顿预测模型了。G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的堆分区数量（即选择收集哪些内存空间），从而尽量满足用户设定的目标停顿时间。如使用过去10次垃圾回收的时间和回收空间的关系，根据目前垃圾回收的目标停顿时间来预测可以收集多少的内存空间。比如最简单的办法是使用算术平均值建立一个线性关系来预测。如过去10次一共收集了10GB的内存，花费了1s，那么在200ms的停顿时间要求下，最多可以收集2GB的内存空间。G1的预测逻辑是基于衰减平均值和衰减标准差。</p>
<p>&emsp;&emsp;衰减平均（Decaying Average）是一种简单的数学方法，用来计算一个数列的平均值，核心是给近期的数据更高的权重，即强调近期数据对结果的影响。衰减平均计算公式如下所示：<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202403/96931711640728.jpg"><img src="https://www.chen0adapter.top/content/uploadfile/202403/96931711640728.jpg" alt="" /></a><br />
&emsp;&emsp;式中α为历史数据权值，1-α为最近一次数据权值。即α越小，最新的数据对结果影响越大，最近一次的数据对结果影响最大。不难看出，其实传统的平均就是α取值为(n-1)/n的情况。<br />
&emsp;&emsp;同理，衰减方差的定义如下：<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202403/f7a41711640779.jpg"><img src="https://www.chen0adapter.top/content/uploadfile/202403/thum-f7a41711640779.jpg" alt="" /></a><br />
&emsp;&emsp;停顿预测模型是以衰减标准差为理论基础实现的，代码如下所示：</p>
<pre><code class="language-c++">hotspot/src/share/vm/gc_implementation/g1/g1CollectorPolicy.hpp
double get_new_prediction(TruncatedSeq* seq) {
  return MAX2(seq-&gt;davg() + sigma() * seq-&gt;dsd(),
              seq-&gt;davg() * confidence_factor(seq-&gt;num()));
}</code></pre>
<p>&emsp;&emsp;在这个预测计算公式中：<br />
&emsp;&emsp;- davg表示衰减均值。<br />
&emsp;&emsp;- sigma()返回一个系数，来自G1ConfidencePercent（默认值为50，sigma为0.5）的配置，表示信赖度。<br />
&emsp;&emsp;- dsd表示衰减标准偏差。<br />
&emsp;&emsp;- confidence_factor表示可信度相关系数，confidence_factor当样本数据不足时（小于5个）取一个大于1的值，并且样本数据越少该值越大。当样本数据大于5时confidence_factor取值为1。这是为了弥补样本数据不足，起到补偿作用。<br />
&emsp;&emsp;- 方法的参数TruncateSeq，顾名思义，是一个截断的序列，它只跟踪序列中最新的n个元素。在G1 GC过程中，每个可测量的步骤花费的时间都会记录到TruncateSeq（继承了AbsSeq）中，用来计算衰减均值、衰减变量、衰减标准偏差等，代码如下所示:</p>
<pre><code class="language-c++">hotspot/src/share/vm/utilities/numberSeq.cpp
void AbsSeq::add(double val) {
  if (_num == 0) {
    // 初始时，还没有历史数据，davg就是当前参数，dvar设置为0
    _davg = val;
    _dvariance = 0.0;
  } else {
    _davg = (1.0 - _alpha) * val + _alpha * _davg;
    double diff = val - _davg;
    _dvariance = (1.0 - _alpha) * diff * diff + _alpha * _dvariance;
  }
}</code></pre>
<p>&emsp;&emsp;这个add方法就是上面两个衰减公式的实现代码。其中_davg为衰减均值，_dvariance为衰减方差，_alpha默认值为0.7。G1的软实时停顿就是通过这样的预测模型来实现的。</p>]]></description>
    <pubDate>Thu, 27 Oct 2022 23:16:00 +0800</pubDate>
    <dc:creator>林立</dc:creator>
    <guid>https://www.chen0adapter.top/?post=23</guid>
</item>
<item>
    <title>JVM探索-2.G1垃圾回收器</title>
    <link>https://www.chen0adapter.top/?post=31</link>
    <description><![CDATA[<p>&emsp;&emsp;G1垃圾回收器为什么名字叫作Garbage First呢？G1是一个并行回收器，它将堆内存分割为许多不相关的区间，每个区间可以术语老年代或者年轻代，并且每个年龄代区间可以是物理上不连续的。老年代区间这个设计理念本就是为了服务于并行后台线程，这些线程的主要工作就是寻找未被引用的对象（未被引用的对象即可被回收），而这样就会产生一种现象，即某些区间的垃圾多于其它区间。垃圾回收时需要暂停应用程序，不然无法防止应用程序干扰，G1 GC可以集中精力在垃圾最多的区间上，并且只花费极少部分时间即可清空这些区间的垃圾，腾出完全空闲的区间，所以G1的名字由来就是：垃圾优先（Garbage First）。<br />
&emsp;&emsp;G1 内部主要有四个操作阶段，即：</p>
<ul>
<li>&emsp;&emsp;年轻代回收（A Young Collection）</li>
<li>&emsp;&emsp;运行在后代的并行循环（A Background，Concurrent Cycle）</li>
<li>&emsp;&emsp;混合回收（A Mixed Collection）</li>
<li>&emsp;&emsp;全量回收（Full GC）</li>
</ul>]]></description>
    <pubDate>Thu, 27 Oct 2022 15:59:00 +0800</pubDate>
    <dc:creator>林立</dc:creator>
    <guid>https://www.chen0adapter.top/?post=31</guid>
</item>
<item>
    <title>JVM探索-1.JVM垃圾回收器及术语</title>
    <link>https://www.chen0adapter.top/?post=24</link>
    <description><![CDATA[<h2>并行</h2>
<p>&emsp;&emsp;指<strong>两个或者多个事件在同一时刻发生</strong>，在现代计算机中通常指多台处理器上同时处理多个任务。</p>
<h2>并发</h2>
<p>&emsp;&emsp;指两个或多个事件在同一时间间隔内发生，在现代计算机中一台处理器“同时”处理多个任务，那么这些任务只能交替运行，从处理器的角度上看任务只能串行执行，从用户的角度看这些任务“并行”执行，实际上是处理器根据一定的策略不断地切换执行这些“并行”的任务。<br />
&emsp;&emsp;<strong>即并发是在宏观上并行，在微观上切换执行。</strong></p>
<h2>JVM中的并行</h2>
<p>&emsp;&emsp;指多个垃圾回收相关线程在操作系统之上并发运行，这里的并行强调的是只有垃圾回收线程工作，程序线程都暂停执行，因此ParNew工作的时候一定发生了STW。...ParTask（如G1ParTask） 指的就是在这些任务运行的时候应用程序都必须暂停。</p>
<h2>JVM中的并发</h2>
<p>&emsp;&emsp;指垃圾回收相关的线程并发运行（如果启动多个线程），这些线程也会和应用程序线程并发运行。...Concurrent...Thread（如ConcurrentG1RefineThread）就是指回收线程和Java应用程序同时运行。</p>
<h1>STW</h1>
<p>&emsp;&emsp;Stop-the-world，直译就是停止一切，在JVM中指停止一切Java应用线程。</p>
<h1>安全点（Safepoint）</h1>
<p>&emsp;&emsp;指JVM在执行一些操作的时需要STW，但并不是任何线程在任何地方都能进入STW，例如我们正在执行一段代码时，线程如何能够停止？设计安全点的目的是，当线程进入到安全点时，线程就会主动停止。</p>
<h1>Mutator</h1>
<p>&emsp;&emsp;在很多英文文献和JVM源码中，经常看到这个单词，它指的是我们的Java应用线程。Mutator的含义是可变的，在这里的含义是因为线程运行，导致了内存的变化。垃圾回收过程中通常需要STW才能使Mutator暂停。</p>
<h1>记忆集</h1>
<p>&emsp;&emsp;简称为RSet。主要记录不同代际对象的引用关系。</p>
<h1>Evacuation</h1>
<p>&emsp;&emsp;转移、撤退或者回收，简称为Evac。在G1回收器中中指的是发现活跃对象，并将对象复制到新地址的过程。</p>
<h1>GC Root</h1>
<p>&emsp;&emsp;垃圾回收的根。在JVM的垃圾回收过程中，需要从GC Root出发标记活跃对象，确保正在使用的对象在垃圾回收后都是存活的。</p>
<h1>根集合（Root Set）。</h1>
<p>&emsp;&emsp;在JVM的垃圾回收过程中，需要从不同的GC Root出发，这些GC Root有线程栈、monitor列表、JNI对象等，而这些GC Root就构成了Root Set。</p>
<h1>Full GC</h1>
<p>&emsp;&emsp;简称为FGC，整个堆的垃圾回收动作。通常Full GC是串行的，G1的Full GC不仅有串行实现，在JDK10及以后还有并行实现。</p>
<h1>再标记（Remark）。</h1>
<p>&emsp;&emsp;指的是并发标记算法中，处理完并发标记后，需要更新并发标记中Mutator变更的引用，这一步需要STW。</p>
<h1>垃圾回收</h1>
<p>&emsp;&emsp;垃圾回收（Garbage Collection，GC）指的是程序不用关心对象在内存中的生存周期，创建后只需要使用对象，不用关心何时释放以及如何释放对象，由JVM自动管理内存并释放这些对象所占用的空间。<br />
&emsp;&emsp;垃圾回收针对的是堆空间，目前垃圾回收算法主要有两类：<br />
&emsp;&emsp;1.引用计数法：在堆内存中分配对象时，会为对象分配一段额外的空间，这个空间用于维护一个计数器，如果对象增加了一个新的引用，则将增加计数器。如果一个引用关系失效则减少计数器。当一个对象的计数器变为0，则说明该对象已经被废弃，处于不活跃状态，可以被回收。引用计数法需要解决循环依赖的问题，比如Python语言里，垃圾回收就使用了引用计数法。<br />
&emsp;&emsp;2.可达性分析法（根引用分析法），基本思路就是将根集合作为起始点，从这些节点开始向下搜索，搜索所走过的路径称为引用链，当一个对象没有被任何引用链访问到时，则证明此对象是不活跃的，可以被回收。<br />
&emsp;&emsp;两种算法各有优缺点，JVM的垃圾回收采用了可达性分析法。<br />
&emsp;&emsp;垃圾回收算法实现主要分为复制（Copy）、标记清除（Mark-Sweep）和标记压缩（Mark-Compact）。<br />
&emsp;&emsp;在回收方法上又可以分为串行回收、并行回收、并发回收。<br />
&emsp;&emsp;在内存管理上可以分为分代管理和非分代管理。</p>
<h1>分代管理</h1>
<p>&emsp;&emsp;分代管理就是把内存划分成不同的区域进行管理，有些对象存活的时间短，有些对象存活的时间长，把存活时间短的对象放在一个区域管理，把存活时间长的对象放在另一个区域管理。那么可以为两个不同的区域选择不同的算法，加快垃圾回收的效率。我们假定内存被划分成2个代：新生代和老生代。把容易死亡的对象放在新生代，通常采用复制算法回收；把预期存活时间较长的对象放在老生代，通常采用标记清除算法。</p>
<h2>复制算法</h2>
<p>&emsp;&emsp;复制算法其实就是在内存间进行分区，此分区的数量可以是两个或多个，根据对象的活跃程度不同将不同分区的活跃对象按照某种计算方式在分区间进行复制。使用两个分区时内存的利用率只有50%；使用多个分区（如3个分区），则可以提高内存的使用率。<br />
&emsp;&emsp;此处演示把堆空间分为1个新生代（分为3个分区：Eden、Survivor0、Survivor1）、1个老生代的收集过程。<br />
&emsp;&emsp;普通对象创建的时候都是放在Eden区，S0和S1分别是两个存活区。第一次垃圾收集前S0和S1都为空，在垃圾收集后，Eden和S0里面的活跃对象（即可以通过根集合到达的对象）都放入了S1区：<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202403/345b1711469526.jpg"><img src="https://www.chen0adapter.top/content/uploadfile/202403/thum-345b1711469526.jpg" alt="" /></a><br />
&emsp;&emsp;回收后Mutator继续运行并产生垃圾，在第二次运行前Eden和S1都有活跃对象，在垃圾收集后，Eden和S1里面的活跃对象（即可以通过根节点到达的对象）都被放入到S0区，一直这样循环收集：<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202403/ea451711469644.jpg"><img src="https://www.chen0adapter.top/content/uploadfile/202403/thum-ea451711469644.jpg" alt="" /></a><br />
&emsp;&emsp;满足最活跃条件（即存活次数最多）的对象晋升老生代区。</p>
<h2>标记清除</h2>
<p>&emsp;&emsp;从根集合出发，遍历对象，把活跃对象入栈，并依次处理。处理方式可以是广度优先搜索也可以是深度优先搜索（通常使用深度优先搜索，节约内存）。标记出活跃对象之后，就可以把不活跃对象清除。<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202403/5f941711469828.jpg"><img src="https://www.chen0adapter.top/content/uploadfile/202403/thum-5f941711469828.jpg" alt="" /></a><br />
&emsp;&emsp;这里仅仅演示了如何找到对象，没有进一步介绍找到对象后如何处理。对于标记清除算法其实还需要额外的数据结构（比如一个链表）来记录可用空间，在对象分配的时候从这个链表中寻找能够容纳对象的空间。当然这里还有很多细节都未涉及，比如在分配时如何找到最合适的内存空间，有First Fit、Best Fit和Worst Fit等方法，这里不再赘述。</p>
<h2>标记压缩</h2>
<p>&emsp;&emsp;标记压缩算法是为了解决标记清除算法中使内存碎片化的问题，除了上述的标记动作之外，还会把活跃对象重新整理从头开始排列，减少内存碎片。</p>
<h1>JVM垃圾回收</h1>
<p>&emsp;&emsp;为了达到最大性能，基于分代管理和回收算法，结合回收的时机，JVM实现了垃圾回收器：串行回收、并行回收、并发标记回收（CMS）和垃圾优先回收。</p>
<h2>串行回收</h2>
<p>&emsp;&emsp;串行回收使用单线程进行垃圾回收，在回收的时候Mutator需要STW。新生代通常采用复制算法，老生代通常采用标记压缩算法。<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202403/efc81711470934.jpg"><img src="https://www.chen0adapter.top/content/uploadfile/202403/efc81711470934.jpg" alt="" /></a></p>
<h2>并行回收</h2>
<p>&emsp;&emsp;并行回收使用多线程进行垃圾回收，在回收的时候Mutator需要暂停，新生代通常采用复制算法，老生代通常采用标记压缩算法。<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202403/b5791711471085.jpg"><img src="https://www.chen0adapter.top/content/uploadfile/202403/b5791711471085.jpg" alt="" /></a></p>
<h2>并发标记回收</h2>
<p>&emsp;&emsp;并发标记回收（CMS）的整个回收期间划分成多个阶段：初始标记、并发标记、重新标记、并发清除等。在初始标记和重新标记阶段需要暂停Mutator，在并发标记和并发清除期间可以和Mutator并发运行。这个算法通常适用于老生代，新生代可以采用并行回收。<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202403/acca1711471165.jpg"><img src="https://www.chen0adapter.top/content/uploadfile/202403/thum-acca1711471165.jpg" alt="" /></a></p>
<h2>垃圾优先回收</h2>
<p>&emsp;&emsp;垃圾优先回收器（Garbage-First，也称为G1）从JDK7 Update 4开始正式提供。G1致力于在多CPU和大内存服务器上对垃圾回收提供软实时目标和高吞吐量。G1垃圾回收器的设计和前3种回收器都不一样，它们在并行、串行以及CMS GC针对堆空间的管理方式上都是连续的。<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202403/3b581711471411.jpg"><img src="https://www.chen0adapter.top/content/uploadfile/202403/thum-3b581711471411.jpg" alt="" /></a><br />
&emsp;&emsp;连续的内存将导致垃圾回收时收集时间过长，停顿时间不可控。因此G1将堆拆成一系列的分区（Heap Region），这样在一个时间段内，大部分的垃圾收集操作只针对一部分分区，而不是整个堆或整个（老生）代。<br />
<a href="https://www.chen0adapter.top/content/uploadfile/202403/af3e1711471438.jpg"><img src="https://www.chen0adapter.top/content/uploadfile/202403/thum-af3e1711471438.jpg" alt="" /></a><br />
&emsp;&emsp;在G1里，新生代就是一系列的内存分区，这意味着不用再要求新生代是一个连续的内存块。类似地，老生代也是由一系列的分区组成。这样也就不需要在JVM运行时考虑哪些分区是老生代，哪些是新生代。<br />
&emsp;&emsp;事实上，G1通常的运行状态是：映射G1分区的虚拟内存随着时间的推移在不同的代之间切换。例如一个G1分区最初被指定为新生代，经过一次新生代的回收之后，会将整个新生代分区都划入未使用的分区中，那它可以作为新生代分区使用，也可以作为老生代分区使用。很可能在完成一个新生代收集之后，一个新生代的分区在未来的某个时刻可用于老生代分区。同样，在一个老生代分区完成收集之后，它就成为了可用分区，在未来某个时候可作为一个新生代分区来使用。<br />
&emsp;&emsp;G1新生代的收集方式是并行收集，采用复制算法。与其他JVM垃圾回收器一样，一旦发生一次新生代回收，整个新生代都会被回收，这也就是我们常说的新生代回收（Young GC）。但是G1和其他垃圾回收器不同的地方在于：<strong>G1会根据预测时间动态改变新生代的大小</strong>。其他垃圾回收新生代的大小也可以动态变化，但这个变化主要是根据内存的使用情况进行的。G1中则是以预测时间为导向，根据内存的使用情况调整新生代分区的数目。<br />
&emsp;&emsp;G1老生代的垃圾回收方式与其他JVM垃圾回收器对老生代处理有着极大的不同。G1老生代的收集不会为了释放老生代的空间对整个老生代做回收。相反，在任意时刻只有一部分老生代分区会被回收，并且，这部分老生代分区将在下一次增量回收时与所有的新生代分区一起被收集。这就是我们所说的混合回收（Mixed GC）。在选择老生代分区的时候，优先考虑垃圾多的分区，这也正是垃圾优先这个名字的由来。<br />
&emsp;&emsp;G1老生代的垃圾回收方式与其他JVM垃圾回收器对老生代处理有着极大的不同。G1老生代的收集不会为了释放老生代的空间对整个老生代做回收。相反，在任意时刻只有一部分老生代分区会被回收，并且，这部分老生代分区将在下一次增量回收时与所有的新生代分区一起被收集。这就是我们所说的混合回收（Mixed GC）。在选择老生代分区的时候，优先考虑垃圾多的分区，这也正是垃圾优先这个名字的由来。<br />
&emsp;&emsp;从实现角度来看，G1算法是复合算法，吸收了以下算法的优势：<br />
&emsp;&emsp;1.列车算法，对内存进行多分区。<br />
&emsp;&emsp;2.CMS，对分区进行并发标记。<br />
&emsp;&emsp;3.最老优先，最老的数据（通常也是垃圾）优先收集。</p>]]></description>
    <pubDate>Tue, 25 Oct 2022 23:00:00 +0800</pubDate>
    <dc:creator>林立</dc:creator>
    <guid>https://www.chen0adapter.top/?post=24</guid>
</item></channel>
</rss>