周梦康

周梦康 查看完整档案

北京编辑沈阳航空航天大学  |  安全工程 编辑阿里巴巴  |  技术专家 编辑 mengkang.net 编辑
编辑
  1. 高质量的付费直播 点击查看 跳槽加薪必看
  2. 关注微博 第一时间免费直播通知
  3. 实在不想付费,可以看我博客 https://mengkang.net

个人动态

周梦康 关注了用户 · 2020-10-26

程序员cxuan @cxuan

Java后端开发,欢迎关注个人微信公众号 Java建设者 及时关注最新技术文章。

关注 7319

周梦康 赞了文章 · 2020-06-25

你不知道的 Web Workers (上)[7.8K 字 | 多图预警]

阅读完本文你将学到以下知识:

  • 进程与线程的区别:进程与线程的概念及单线程与多线程;
  • 浏览器内核的相关知识:GUI 渲染线程、JavaScript 引擎线程、事件触发线程等;
  • Web Workers 是什么:Web Workers 的限制与能力及主线程与 Web Workers 之间如何通信;
  • Web Workers 的分类:Dedicated Worker、Shared Worker 和 Service Workers;
  • Web Workers API:Worker 构造函数及如何观察 Dedicated Worker 等。

下面我们开始步入正题,为了让大家能够更好地理解和掌握 Web Workers,在正式介绍 Web Workers 之前,我们先来介绍一些与 Web Workers 相关的基础知识。

一、进程与线程的区别

在介绍进程与线程的概念前,我们先来看个进程与线程之间关系形象的比喻:

如上图所示,进程是一个工厂,它有独立的资源,线程是工厂中的工人,多个工人协作完成任务,工人之间共享工厂内的资源,比如工厂内的食堂或餐厅。此外,工厂(进程)与工厂(进程)之间是相互独立的。为了让大家能够更直观地理解进程与线程的区别,我们继续来看张图:

由上图可知,操作系统会为每个进程分配独立的内存空间,一个进程由一个或多个线程组成,同个进程下的各个线程之间共享程序的内存空间。相信通过前面两张图,小伙伴们对进程和线程之间的区别已经有了一定的了解,那么实际情况是不是这样呢?这里我们打开 macOS 操作系统下的活动监视器,来看一下写作本文时所有进程的状态:

通过上图可知,我们常用的软件,比如微信和搜狗输入法都是一个独立的进程,拥有不同的 PID(进程 ID),而且图中的每个进程都含有多个线程,以微信进程为例,它就含有 36 个线程。那么什么是进程和线程呢?下面我们来介绍进程和线程的概念。

1.1 进程的概念

