Hi there

Welcome to my blog…

04. Elasticsearch 节点启动

启动流程核心工作 总体而言,节点启动流程的任务包括: 解析配置,包括配置文件和命令行参数。 检查外部环境和内部环境,例如,JVM版本、操作系统内核参数等。 初始化内部资源,创建内部模块,初始化探测器。 启动各个子模块和keepalive线程。 启动流程分析 启动入口:org.elasticsearch.bootstrap.Elasticsearch#main(java.lang.String[]) 检测外部环境 在创建Node时覆盖validateNodeBeforeAcceptingRequests方法,在接收请求之前执行BootstracpChecks.check执行各项外部环境检查: node = new Node(environment) { @Override protected void validateNodeBeforeAcceptingRequests( final BootstrapContext context, final BoundTransportAddress boundTransportAddress, List<BootstrapCheck> checks) throws NodeValidationException { BootstrapChecks.check(context, boundTransportAddress, checks); } }; 执行的检查包括: // the list of checks to execute static List<BootstrapCheck> checks() { final List<BootstrapCheck> checks = new ArrayList<>(); checks.add(new HeapSizeCheck()); final FileDescriptorCheck fileDescriptorCheck = Constants.MAC_OS_X ? new OsXFileDescriptorCheck() : new FileDescriptorCheck(); checks.add(fileDescriptorCheck); checks.add(new MlockallCheck()); if (Constants....

March 20, 2024

03. Elasticsearch allocation模块分析

节点发现 当一个节点启动时,首先会尝试连接到配置列表(discovery.seed_hosts)中指定的种子节点,获取集群中其他节点的信息。并从其他节点接收集群的状态信息,包括集群中的其他节点列表,集群配置以及主节点的信息等。并尝试加入一个现有的集群,如果找不到,可以选择自我选举成为主节点,或者根据配置等待加入一个集群。 选举主节点 假设有若干节点正在启动,集群启动的第一件事是从已知的活跃机器列表中选择一个作为主节点,选主之后的流程由主节点触发。 ES的选主算法是基于Bully算法的改进,主要思路是对节点ID排序,取ID值最大的节点作为Master,每个节点都运行这个流程。 基于节点ID排序的简单选举算法有三个附加约定条件: 参选人数需要过半,达到 quorum(多数)后就选出了临时的主。 得票数需过半。某节点被选为主节点,必须判断加入它的节点数过半,才确认Master身份。 当探测到节点离开事件时,必须判断当前节点数是否过半。如果达不到 quorum,则放弃Master身份,重新加入集群。 选举集群元信息 Master的第一个任务是选举元信息,让各节点把各自存储的元信息发过来,根据版本号确定最新的元信息,然后把这个信息广播下去,这样集群的所有节点都有了最新的元信息。集群元信息的选举包括两个级别:集群级和索引级。 为了集群一致性,参与选举的元信息数量需要过半,Master发布集群状态成功的规则也是等待发布成功的节点数过半。在选举过程中,不接受新节点的加入请求。集群元信息选举完毕后,Master发布首次集群状态,然后开始选举shard级元信息。 allocation过程 选举shard级元信息,构建内容路由表,是在allocation模块完成的。 在初始阶段,所有的shard都处于ShardRoutingState.UNASSIGNED(未分配)状态。ES中通过分配过程决定哪个分片位于哪个节点,重构内容路由表。 1. 选主分片 此时,Master不知道主分片在哪,它向集群的所有节点询问:大家把[website][0]分片的元信息发给我。然后根据某种策略选一个分片作为主分片。ES 5.x以下的版本,通过对比shard级元信息的版本号来决定。 2. 选副本分片 主分片选举完成后,从上一个过程汇总的 shard信息中选择一个副本作为副本分片。 最后,allocation过程中允许新启动的节点加入集群。 index recovery 为什么需要recovery?对于主分片来说,可能有一些数据没来得及刷盘;对于副分片来说,一是没刷盘,二是主分片写完了,副分片还没来得及写,主副分片数据不一致。 1. 主分片recovery 主分片通过将最后一次提交(Lucene的一次提交就是一次 fsync 刷盘的过程)之后的 translog中进行重放,建立Lucene索引,如此完成主分片的recovery。 2. 副本分片recovery 副本分片需要恢复成与主分片一致(类似Redis主从之间需要进行同步)。在6.0版本中,恢复分成两阶段执行。 · phase1:在主分片所在节点,获取translog保留锁,从获取保留锁开始,会保留translog不受其刷盘清空的影响。然后调用Lucene接口把shard做快照,这是已经刷磁盘中的分片数据。把这些shard数据复制到副本节点。在phase1完毕前,会向副分片节点发送告知对方启动engine,在phase2开始之前,副分片就可以正常处理写请求了。 · phase2:对translog做快照,这个快照里包含从phase1开始,到执行translog快照期间的新增索引。将这些translog发送到副分片所在节点进行重放。 参考 Elasticsearch源码解析与优化实战

March 18, 2024

02. Elasticsearch 集群启动简介

节点发现 当一个节点启动时,首先会尝试连接到配置列表(discovery.seed_hosts)中指定的种子节点,获取集群中其他节点的信息。并从其他节点接收集群的状态信息,包括集群中的其他节点列表,集群配置以及主节点的信息等。并尝试加入一个现有的集群,如果找不到,可以选择自我选举成为主节点,或者根据配置等待加入一个集群。 选举主节点 假设有若干节点正在启动,集群启动的第一件事是从已知的活跃机器列表中选择一个作为主节点,选主之后的流程由主节点触发。 ES的选主算法是基于Bully算法的改进,主要思路是对节点ID排序,取ID值最大的节点作为Master,每个节点都运行这个流程。 基于节点ID排序的简单选举算法有三个附加约定条件: 参选人数需要过半,达到 quorum(多数)后就选出了临时的主。 得票数需过半。某节点被选为主节点,必须判断加入它的节点数过半,才确认Master身份。 当探测到节点离开事件时,必须判断当前节点数是否过半。如果达不到 quorum,则放弃Master身份,重新加入集群。 选举集群元信息 Master的第一个任务是选举元信息,让各节点把各自存储的元信息发过来,根据版本号确定最新的元信息,然后把这个信息广播下去,这样集群的所有节点都有了最新的元信息。集群元信息的选举包括两个级别:集群级和索引级。 为了集群一致性,参与选举的元信息数量需要过半,Master发布集群状态成功的规则也是等待发布成功的节点数过半。在选举过程中,不接受新节点的加入请求。集群元信息选举完毕后,Master发布首次集群状态,然后开始选举shard级元信息。 allocation过程 选举shard级元信息,构建内容路由表,是在allocation模块完成的。 在初始阶段,所有的shard都处于ShardRoutingState.UNASSIGNED(未分配)状态。ES中通过分配过程决定哪个分片位于哪个节点,重构内容路由表。 1. 选主分片 此时,Master不知道主分片在哪,它向集群的所有节点询问:大家把[website][0]分片的元信息发给我。然后根据某种策略选一个分片作为主分片。ES 5.x以下的版本,通过对比shard级元信息的版本号来决定。 2. 选副本分片 主分片选举完成后,从上一个过程汇总的 shard信息中选择一个副本作为副本分片。 最后,allocation过程中允许新启动的节点加入集群。 index recovery 为什么需要recovery?对于主分片来说,可能有一些数据没来得及刷盘;对于副分片来说,一是没刷盘,二是主分片写完了,副分片还没来得及写,主副分片数据不一致。 1. 主分片recovery 主分片通过将最后一次提交(Lucene的一次提交就是一次 fsync 刷盘的过程)之后的 translog中进行重放,建立Lucene索引,如此完成主分片的recovery。 2. 副本分片recovery 副本分片需要恢复成与主分片一致(类似Redis主从之间需要进行同步)。在6.0版本中,恢复分成两阶段执行。 · phase1:在主分片所在节点,获取translog保留锁,从获取保留锁开始,会保留translog不受其刷盘清空的影响。然后调用Lucene接口把shard做快照,这是已经刷磁盘中的分片数据。把这些shard数据复制到副本节点。在phase1完毕前,会向副分片节点发送告知对方启动engine,在phase2开始之前,副分片就可以正常处理写请求了。 · phase2:对translog做快照,这个快照里包含从phase1开始,到执行translog快照期间的新增索引。将这些translog发送到副分片所在节点进行重放。 参考 Elasticsearch源码解析与优化实战