进程(英语:process),是指计算机中已运行的程序。进程曾经是分时系统的基本运作单位。在面向进程设计的系统(如早期的 UNIX,Linux 2.4 及更早的版本)中,进程是程序的基本执行实体;在面向线程设计的系统(如当代多数操作系统、Linux 2.6 及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。

程序本身只是指令、数据及其组织形式的描述,进程才是程序的真正运行实例。若干进程有可能与同一个程序相关系,且每个进程皆可以同步或异步的方式独立运行。现代计算机系统可在同一段时间内以进程的形式将多个程序加载到存储器中,并借由时间共享(或称时分复用),以在一个处理器上表现出同时运行的感觉。

1.2 线程的概念

线程(英语:thread)是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如 Win32 线程;由用户进程自行调度的用户线程,如 Linux 平台的 POSIX Thread;或者由内核与用户进程,如 Windows 7 的线程,进行混合调度。

同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。 但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。一个进程可以有很多线程,每条线程并行执行不同的任务。

1.3 单线程与多线程

如果一个进程只有一个线程,我们称之为单线程。单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。单线程处理的优点:同步应用程序的开发比较容易,但由于需要在上一个任务完成后才能开始新的任务,所以其效率通常比多线程应用程序低。

如果完成同步任务所用的时间比预计时间长,应用程序可能会不响应。针对这个问题,我们可以考虑使用多线程,即在进程中使用多个线程,这样就可以处理多个任务。

对于 Web 开发者熟悉的 JavaScript 来说,它运行在浏览器中,是单线程的,每个窗口一个 JavaScript 线程,既然是单线程的,在某个特定的时刻,只有特定的代码能够被执行,其它的代码会被阻塞。

JS 中其实是没有线程概念的,所谓的单线程也只是相对于多线程而言。JS 的设计初衷就没有考虑这些,针对 JS 这种不具备并行任务处理的特性,我们称之为 “单线程”。 —— 来自知乎 “如何证明 JavaScript 是单线程的?” @云澹的回答

其实在浏览器内核(渲染进程)中除了 JavaScript 引擎线程之外,还含有 GUI 渲染线程、事件触发线程、定时触发器线程等。因此对于浏览器的渲染进程来说,它是多线程的。接下来我们来简单介绍浏览器内核。

二、浏览器内核

浏览器最核心的部分是 “Rendering Engine”,即 “渲染引擎”,不过我们一般习惯将之称为 “浏览器内核”。 它主要包括以下线程:

下面我们来分别介绍渲染进程中的每个线程。

2.1 GUI 渲染线程

GUI 渲染线程负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行。

2.2 JavaScript 引擎线程

JavaScript 引擎线程负责解析 JavaScript 脚本并运行相关代码。 JavaScript 引擎一直等待着任务队列中任务的到来,然后进行处理,一个Tab页(Renderer 进程)中无论什么时候都只有一个 JavaScript 线程在运行 JavaScript 程序。

需要注意的是,GUI 渲染线程与 JavaScript 引擎线程是互斥的,所以如果 JavaScript 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染被阻塞。

2.3 事件触发线程

当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JavaScript 引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX 异步请求等,但由于 JavaScript 引擎是单线程的,所有这些事件都得排队等待 JavaScript 引擎处理。

2.4 定时触发器线程

浏览器定时计数器并不是由 JavaScript 引擎计数的,这是因为 JavaScript 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确,所以通过单独线程来计时并触发定时是更为合理的方案。我们日常开发中常用的 setInterval 和 setTimeout 就在该线程中。

2.5 Http 异步请求线程

在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理。

前面我们已经知道了,由于 JavaScript 引擎与 GUI 渲染线程是互斥的,如果 JavaScript 引擎执行了一些计算密集型或高延迟的任务,那么会导致 GUI 渲染线程被阻塞或拖慢。那么如何解决这个问题呢?嘿嘿,当然是使用本文的主角 —— Web Workers。

三、Web Workers 是什么

Web Worker 是 HTML5 标准的一部分,这一规范定义了一套 API,它允许一段 JavaScript 程序运行在主线程之外的另外一个线程中。Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。

在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,可以在独立线程中处理一些计算密集型或高延迟的任务,从而允许主线程(通常是 UI 线程)不会因此被阻塞或拖慢。

(图片来源:https://thecodersblog.com/web...

3.1 Web Workers 的限制与能力

通常情况下,你可以在 Worker 线程中运行任意的代码,但注意存在一些例外情况,比如:直接在 worker 线程中操纵 DOM 元素,或使用 window 对象中的某些方法和属性。 大部分 window 对象的方法和属性是可以使用的,包括 WebSockets,以及诸如 IndexedDB 和 FireFox OS 中独有的 Data Store API 这一类数据存储机制。

下面我们以 Chrome 和 Opera 所使用的 Blink 渲染引擎为例,介绍该渲染引擎下 Web Worker 中所支持的常用 APIs:

  • Cache:Cache 接口为缓存的 Request / Response 对象对提供存储机制,例如,作为ServiceWorker 生命周期的一部分。
  • CustomEvent:用于创建自定义事件。
  • Fetch:Fetch API 提供了一个获取资源的接口(包括跨域请求)。任何使用过 XMLHttpRequest 的人都能轻松上手,而且新的 API 提供了更强大和灵活的功能集。
  • Promise:Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。
  • FileReader:FileReader 对象允许 Web 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。
  • IndexedDB:IndexedDB 是一种底层 API,用于客户端存储大量结构化数据,包括文件/二进制大型对象(blobs)。
  • WebSocket:WebSocket 对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。
  • XMLHttpRequest:XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。

更多信息请参见: Functions and classes available to workers

3.2 主线程与 Web Workers 之间的通信

主线程和 Worker 线程相互之间使用 postMessage() 方法来发送信息,并且通过 onmessage 这个事件处理器来接收信息。数据的交互方式为传递副本,而不是直接共享数据。主线程与 Worker 线程的交互方式如下图所示:

(图片来源:https://viblo.asia/p/simple-web-workers-workflow-with-webpack-3P0lPkobZox

除此之外,Worker 还可以通过 XMLHttpRequest 来访问网络,只不过 XMLHttpRequest 对象的 responseXMLchannel 这两个属性的值将总是 null

四、Web Workers 的分类

Web Worker 规范中定义了两类工作线程,分别是专用线程 Dedicated Worker 和共享线程 Shared Worker,其中,Dedicated Worker 只能为一个页面所使用,而 Shared Worker 则可以被多个页面所共享。

4.1 Dedicated Worker

一个专用 Worker 仅仅能被生成它的脚本所使用,其浏览器支持情况如下:

(图片来源:https://caniuse.com/#search=Web%20Workers

需要注意的是,由于 Web Worker 有同源限制,所以在进行本地调试或运行以下示例的时候,需要先启动本地服务器,直接使用 file:// 协议打开页面的时候,会抛出以下异常:

Uncaught DOMException: Failed to construct 'Worker': 
Script at 'file:///**/*.js' cannot be accessed from origin 'null'.
4.1.1 专用线程 Dedicated Worker:Ping/Pong

index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>专用线程 Dedicated Worker —— Ping/Pong</title>
  </head>
  <body>
    <h3>阿宝哥:专用线程 Dedicated Worker —— Ping/Pong</h3>
    <script>
      if (window.Worker) {
        let worker = new Worker("dw-ping-pong.js");
        worker.onmessage = (e) =>
          console.log(`Main: Received message - ${e.data}`);
        worker.postMessage("PING");
      } else {
        console.log("呜呜呜,不支持 Web Worker");
      }
    </script>
  </body>
</html>

dw-ping-pong.js

onmessage = (e) => {
  console.log(`Worker: Received message - ${e.data}`);
  postMessage("PONG");
}

以上代码成功运行后,浏览器控制台会输出以下结果:

Worker: Received message - PING
Main: Received message - PONG

每个 Web Worker 都可以创建自己的子 Worker,这允许我们将任务分散到多个线程。创建子 Worker 也很简单,具体我们来看个例子。

4.1.2 专用线程 Dedicated Sub Worker:Ping/Pong

index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>专用线程 Dedicated Sub Worker —— Ping/Pong</title>
  </head>
  <body>
    <h3>阿宝哥:专用线程 Dedicated Sub Worker —— Ping/Pong</h3>
    <script>
      if (window.Worker) {
        let worker = new Worker("dw-ping-pong.js");
        worker.onmessage = (e) =>
          console.log(`Main: Received message - ${e.data}`);
        worker.postMessage("PING");
      } else {
        console.log("呜呜呜,不支持 Web Worker");
      }
    </script>
  </body>
</html>

dw-ping-pong.js

onmessage = (e) => {
  console.log(`Worker: Received message - ${e.data}`);
  setTimeout(() => {
    let worker = new Worker("dw-sub-ping-pong.js");
    worker.onmessage = (e) => console.log(`Worker: Received from sub worker - ${e.data}`);
    worker.postMessage("PING");
  }, 1000);
  postMessage("PONG");
};

dw-sub-ping-pong.js

onmessage = (e) => {
  console.log(`Sub Worker: Received message - ${e.data}`);
  postMessage("PONG");
};

以上代码成功运行后,浏览器控制台会输出以下结果:

Worker: Received message - PING
Main: Received message - PONG
Sub Worker: Received message - PING
Received from sub worker - PONG
4.1.3 专用线程 Dedicated Worker:importScripts

其实在 Web Worker 中,我们也可以使用 importScripts 方法将一个或多个脚本同步导入到 Web Worker 的作用域中。同样我们来举个例子。

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>专用线程 Dedicated Worker —— importScripts</title>
  </head>
  <body>
    <h3>阿宝哥:专用线程 Dedicated Worker —— importScripts</h3>
    <script>
      let worker = new Worker("worker.js");
      worker.onmessage = (e) => console.log(`Main: Received kebab case message - ${e.data}`);
      worker.postMessage(
        "Hello, My name is semlinker."
      );
    </script>
  </body>
</html>

worker.js

importScripts("https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.15/lodash.min.js");

onmessage = ({ data }) => {
  postMessage(_.kebabCase(data));
};

以上代码成功运行后,浏览器控制台会输出以下结果:

Main: Received kebab case message - hello-my-name-is-semlinker
4.1.4 专用线程 Dedicated Worker:inline-worker

在前面的例子中,我们都是使用外部的 Worker 脚本来创建 Web Worker 对象。其实你也可以通过 Blob URL 或 Data URL 的形式来创建 Web Worker,这类 Worker 也被称为 Inline Worker。

1. 使用 Blob URL 创建 Inline Worker

Blob URL/Object URL 是一种伪协议,允许 Blob 和 File 对象用作图像,下载二进制数据链接等的 URL 源。在浏览器中,我们使用 URL.createObjectURL 方法来创建 Blob URL,该方法接收一个 Blob 对象,并为其创建一个唯一的 URL,其形式为 blob:<origin>/<uuid>,对应的示例如下:

blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641

浏览器内部为每个通过 URL.createObjectURL 生成的 URL 存储了一个 URL → Blob 映射。因此,此类 URL 较短,但可以访问 Blob。生成的 URL 仅在当前文档打开的状态下才有效。它允许引用 <img><a> 中的 Blob,但如果你访问的 Blob URL 不再存在,则会从浏览器中收到 404 错误。

const url = URL.createObjectURL(
  new Blob([`postMessage("Dedicated Worker created by Blob")`])
);

let worker = new Worker(url);
worker.onmessage = (e) =>
  console.log(`Main: Received message - ${e.data}`);

除了在代码中使用字符串动态创建 Worker 脚本,也可以把 Worker 脚本使用类型为 javascript/workerscript 标签内嵌在页面中,具体如下所示:

<script id="myWorker" type="javascript/worker">
   self['onmessage'] = function(event) {
     postMessage('Hello, ' + event.data.name + '!');
   };
</script>

接着就是通过 script 对象的 textContent 属性来获取对应的内容,然后使用 Blob API 和 createObjectURL API 来最终创建 Web Worker:

<script>
  let workerScript = document.querySelector('#myWorker').textContent;
  let blob = new Blob(workerScript, {type: "text/javascript"});
  let worker = new Worker(URL.createObjectURL(blob));
</script>

2. 使用 Data URL 创建 Inline Worker

Data URLs 由四个部分组成:前缀(data:)、指示数据类型的 MIME 类型、如果非文本则为可选的 base64 标记、数据本身:

data:[<mediatype>][;base64],<data>

mediatype 是个 MIME 类型的字符串,例如 "image/jpeg" 表示 JPEG 图像文件。如果被省略,则默认值为 text/plain;charset=US-ASCII。如果数据是文本类型,你可以直接将文本嵌入(根据文档类型,使用合适的实体字符或转义字符)。如果是二进制数据,你可以将数据进行 base64 编码之后再进行嵌入。

const url = `data:application/javascript,${encodeURIComponent(
  `postMessage("Dedicated Worker created by Data URL")`
)}`;

let worker = new Worker(url);
worker.onmessage = (e) =>
  console.log(`Main: Received message - ${e.data}`);

4.2 Shared Worker

一个共享 Worker 是一种特殊类型的 Worker,可以被多个浏览上下文访问,比如多个 windows,iframes 和 workers,但这些浏览上下文必须同源。相比 dedicated workers,它们拥有不同的作用域。其浏览器支持情况如下:

(图片来源:https://caniuse.com/#search=Web%20Workers

与常规的 Worker 不同,首先我们需要使用 onconnect 方法等待连接,然后我们获得一个端口,该端口是我们与窗口之间的连接。

4.2.1 共享线程 Shared Worker:点赞计数器

index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>共享线程 Shared Worker</title>
  </head>
  <body>
    <h3>阿宝哥:共享线程 Shared Worker</h3>
    <button id="likeBtn">点赞</button>
    <p>阿宝哥一共收获了<span id="likedCount">0</span>个👍</p>
    <script>
      let likes = 0;
      let likeBtn = document.querySelector("#likeBtn");
      let likedCountEl = document.querySelector("#likedCount");

      let worker = new SharedWorker("shared-worker.js");
      worker.port.start();

      likeBtn.addEventListener("click", function () {
        worker.port.postMessage("like");
      });

      worker.port.onmessage = function (val) {
        likedCountEl.innerHTML = val.data;
      };
    </script>
  </body>
</html>

shared-worker.js

let a = 666;

console.log("shared-worker");
onconnect = function (e) {
  var port = e.ports[0];

  port.onmessage = function () {
    port.postMessage(a++);
  };
};

在 Shared Worker 的示例页面上有一个 点赞 按钮,每次点击时点赞数会加 1。首先你新开一个窗口,然后点击几次。然后新开另一个窗口继续点击,这时你会发现当前页面显示的点赞数是基于前一个页面的点赞数继续累加。

4.2.2 调试 Shared Workers

在实际项目开发过程中,若需要调试 Shared Workers 中的脚本,可以通过 chrome://inspect 来进行调试,具体步骤如下图所示:

4.3 Service Workers

Service workers 本质上充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。

(图片来源:https://www.pavlompas.com/blo...

Service workers 的浏览器支持情况如下:

由于 Service workers 不是本文的重点,这里阿宝哥就不展开介绍了,感兴趣的小伙伴请自行了解一下。下面我们开始介绍 Web Workers API。

五、Web Workers API

Worker() 构造函数创建一个 Worker 对象,该对象执行指定的URL脚本。这个脚本必须遵守同源策略 。如果违反同源策略,则会抛出一个 SECURITY_ERR 类型的 DOMException。

5.1 Worker 构造函数

Worker 构造函数的语法为:

const myWorker = new Worker(aURL, options);

相关的参数说明如下:

  • aURL:是一个 DOMString 表示 worker 将执行的脚本的 URL。它必须遵守同源策略。
  • options(可选):包含可在创建对象实例时设置的选项属性的对象。可用属性如下:

    • type:用以指定 Worker 类型的 DOMString 值. 该值可以是 classic 或 module。如果未指定,将使用默认值 classic。
    • credentials:用以指定 worker 凭证的 DOMString 值。该值可以是 omit,same-origin 或 include。如果未指定,或者 type 是 classic,将使用默认值 omit (不要求凭证)。
    • name:在 DedicatedWorkerGlobalScope 的情况下,用来表示 Worker 的 scope 的一个 DOMString 值,主要用于调试目的。

需要注意的是,在创建 Web Worker 的时候,可能会出现以下异常:

  • 当 document 不被允许启动 worker 的时候,将抛出一个 SecurityError 异常。比如:如果提供的 aURL 有语法错误,或者与同源策略相冲突(跨域访问)。
  • 如果 worker 的 MIME 类型不正确,将抛出一个 NetworkError 异常。worker 的 MIME 类型必须是 text/javascript
  • 如果 aURL 无法被解析(格式错误),将抛出一个 SyntaxError 异常。

示例

const worker = new Worker("task.js");

当我们调用 Worker 构造函数后会返回一个 Worker 线程对象,用来供主线程操作 Worker。Worker 线程对象的属性和方法如下:

  • Worker.onerror:指定 error 事件的监听函数。
  • Worker.onmessage:指定 message 事件的监听函数,发送过来的数据在 Event.data 属性中。
  • Worker.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
  • Worker.postMessage():向 Worker 线程发送消息。
  • Worker.terminate():立即终止 Worker 线程。

5.2 Dedicated Worker 示例

下面我们再来举一个 Dedicated Worker 的例子:

index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Dedicated Worker Demo</title>
  </head>
  <body>
    <h3>Dedicated Worker Demo</h3>
    <script>
      const worker = new Worker("task.js");

      worker.postMessage({
        id: 666,
        msg: "Hello Semlinker",
      });

      worker.onmessage = function (message) {
        let data = message.data;
        console.log(`Main: Message from worker ${JSON.stringify(data)}`);
        worker.terminate();
      };

      worker.onerror = function (error) {
        console.log(error.filename, error.lineno, error.message);
      };
    </script>
  </body>
</html>

task.js

而 Dedicated Worker 所执行的代码如下所示:

onmessage = function (message) {
  let data = message.data;
  console.log(`Worker: Message from main thread ${JSON.stringify(data)}`);
  data.msg = "Hi from task.js";
  postMessage(data);
};

以上代码成功运行后,控制台会输出以下结果:

Worker: Message from main thread {"id": 666,"msg": "Hello Semlinker"}
worker-demo.html:20 Main: Message from worker {"id":666, "msg":"Hi from task.js"}

为了让大家更好的理解 Web Worker 的工作流程,我们来了解一下 WebKit 加载并执行 Worker 线程的流程:

(图片来源:http://www.alloyteam.com/2015...

5.3 观察 Dedicated Worker

看到这里相信有些小伙伴会好奇,介绍了那么多 Web Worker 的相关知识,在哪里可以直观地感受到 Web Worker,接下来我们将从以下两个角度来观察它。

5.3.1 开发者工具

这里阿宝哥以 Chrome 浏览器为例,首先打开 Chrome 开发者工具,然后选择 Sources -> Page

5.3.2 Chrome 任务管理器 & 活动监视器

打开 Chrome 任务管理器之后,我们可以找到当前 Tab 页对应的进程 ID,即为 5194,接着我们打开 macOS 下的活动监视器,然后选中 5194 进程,然后对该进程进行取样操作:

取样完成后,可以看到当前渲染进程中完整的线程信息,红框中标出的就是我们想要找的 Dedicated Worker

本来是想一口气写完 “你不知道的 Web Workers”,但考虑到部分小伙伴们的感受,避免出现以下群友提到的情况,阿宝哥决定拆成上下两篇。

下篇阿宝哥将着重介绍 Web Worker 一些常见的使用场景和 Deno Web Workers 的相关实现,感兴趣的小伙们记得持续关注阿宝哥哟。

六、参考资源

七、推荐阅读

查看原文

赞 39 收藏 22 评论 4

周梦康 收藏了文章 · 2020-04-07

pkg-config 学习笔记

术语对照表:
参数 - option
选项 - flag

基本概念

在编译和链接时,提供必要的库文件细节。元数据存储在 pkg-config 文件里面,文件后缀 .pc ,文件需要存放在 pkg-config 工具能够找到的特定位置。

文件内容包括预定义的元数据关键词和形式自由的变量。
示例内容如下:

prefix=/usr/local
exec_prefix=${prefix}
includedir=${prefix}/include
libdir=${exec_prefix}/lib

Name: foo
Description: The foo library
Version: 1.0.0
Cflags: -I${includedir}/foo
Libs: -L${libdir} -lfoo

关键字定义比如 Name:XXX ,后面跟着一个冒号 : 和值。关键字由 pkg-config 工具定义。
变量比如 prifix=XXX ,后面跟着一个等号 = 和值。变量一方面可以简化 .pc 文件的书写,一方面可以存储一些 pkg-config 工具无法覆盖的数据。

下面是对关键字字段的一个简要介绍,更深入的部分见 编写 部分(也就是下一节):

  • Name:库或者包的一个别名,方面人类阅读。pkg-config 工具使用的名字是 .pc 文件的文件名,不会用到这个名字。

  • Description:对包的一个简要描述。

  • URL:人们可以从这个地址下载包,并获得更多有关包的信息。

  • Version:包的版本号

  • Requires:这个包依赖的包的列表。依赖的包的版本号可以通过比较操作符指定(例如 = 、<=)

  • Requires.private:这个包依赖的包的列表。但是目标应用无法使用这些包。

  • Conflicts:可选字段,描述哪些包与此包冲突。

  • Cflags:不支持 pkg-config 的一些编译选项和库依赖。如果依赖的库支持 pkg-config ,那么这个库就应该放到前面的 Requires 或者 Requires.private 字段中去。

  • Libs:不支持 pkg-config 的一些链接选项和库依赖。

  • Libs.private:不支持 pkg-config 的一些用于私有库依赖的链接选项。

编写

在给一个包创建 pkg-config 文件(后面简称为 pc 文件)的时候首先要决定这些文件的发布方式,最好是每个库都有一个对应的 pc 文件,这样每个包至少有跟库的数量一致的 pc 文件。

包的名字由 pc 元数据文件的文件名决定。也就是文件名中除去 .pc 后缀的那部分。一个常见的选择是让库名字和 .pc 文件的名字一致。例如,下载了 libfoo.so 的包应该有一个对应的 libfoo.pc 文件包含相关的 pkg-config 元数据。但是不一定要这要,把pc文件命名为 foo.pc 或者 foolib.pc 也是可行的。

Name、Description、URL字段都是纯信息,很容易填。 Version 字段有一点点麻烦,需要确保该数据能被用户所使用。pkg-config 使用 RPM 的算法来进行版本比较。建议使用通过句点 . 分开的十进制数,如果使用字母的话会出现不可预知的错误。版本号必须单调递增,并且足以制定这个库文件。通常使用包的版本号就足够了,而且这样方面用户进行追踪。

在介绍更有用的字段之前,先介绍一下变量的定义。变量最常见的用途是指定安装路径,使用变量可以使元数据字段保持简洁。变量是递归地进行展开的,因此跟自动生成的路径一起使用十分有用(这句话没看懂)。

prefix=/usr/local
includedir=${prefix}/include

Cflags: -I${includedir}/foo

pkg-config 里面最重要的元数据字段是 Requires、 Requires.private 、 Cflags 、 Libs 和 Libs.private 。它们定义了外部项目编译链接这个库时所需要元数据。

Requires 和 Requires.private 定义了这个库所需的其他模块。通常建议使用 Require 的 private 变体,以避免把不必要的库暴露给用户程序。如果用户程序不会用到依赖库的符号,那么这个库经不应该被直接链接到用户程序上。更详尽的讨论参见 overlinking

因为 pkg-config 总是暴露 Requires 库的链接选项,所以这些模块会成为程序的直接依赖。另一方面,Require.private 中的库在静态链接的时候只会被包含。因此,通常比较合适的做法是仅在 Requires 字段添加同一个包里面的模块。 (老外的写作水平也是堪忧啊,各种 包、模块、库词汇混杂在一起使用。)

Libs 包含使用库所必需的链接选项。此外,Libs 和 Libs.private 还包含 pkg-config 不支持的其他库的链接选项。同样的,推荐把外部库的链接选项放到 Libs.private 字段里面去。

Cflags 包含这个库所必需的编译选项。与 Libs 不同的是这个字段没有 private 变体。因为不管是怎么样的链接场景,数据类型和宏定义都是需要的。

使用

假设系统安装了 .pc 文件,那么 pkg-config 工具就是用来抽取元数据的。执行 pkg-config --help 可以查看其各个参数的简要描述。更详细的描述可以在 pkg-config(1) 手册页中找到。本节会简要介绍一些常用的情况。

假设一个系统有个两个模块,foo 和 bar 。他们的 pc 文件可以是这样:

foo.pc:

prefix=/usr
exec_prefix=${prefix}
includedir=${prefix}/include
libdir=${exec_prefix}/lib

Name: foo
Description: The foo library
Version: 1.0.0
Cflags: -I${includedir}/foo
Libs: -L${libdir} -lfoo

bar.pc:

prefix=/usr
exec_prefix=${prefix}
includedir=${prefix}/include
libdir=${exec_prefix}/lib

Name: bar
Description: The bar library
Version: 2.1.2
Requires.private: foo >= 0.7
Cflags: -I${includedir}
Libs: -L${libdir} -lbar

模块的版本号可以通过 --modversion 参数查看

$ pkg-config --modversion foo
1.0.0
$ pkg-config --modversion bar
2.1.2

每个模块所需要的链接选项可以通过 --libs 参数查看

$ pkg-config --libs foo
-lfoo
$ pkg-config --libs bar
-lbar

注意 pkg-config 隐去了部分 Libs 中的字段。这是因为当它看到 -L 选项时,知道 ${libdir} 路径 /usr/lib 是系统链接器的查找路径。使用 -L 可以避免 pkg-config 干涉链接操作。

而且,尽管 foo 被 bar 所依赖,foo 的链接选项却没有输出。这是因为 foo 并不是用户应用直接需要的模块。而如果要静态链接一个 bar 应用,需要同时设置两个链接选项。

$ pkg-config --libs --static bar
-lbar -lfoo

pkg-config 需要把两个链接选项都输出来,确保静态链接的应用能够找到所有需要的符号。另一方面,它会把所有 Cflags
输出。

$ pkg-config --cflags bar
-I/usr/include/foo
$ pkg-config --cflags --static bar
-I/usr/include/foo

另一个常用的参数 --exists ,可以用来检测一个模块的可用性。

$ pkg-config --exists foo
$ echo $?
0

pkg-config 的一个最优秀的特性是提供了版本检测。它可以检测是否存在满足条件的版本。

$ pkg-config --libs "bar >= 2.7"
Requested 'bar >= 2.7' but version of bar is 2.1.2

有些命令在使用 --print-errors 参数后会进行更详尽的输出。

$ pkg-config --exists --print-errors xoxo
Package xoxo was not found in the pkg-config search path.
Perhaps you should add the directory containing `xoxo.pc'
to the PKG_CONFIG_PATH environment variable
No package 'xoxo' found

上面的输出信息提到了 PKG_CONFIG_PATH 环境变量。这个变量用来增加 pkg-config 的搜索路径。在一个典型的 Unix 系统中,它会搜索 /usr/lib/pkgconfig/usr/share/pkgconfig 。这两个路径可以覆盖系统安装的模块。但是,有些本地模块可能下载到了其他路径中,例如 /usr/local
这种情况下,有必要把这个搜索路径添加进去以便 pkg-config 可以定位 pc 文件。

$ pkg-config --modversion hello
Package hello was not found in the pkg-config search path.
Perhaps you should add the directory containing `hello.pc'
to the PKG_CONFIG_PATH environment variable
No package 'hello' found
$ export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
$ pkg-config --modversion hello
1.0.0

把 pkg-config 模块集成到用户项目中的时候,使用自动配置(autoconf)的宏可以简化这个过程。

  • PKG_PREREQ(MIN-VERSION):确保所使用的自动配置宏高于或等于 MIN-VERSION 。

  • PKG_PROG_PKG_CONFIG([MIN-VERSION]):定位 pkg-config 工具在系统中的位置,并检测其版本的兼容性。

  • PKG_CHECK_EXISTS(MODULES,[ACTION-IF-FOUND],[ACTION-IF-NOT-FOUND]):检测某些模块是否存在。

  • PKG_CHECK_MODULES(VARIABLE-PREFIX,MODULES,[ACTION-IF-FOUND],[ACTION-IF-NOT-FOUND]):检测某些模块是否存在,如果存在,根据 pkg-config --cflags 和 pkg-config --libs 的输出设置 <VARIABLE-PREFIX>_CFLAGS 和 <VARIABLE-PREFIX>_LIBS 。

常见问题

查看原文

周梦康 赞了文章 · 2020-04-07

pkg-config 学习笔记

术语对照表:
参数 - option
选项 - flag

基本概念

在编译和链接时,提供必要的库文件细节。元数据存储在 pkg-config 文件里面,文件后缀 .pc ,文件需要存放在 pkg-config 工具能够找到的特定位置。

文件内容包括预定义的元数据关键词和形式自由的变量。
示例内容如下:

prefix=/usr/local
exec_prefix=${prefix}
includedir=${prefix}/include
libdir=${exec_prefix}/lib

Name: foo
Description: The foo library
Version: 1.0.0
Cflags: -I${includedir}/foo
Libs: -L${libdir} -lfoo

关键字定义比如 Name:XXX ,后面跟着一个冒号 : 和值。关键字由 pkg-config 工具定义。
变量比如 prifix=XXX ,后面跟着一个等号 = 和值。变量一方面可以简化 .pc 文件的书写,一方面可以存储一些 pkg-config 工具无法覆盖的数据。

下面是对关键字字段的一个简要介绍,更深入的部分见 编写 部分(也就是下一节):

  • Name:库或者包的一个别名,方面人类阅读。pkg-config 工具使用的名字是 .pc 文件的文件名,不会用到这个名字。

  • Description:对包的一个简要描述。

  • URL:人们可以从这个地址下载包,并获得更多有关包的信息。

  • Version:包的版本号

  • Requires:这个包依赖的包的列表。依赖的包的版本号可以通过比较操作符指定(例如 = 、<=)

  • Requires.private:这个包依赖的包的列表。但是目标应用无法使用这些包。

  • Conflicts:可选字段,描述哪些包与此包冲突。

  • Cflags:不支持 pkg-config 的一些编译选项和库依赖。如果依赖的库支持 pkg-config ,那么这个库就应该放到前面的 Requires 或者 Requires.private 字段中去。

  • Libs:不支持 pkg-config 的一些链接选项和库依赖。

  • Libs.private:不支持 pkg-config 的一些用于私有库依赖的链接选项。

编写

在给一个包创建 pkg-config 文件(后面简称为 pc 文件)的时候首先要决定这些文件的发布方式,最好是每个库都有一个对应的 pc 文件,这样每个包至少有跟库的数量一致的 pc 文件。

包的名字由 pc 元数据文件的文件名决定。也就是文件名中除去 .pc 后缀的那部分。一个常见的选择是让库名字和 .pc 文件的名字一致。例如,下载了 libfoo.so 的包应该有一个对应的 libfoo.pc 文件包含相关的 pkg-config 元数据。但是不一定要这要,把pc文件命名为 foo.pc 或者 foolib.pc 也是可行的。

Name、Description、URL字段都是纯信息,很容易填。 Version 字段有一点点麻烦,需要确保该数据能被用户所使用。pkg-config 使用 RPM 的算法来进行版本比较。建议使用通过句点 . 分开的十进制数,如果使用字母的话会出现不可预知的错误。版本号必须单调递增,并且足以制定这个库文件。通常使用包的版本号就足够了,而且这样方面用户进行追踪。

在介绍更有用的字段之前,先介绍一下变量的定义。变量最常见的用途是指定安装路径,使用变量可以使元数据字段保持简洁。变量是递归地进行展开的,因此跟自动生成的路径一起使用十分有用(这句话没看懂)。

prefix=/usr/local
includedir=${prefix}/include

Cflags: -I${includedir}/foo

pkg-config 里面最重要的元数据字段是 Requires、 Requires.private 、 Cflags 、 Libs 和 Libs.private 。它们定义了外部项目编译链接这个库时所需要元数据。

Requires 和 Requires.private 定义了这个库所需的其他模块。通常建议使用 Require 的 private 变体,以避免把不必要的库暴露给用户程序。如果用户程序不会用到依赖库的符号,那么这个库经不应该被直接链接到用户程序上。更详尽的讨论参见 overlinking

因为 pkg-config 总是暴露 Requires 库的链接选项,所以这些模块会成为程序的直接依赖。另一方面,Require.private 中的库在静态链接的时候只会被包含。因此,通常比较合适的做法是仅在 Requires 字段添加同一个包里面的模块。 (老外的写作水平也是堪忧啊,各种 包、模块、库词汇混杂在一起使用。)

Libs 包含使用库所必需的链接选项。此外,Libs 和 Libs.private 还包含 pkg-config 不支持的其他库的链接选项。同样的,推荐把外部库的链接选项放到 Libs.private 字段里面去。

Cflags 包含这个库所必需的编译选项。与 Libs 不同的是这个字段没有 private 变体。因为不管是怎么样的链接场景,数据类型和宏定义都是需要的。

使用

假设系统安装了 .pc 文件,那么 pkg-config 工具就是用来抽取元数据的。执行 pkg-config --help 可以查看其各个参数的简要描述。更详细的描述可以在 pkg-config(1) 手册页中找到。本节会简要介绍一些常用的情况。

假设一个系统有个两个模块,foo 和 bar 。他们的 pc 文件可以是这样:

foo.pc:

prefix=/usr
exec_prefix=${prefix}
includedir=${prefix}/include
libdir=${exec_prefix}/lib

Name: foo
Description: The foo library
Version: 1.0.0
Cflags: -I${includedir}/foo
Libs: -L${libdir} -lfoo

bar.pc:

prefix=/usr
exec_prefix=${prefix}
includedir=${prefix}/include
libdir=${exec_prefix}/lib

Name: bar
Description: The bar library
Version: 2.1.2
Requires.private: foo >= 0.7
Cflags: -I${includedir}
Libs: -L${libdir} -lbar

模块的版本号可以通过 --modversion 参数查看

$ pkg-config --modversion foo
1.0.0
$ pkg-config --modversion bar
2.1.2

每个模块所需要的链接选项可以通过 --libs 参数查看

$ pkg-config --libs foo
-lfoo
$ pkg-config --libs bar
-lbar

注意 pkg-config 隐去了部分 Libs 中的字段。这是因为当它看到 -L 选项时,知道 ${libdir} 路径 /usr/lib 是系统链接器的查找路径。使用 -L 可以避免 pkg-config 干涉链接操作。

而且,尽管 foo 被 bar 所依赖,foo 的链接选项却没有输出。这是因为 foo 并不是用户应用直接需要的模块。而如果要静态链接一个 bar 应用,需要同时设置两个链接选项。

$ pkg-config --libs --static bar
-lbar -lfoo

pkg-config 需要把两个链接选项都输出来,确保静态链接的应用能够找到所有需要的符号。另一方面,它会把所有 Cflags
输出。

$ pkg-config --cflags bar
-I/usr/include/foo
$ pkg-config --cflags --static bar
-I/usr/include/foo

另一个常用的参数 --exists ,可以用来检测一个模块的可用性。

$ pkg-config --exists foo
$ echo $?
0

pkg-config 的一个最优秀的特性是提供了版本检测。它可以检测是否存在满足条件的版本。

$ pkg-config --libs "bar >= 2.7"
Requested 'bar >= 2.7' but version of bar is 2.1.2

有些命令在使用 --print-errors 参数后会进行更详尽的输出。

$ pkg-config --exists --print-errors xoxo
Package xoxo was not found in the pkg-config search path.
Perhaps you should add the directory containing `xoxo.pc'
to the PKG_CONFIG_PATH environment variable
No package 'xoxo' found

上面的输出信息提到了 PKG_CONFIG_PATH 环境变量。这个变量用来增加 pkg-config 的搜索路径。在一个典型的 Unix 系统中,它会搜索 /usr/lib/pkgconfig/usr/share/pkgconfig 。这两个路径可以覆盖系统安装的模块。但是,有些本地模块可能下载到了其他路径中,例如 /usr/local
这种情况下,有必要把这个搜索路径添加进去以便 pkg-config 可以定位 pc 文件。

$ pkg-config --modversion hello
Package hello was not found in the pkg-config search path.
Perhaps you should add the directory containing `hello.pc'
to the PKG_CONFIG_PATH environment variable
No package 'hello' found
$ export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
$ pkg-config --modversion hello
1.0.0

把 pkg-config 模块集成到用户项目中的时候,使用自动配置(autoconf)的宏可以简化这个过程。

  • PKG_PREREQ(MIN-VERSION):确保所使用的自动配置宏高于或等于 MIN-VERSION 。

  • PKG_PROG_PKG_CONFIG([MIN-VERSION]):定位 pkg-config 工具在系统中的位置,并检测其版本的兼容性。

  • PKG_CHECK_EXISTS(MODULES,[ACTION-IF-FOUND],[ACTION-IF-NOT-FOUND]):检测某些模块是否存在。

  • PKG_CHECK_MODULES(VARIABLE-PREFIX,MODULES,[ACTION-IF-FOUND],[ACTION-IF-NOT-FOUND]):检测某些模块是否存在,如果存在,根据 pkg-config --cflags 和 pkg-config --libs 的输出设置 <VARIABLE-PREFIX>_CFLAGS 和 <VARIABLE-PREFIX>_LIBS 。

常见问题

查看原文

赞 1 收藏 5 评论 0

周梦康 关注了专栏 · 2020-02-03

while(1) { }

NULL

关注 246

周梦康 赞了文章 · 2020-02-03

让这世界再多一份 GNU m4 教程 (1)

事实上,这个世界并没有几份 GNU m4 教程。

这个文档系列是我第一次认真学习 GNU m4 并进行了一些实践之后的一些总结。由于我在撰写此文的过程中充满着像 m4 展开一个又一个宏一般的耐心,因此这篇文章会比较长。在这个信息碎片化的时代,似乎没有很多人愿意去看很长的文章,大家更喜欢干货。为了节省大家的时间,必须声明,这个文档系列没有干货,它是写给我自己或者那些像我自己的人看的。

什么是宏

书名是『宏』,它被作者展开为这本书的全部内容。药瓶上的标签是『宏』,将药片从瓶中倾倒出来,就是这个宏的展开结果。被用的最多的『宏』,应该是 Internet 的超级链接。每当你点击一个超级链接,就相当于将这个宏展开为网页中的内容。生活中,类似的例子还有很多,只要你给某种具体的事物贴上了一个标签,那么这个标签就相当于宏。

人类非常喜欢给事物贴标签,尽管无论他们贴与不贴,那些事物本身依然是存在的。在编程中,如果你想给一段代码贴标签,最简单最直接的办法就是使用宏。那些还在用汇编语言编程的人,他们是离不开宏的,因为汇编语言本身就是将一大堆标签贴在了更大的一堆机器代码上。如果所用的编程语言不提供宏功能,可以用这种编程语言为一段代码制作一个标签——函数,不过这种标签就不是宏了,而且要付出一些性能上的代价,因为标签的展开过程被推迟到程序的运行过程。

C 语言自诞生后,只用了 5 年就让汇编语言归隐山林了,这可能要归功于 Unix 的成功以及 Dennis Ritchie 的忽悠。Steve Johnson——yacc, lint, spell 以及 PCC(Portable C Compiler)的作者说:『Dennis Ritchie 告诉所有人,C 函数的调用开销真的很小很小。于是人人都开始编写小函数,搞模块化。然而几年后,我们发现在 PDF-11 中函数的调用开销依然非常大,而 VAX 机器上的代码往往在 CALL 指令上花费掉 50% 的运行时间。Dennis 对我们撒了谎!但为时已晚,我们已经欲罢不能……』

现代的编程语言,几乎都赞同用函数来取代宏。拥护者们往往会给出一些冠冕堂皇的理由是,诸如不必额外实现一个宏处理器,函数比宏更安全并且更容易调试。事实上,他们的理由仅仅是迎合现实而已。如果将这些人扔进时空裂缝让他们穿越到 Ken Thompson 编写 Unix 系统的时代,让他们也在一台废弃的 PDP-7 型号的计算机上写程序。在这种内存只有 8KB 的计算机上,那些冠冕堂皇的理由近乎与科幻小说等价。函数之所以能够取代宏,仅仅是因为 CPU 的计算速度比过去更快了,内存比以前更大了,牺牲一些程序性能,让编程工作更容易一些,这样比较合算而已。编程语言的性能与机器的性能似乎总是成反比的。

宏被很多人主观的弃用了,得益于现代编程语言的表达能力,他们似乎几乎不需要用宏,于是他们作出结论:宏过时了。事实上,宏会永远居于众编程语言之上的,因为前者总是能够生成后者。编程专家总是会告诉我们,要慎用宏。胆子小的程序猿看到宏就躲得远远的,以至于他们总觉得那些使用宏的代码是糟糕的,是不安全的。事实上,在编程中,若能恰如其分的使用宏,可以让代码更加简洁易读,特别是对 C 语言这种表现力不足的语言。

例如下面 C 代码中的宏:

#define DEF_PAIR_OF(dtype) \
typedef struct pair_of_##dtype { \
        dtype first; \
        dtype second; \
} pair_of_##dtype##_t 

DEF_PAIR_OF(int);
DEF_PAIR_OF(double);
DEF_PAIR_OF(MyStruct);

是不是有点 C++ 模板的意味?像 C 标准库提供的 qsort 函数所接受的回调函数,也可以用类似的方法半自动生成。有关 C 语言宏的基本规则与技巧,可参考『宏定义的黑魔法 - 宏菜鸟起飞手册』。即使是表达能力很强的现代编程语言,在处理复杂问题上,也无法避免代码自身的频繁重复,妥善的使用宏总是可以消除这种重复,甚至可以创造一些 DSL(领域专用语言)。

在代码中适当的运用宏,创造优雅易读的代码,这样或许更能体现编程是一种艺术。虽然有些编程语言未提供宏功能,但是我们总是会有 GNU m4 这种通用的宏处理器可用。

GNU m4 简介

m4 是一种宏处理器,它扫描用户输入的文本并将其输出,期间如果遇到宏就将其展开后输出。宏有两种,一种是内建的,另一种是用户定义的,它们能接受任意数量的参数。除了做展开宏的工作之外,m4 内建的宏能够加载文件,执行 Shell 命令,做整数运算,操纵文本,形成递归等等。m4 可用作编译器的前端,或者单纯作为宏处理器来用

所有的 Unix 系统都会提供 m4 宏处理器,因为它是 POSIX 标准的一部分。通常只有很少一部分人知道它的存在,这些发现了 m4 的人往往会在某个方面成为专家。这不是我说的,这是 m4 手册说的。

其实,手册里的原话翻译过来应该是:通常只有很少一部分人知道它的存在,发现它的人往往会成为它的忠实用户。(这里要多谢 @mohu3g 的正本清源之举)

有些人对 m4 非常着迷,他们先是用 m4 解决一些简单的问题,然后解决了一个比一个更大的问题,直至掌握如何编写一个复杂的 m4 宏集。若痴迷于此,往往会对一些简单的问题写出复杂的 m4 脚本,然后耗费很多时间去调试,反而不如直接手动解决问题更有效。所以,对于程序猿中的强迫症患者,要对 m4 有所警惕,它可能会危及你的健康。这也不是我说的,是 m4 手册说的。

m4 基本工作过程

上文提到『m4 是一种宏处理器,它扫描用户输入的文本并将其输出,期间如果遇到宏就将其展开后输出』,其实更正式的说,应该是:m4 从文本输入流中获取文本并将其发送到文本输出流,期间如果遇到宏就将其展开后发送到文本输出流。

在 Brian Kernighan 与 Dennis Ritchie 合著的《C Programming Language》中将(Stream)定义为『与磁盘或其它外围设备关联的数据的源或目的地』。基于这个定义,m4 的输入流就是与磁盘或其它外围设备关联的数据的源,其输出流就是与磁盘或其它外围设备关联的数据的源或目的地,只不过 m4 希望它的输入流与输出流的内容是文本。如果你不那么较真,可以将理解为文件,对于 m4 而言,就是文本文件,但是下文会坚持使用流的概念。

m4 使用流的概念并非巧合,如果说巧合,也只是因为它的作者恰好也是 Dennis Ritchie。

m4 是如何从输入流中获取文本并将其发送到输出流的?肯定不是简单的读取文本就了事,因为 m4 有一个任务是『遇到宏就将其展开』。这意味着 m4 在从输入流中读取文本的过程中至少需要检测所读取的某段文本是不是宏。也就是说,从 m4 的角度来看,它首先要将输入流所提供的文本分为两类:非宏。如果 m4 读取的是一段文本是非宏,它基本上会将它们直接发送到输出流。之所以说是『基本上』,是因为非宏的文本会被进一步分类处理,其中细节后文会讲。如果 m4 读取的文本片段是,m4 就会将它展开,然后将展开结果发送到输出流。

m4 的工作过程具有一定程度的即时性,它不需要将输入流中全部信息都读取出来,然后再进行处理,而是扮演了一种过滤器的角色。从用户的角度来看,文本流入 m4,然后又流出。

从图灵的角度来看 m4,输入流与输出流可以衔接起来构成一条无限延伸的纸带,m4 是这条纸带的读写头,所以 m4 是一种图灵机。事实上,m4 的确是一种图灵机。因此 m4 的计算能力与任何一种编程语言等同,区别只体现在编程效率以及所编写的程序的运行效率方面。感觉基于 m4 来讲解计算机原理还是挺不错的。

m4 的工作空间

m4 既然是图灵机,它至少需要有一个『状态寄存器』,否则它无法判断当前从输入流中读取的文本是宏还是非宏。为了提高文本处理效率,还应该有一个缓存空间,使得 m4 在这一空间中高效工作。现代的 CPU,没有缓存的应该很罕见。

m4 缓存的容量为 512KB。当它满了的时候,m4 会将自动将其中的内容妥善的保存到一份临时文件中备用。所以,只要你的磁盘或其它外围设备的容量足够,就不要担心 m4 无法处理大文件。

注意,m4 缓存,这个概念是我瞎杜撰的。GNU m4 官方文档没这个概念,官方的概念是转移(Diversion)。

类似 CPU 的多级缓存,m4 的缓存空间也是划分了级别的。符合 POSIX 标准的 m4,可将缓存空间划分为 10 种级别,编号依次为 0, 1, 2, ..., 9。GNU m4 对缓存空间的级别数量不作限制。

m4 默认在 0 号缓存中工作,它在这个缓存对文本进行处理,然后将其发送到输出流。使用 m4 内建的宏 divert,可以从当前缓存切换到其他缓存。例如:

divert(3)

就从当前的缓存切换到 3 号缓存了,然后 m4 就在 3 号缓存中对输入流中的文本进行处理。如果不继续使用 divert 进行缓存切换,m4 会一直在 3 号缓存中工作,直到输入流终结。最后,m4 会将各个缓存中的文本汇总到 0 号缓存中。

缓存的汇总过程是按照缓存级别进行的。m4 会根据缓存级别的编号的增序进行汇总。例如,它总是先将 1 号缓存的内容汇总到 0 号缓存中,然后将 2 号缓存的内容汇总到 0 号缓存中,以此类推,最后将 0 号缓存中的内容依序发送到输出流中。

划分了级别的缓存,像是一道一道分水岭,使得文本流像河流一样拥有支流,不同的支流最终又汇集到一起,奔流到海……是不是有些气势恢宏的感觉,然而你也应该考虑到这样的现实:百川东到海,何时复西归?也就是说,文本流经 m4 的过程也像河流入海一样的不可逆。这是宏最大的弱点。在程序中滥用宏,形同过度开采水资源。

软件领域有一门学科,叫逆向工程,研究如何借助反汇编技术重现某个程序的原有逻辑。具体技术我不是很了解,但是幸好有这门学科,否则我的显卡很难在新版本的 Linux 内核上工作。因为 Nvidia 官方的 Linux 驱动自某个版本之后就宣布不再支持我这种型号的显卡了,而 Nvidia 官方驱动已经被大神实施逆向工程产生了 Nouveau 驱动,而后者又被集成到了 Linux 内核中。

似乎跑题了,我想表达的是,逆向工程固然能够在一定程度上复原某个程序的源码,但它却永远无法基于宏的展开结果重现宏的定义,只有宏的作者才知道当初究竟发生了什么。

这时,你应该有一个问题。如果你真的想学习 m4,那就必须要有这个问题——m4 为什么要对缓存划分级别?回顾一下上文,各个缓存的汇总过程是遵循特定次序的。有了这种分级的缓存汇总机制,你就有能力借助缓存来控制文本的支流,决定哪条支流先汇入 0 号缓存。你可以说这样你有机会扮演大禹,但是我觉得这更像铁路调度员所做的事。对于铁路调度员而言,文本流是他要调度的一组列车。

暗黑缓存

更有趣的是,m4 也提供了暗黑缓存,它的编号是 -1。GNU m4 对暗黑缓存也不限制数量,只要它们的编号是负数就可以。

暗黑缓存,似乎有点恐怖,实际上你可以将它们理解为地下河。也就是流过暗黑缓存的文本,m4 会将它们汇总到 0 号缓存,汇总过程按照暗黑缓存编号的递减次序进行的,但是 m4 不会将暗黑缓存汇总的内容发送到输出流。这没什么不好理解的,现实中没有什么东西是负数的。

在 m4 的应用中,暗黑缓存的主要作用就是作为宏定义的空间。如果在 0 号缓存定义一个宏,例如:

divert(0)
define(say_hello_world, Hello World!)

定义了一个名为 say_hello_world 的 m4 宏。宏定义语句『展开』为一个长度为 0 的字符串,然后发送到输出流。长度为 0 的字符串,就是空文本,即使它被发送到输出流,对输出流不会产生任何影响,但是 say_hello_world 宏之前,也就是 divert(0) 之后存在一个换行符,m4 会将这个换行符发送到输出流。除非你原本就希望输出流中需要这个换行符,否则你就在输出流中引入了一个额外的换行符,通常情况下,它不是你想要的结果。为了更好的说明这一点,可以看下面的示例:

divert(0)
define(say_hello_world, Hello World!)
say_hello_world

这个示例就是在上述代码中又增加了一行文本,它表示调用了上一行所定义的 say_hello_world 宏。假设示例代码保存在 hello.m4 文件中,然后执行以下命令:

$ m4 hello.m4

此时,hello.m4 就是 m4 的输入流。m4 从输入流中读取文本,处理文本,然后将处理结果发送到输出流。此时,输出流是系统的标准输出设备(stdout),也就是当前的终端屏幕。

执行上述命令后,我们期望的结果通常是:

$ m4 hello.m4
say_hello_world

然而,m4 输出的却是:

$ m4 hello.m4 


Hello World!

Hello World! 前面出现了两处空行,一处是 divert 语句后面的换行符导致的,另处是 say_hello_world 宏定义语句后面的换行符导致的。

如果将 say_hello_world 宏定义语句放在暗黑缓存中,可以解决一半问题。例如:

divert(-1)
define(say_hello_world, Hello World!)
divert(0)
say_hello_world

再次执行 m4 命令,可得:

$ m4 hello.m4 

Hello World!

现在 Hello World! 前面只有 1 处空行了,它是 divert(0) 后面的换行符导致的。要消除它,有两种方法。第一种方法就是 divert(0) 后面不换行,例如:

divert(-1)
define(say_hello_world, Hello World!)
divert(0)say_hello_world

另一种方法是使用 m4 内建的 dnl 宏,它会从将它被调用的位置到后面的第一个换行符之间的文本(包括换行符本身)一并删除,例如:

divert(-1)
define(say_hello_world, Hello World!)
divert(0)dnl
say_hello_world

这两种方法输出的结果是相同的。为了让文本具有更好的可读性,通常用 dnl 来做这样的事。

挑战

(1) 对于以下 m4 代码

divert(-1)
define(say, )
define(hello, HELLO)
define(world, WORLD!)
divert(0)dnl
say hello world

推测一下 m4 的处理结果,然后执行 m4 命令检验所做的推测是否正确。

(2) 对于以下 m4 代码

divert(2)
define(say, )
define(hello, HELLO)
divert(1)
define(world, WORLD!)
divert(0)dnl
say hello world

推测一下 m4 的处理结果,然后执行 m4 命令检验所做的推测是否正确。

下一篇让这世界再多一份 GNU m4 教程 (2)

查看原文

赞 15 收藏 46 评论 5

周梦康 赞了文章 · 2020-01-13

研发工程师该如何成长?--好未来学而思网校负责人2019年总结

作者:学而思网校 陈雷
原文地址:https://mp.weixin.qq.com/s/Tb...
大家好,我是陈雷,网校事业部互联网研发系统的负责人。今天分享的主题是“研发工程师该如何成长”

很多年轻的研发伙伴都有关于“研发工程师该如何成长”的困惑,大家普遍对如何成长为技术专家、如何在繁忙的工作中持续学习充满了困惑,这其实是每一位研发伙伴成长过程中的必经之路。我来网校小一年了,这一年里一直致力于“打造学习型组织”,经过一年的摸索和尝试,有了一点点的成绩和心得,在新一年的开始,我想对2019年做一个总结,也跟大家分享一下我的心得。

初到网校,我跟不少伙伴做过1对1的沟通,伙伴们普遍对自己的成长充满焦虑:工作太忙,甚至每天都在加班,没有时间学习;需求太琐碎,做了很多工作但是并没有感到自己有什么成长;希望有牛人带一下,但是貌似缺少这种导师;整个团队的技术氛围不是太强,没有什么提升的方法。

所有的这些都是对成长的焦虑。

其实这种适度的焦虑是非常好的事情,这是我们对成长的渴望,只是这种渴望在内心中映射成了焦虑;适度的焦虑对一个人的成长是有很好促进作用的,这种焦虑驱使我们定位一个问题到深夜,这种焦虑驱使我们攻克一个个技术难点,这种焦虑驱使我们把一个个需求做到极致。相反,有些伙伴失去了这种焦虑,甚至部分伙伴变成了抱怨,如果没有这种焦虑,那么可能已经陷入了“舒适区”,这种舒适会让我们失去成长的动力,错失很多好机会;如果焦虑变成了抱怨,那更是非常危险的,抱怨的人很难去主动解决问题,很多时候大家坐在一起,欢乐地调侃和吐槽半天,但并不解决问题。

那么,如何将适度的焦虑和渴望转化为成长的助力呢?

我总结方法为:自我管理-结伴学习-学以致用-总结反思。

1 自我管理:自律更自由

首先最重要的一点是自我管理,很多促进成长和学习的事情都是反人性的,如何控制和管理本性上的“懒惰”,核心思路就是自我管理(自律)。我非常喜欢Keep App的Slogan:“自律给我自由”。 丘吉尔在25岁的时候说:“我渴望自由”;过了25年以后,他说“自律很重要”;又过了25年,他75岁的时候说“自律就是自由”。

用在我们研发伙伴身上又何尝不是呢?很多时候,我们看到一些牛人的成长很励志,于是下决心要去坚持每天晚上阅读技术书籍,但是坚持了3天,意志开始下降,“要不看会儿抖音再看书吧?”,“今天先休息一下,打一局游戏,明天一定继续看书”……;这是不能自律的表现,很多时候我们管理不好自己,被惰性主导自己的思想。我也是一样的,很多时候都“犯懒”,那么我是怎么管理自己的呢? 我自己有个excel做记录,见图-1:

image.png

          图-1 读书记录

新年伊始,大家总会立一些Flag,我一般会设定大大小小50个目标,包括工作、学习、健身和家庭等方面,以图-1中读书记录为例,会精读或者粗读工作中需要的书,并伴有仪式感,每读完一本书,就会拍照录入到excel中,这样会促使自己坚持下去。随着时间的推移,会发现这件事会给自己带来很多收获,遇到一些问题思考时,貌似在哪本书里见过类似的场景,即可翻出来看一下,结合实际去解决。除了读书以外,自我管理还包含运动的管理,见图-2:
image.png

          图-2 运动/健康管理

图-2是我最近一段时间跑步的记录和体重的管理,作为一个研发人员,从大学时期就养成了熬夜的习惯,随着年纪的增长,头发日渐稀少,健康问题也越来越严重,脂肪肝,高血脂;另外形象也越来越差,大肚腩,弓背探头;有一次一个伙伴跟我开玩笑说:“快点减肥吧,不然都影响网校研发形象啦”,于是我开始了减肥健身之旅,每天坚持“stay hungry”,保证每顿饭只吃6-7分饱,基本不吃碳水,每天无论多晚到家都坚持走/跑40分钟,微微出汗,坚持了两个多月,肚子就小了很多,(PS:带一下货,这个走步机是在拼多多上买的,只要500元,噪声也不大)。另外在拼多多上买了驼背矫正器,每天坚持穿上,争取把驼背探头的毛病改掉。

自律很难,但是真的是“自律更自由”。

2 结伴学习:一群人走的更远

第二点就是结伴学习,设定目标后,一个人去坚持自己的目标真的很难坚持到底。早在3年前,我和团队就采用结伴学习的方式去研读内外部代码,一起探讨和丰富技术知识。来到网校以后,在网校也发起了“早读会”,让我自豪的是,从2019年3月7日起,我们一直坚持到现在,每天9点-10点半,中间除了重要会议或者搬家暂停几次外,没有间断过,并且我们使用讲座的技术录了视频,见图-3:
image.png

          图-3 后端早读会

图-3是后端研发伙伴在这10个月中的学习,大家一起学习了PHP的底层源码,Redis的源码,Nginx的源码,Twemproxy的源码,Swoole的源码,以及分享和探讨了网校商城的架构设计,CRM系统的设计,活动编排系统等等业务的设计和代码。大家一起学习和探讨,还有部分伙伴会做大家的讲师,这些伙伴不断地精进自己的技术、表达能力和思维能力。思否上链接如下:https://segmentfault.com/a/11...

前端伙伴们的梦想是“把好未来的前端(FE)团队建设成为行业内极具影响力的前端团队”,从2019年5月8日开始,每天都交流和探讨前端的技术。大家都知道,前端技术的特点是变化特别快,一段时间不学习就容易跟不上,无法精进和更新自己的技术。所以前端的伙伴们非常积极,建立了知识分享抢占机制,每周五会抛出下周5天的分享名额,大家踊跃去抢,这样几乎所有的伙伴都可以成为分享讲师,展现自己,提升自己的演讲能力,精进对技术理论的理解。图-4是前端伙伴的早读记录,早读会时间是9:30到10:30:
image.png

          图-4 前端早读会

最近一个月,在集团技术委员会的指导下,我们建立了集团的PHP群、Java群、Go群和大前端群,为了更多的伙伴一起学习和探讨,我们开始了钉钉直播,方便一些伙伴早晨通勤时能够一起学习,见图-5:
image.png

          图-5 PHP/GO/前端钉钉直播

另外,我们对于比较好的课程做了升级,在网校开办了知更鸟课程,课程内容落地到了思学堂上(内部学习网站),见图-6:
image.png

          图-6 知更鸟课程

以及在集团开办了PHP精英课程,课程内容落地到了TTC上。PHP精英课程2019年举行了8期,分别讲授了设计模式、Swoole应用、高性能服务设计、PHP7底层实现以及MySQL应用,见图-7,学员对课程反馈良好。
image.png
图-7 PHP精英训练营

结伴学习的好处是,当一个人被“懒惰”控制时,会有一起前行的伙伴一起监督和拉拽。正如“一群人走的更远”这句话,结伴学习能让我们甩掉惰性,坚持学习技术理论。

3 学以致用才是真的学会

纸上得来终觉浅,绝知此事要躬行。在学习了丰富的技术理论之后,如何把理论知识用到工作中呢?这就需要 “学以致用”。而技术成长最重要的就是“学以致用”。

很多伙伴抱怨需求太多,工作太忙,压根没有时间学习,其实工作和学习本身,是密不可分的,工作本来就是学习的一部分,是学习的实战,是学习的反馈,学习如果脱离工作的实战,是没有任何价值的。

经常有伙伴问我一些问题,比如“学习PHP/Redis底层源码,我是不是要先把C语言和Linux的基本知识全部学一遍”,其实这是个误区,如果花费大量的时间去学习和准备,可能在学习这些基础的时候就放弃了,或者当你用到的时候也忘得差不多了。最好的办法是用到什么知识难点时再去补充学习,这样会更高效,更有针对性。

再比如,还有伙伴问我“学习PHP/Redis底层源码,对平时工作有什么帮助呢”,举几个例子:

我们生产环境每天凌晨2点50,所有机器同时报警CPU负载过高,追查下去发现这一时刻所有FPM进程重启导致的,这就涉及到我们学习过的FPM进程管理模型,可以思考下还有什么方案能够避免FPM同时重启呢?

我们都知道502/504等状态码含义,曾经生产环境网关偶尔的会出现502报警,这种偶然现象该如何定位排查呢?这离不开我们对TCP协议,HTTP协议等的理解以及抓包实践过程。另外看到伙伴周报提到,周末双活演练过程中,发现存在偶尔超时现象,最终定位同样是由于TCP配置不合理造成的。

Redis是我们工作中不可缺少的缓存数据库,相信学习Redis源码后,对应Redis命令的使用会更加得心应手,同时避免一些不合理使用;下半年我们将Redis的使用从单实例模式升级成集群模式,这时候结合学习过的Redis源码与Twemproxy源码,可以更好的理解我们这么做的意义。

技术研发工作是一门实践科学,从书本上得到的理论和知识,如果没有在实战中应用和检验,基本上是没有价值的。彻底搞明白自己工作的技术和所在的业务领域,是最务实高效的做法,工作和学习割裂,会导致工作和学习都做不好。

另外还有一个非常重要的点,不同的领域之间其实是触类旁通的,当你对一个领域能够熟练掌握,以至精通,并且能总结出方法论后,很容易就能上手别的领域,花3-5年时间彻底研究透一个领域,对于刚工作的伙伴来说,是非常非常重要的,甚至是必须的,所以伙伴们需要沉下心来,厚积薄发,在一个领域打透,之后再去拓展自己的知识面,做到“举一反三”。

学以致用才是真的学会,才能真的成长。

04 学会总结和反思

最后要培养总结反思的习惯。

有一个理论,叫“刻意训练10000个小时,就可以成为某个领域的专家”,所谓的刻意训练,其中有个非常重要的方法论是“总结和反思”,这个跟我们好未来文化很是匹配。

在增研前端晨读会中,发现一个很有意思的现象:前端伙伴们每周五会抛出下周5天的分享名额,需要伙伴们去抢占分享机会。为什么会出现这种情况呢?因为他们明白,分享离不开总结与反思,分享是一个技术成长,以及锻炼思维能力和表达能力的好机会。

写文章同样是一个非常好的总结与反思过程,同时也是一个提升自己知名度的过程。今年上半年我们的“学而思网校技术公众号”诞生了,每周都会有伙伴积极投稿,发表自己优秀的文章。下半年TTC社区发布,至今看到了非常多的高质量文章。

学习不应该只是被动的接受书本上的,接受别人的知识。在学习过程中,凡事要多问几个为什么,需要明白的是任何方案、任何决策都有其背后的逻辑存在;比如在Redis切Twemproxy时,思考下单实例切集群的意义在哪?多种集群方案Redis/Codis/Twemproxy各自的特点都是什么?

在工作中反反复复去实战,检验自己学习的理论知识,不断精进和提升自己对理论的理解,建立自己的经验库和方法论,这个过程中训练出来的思考能力、总结能力和反思能力是最宝贵的,也是自身价值提升最重要的部分。

新年伊始,伙伴们可以给自己树立一些目标,然后加以行动,对自己要“狠”,要自律,跟一群志同道合的伙伴一起,朝着目标去学习,去实战,在工作中落地,在过程中不断总结和反思。

新的一年,希望跟好未来研发的伙伴们一起:

奋斗更快乐,自律更自由!

一群有梦想的人走得更远,活得更充实。

学以致用,刻意训练自己10000小时。

不断地总结和反思,一起成长!一起用科技推动教育进步!

好未来技术委员会开发子通道主席 陈雷
2020年1月7日

查看原文

赞 33 收藏 8 评论 6

周梦康 收藏了文章 · 2020-01-08

🔥 万字精美图文带你掌握JVM垃圾回收

本篇内容干货太多,耗费作者大量心力,强烈建议读者朋友们先收藏后观看😉🙇🙏

现在正值年底,估计有很多兄弟们在准备面试,希望本篇能给各位带来帮助。

转载请注明出处,原创不易!

前言

往期文章:

垃圾回收( Garbage Collection 以下简称 GC)诞生于1960年 MIT 的 Lisp 语言,有半个多世纪的历史。在Java 中,JVM 会对内存进行自动分配与回收,其中 GC 的主要作用就是清楚不再使用的对象,自动释放内存

GC 相关的研究者们主要是思考这3件事情。

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

本文也大致按照这个思路,为大家描述垃圾回收的相关知识。因为会有很多内存区域相关的知识,希望读者先学习完精美图文带你掌握 JVM 内存布局再来阅读本文。

在这里先感谢周志明大佬的新鲜出炉的大作:《深入理解Java 虚拟机》- 第3版

拜读之后对JVM有了更深的理解,强烈推荐大家去看。

本文的主要内容如下(建议大家在阅读和学习的时候,也大致按照以下的思路来思考和学习):

  • 哪些内存需要回收?即GC 发生的内存区域?
  • 如何判断这个对象需要回收?即GC 的存活标准

    这里又能够引出以下的知识概念:

    • 引用计数法
    • 可达性分析法
    • 引用的种类和特点、区别 (强引用、软引用、弱引用、虚引用)
    • 延伸知识:(WeakHashMap) (引用队列)
  • 有了对象的存活标准之后,我们就需要知道GC 的相关算法(思想)

    • 标记-清除(Mark-Sweep)算法
    • 复制(Copying)算法
    • 标记-整理(Mark-Compact)算法
  • 在下一步学习之前,还需要知道一些GC的术语📖,防止对一些概念描述出现混淆
  • 知道了算法之后,自然而然我们到了JVM中对这些算法的实现和应用,即各种垃圾收集器(Garbage Collector)

    • 串行收集器
    • 并行收集器
    • CMS 收集器
    • G1 收集器

一、GC 的 目标区域

一句话:GC 主要关注 堆和方法区

精美图文带你掌握 JVM 内存布局一文中,理解介绍了Java 运行时内存的分布区域和特点。

其中我们知道了程序计数器、虚拟机栈、本地方法栈3个区域是随线程而生,随线程而灭的。栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由JIT编译器进行一些优化,但在本章基于概念模型的讨论中,大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的。GC 关注的也就是这部分的内存区域。

image.png

二、GC 的存活标准

知道哪些区域的内存需要被回收之后,我们自然而然地想到了,如何去判断一个对象需要被回收呢?(回收对象...没对象的我听着怎么有点怪怪的😂)

对于如何判断对象是否可以回收,有两种比较经典的判断策略。

  • 引用计数算法
  • 可达性分析算法

1. 引用计数法

在对象头维护着一个 counter 计数器,对象被引用一次则计数器 +1;若引用失效则计数器 -1。当计数器为 0 时,就认为该对象无效了。

主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。发生循环引用的对象的引用计数永远不会为0,结果这些对象就永远不会被释放。

image.png

2. 可达性分析算法 ⭐

GC Roots 为起点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots 没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

Java 中,GC Roots 是指:

  • Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中常量引用的对象
  • 方法区中类静态属性引用的对象

image.png

3. Java 中的引用 ⭐

Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱

这样子设计的原因主要是为了描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

也就是说,对不同的引用类型,JVM 在进行GC 时会有着不同的执行策略。所以我们也需要去了解一下。

a. 强引用(Strong Reference)

MyClass obj = new MyClass(); // 强引用

obj = null // 此时‘obj’引用被设为null了,前面创建的'MyClass'对象就可以被回收了

只要强引用存在,垃圾收集器永远不会回收被引用的对象,只有当引用被设为null的时候,对象才会被回收。但是,如果我们错误地保持了强引用,比如:赋值给了 static 变量,那么对象在很长一段时间内不会被回收,会产生内存泄漏。

b. 软引用(Soft Reference)

软引用是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

SoftReference<MyClass> softReference = new SoftReference<>(new MyClass());

c. 弱引用(Weak Reference)

弱引用的强度比软引用更弱一些。当 JVM 进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。

WeakReference<MyClass> weakReference = new WeakReference<>(new MyClass());
弱引用可以引申出来一个知识点, WeakHashMap&ReferenceQueue

ReferenceQueue 是GC回调的知识点。这里因为篇幅原因就不细讲了,推荐引申阅读:ReferenceQueue的使用

d. 幻象引用/虚引用(Phantom References)

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

PhantomReference<MyClass> phantomReference = new PhantomReference<>(new MyClass(), new ReferenceQueue<>());

三、GC 算法 ⭐

有了判断对象是否存活的标准之后,我们再来了解一下GC的相关算法。

  • 标记-清除(Mark-Sweep)算法
  • 复制(Copying)算法
  • 标记-整理(Mark-Compact)算法

1. 标记-清除(Mark-Sweep)算法

标记-清除算法在概念上是最简单最基础的垃圾处理算法。

该方法简单快速,但是缺点也很明显,一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

image.png

后续的收集算法都是基于这种思路并对其不足进行改进而得到的。

2. 复制(Copying)算法

复制算法改进了标记-清除算法的效率问题。

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

缺点也是明显的,可用内存缩小到了原先的一半

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。

在前面的文章中我们提到过,HotSpot默认的Eden:survivor1:survivor2=8:1:1,如下图所示。

延伸知识点:内存分配担保

当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)

内存的分配担保就好比我们去银行借款,如果我们信誉很好,在98%的情况下都能按时偿还,于是银行可能会默认我们下一次也能按时按量地偿还贷款,只需要有一个担保人能保证如果我不能还款时,可以从他的账户扣钱,那银行就认为没有风险了。内存的分配担保也一样,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

image.png

3. 标记-整理算法

前面说了复制算法主要用于回收新生代的对象,但是这个算法并不适用于老年代。因为老年代的对象存活率都较高(毕竟大多数都是经历了一次次GC千辛万苦熬过来的,身子骨很硬朗😎)

根据老年代的特点,提出了另外一种标记-整理(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

image.png

4. 分代收集算法

有没有注意到了,我们前面的表述当中就引入了新生代、老年代的概念。准确来说,是先有了分代收集算法的这种思想,才会将Java堆分为新生代和老年代。这两个概念之间存在着一个先后因果关系。

这个算法很简单,就是根据对象存活周期的不同,将内存分块。在Java 堆中,内存区域被分为了新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

就如我们在介绍上面的算法时描述的,在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用 “标记—清理” 或者 “标记—整理” 算法 来进行回收。

  • 新生代:复制算法
  • 老年代:标记-清除算法、标记-整理算法

5. 重新回顾 创建对象时触发GC的流程

这里重新回顾一下精美图文带你掌握 JVM 内存布局里面JVM创建一个新对象的内存分配流程图。这张图也描述了GC的流程。

image.png

四、GC 术语 📖

在学习垃圾收集器知识点之前,需要向读者大大们科普一些GC的术语😊,方便你们后面理解。

  • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

    • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
    • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,读者需按上下文区分到底是指老年代的收集还是整堆收集。
    • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
  • 并行(Parallel) :在JVM运行时,同时存在应用程序线程和垃圾收集器线程。 并行阶段是由多个GC 线程执行,即GC 工作在它们之间分配。
  • 串行(Serial):串行阶段仅在单个GC 线程上执行。
  • STW :Stop The World 阶段,应用程序线程被暂停,以便GC线程 执行其工作。 当应用程序因为GC 暂停时,这通常是由于Stop The World 阶段。
  • 并发(Concurrent):用户线程与垃圾收集器线程同时执行,不一定是并行执行,可能是交替执行(竞争)
  • 增量:如果一个阶段是增量的,那么它可以运行一段时间之后由于某些条件提前终止,例如需要执行更高优先级的GC 阶段,同时仍然完成生产性工作。 增量阶段与需要完全完成的阶段形成鲜明对比。

五、垃圾收集器 ⭐

知道了算法之后,自然而然我们到了JVM中对这些算法的实现和应用,即各种垃圾收集器(Garbage Collector)

首先要认识到的一个重要方面是,对于大多数JVM,需要两种不同的GC算法,一种用于清理新生代,另一种用于清理老年代

意思就是说,在JVM中你通常会看到两种收集器组合使用。下图是JVM 中所有的收集器(Java 8 ),其中有连线的就是可以组合的。

为了减小复杂性,快速记忆,我这边直接给出比较常用的几种组合。其他的要么是已经废弃了要么就是在现实情况下不实用的。

新生代老年代JVM options
SerialSerial Old-XX:+UseSerialGC
Parallel ScavengeParallel Old-XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel NewCMS-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
G1G1-XX:+UseG1GC

接下去我们开始具体介绍上各个垃圾收集器。这里需要提一下的是,我这边是将垃圾收集器分成以下几类来讲述的:

  • Serial GC
  • Parallel GC
  • Concurrent Mark and Sweep (CMS)
  • G1 - Garbage First

理由无他,我觉得这样更符合理解的思路,你更好理解。

4.1 串行收集器

Serial 翻译过来可以理解成单线程。单线程收集器有Serial 和 Serial Old 两种,它们的唯一区别就是:Serial 工作在新生代,使用“复制”算法,Serial Old 工作在老年代,使用“标志-整理”算法。所以这里将它们放在一起讲。

串行收集器收集器是最经典、最基础,也是最好理解的。它们的特点就是单线程运行及独占式运行,因此会带来很不好的用户体验。虽然它的收集方式对程序的运行并不友好,但由于它的单线程执行特性,应用于单个CPU硬件平台的性能可以超过其他的并行或并发处理器。

“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束(STW阶段)

image.png

STW 会带给用户恶劣的体验,所以从JDK 1.3开始,一直到现在最新的JDK 13,HotSpot虚拟机开发团队为消除或者降低用户线程因垃圾收集而导致停顿的努力一直持续进行着,从Serial收集器到Parallel收集器,再到Concurrent Mark Sweep(CMS)和Garbage First(G1)收集器,最终至现在垃圾收集器的最前沿成果Shenandoah和ZGC等。

虽然新的收集器很多,但是串行收集器仍有其适合的场景。迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效。对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的,单线程没有线程交互开销。(这里实际上也是一个时间换空间的概念)

通过JVM参数 -XX:+UseSerialGC 可以使用串行垃圾回收器(上面表格也有说明)

4.2 并行收集器

按照程序发展的思路,单线程处理之后,下一步很自然就到了多核处理器时代,程序多线程并行处理的时代。并行收集器是多线程的收集器,在多核CPU下能够很好的提高收集性能。

image.png

这里我们会介绍:

  • ParNew
  • Parallel Scavenge
  • Parallel Old

这里还是提供太长不看版白话总结,方便理解。因为我知道有些人刚开始学习JVM 看这些名词都会觉得头晕。

  • ParNew收集器 就是 Serial收集器的多线程版本,基于“复制”算法,其他方面完全一样,在JDK9之后差不多退出历史舞台,只能配合CMS在JVM中发挥作用。
  • Parallel Scavenge 收集器 和 ParNew收集器类似,基于“复制”算法,但前者更关注可控制的吞吐量,并且能够通过-XX:+UseAdaptiveSizePolicy打开垃圾收集自适应调节策略的开关。
  • Parallel Old 就是 Parallel Scavenge 收集器的老年代版本,基于“标记-整理”算法实现。

a. ParNew 收集器

ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作

但是从G1 出来之后呢,ParNew的地位就变得微妙起来,自JDK 9开始,ParNew加CMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。官方希望它能完全被G1所取代,甚至还取消了『ParNew + Serial Old』 以及『Serial + CMS』这两组收集器组合的支持(其实原本也很少人这样使用),并直接取消了-XX:+UseParNewGC参数,这意味着ParNew 和CMS 从此只能互相搭配使用,再也没有其他收集器能够和它们配合了。可以理解为从此以后,ParNew 合并入CMS,成为它专门处理新生代的组成部分。

b. Parallel Scavenge收集器

Parallel Scavenge收集器与ParNew收集器类似,也是使用复制算法的并行的多线程新生代收集器。但Parallel Scavenge收集器关注可控制的吞吐量(Throughput)

注:吞吐量是指CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /( 运行用户代码时间 + 垃圾收集时间 )

Parallel Scavenge收集器提供了几个参数用于精确控制吞吐量和停顿时间:

参数作用
--XX: MaxGCPauseMillis最大垃圾收集停顿时间,是一个大于0的毫秒数,收集器将回收时间尽量控制在这个设定值之内;但需要注意的是在同样的情况下,回收时间与回收次数是成反比的,回收时间越小,相应的回收次数就会增多。所以这个值并不是越小越好。
-XX: GCTimeRatio吞吐量大小,是一个(0, 100)之间的整数,表示垃圾收集时间占总时间的比率。
XX: +UseAdaptiveSizePolicy这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)

c. Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,多线程,基于“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的。

由于如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别无选择(Parallel Scavenge无法与CMS收集器配合工作),Parallel Old收集器的出现就是为了解决这个问题。Parallel Scavenge和Parallel Old收集器的组合更适用于注重吞吐量以及CPU资源敏感的场合

4.3 ⭐ Concurrent Mark and Sweep (CMS)

CMS(Concurrent Mark Sweep,并发标记清除) 收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。

从名字就可以知道,CMS是基于“标记-清除”算法实现的。它的工作过程相对于上面几种收集器来说,就会复杂一点。整个过程分为以下四步:

1)初始标记 (CMS initial mark):主要是标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。

2)并发标记 (CMS concurrent mark):根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞没有 STW

3)重新标记(CMS remark):顾名思义,就是要再标记一次。为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾

这里举一个很形象的例子:

就比如你和你的小伙伴(多个GC线程)给一条长走廊打算卫生,从一头打扫到另一头。当你们打扫到走廊另一头的时候,可能有同学(用户线程)丢了新的垃圾。所以,为了打扫干净走廊,需要你示意所有的同学(用户线程)别再丢了(进入STW阶段),然后你和小伙伴迅速把刚刚的新垃圾收走。当然,因为刚才已经收过一遍垃圾,所以这次收集新产生的垃圾,用不了多长时间(即:STW 时间不会很长)。

4)并发清除(CMS concurrent sweep):

image.png

❔❔❔ 提问环节:为什么CMS要使用“标记-清除”算法呢?刚才我们不是提到过“标记-清除”算法,会留下很多内存碎片吗?

确实,但是也没办法,如果换成“标记 - 整理”算法,把垃圾清理后,剩下的对象也顺便整理,会导致这些对象的内存地址发生变化,别忘了,此时其它线程还在工作,如果引用的对象地址变了,就天下大乱了

对于上述的问题JVM提供了两个参数:

参数作用
--XX: +UseCMS-CompactAtFullCollection(默认是开启的,此参数从JDK 9开始废弃)用于在CMS收集器不得不进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
--XX: CMSFullGCsBeforeCompaction(此参数从JDK 9开始废弃)这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)