March 17, 2024

01. Elasticsearch基本概念

基本概念 Elasticsearch是实时的分布式搜索分析引擎,内部使用Lucene进行索引与搜索。 特点: 准实时:新增数据秒级可以被检索到 分布式:支持动态调整集群规模,弹性扩容。目前,ES比较适合中等数据量的业务,支持“上百”个节点,不适合存储海量数据。 Lucene:全文检索框架,提供建立检索、执行搜索等接口。 索引结构 ES是面向文档的,一般使用json作为文档的序列化格式。 在存储结构上,由_index, _type, _id唯一标识一个文档。(_type已经废弃了) 分片(shard) 一个索引可以分为多个分片。同一个分片可以存在多个副本。为了应对并发更新问题,ES将数据副本分为主从两部分,即主分片(primary shard)和副分片(replica shard)。主数据作为权威数据,写过程先写主分片,成功后在写副分片,恢复阶段以主分片为准。(主从模式) 一个ES索引可以包含多个分片,一个分片对应一个Lucene索引,它本身是一个完整的搜索引擎,可以独立执行索引和搜索任务。Lucene索引又由很多分段(Segment)做成,每个分段都是一个倒排索引。ES每次“refresh”都会生成一个新的分段,其中包含若干文档的数据。在每个分段内部,文档的不同字段被单独建立索引。每个字段的值由若干词(Term)组成,Term是原文本内容经过分词器处理和语言处理后的最终结果。 段合并 在ES中,每秒清空一次写缓冲,将这些数据写入文件,这个过程称为refresh,每次refresh会创建一个新的Lucene段。因此需要通过一定的策略将这些较小的段合并为大的段,常用的方案是选择大小相似的分段进行合并。在合并过程中,标记为删除的数据不会写入新分段,当合并过程结束,旧的分段数据被删除,标记删除的数据才从磁盘删除。 集群内部原理 ES使用主从(Master-Slave)模式。 集群节点角色 主节点(Master node) 主节点负责集群层面的相关操作,管理集群变更。主节点是全局唯一的,将从有资格称为master的节点中进行选举。 数据节点(Data node) 负责保存数据、执行数据相关操作:CRUD、搜索、聚合等。 协调节点(Coordinating node) 协调节点负责处理客户端的请求,然后将请求转发给保存数据的数据节点。然后,将每个数据节点的结果合并为单个全局结果。 其他:预处理节点(Ingest node)、部落节点(Tribe node)。 集群健康状态 Green,所有的主分片和副分片都正常运行。 Yellow,所有的主分片都正常运行,但不是所有的副分片都正常运行。 Red,有主分片没能正常运行。 每个索引也有上述三种状态。 集群状态 集群状态元数据是全局信息,元数据包含内容路由信息、配置信息等,其中最重要的是内容路由信息,它描述了“哪个分片位于哪个节点”的路由信息。 集群状态由主节点负责维护,如果主节点从数据节点接收更新,则将这些更新广播到集群的其他节点,让每个节点上的集群状态保持最新。 集群扩容 当扩容集群、添加节点时,分片会均衡地分配到集群的各个节点,从而对索引和搜索过程进行负载均衡,这些都是系统自动完成的。 主要内部模块简介 cluster cluster模块是主节点执行集群管理的封装实现,管理集群状态,维护集群层面的配置信息。主要功能如下: 管理集群状态,将新生成的集群状态发布到集群的所有节点。 调用allocation模块执行分片分配,决策哪些分片应该分配到哪个节点。 在集群各节点中直接迁移分片,保持数据平衡。 allocation 封装了分片分配相关的功能和策略,包括主分片的分配和副分片的分配,本模块由主节点调用。创建新索引、集群完全重启都需要分片分配的过程。 discovery discovery模块负责发现集群中的节点,以及选举主节点。从某种角度来说,发现模块起到类似ZooKeeper的作用,选主并管理集群拓扑。 gateway 负责对收到master广播下来的集群状态(cluster state)数据的持久化保存,并在集群完全重启时恢复它们。 indices indices模块管理全局级的索引设置,不包括索引级的(索引设置分为全局级和每个索引级)。 它还封装了索引数据恢复功能。集群启动阶段需要的主分片恢复和副分片恢复就是在这个模块实现的。 HTTP HTTP模块允许通过JSON over HTTP的方式访问ES的API,HTTP模块本质上是完全异步的,这意味着没有阻塞线程等待响应。使用异步通信进行 HTTP 的好处是解决了 C10k 问题(10k量级的并发连接)。 Transport 传输模块用于集群内节点之间的内部通信。从一个节点到另一个节点的每个请求都使用传输模块。 传输模块使用 TCP 通信,每个节点都与其他节点维持若干 TCP 长连接。内部节点间的所有通信都是本模块承载的。...

March 16, 2024

2023 Review

英语和国际化 年初确立了学习英语的目标,并且面向国际化接受更多的信息渠道。回顾历程,取得进步时显著的,但是还是不足以应对庞大多变的英语内容。今年在这边主要的变化主要是: Youtube:既能学到庞大的知识,也能接触实际的英语。 墨墨背单词:词汇量是基础,特别是期刊和书籍中使用的词汇特别庞杂。 口语:Coach Shane 读书 1. 历史深处的忧虑 (豆瓣) 学了这么多年书,这本书对我的触动实在是太大了。真正从这边书了解到美国的文化、政治和生活方式。都说美国是一个法治国家,可是这到底又意味着什么呢? 2. 刷新 (豆瓣) 深入了解纳德拉。带领微软重新走向辉煌的人物,准确把握住了云优先和人工智能,并且和微软很好的融合在一起。 3. 李光耀观天下 (豆瓣) 对国际形势的洞察以及新加坡的局限都有清晰的认识以及独到的看法。很适合加深对国际形势的了解。 做孩子最好的英语学习规划师 (豆瓣)、做孩子最好的英语学习规划师2:懒人解决方案 (豆瓣) 国内最好的孩子二语习得方法了,还需要不断坚持✊。 其他 活着 (豆瓣) 为什么学生不喜欢上学? (第2版) (豆瓣) 人类简史 (豆瓣) 长安的荔枝 (豆瓣) 纳瓦尔宝典 (豆瓣) 影响力 (豆瓣) 技术 ChatGPT无疑是今天科技行业最重大的发明了,也是带来了很多的争议。 Hacker News、Reddit - Dive into anything 工具 Obsidian、Z-library 2024 展望

January 4, 2024

Java注解在Class文件中的表示

之前在开发框架的时候大量使用了注解简化框架的使用和理解成本。所以整理一下Java注解在Class文件中的具体表示。 在Java的class文件中,注解被存储在一个叫做RuntimeVisibleAnnotations或者RuntimeInvisibleAnnotations的属性中。这两个属性分别对应运行时可见的注解和运行时不可见的注解。 每个注解在class文件中的表示形式如下: u2类型的type_index,指向一个CONSTANT_Utf8_info结构,表示注解接口的名称。 u2类型的num_element_value_pairs,表示注解的键值对数量。 element_value_pairs列表,表示注解的键值对列表。每个键值对由一个element_value_pairs结构表示,包含一个u2类型的element_name_index和一个element_value结构。element_name_index指向一个CONSTANT_Utf8_info结构,表示键的名称。element_value结构表示值,可以是常量、枚举常量、类信息、注解类型或者数组。 例如,对于如下的注解: @MyAnnotation(name="test", value=1) 在class文件中的表示形式可能如下: RuntimeVisibleAnnotations: num_annotations = 1 annotations[0]: type_index = #22 (#22 -> CONSTANT_Utf8_info: "LMyAnnotation;") num_element_value_pairs = 2 element_value_pairs[0]: element_name_index = #23 (#23 -> CONSTANT_Utf8_info: "name") value = CONSTANT_Utf8_info: "test" element_value_pairs[1]: element_name_index = #24 (#24 -> CONSTANT_Utf8_info: "value") value = CONSTANT_Integer_info: 1 这只是一个简化的表示,实际的表示会更复杂。具体的表示方式可以参考Java虚拟机规范。