另外,由于最后一步并发清除时,并不阻塞其它线程,所以还有一个副作用,在清理的过程中,仍然可能会有新垃圾对象产生,只能等到下一轮 GC,才会被清理掉

4.4 ⭐ G1 - Garbage First

JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器。

鉴于 CMS 的一些不足之外,比如: 老年代内存碎片化,STW 时间虽然已经改善了很多,但是仍然有提升空间。G1 就横空出世了,它对于堆区的内存划思路很新颖,有点算法中分治法“分而治之”的味道。具体什么意思呢,让我们继续看下去。

G1 将连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂

Region中还有一类特殊的Humongous区域,专门用来存储大对象G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中

Humongous,简称 H 区,是专用于存放超大对象的区域,通常 >= 1/2 Region SizeG1的大多数行为都把Humongous Region作为老年代的一部分来进行看待

image.png

认识了G1中的内存规划之后,我们就可以理解为什么它叫做"Garbage First"。所有的垃圾回收,都是基于 region 的。G1根据各个Region回收所获得的空间大小以及回收所需时间等指标在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大(垃圾)的Region,从而可以有计划地避免在整个Java堆中进行全区域的垃圾收集。这也是 "Garbage First" 得名的由来。

G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次GC。

❔❔❔ 提问环节:

一个对象和它内部所引用的对象可能不在同一个 Region 中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析?

这里就需要引入 Remembered Set 的概念了。

答案是不需要,每个 Region 都有一个 Remembered Set (记忆集)用于记录本区域中所有对象引用的对象所在的区域,进行可达性分析时,只要在 GC Roots 中再加上 Remembered Set 即可防止对整个堆内存进行遍历

再提一个概念,Collection Set :简称 CSet,记录了等待回收的 Region 集合,GC 时这些 Region 中的对象会被回收(copied or moved)

G1 运作步骤

如果不计算维护 Remembered Set 的操作,G1 收集器的工作过程分为以下几个步骤:

  • 初始标记(Initial Marking):Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
  • 并发标记(Concurrent Marking):使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢。
  • 最终标记(Final Marking):Stop The World,使用多条标记线程并发执行。
  • 筛选回收(Live Data Counting and Evacuation):回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行。(还会更新Region的统计数据,对各个Region的回收价值和成本进行排序)

image.png

从上述阶段的描述可以看出,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量

G1 的 Minor GC/Young GC

在分配一般对象时,当所有eden region使用达到最大阈值并且无法申请足够内存时,会触发一次YGC。每次YGC会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。

image.png

下面是一段经过抽取的GC日志:

GC pause (G1 Evacuation Pause) (young)
  ├── Parallel Time
    ├── GC Worker Start
    ├── Ext Root Scanning
    ├── Update RS
    ├── Scan RS
    ├── Code Root Scanning
    ├── Object Copy
  ├── Code Root Fixup
  ├── Code Root Purge
  ├── Clear CT
  ├── Other
    ├── Choose CSet
    ├── Ref Proc
    ├── Ref Enq
    ├── Redirty Cards
    ├── Humongous Register
    ├── Humongous Reclaim
    ├── Free CSet  