October 16, 2023

Guava Cache 之 Expire vs Refresh

Cache LocalCache是Guava Cache本地缓存的实现类,可以看到LocalCache跟ConcurentHashMap有同样的继承关系。其实,在底层实现逻辑上,Cache也是借鉴了ConcurentHashMap的实现,也是将table划分为多个的Segment,提高读写的并法度,每个Segment是一个支持并发读的哈希表实现。跟ConcurentHashMap类似,不同的Segment可以支持并发的写操作。 Cache跟ConcurentHashMap主要的不同是,ConcurentHashMap当某个key不在用时,需要手动进行删除。而Cache最大的一个特点是根据配置的参数大小,可以自动过期掉其中的entry。主要的过期策略有:基于最大数量的过期,基于时间的过期,以及基于引用的过期。本文主要讨论基于时间的几个过期参数。 refresh和expire的不同点 Guava Cache提供了下面三种基于时间的过期和刷新机制: expireAfterAccess:当缓存项在指定的时间段内没有被读或写就会被回收 expireAfterWrite:当缓存项在指定的时间段内没有更新就会被回收。 refreshAfterWrite:当缓存项上一次更新操作之后的多久会被刷新。 源码分析 对于expire和refresh,CacheBuilder里有下面三个字段: long expireAfterWriteNanos = UNSET_INT; long expireAfterAccessNanos = UNSET_INT; long refreshNanos = UNSET_INT; 因为CacheBuilder是Builder模式,有以下三个方法设置参数: refreshAfterWrite方法: /** * Specifies that active entries are eligible for automatic refresh once a fixed duration has * elapsed after the entry's creation, or the most recent replacement of its value. The semantics * of refreshes are specified in {@link LoadingCache#refresh}, and are performed by calling * {@link CacheLoader#reload}....

May 22, 2019

算法精进之 k-d树

概述 k-d树(k-dimensional树的简称),是一种分割k维数据空间的数据结构。主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索)。K-D树是二进制空间分割树的特殊的情况。 算法原理 k-d tree是每个节点均为k维数值点的二叉树,其上的每个节点代表一个超平面,该超平面垂直于当前划分维度的坐标轴,并在该维度上将空间划分为两部分,一部分在其左子树,另一部分在其右子树。即若当前节点的划分维度为d,其左子树上所有点在d维的坐标值均小于当前值,右子树上所有点在d维的坐标值均大于等于当前值,本定义对其任意子节点均成立。 树的构建 一个平衡的k-d tree,其所有叶子节点到根节点的距离近似相等。但一个平衡的k-d tree对最近邻搜索、空间搜索等应用场景并非是最优的。 常规的k-d tree的构建过程为:循环依序取数据点的各维度来作为切分维度,取数据点在该维度的中值作为切分超平面,将中值左侧的数据点挂在其左子树,将中值右侧的数据点挂在其右子树。递归处理其子树,直至所有数据点挂载完毕。 切分维度选择优化 构建开始前,对比数据点在各维度的分布情况,数据点在某一维度坐标值的方差越大分布越分散,方差越小分布越集中。从方差大的维度开始切分可以取得很好的切分效果及平衡性。 中值选择优化 第一种,算法开始前,对原始数据点在所有维度进行一次排序,存储下来,然后在后续的中值选择中,无须每次都对其子集进行排序,提升了性能。 第二种,从原始数据点中随机选择固定数目的点,然后对其进行排序,每次从这些样本点中取中值,来作为分割超平面。该方式在实践中被证明可以取得很好性能及很好的平衡性。 例子 本文采用常规的构建方式,以二维平面点(x,y)的集合(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)为例结合下图来说明k-d tree的构建过程。 构建根节点时,此时的切分维度为x,如上点集合在x维从小到大排序为(2,3),(4,7),(5,4),(7,2),(8,1),(9,6);其中值为(7,2)。(注:2,4,5,7,8,9在数学中的中值为(5 + 7)/2=6,但因该算法的中值需在点集合之内,所以本文中值计算用的是len(points)//2=3, points[3]=(7,2)) (2,3),(4,7),(5,4)挂在(7,2)节点的左子树,(8,1),(9,6)挂在(7,2)节点的右子树。 构建(7,2)节点的左子树时,点集合(2,3),(4,7),(5,4)此时的切分维度为y,中值为(5,4)作为分割平面,(2,3)挂在其左子树,(4,7)挂在其右子树。 构建(7,2)节点的右子树时,点集合(8,1),(9,6)此时的切分维度也为y,中值为(9,6)作为分割平面,(8,1)挂在其左子树。至此k-d tree构建完成。 上述的构建过程结合下图可以看出,构建一个k-d tree即是将一个二维平面逐步划分的过程。 实现 def kd_tree(points, depth): if 0 == len(points): return None cutting_dim = depth % len(points[0]) medium_index = len(points) // 2 points.sort(key=itemgetter(cutting_dim)) node = Node(points[medium_index]) node.left = kd_tree(points[:medium_index], depth + 1) node.right = kd_tree(points[medium_index + 1:], depth + 1) return node 寻找d维最小坐标值点 若当前节点的切分维度是d 因其右子树节点均大于等于当前节点在d维的坐标值,所以可以忽略其右子树,仅在其左子树进行搜索。若无左子树,当前节点即是最小坐标值节点。...

May 19, 2019

ElasticSearch 入门

基本概念 Node与Cluster ElasticSearch作为分布式的搜索引擎,支持多台服务器协同工作,每台服务器上也可以同时运行多个实例。 每个实例称为一个节点(node)。每个节点有cluster.name属性,相同的cluster.name的节点构成一个集群(cluster)。 Index ElasticSearch会索引所有字段,经过处理后写入一个反向索引(Inverted Index)。查找数据的时候,直接查找该索引。 所以,Elastic 数据管理的顶层单位就叫做 Index(索引)。它是单个数据库的同义词。每个 Index (即数据库)的名字必须是小写。 下面的命令可以查看当前节点的所有 Index。 $ curl -X GET 'http://localhost:9200/_cat/indices?v' Document Index 里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。 Document 使用 JSON 格式表示,下面是一个例子。 { "user": "张三", "title": "工程师", "desc": "数据库管理" } 同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。 Type Document 可以分组,比如weather这个 Index 里面,可以按城市分组(北京和上海),也可以按气候分组(晴天和雨天)。这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document。 不同的 Type 应该有相似的结构(schema),举例来说,id字段不能在这个组是字符串,在另一个组是数值。这是与关系型数据库的表的一个区别。性质完全不同的数据(比如products和logs)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到)。 下面的命令可以列出每个 Index 所包含的 Type。 $ curl 'localhost:9200/_mapping?pretty=true' 根据规划,Elastic 6.x 版只允许每个 Index 包含一个 Type,7.x 版将会彻底移除 Type。...

May 19, 2019

模拟直接内存溢出

最近在重新阅读一遍《深入理解 Java 虚拟机》,因为现在一直用着Java 8环境,就用Java 8跑一下书上的例子。不过,书上是基于Java 6/7,经过Java 8的改进,很多例子都没法测试,主要是方法区/永久区的例子,在Java 8中是元数据区。不过本文是模拟直接内存OOM的例子。 书上的例子调用unsafe.allocateMemory()方法分配内存,但是没有触发OOM,程序正常运行。 /** * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * @author zzm */ public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws Exception { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB); } } } 在StackOverflow上找到别人的提问:Why XX:MaxDirectMemorySize can’t limit Unsafe.allocateMemory?,遇到了和我一样的问题。下面说一下具体的原因: Unsafe.allocateMemory()是系统调用的os::malloc一个包装,并没有关心VM要求的内存限制,所以也就绕过了MaxDirectMemorySize的限制。 而ByteBuffer.allocateDirect()在会在调用这个方法之前,调用Bits.reserveMemory(),这个方法将检查进程的内存占用情况并抛出异常。 修改后的代码如下: /** * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * @author zzm */ public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws Exception { Field unsafeField = Unsafe....

May 11, 2019