由这段GC日志我们可知,整个YGC由多个子任务以及嵌套子任务组成,且一些核心任务为:Root Scanning,Update/Scan RS,Object Copy,CleanCT,Choose CSet,Ref Proc,Humongous Reclaim,Free CSet

推荐阅读:深入理解G1的GC日志

这篇文章通过G1 GC日志介绍了GC的几个步骤。对上面英文单词概念不清楚的可以查阅。

英文好的更推荐这篇:garbage-collection-algorithms-implementations

G1 的 Mixed GC

当越来越多的对象晋升到老年代Old Region 时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,是收集整个新生代以及部分老年代的垃圾收集。除了回收整个Young Region,还会回收一部分的Old Region ,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些Old Region 进行收集,从而可以对垃圾回收的耗时时间进行控制。

Mixed GC的整个子任务和YGC完全一样,只是回收的范围不一样。

image.png

注:G1 一般来说是没有FGC的概念的。因为它本身不提供FGC的功能。

如果 Mixed GC 仍然效果不理想,跟不上新对象分配内存的需求,会使用 Serial Old GC 进行 Full GC强制收集整个 Heap。

相比CMS,G1总结有以下优点:

  • G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行。
  • G1 能预测 GC 停顿时间, STW 时间可控(G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.)
关于G1实际上还有很多的细节可以讲,这里希望读者去阅读《深入理解Java虚拟机》或者其他资料来延伸学习,查漏补缺。

相关参数:

参数作用
-XX:+UseG1GC采用 G1 收集器
-XX:G1HeapRegionSize每个Region的大小
更多的参数和调优参考详见:分析和性能来调整和调优 G1 GC

后记

本系列关于JVM 垃圾回收的知识就到这里了。

因为篇幅的关系,也受限于能力水平,本文很多细节没有涉及到,只能算是为学习JVM的同学打开了一扇的门(一扇和平常看到的文章相比要大那么一点点的门,写了这么久允许我自恋一下吧😂😂)。希望不过瘾的同学能自己更加深入的学习。

如果本文有帮助到你,希望能点个赞,这是对我的最大动力🤝🤝🤗🤗。

参考

查看原文

认证与成就

  • SegmentFault 讲师
  • 获得 1288 次点赞
  • 获得 231 枚徽章 获得 18 枚金徽章, 获得 94 枚银徽章, 获得 119 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • 深入 PHP 内核

    深入分析解读 PHP 源码,主要版本 5.3 ,后面补充了部分 7.0 的内容

注册于 2013-12-09
个人主页被 18.2k 人浏览