第20章 - 性能优化与最佳实践
20.1 性能问题的来源与度量
20.1.1 五类开销
JTS 应用的性能问题,几乎都落在下面五类里。定位问题时先分类,比盲目“优化代码”有效得多:
| 类别 | 典型表现 | 常见原因 | 优化方向 |
|---|---|---|---|
| IO 开销 | CPU 不高但慢 | WKT 文本解析、数据库往返、文件反复读 | 换 WKB/二进制、批量读写 |
| 算法复杂度 | 数据量翻倍耗时翻四倍 | 逐对求交、逐对 union、无索引的包含判断 | 建索引、批量算法 |
| 对象分配 | GC 频繁、CPU 抖动 | 循环里 getCoordinates()、反复建 GeometryFactory |
复用、少拷贝 |
| 无效输入 | 偶发极慢或抛异常 | 自相交、近重合边触发大量 noding | 入口校验/修复 |
| 并发争用 | 核多但吞吐不涨 | 共享可变几何、锁粒度大 | 线程安全边界划分 |
优化第一定律:先测量,再动手。 没有 profile 的“我觉得这里慢”经常是错的——JTS 的热点往往在 Coordinate 数组分配、Envelope 计算、noding 的相交检测上,而不是调用方以为的“几何对象创建”。
20.1.2 度量流程
确定基线(固定数据集、固定 JVM、固定 JTS 版本)
│
▼
端到端计时(找出慢的业务步骤) ← 先粗后细
│
▼
采样剖析(JFR / async-profiler) ← 看热点方法栈
│
▼
微基准(JMH)验证单个优化 ← 排除噪声
│
▼
回归对比(吞吐 + P99 + 内存 + GC)
│
▼
灰度上线,监控真实指标
三个常见误区:
- 用
System.currentTimeMillis()在业务代码里打点,同一毫秒内多次调用测不出差异; - 不预热就测,JIT 还没编译完,结论反着来;
- 只测平均值,不看 P99 与 GC——线上事故往往发生在长尾。
20.1.3 复杂度直觉
| 操作 | 典型复杂度 | 备注 |
|---|---|---|
Geometry.getArea() / getLength() |
O(n) | 每次重新遍历坐标 |
Geometry.getEnvelopeInternal() |
O(1)(缓存) | 首次计算 O(n),之后返回缓存引用 |
Envelope.contains(Coordinate) |
O(1) | 便宜,适合预筛 |
Geometry.intersects(Geometry) |
最坏 O(n·m) | 无索引时逐段比较 |
| PreparedGeometry 谓词 | 约 O(log n + k) | 预建线段索引与点定位 |
| STRtree 查询 | O(log n + k) | 需先 build |
| OverlayNG(面-面) | 约 O((n+m) log(n+m)) 起步 | noding 是主要成本 |
Geometry.getCoordinates() |
O(n) + 分配 | 高频调用要缓存 |
“O(n log n) 起步”的意思是:叠加分析除了 noding,还要构环、标注、输出,常数项很大。减少顶点数往往比换算法更有效。
20.2 PreparedGeometry:重复谓词的加速器
20.2.1 原理
同一个面与成千上万个点/线做 intersects / contains 判断时,普通 API 每次都要重新遍历面的所有线段。PreparedGeometry 把面“预处理”成带索引的结构:
- PreparedPolygon 懒加载两类索引:线段集合的 FastSegmentSetIntersectionFinder(用于 intersects)与 IndexedPointInAreaLocator(用于点定位);
- 矩形特判:若几何是矩形,直接走 RectangleIntersects / RectangleContains 的快速路径;
- 包络预筛:先用包络排除明显不相交的输入。
普通 intersects: Prepared intersects:
每次遍历所有线段 首次构建线段索引(懒加载)
O(n) per call 之后每次约 O(log n + k)
──────────────── ────────────────────
1 万次调用 → 1 万次遍历 1 万次调用 → 1 次构建 + 1 万次查询
20.2.2 API 与方法
PreparedGeometryFactory.prepare(Geometry) 是入口,返回 PreparedGeometry 接口。接口方法与语义如下:
| 方法 | 等价 Geometry 方法 | 说明 |
|---|---|---|
intersects(g) |
intersects |
支持 GeometryCollection 输入 |
contains(g) |
contains |
完整包含语义 |
containsProperly(g) |
无直接等价 | 测试几何完全在内部,不碰边界 |
covers(g) |
covers |
覆盖(允许边界接触) |
coveredBy(g) |
coveredBy |
反向覆盖 |
crosses(g) |
crosses |
跨立 |
disjoint(g) |
disjoint |
分离 |
overlaps(g) |
overlaps |
部分重叠 |
touches(g) |
touches |
接触 |
within(g) |
within |
被包含 |
getGeometry() |
— | 取回原几何 |
containsProperly 是 PreparedGeometry 独有语义:测试几何的每个点都在目标内部,完全不碰边界。它比 contains 便宜,非常适合“大面里筛小面”的场景——若小面 properly contained,则它与大面的交集就是它自己,可以直接跳过昂贵的求交。
20.2.3 用法与效果
import java.util.ArrayList;
import java.util.List;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.prep.PreparedGeometry;
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
import org.locationtech.jts.io.WKTReader;
public class PreparedDemo {
public static void main(String[] args) throws Exception {
WKTReader r = new WKTReader();
Polygon city = (Polygon) r.read(
"POLYGON ((0 0, 100 0, 100 100, 0 100, 0 0))");
GeometryFactory gf = new GeometryFactory();
List<Point> points = new ArrayList<Point>();
for (int i = 0; i < 100000; i++) {
double x = (i * 37 % 200) - 50; // 固定伪随机,结果可复现
double y = (i * 91 % 200) - 50;
points.add(gf.createPoint(new Coordinate(x, y)));
}
// 1) 普通方式
long t0 = System.nanoTime();
int count1 = 0;
for (Point p : points) {
if (city.intersects(p)) count1++;
}
long t1 = System.nanoTime();
// 2) Prepared 方式
PreparedGeometry prep = PreparedGeometryFactory.prepare(city);
long t2 = System.nanoTime();
int count2 = 0;
for (Point p : points) {
if (prep.intersects(p)) count2++;
}
long t3 = System.nanoTime();
System.out.println("结果一致 = " + (count1 == count2));
System.out.println("普通耗时 = " + (t1 - t0) / 1_000_000 + " ms");
System.out.println("Prepared 耗时 = " + (t3 - t2) / 1_000_000 + " ms");
// 典型量级:普通 100~300 ms,Prepared 5~20 ms(取决于多边形顶点数)
}
}
上面的计时只用于示意数量级,正式对比请用 20.8 节的 JMH 写法。要点是:PreparedGeometry 的收益随“调用次数 / 顶点数”增长;只调用一两次时,构建索引反而是负担。
20.2.4 复用、线程安全与生命周期
Javadoc 明确:PreparedGeometry 的实现是线程安全的,可以在多线程间共享(这正是 prepared 状态的价值所在)。但要注意:
PreparedGeometryFactory本身无状态,可复用;- 被准备的 Geometry 必须不变:一旦修改了原几何(
setSRID、normalize()、apply修改坐标),索引就与几何不一致; - PreparedGeometry 内部索引是懒加载的,第一次调用才构建,因此“构建成本”发生在首次查询而非 prepare 时;
- 1.20 修复了 PreparedGeometry 对 EMPTY 元素的处理(版本历史 #904),老版本需注意空元素。
20.3 空间索引:STRtree、HPRtree 与查询优化
20.3.1 STRtree 的半静态语义
STRtree 是按 Sort-Tile-Recursive 算法打包的 R 树,特点是“插入后构建,构建后只读”:
- 插入:
insert(Envelope, Object);插入不是线程安全的,多线程插入必须外部同步; - 构建:第一次查询时自动构建(也可显式
build());构建后不能再插入,源码里是Assert.isTrue(!built, ...); - 查询:
query(Envelope)、query(Envelope, ItemVisitor)是线程安全的; - 删除:
remove(Envelope, Object)允许,但会破坏打包结构,频繁删除应重建。
典型生命周期:
new STRtree() → insert × N → 第一次 query 触发 build → 只读查询
│
└─ 再 insert → Assert 失败(禁用)
20.3.2 批量空间连接
最常见的用法不是“树查询”,而是空间连接:给 A 集合建索引,遍历 B 集合查询候选,再精确判断。
import java.util.ArrayList;
import java.util.List;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.index.strtree.STRtree;
import org.locationtech.jts.io.WKTReader;
public class StrTreeJoinDemo {
public static void main(String[] args) throws Exception {
WKTReader r = new WKTReader();
Geometry parcels = r.read(
"MULTIPOLYGON (((0 0, 10 0, 10 10, 0 10, 0 0)),"
+ " ((10 0, 20 0, 20 10, 10 10, 10 0)))");
Geometry[] quakes = new Geometry[] {
r.read("POINT (5 5)"),
r.read("POINT (15 5)"),
r.read("POINT (30 30)")
};
// 1) 给面建索引(按部件逐个插入,便于交并时定位)
STRtree index = new STRtree();
for (int i = 0; i < parcels.getNumGeometries(); i++) {
Geometry poly = parcels.getGeometryN(i);
index.insert(poly.getEnvelopeInternal(), poly);
}
index.build();
// 2) 遍历点,先包络查询,再精确判断
for (Geometry q : quakes) {
Envelope env = q.getEnvelopeInternal();
List candidates = index.query(env);
boolean hit = false;
for (Object o : candidates) {
Geometry poly = (Geometry) o;
if (poly.intersects(q)) { hit = true; break; }
}
System.out.println(q + " 落在面内 = " + hit);
}
}
}
要点:
- 索引项存“部件”而不是整个 MultiPolygon,查询结果更精确;
getEnvelopeInternal()返回内部缓存引用(不拷贝),查询时直接传,避免每次getEnvelope()复制;- 精确判断仍要
intersects,索引只负责减少候选。
20.3.3 HPRtree:静态批量索引
HPRtree(Hilbert-Packed R-tree)是 1.16 引入的静态索引,用 Hilbert 曲线排序后按块打包,内存更省、构建更快。Javadoc 的结论是:HPRtree 比 STRtree 略快,且对象分配更少,但不保证对 JTS 操作带来显著提升。
使用方式与 STRtree 类似,但必须显式 build()(或首次查询时自动构建):
import java.util.List;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.index.hprtree.HPRtree;
public class HprTreeDemo {
public static void main(String[] args) {
HPRtree tree = new HPRtree();
for (int i = 0; i < 1000; i++) {
double x = (i * 31 % 1000);
double y = (i * 17 % 1000);
tree.insert(new Envelope(x, x + 1, y, y + 1), "item-" + i);
}
tree.build();
System.out.println("size = " + tree.size());
List hits = tree.query(new Envelope(0, 100, 0, 100));
System.out.println("命中 = " + hits.size());
}
}
20.3.4 索引选择
| 索引 | 构建方式 | 动态插入 | 查询线程安全 | 适用场景 |
|---|---|---|---|---|
| STRtree | 首次查询自动构建 | 构建前可插 | 是 | 通用、半静态数据 |
| HPRtree | 显式 build / 首次查询 | 构建前可插 | 是 | 静态大数据、批量查询 |
| Quadtree | 增量插入 | 支持(1.19 起线程安全) | 需查版本 | 动态更新、均匀分布 |
| IntervalRTree | 一维区间 | 构建后只读 | — | 线段/区间索引 |
| KdTree | 增量插入 | 支持 | — | 最近邻、点集 |
经验法则:数据量 > 数千且查询多于一次,就值得建索引;数据量小或只查一次,线性扫描更快。
20.4 批量 overlay 与 union 策略
20.4.1 逐对 union 的平方级陷阱
把 N 个面合并成一张图,最直觉的写法是循环:
Geometry result = geoms.get(0);
for (int i = 1; i < geoms.size(); i++) {
result = result.union(geoms.get(i)); // 每次都与“越来越大”的结果求并
}
这段代码的问题是:第 i 次 union 的输入规模是“已合并结果 + 新几何”,越往后越慢,整体接近 O(N²),而且中间结果反复拷贝。N=1000 时可能比正确写法慢几十倍。
20.4.2 正确姿势
| 场景 | 推荐 API | 说明 |
|---|---|---|
| 任意几何集合的一元并 | UnaryUnionOp.union(Collection) | 内部按维度分组、用索引加速 |
| 多边形集合的级联合并 | CascadedPolygonUnion.union(Collection) | 自底向上两两合并,避免超大中间结果 |
| 需要稳健性 | OverlayNGRobust.union(Collection) | 一元并 + OverlayNG 回退 |
| 合法覆盖率合并 | CoverageUnion.union(Geometry[]) | 利用公共边精确匹配,最快 |
| 两个几何求并 | Geometry.union 或 OverlayNG |
只有两个时无需批量算法 |
import java.util.ArrayList;
import java.util.List;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.operation.overlayng.OverlayNGRobust;
import org.locationtech.jts.operation.union.CascadedPolygonUnion;
import org.locationtech.jts.operation.union.UnaryUnionOp;
public class BatchUnionDemo {
public static void main(String[] args) throws Exception {
WKTReader r = new WKTReader();
List<Geometry> polys = new ArrayList<Geometry>();
for (int i = 0; i < 200; i++) {
double x = (i % 20) * 10.0;
double y = (i / 20) * 10.0;
polys.add(r.read("POLYGON ((" + x + " " + y + ", "
+ (x + 10) + " " + y + ", " + (x + 10) + " " + (y + 10) + ", "
+ x + " " + (y + 10) + ", " + x + " " + y + "))"));
}
// 1) 一元并(通用)
Geometry u1 = UnaryUnionOp.union(polys);
// 2) 级联多边形并(纯面集合更快)
Geometry u2 = CascadedPolygonUnion.union(polys);
// 3) 稳健一元并
Geometry u3 = OverlayNGRobust.union(polys);
System.out.println(u1.getArea());
System.out.println(u2.getArea());
System.out.println(u3.getArea());
// 相邻面共享边,三者面积一致;差别在耗时
}
}
20.4.3 先用索引减少参与运算的几何
如果只是“把互相接触的一堆面合并成若干块”,不需要对全部要素做一元并。更好的策略是:
- 用 STRtree 找每个面的相邻面(包络相交);
- 用并查集把相邻面分组;
- 每组内部用 CascadedPolygonUnion 合并。
这样把一次大 union 拆成若干小 union,既降低了 noding 规模,也便于并行。
N 个面
│
▼
STRtree 包络查询 → 邻接图
│
▼
并查集分组
│
├── 连通块 1 → union → 结果 1
├── 连通块 2 → union → 结果 2
└── ...
│
▼
MultiPolygon 汇总(块之间不相交,无需再 union)
20.5 前置处理:校验、修复与简化
20.5.1 无效输入的隐性成本
无效几何对性能的伤害不是线性的:一次自相交可能让 noding 进入大量重复求交,甚至触发 OverlayNGRobust 的多级回退(浮点 → 5 轮吸附 → snap-rounding),单次耗时可能是正常情况的百倍。更糟的是,它可能“有时成功有时失败”,让性能问题显得随机。
入口处花 1 毫秒做校验,胜过运行时花 1 秒重试。 校验与修复见第 17 章,这里只强调顺序:
解析 → 坐标有限性检查 → IsValidOp → GeometryFixer(按需)
→ 精度约简(按需,统一网格)→ 入库/进入计算池
20.5.2 先简化后运算
顶点数是叠加/缓冲成本的主因。如果业务允许较小误差,先简化能带来数量级收益:
- DouglasPeuckerSimplifier:距离容差,快,但可能产生自交/丢拓扑;
- TopologyPreservingSimplifier:保持拓扑,稍慢;
- CoverageSimplifier:整组覆盖率保持,适合面群(见第 17 章);
- GeometryPrecisionReducer:降精度、去重复点,顺便减少顶点。
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.precision.GeometryPrecisionReducer;
import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;
import org.locationtech.jts.simplify.TopologyPreservingSimplifier;
public class PreprocessDemo {
public static void main(String[] args) throws Exception {
WKTReader r = new WKTReader();
Geometry g = r.read(
"POLYGON ((0 0, 5 0.001, 10 0, 10 5, 9.999 10, 5 10.001,"
+ " 0 10, 0.001 5, 0 0))");
System.out.println("原始顶点 = " + g.getNumPoints()); // 9
Geometry dp = DouglasPeuckerSimplifier.simplify(g, 0.01);
System.out.println("DP 简化后 = " + dp.getNumPoints());
Geometry tp = TopologyPreservingSimplifier.simplify(g, 0.01);
System.out.println("拓扑保持后 = " + tp.getNumPoints());
Geometry pr = GeometryPrecisionReducer.reduce(g, new PrecisionModel(-0.5));
System.out.println("精度约简后 = " + pr.getNumPoints());
}
}
20.5.3 前置处理的收益边界
不是所有场景都适合前置简化:
- 如果下游需要精确面积,简化会引入系统偏差;
- 如果数据要参与覆盖率校验,逐要素简化会破坏公共边匹配,必须用 CoverageSimplifier;
- 如果数据只是过滤/筛选,可以先不简化,等真正需要几何运算时再处理。
优化顺序建议:先修正确性,再减数据量,最后调算法参数。反过来做,往往白忙。
20.6 对象分配与内存
20.6.1 JTS 的对象模型
一次几何运算会创建大量短命对象:Coordinate、CoordinateSequence、Envelope、LineSegment、Edge、SegmentString……在 100 万顶点的数据上,这些对象的分配与回收往往比算法本身更耗时。
| 对象 | 典型构成 | 估算内存(64 位 JVM,含对象头) | 备注 |
|---|---|---|---|
Coordinate |
3 个 double(x, y, z) | 约 40 字节 | 默认带 z,即使不用 |
CoordinateXY |
2 个 double | 约 32 字节 | 2D 数据可省 20% |
CoordinateArraySequence |
Coordinate 引用数组 | 8 字节/点 + 对象 | 默认实现,灵活但对象多 |
| PackedCoordinateSequence(double 2D) | double 数组 | 16 字节/点 | 紧凑,无逐个对象 |
| PackedCoordinateSequence(float 2D) | float 数组 | 8 字节/点 | 省内存,损精度 |
Envelope |
4 个 double | 约 48 字节 | 高频创建要复用 |
默认工厂使用的是 CoordinateArraySequenceFactory(源码 GeometryFactory.getDefaultCoordinateSequenceFactory() 返回 CoordinateArraySequenceFactory.instance())。如果你看到“JTS 默认用 Packed 序列”的说法,那是不准确的:要紧凑存储必须显式指定 PackedCoordinateSequenceFactory。
20.6.2 高频反例:循环里调用 getCoordinates()
// 反例:每次 getCoordinates() 都新建数组与坐标对象
double sumX = 0;
for (int k = 0; k < lines.size(); k++) {
Geometry g = lines.get(k);
Coordinate[] cs = g.getCoordinates(); // 每次分配
for (int i = 0; i < cs.length; i++) {
sumX += cs[i].x;
}
}
// 正例:直接遍历 CoordinateSequence,避免中间数组
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
double sumX2 = 0;
for (int k = 0; k < lines.size(); k++) {
Geometry g = lines.get(k);
if (!(g instanceof LineString)) continue;
CoordinateSequence seq = ((LineString) g).getCoordinateSequence();
for (int i = 0; i < seq.size(); i++) {
sumX2 += seq.getX(i); // 无额外分配
}
}
注意 seq.getX(i) 是否高效取决于序列实现:CoordinateArraySequence 每次会读 Coordinate 对象,Packed 序列直接读数组。无论哪种,都比先拷贝一整个数组便宜。
20.6.3 复用 Envelope 与工厂
- GeometryFactory:Javadoc 明确“线程安全”,应按 (SRID, PrecisionModel, CoordinateSequenceFactory) 组合缓存复用,不要在每个要素里
new GeometryFactory(); - Envelope:查询循环里复用同一个
Envelope,用init/expandToInclude重置,减少短命对象; - WKTReader / WKBReader:解析器本身可复用(无状态或轻状态),但注意 WKTReader 的工厂配置在构造时固定。
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.index.strtree.STRtree;
public class EnvelopeReuseDemo {
public static void main(String[] args) {
STRtree tree = new STRtree();
// ... 假设已插入大量要素
// 反例:每次查询 new Envelope
// for (Geometry q : queries) {
// List hits = tree.query(new Envelope(q.getEnvelopeInternal()));
// }
// 正例:复用 Envelope
Envelope env = new Envelope();
Geometry[] queries = new Geometry[0];
for (int i = 0; i < queries.length; i++) {
env.init(queries[i].getEnvelopeInternal());
tree.query(env);
}
}
}
另外要记住:getEnvelopeInternal() 返回内部缓存引用,只读;getEnvelope() 返回副本。查询时用前者省一次拷贝,但绝不能修改它。
20.6.4 大结果集与内存峰值
- 不要把百万要素一次性读进
List<Geometry>,用分批(每批 1 万)处理; - 中间结果及时释放引用,避免在同一作用域持有多个大几何;
- 输出时用流式 WKBWriter/WKTWriter(
write(Geometry, Writer)),不要先拼成巨型字符串; - 监控 GC 分配速率,而不是只看堆占用:高分配速率往往意味着大量短命对象。
20.7 线程安全边界
20.7.1 安全矩阵
| 对象/操作 | 多线程读 | 多线程写 | 说明 |
|---|---|---|---|
| GeometryFactory | 安全 | — | 不可变,Javadoc 明确线程安全 |
| Geometry(只读方法) | 安全 | — | 不修改内部状态 |
| Geometry(setSRID/userData/normalize/apply 修改) | 不安全 | 不安全 | 会改变对象状态 |
| PreparedGeometry | 安全 | — | Javadoc 明确线程安全 |
| STRtree query | 安全 | — | 查询无状态 |
| STRtree insert / build | — | 不安全 | 插入需外部同步 |
| HPRtree build | — | 不安全 | 构建期单线程 |
| OverlayNG 实例 | 不安全 | 不安全 | 有内部状态,一次一个线程 |
| Noder / LineIntersector / Simplifier 实例 | 不安全 | 不安全 | 有可变字段 |
| GeometryFactory 创建的新几何 | — | — | 结果对象归本线程 |
20.7.2 最容易踩的坑
坑一:并发 normalize。 有人为了“结果稳定”在多线程里对共享几何调用 normalize()——该方法会就地重排坐标与环方向,多线程同时调用会把几何改坏。
坑二:边读边 setSRID。 一个线程在解析后 setSRID,另一个线程已经在用它建索引,索引与 SRID 无关但几何字段被改,存在可见性问题。
坑三:复用 Simplifier / Intersector 实例。 例如把同一个 RobustLineIntersector 放进多个线程共享,内部 intPt 数组会被覆盖,结果随机错乱。正确做法是每个线程一个实例(ThreadLocal 或方法内 new)。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.prep.PreparedGeometry;
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
public class ParallelPrepareDemo {
public static void main(String[] args) throws Exception {
GeometryFactory gf = new GeometryFactory();
final Geometry bigPolygon =
gf.toGeometry(new Envelope(0, 100, 0, 100));
final PreparedGeometry prep =
PreparedGeometryFactory.prepare(bigPolygon); // 线程安全,可共享
final List<Geometry> batch = new ArrayList<Geometry>();
for (int i = 0; i < 10000; i++) {
batch.add(gf.createPoint(
new Coordinate((i * 37 % 200) - 50, (i * 91 % 150) - 25)));
}
ExecutorService pool = Executors.newFixedThreadPool(4);
List<Future<Integer>> futures = new ArrayList<Future<Integer>>();
for (int t = 0; t < 4; t++) {
final int from = t * 2500;
final int to = from + 2500;
futures.add(pool.submit(new Callable<Integer>() {
public Integer call() {
int count = 0;
for (int i = from; i < to; i++) {
if (prep.intersects(batch.get(i))) count++;
}
return count;
}
}));
}
int total = 0;
for (Future<Integer> f : futures) total += f.get();
pool.shutdown();
System.out.println("命中总数 = " + total);
}
}
20.7.3 并行模式
┌──────────────────────────────────────────────────────┐
│ 共享只读: GeometryFactory、PreparedGeometry、STRtree │
├──────────────────────────────────────────────────────┤
│ 线程私有: OverlayNG、Noder、LineIntersector、 │
│ 结果几何、临时 CoordinateSequence │
├──────────────────────────────────────────────────────┤
│ 任务切分: 按图幅/行政区/批次切分,避免锁 │
└──────────────────────────────────────────────────────┘
并行前先问三个问题:
- 任务粒度够大吗?(每次任务至少几十毫秒,否则线程调度成本占比过高)
- 共享对象是否真的只读?
- 内存峰值能否承受“核数 × 单任务内存”?
20.8 JMH 基准与热点定位
20.8.1 为什么不能用 nanoTime 做正式对比
业务代码里的 System.nanoTime() 计时受这些因素干扰:
- JIT 预热:前几千次调用是解释执行/C1 编译,慢一个数量级;
- 死代码消除:计算结果没被使用,JIT 可能直接删掉;
- 常量折叠:输入是常量时,整个计算可能被优化掉;
- GC 抖动:一次 Full GC 就能让平均值翻倍;
- CPU 频率与调度:同一台机器不同时刻差异可达 20%。
JMH 通过预热、多次 fork、黑盒消费、统计分布来消除这些干扰,是 JVM 微基准的事实标准。
20.8.2 JMH 工程配置
Maven 依赖(JMH 1.37):
<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
<scope>provided</scope>
</dependency>
</dependencies>
打包可执行基准 jar 需要 shade 插件并把主类设为 org.openjdk.jmh.Main,然后用 java -jar target/benchmarks.jar 运行。
20.8.3 基准类示例
import java.util.concurrent.TimeUnit;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.prep.PreparedGeometry;
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
import org.locationtech.jts.io.WKTReader;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(2)
public class PredicateBenchmark {
private Geometry polygon;
private PreparedGeometry prepared;
private Geometry probe;
@Setup
public void setup() throws Exception {
WKTReader r = new WKTReader();
polygon = r.read("POLYGON ((0 0, 100 0, 100 100, 0 100, 0 0))");
prepared = PreparedGeometryFactory.prepare(polygon);
probe = r.read("LINESTRING (10 10, 90 90)");
}
@Benchmark
public void plainIntersects(Blackhole bh) {
bh.consume(polygon.intersects(probe));
}
@Benchmark
public void preparedIntersects(Blackhole bh) {
bh.consume(prepared.intersects(probe));
}
}
结果解读要点:
- Score 是每次操作的平均耗时(AverageTime),越低越好;
- 看 Error 与置信区间,差异小于误差范围的结论不可信;
- 同时看 gc.alloc.rate(JMH 的 GC profiler)判断分配压力;
- 对照组要公平:同一个 probe、同一份数据、同样的预热。
20.8.4 热点定位工具
| 工具 | 用法 | 适合发现 |
|---|---|---|
| JFR | -XX:StartFlightRecording=duration=60s,filename=app.jfr |
CPU 热点、分配、锁、GC |
| async-profiler | -agentpath:libasyncProfiler.so=start,event=cpu,file=prof.html |
低开销 CPU/分配火焰图 |
| JConsole / VisualVM | 图形化 | 内存、线程、GC |
| GC 日志 | -Xlog:gc*:file=gc.log |
停顿、晋升、分配速率 |
| JIT 日志 | -XX:+PrintCompilation |
编译行为异常 |
读 flame graph 的原则:先看宽(占比大的栈),再看JTS 边界内的 self time。如果大部分时间在 Coordinate.copy、Arrays.copyOf、Envelope.expandToInclude,说明分配/拷贝是主因;如果在 MCIndexNoder、SegmentIntersector 里,说明 noding 规模是主因。
20.8.5 基准反例
| 反例 | 问题 |
|---|---|
| 只测一次、不预热 | 测到的是解释执行时间 |
| 不用 Blackhole 消费结果 | JIT 删掉计算 |
| 输入是常量 | 常量折叠 |
| 每个基准方法里都 new 数据 | 测到的是构造时间 |
| 不同基准共享可变状态 | 线程/缓存污染 |
| 只报告平均值 | 长尾问题被掩盖 |
20.9 OverlayNG 调优
20.9.1 先选对精度策略
第 18 章讲过三档策略。从性能角度:
- 浮点 OverlayNG 最快,但可能抛异常;
- OverlayNGRobust 在成功路径与浮点几乎同速,失败才回退;
- 固定精度 snap-rounding 比浮点略慢(要维护 hot pixel 索引),但避免了回退。
经验:批处理用固定精度(结果可复现、不触发回退),交互式用浮点 + 捕获异常。
20.9.2 减少输入规模
叠加分析的耗时可粗略分解:
总时间 ≈ noding(线段求交) + 构环/标注 + 输出构造
↑ 主导项,随线段数与交点数增长
因此最有效的优化是减少参与 noding 的线段:
- 包络预筛:包络不相交直接返回空/原几何,根本不进 overlay;
- 前置精度约简:去掉重复点、把顶点钉到网格,减少近重合边;
- 前置简化:在误差允许时降低顶点数;
- 空间分区:把一个巨型 overlay 拆成多个小区域 overlay,再拼接(见 20.10)。
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.operation.overlayng.OverlayNG;
import org.locationtech.jts.operation.overlayng.OverlayNGRobust;
import org.locationtech.jts.precision.GeometryPrecisionReducer;
public class OverlayTuningDemo {
public static void main(String[] args) throws Exception {
WKTReader r = new WKTReader();
Geometry a = r.read("POLYGON ((0 0, 100 0, 100 100, 0 100, 0 0))");
Geometry b = r.read("POLYGON ((50 50, 150 50, 150 150, 50 150, 50 50))");
// 0) 包络预筛:不相交就不算
Envelope ea = a.getEnvelopeInternal();
Envelope eb = b.getEnvelopeInternal();
if (!ea.intersects(eb)) {
System.out.println("包络不相交,跳过 overlay");
return;
}
// 1) 前置精度约简(可选)
PrecisionModel pm = new PrecisionModel(-0.001);
Geometry a2 = GeometryPrecisionReducer.reduce(a, pm);
Geometry b2 = GeometryPrecisionReducer.reduce(b, pm);
// 2) 三种策略(正式对比请用 JMH)
long t0 = System.nanoTime();
Geometry g1 = OverlayNG.overlay(a2, b2, OverlayNG.INTERSECTION);
long t1 = System.nanoTime();
Geometry g2 = OverlayNG.overlay(a2, b2, OverlayNG.INTERSECTION, pm);
long t2 = System.nanoTime();
Geometry g3 = OverlayNGRobust.overlay(a2, b2, OverlayNG.INTERSECTION);
long t3 = System.nanoTime();
System.out.println("浮点 = " + (t1 - t0) / 1000 + " 微秒,面积=" + g1.getArea());
System.out.println("固定 = " + (t2 - t1) / 1000 + " 微秒,面积=" + g2.getArea());
System.out.println("稳健 = " + (t3 - t2) / 1000 + " 微秒,面积=" + g3.getArea());
}
}
20.9.3 调试开关不要留在生产
setOutputEdges、setOutputNodedEdges、setOutputResultEdges 会让 OverlayNG 保留并输出中间结构,内存和耗时都显著增加;setOptimized(false) 会关闭包络裁剪等优化。这些开关只用于问题复现,生产代码里必须关闭。
20.9.4 判断“该不该用 overlay”
| 需求 | 更便宜的替代 |
|---|---|
| 只要判断是否相交/包含 | PreparedGeometry 谓词 |
| 只要按范围过滤 | Envelope 判断 + STRtree |
| 把覆盖率合并成一张图 | CoverageUnion |
| 合并互相接触的面群 | STRtree 分组 + CascadedPolygonUnion |
| 求两个几何的精确交集 | OverlayNG / OverlayNGRobust(必须用) |
| 缓冲区后求并 | 直接 buffer(内部处理并) |
能不算 overlay 就不算,是 JTS 性能优化里回报最高的一条。
20.10 分块与并行处理
20.10.1 为什么要分块
单机处理省级、国家级数据时,一次性把所有几何放进内存做 overlay 有三个问题:
- 内存峰值:中间 noding 结构可能与输入同量级甚至更大;
- 无法并行:一个巨型 overlay 只能单线程跑;
- 失败代价高:一次异常要全部重算。
分块处理把大任务拆成互相独立的小任务,同时解决这三点。
20.10.2 分块方式与边界处理
| 分块方式 | 优点 | 注意 |
|---|---|---|
| 规则网格(瓦片) | 实现简单、负载均衡 | 要素可能横跨多块 |
| 行政区/图幅 | 符合业务、便于质检 | 大小不均,需二次均衡 |
| 按要素数量批次 | 内存可控 | 空间局部性差,跨批要素丢失关系 |
| 按空间索引分桶(Hilbert) | 局部性好 | 需要预处理排序 |
无论哪种方式,分块边界都必须外扩 buffer,否则跨块要素在块内看起来“被切断”,计算结果不完整:
瓦片 A(输出范围) 瓦片 A 的处理范围(外扩 buffer)
┌──────────────┐ ┌──────────────────────────┐
│ │ │ ┌──────────────┐ │
│ │ │ │ 输出区 │ │
│ │ │ └──────────────┘ │
└──────────────┘ └──────────────────────────┘
只裁不扩 → 跨界要素丢失 先扩后裁 → 结果完整
20.10.3 并行处理骨架
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.index.strtree.STRtree;
public class ParallelTileDemo {
/** 单个瓦片任务:查询 → 相交 → 裁回输出范围 */
static Geometry processTile(Envelope outEnv, double buffer,
STRtree index, GeometryFactory gf) {
Envelope queryEnv = new Envelope(outEnv);
queryEnv.expandBy(buffer);
List candidates = index.query(queryEnv); // 线程安全
List<Geometry> clipped = new ArrayList<Geometry>();
for (Object o : candidates) {
Geometry g = (Geometry) o;
Geometry hit = g.intersection(gf.toGeometry(queryEnv));
if (!hit.isEmpty()) clipped.add(hit);
}
if (clipped.isEmpty()) return gf.createGeometryCollection();
Geometry merged = clipped.get(0);
for (int i = 1; i < clipped.size(); i++) {
merged = merged.union(clipped.get(i)); // 小规模,局部并
}
// 裁回瓦片输出范围
return merged.intersection(gf.toGeometry(outEnv));
}
public static void main(String[] args) {
GeometryFactory gf = new GeometryFactory();
STRtree index = new STRtree();
// ... 向 index 插入数据并 build()
List<Envelope> tiles = new ArrayList<Envelope>();
for (int i = 0; i < 100; i++) {
double x = (i % 10) * 1000;
double y = (i / 10) * 1000;
tiles.add(new Envelope(x, x + 1000, y, y + 1000));
}
List<Geometry> results = new ArrayList<Geometry>();
IntStream.range(0, tiles.size()).parallel()
.mapToObj(i -> processTile(tiles.get(i), 50, index, gf))
.forEach(results::add);
System.out.println("瓦片结果数 = " + results.size());
}
}
这段代码有三个必须注意的点:
results::add在并行流里不是线程安全的。示例为了简洁这样写是错误的,正确做法是用collect收集,或每个线程一个局部 List 再合并;STRtree的查询是线程安全的,但必须在并行之前 build 完成;- 每个瓦片内部仍然做了多次
union,如果要素很多,应该换成CascadedPolygonUnion或 CoverageUnion(合法覆盖率时)。
修正后的收集方式:
List<Geometry> results = IntStream.range(0, tiles.size()).parallel()
.mapToObj(i -> processTile(tiles.get(i), 50, index, gf))
.collect(java.util.stream.Collectors.toList());
20.10.4 结果拼接
分块结果的拼接策略取决于业务:
- 计数/统计:直接汇总,注意跨块要素被重复计数——计数前按要素 ID 去重;
- 面合并:块间边界会有重复覆盖,用一元并(
UnaryUnionOp)或先确保覆盖率合法再用 CoverageUnion; - 线/点:需要去重(相同坐标的端点重复);
- 输出图层:每块单独输出时,相邻块边界处会有接缝,渲染上通常可接受,拓扑上不连续。
20.10.5 并行的边界条件
- 任务粒度:单任务 < 10 ms 时,线程调度与结果合并成本占比过高;
- 内存:并行度 × 单任务峰值内存必须小于堆上限,否则频繁 Full GC 反而更慢;
- 异常传播:并行流里的异常会被包装成
CompletionException,要解包记录; - 不要共享可变几何:见 20.7 的安全矩阵;
- 确定性:并行结果顺序不稳定,输出前排序(按 ID 或坐标)保证可复现。
20.11 缓存策略
20.11.1 哪些东西值得缓存
| 缓存对象 | 收益 | 失效条件 |
|---|---|---|
| PreparedGeometry | 高频谓词调用提速数倍到数十倍 | 原几何被修改 |
| STRtree / HPRtree | 避免重复构建 | 数据变更 |
| WKT 解析结果 | 避免重复解析 | 数据变更 |
| 投影转换结果 | 避免重复转换 | 源 CRS/目标 CRS 变化 |
| IsValidOp 结论 | 避免重复校验 | 几何变更 |
| Envelope | JTS 已在 Geometry 内部缓存 | 几何变更 |
| 空间查询结果 | 热点区域查询加速 | 数据或查询条件变化 |
20.11.2 缓存的两个坑
坑一:缓存了可变对象。 把 Geometry 缓存后,别处调用 normalize() 或 setSRID 会改变它,缓存与索引全部失效。缓存前应 copy(),或约定“缓存对象只读”。
坑二:键的设计。 用几何对象的 hashCode 做键基本无意义(JTS 的 Geometry 没有按坐标重写 hashCode);应该用业务主键 + 数据版本号。
20.11.3 一个线程安全的 LRU 缓存
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.prep.PreparedGeometry;
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
public class PreparedCache {
private static final int MAX_ENTRIES = 1024;
private final Map<String, PreparedGeometry> cache =
Collections.synchronizedMap(new LinkedHashMap<String, PreparedGeometry>(
MAX_ENTRIES, 0.75f, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<String, PreparedGeometry> eldest) {
return size() > MAX_ENTRIES;
}
});
public PreparedGeometry get(String key, Geometry geom) {
PreparedGeometry cached = cache.get(key);
if (cached != null) return cached;
PreparedGeometry prepared = PreparedGeometryFactory.prepare(geom.copy());
cache.put(key, prepared);
return prepared;
}
}
说明:
synchronizedMap保证单次 get/put 原子,但“检查-构建-放入”整体不是原子的,高并发下可能重复构建同一个条目(可接受,用computeIfAbsent可改善);- 缓存里存的是
geom.copy()的 prepared 版本,避免外部修改影响; LinkedHashMap的 accessOrder=true 实现 LRU;- 分布式场景不要把 PreparedGeometry 序列化传输(内部索引不可序列化/无意义),应各节点本地构建。
20.11.4 缓存分层
L1:JVM 内(PreparedGeometry、STRtree、解析结果)
L2:进程外(Redis/Memcached,存 WKB + 元数据)
L3:数据库(物化视图、空间索引)
L1 追求纳秒~微秒级,适合热点几何;L2 追求跨进程共享,适合跨请求结果;L3 适合全量数据。不要用 L2 缓存几何对象本身(序列化开销 + 精度问题),缓存 WKB 或查询结果 ID。
20.12 性能优化检查清单(含反例)
20.12.1 检查清单
| 序号 | 检查项 | 通过标准 |
|---|---|---|
| 1 | 是否有基线数据与基线耗时 | 有固定数据集与可复现脚本 |
| 2 | 是否定位到热点 | 有 profile 证据,不是猜测 |
| 3 | 重复谓词是否用 PreparedGeometry | 循环内谓词调用已准备 |
| 4 | 批量空间查询是否建索引 | STRtree/HPRtree,构建一次复用 |
| 5 | 是否避免逐对 union | 用 UnaryUnionOp/CascadedPolygonUnion |
| 6 | 覆盖率合并是否用 CoverageUnion | 先校验覆盖率,再合并 |
| 7 | 是否在入口修复无效几何 | 校验/修复前置,避免回退风暴 |
| 8 | 是否减少顶点数 | 允许误差时简化/精度约简 |
| 9 | 是否避免循环内 getCoordinates | 用 CoordinateSequence 遍历 |
| 10 | 是否复用 GeometryFactory | 按 SRID/PM 缓存工厂 |
| 11 | 是否避免不必要拷贝 | 用 getEnvelopeInternal、stream 写出 |
| 12 | 线程安全边界是否清晰 | 只读共享、状态私有、索引先构建 |
| 13 | 是否有并行度与内存上限 | 并行度 × 单任务内存 < 堆 |
| 14 | 缓存是否可失效 | 有版本号/TTL/主动清空 |
| 15 | 是否有回归基准 | 每次升级跑 JMH 与端到端 |
| 16 | 输出是否流式 | 大结果不拼巨型字符串 |
| 17 | 日志是否精简 | 热路径不打 WKT |
| 18 | 是否监控 GC 与异常率 | 有分配速率、TopologyException 率指标 |
20.12.2 反例集
| 反例 | 代价 | 正确做法 |
|---|---|---|
for (g : list) result = result.union(g) |
O(N²) | UnaryUnionOp / CascadedPolygonUnion |
循环里 g.getCoordinates() |
大量数组分配 | CoordinateSequence 遍历 |
每次查询 new STRtree |
重复构建 | 构建一次,只读复用 |
每次谓词前 isValid() |
校验成本翻倍 | 入口校验一次并缓存 |
| 用 WKT 做中间结果 | 解析/序列化双慢 | 传对象或 WKB |
在循环里 new GeometryFactory() |
无谓对象 + 破坏 PM 一致性 | 复用工厂 |
| 百万要素一次读入内存 | OOM/长 GC | 分批流式 |
| 用并行流处理几百个小任务 | 调度开销大于收益 | 串行或合并任务 |
热路径打 System.out.println(wkt) |
IO + 字符串拼接 | 关闭或降级日志 |
| 对无效几何反复重试 overlay | 回退链放大耗时 | 先修复再算 |
20.12.3 优化决策树
性能不达标
│
▼
有 profile 吗? ── 否 ──► 先做 JFR/async-profiler 采样
│ 是
▼
热点在 IO/解析? ── 是 ──► 换 WKB、批量读写、流式处理
│ 否
▼
热点在谓词/查询? ── 是 ──► PreparedGeometry + 空间索引
│ 否
▼
热点在 overlay/noding? ── 是 ──► 减少顶点/前置修复/分块/换批量 union
│ 否
▼
热点在 GC/分配? ── 是 ──► 减少 getCoordinates/copy、复用 Envelope/工厂
│ 否
▼
热点在锁/线程? ── 是 ──► 缩小临界区、只读共享、任务私有状态
│ 否
▼
检查数据本身(无效几何、超大要素、异常坐标)
20.13 常见问题与避坑
问:PreparedGeometry 为什么没有更快? 检查三点:调用次数是否足够多(少于几百次时构建成本占比高);几何是否是小面(顶点少时收益有限);是否每次都在重新 prepare。
问:STRtree 插入报 “Cannot insert items into an STR packed R-tree after it has been built”? 树在第一次 query 时已自动 build,之后不能再插入。要么先插完再查,要么换 Quadtree(支持增量)。
问:并行之后结果不稳定,甚至偶发错误? 大概率共享了可变对象:复用同一个 OverlayNG/Noder/LineIntersector 实例,或对共享 Geometry 调用了 normalize/setSRID。按 20.7 的矩阵检查。
问:为什么简单的 intersects 也慢? 可能是面有几十万顶点且被反复调用(用 PreparedGeometry),也可能是包络很大导致候选过多(加索引或分块)。
问:union 大集合时内存暴涨怎么办? 改用 CascadedPolygonUnion(自底向上合并,中间结果小);或先用 STRtree 分组,再逐组合并;或分块 + CoverageUnion。
问:简化之后结果面积变化很大? Douglas-Peucker 不保拓扑也不保面积,容差过大就会明显改变形状。面积敏感场景用 TopologyPreservingSimplifier 或 CoverageSimplifier,并把容差控制在业务精度内。
问:JMH 结果和线上表现差距很大? 基准通常是单线程、热数据、无 IO;线上有 GC、锁、网络、数据库。JMH 只用于比较算法/实现的相对优劣,端到端压测不可替代。
问:缓存 PreparedGeometry 会不会内存泄漏? 会,如果没有上限和失效策略。用有界 LRU + 业务版本号,几何变更时主动清空对应条目。
20.14 本章小结
- 性能优化从测量开始:端到端计时 → 采样剖析 → JMH 微基准 → 回归对比,拒绝凭感觉优化。
- JTS 的五类开销是 IO、算法复杂度、对象分配、无效输入、并发争用;不同类别对应不同手段。
- PreparedGeometry 把重复谓词从 O(n) 降到近似 O(log n),线程安全、懒加载,适合“一个面 vs 海量对象”;但构建有成本,少次调用不划算。
- STRtree 是半静态索引:插入需在构建前、查询线程安全、构建后不可插入;HPRtree 更适合静态大数据;数据量大且多次查询才值得建索引。
- 批量 union 不要逐对循环:用 UnaryUnionOp、CascadedPolygonUnion、OverlayNGRobust.union;合法覆盖率用 CoverageUnion。
- 前置校验/修复/简化往往比调算法参数更有效;无效几何会触发 overlay 的多级回退,代价是数量级的。
- 减少对象分配:用 CoordinateSequence 遍历代替
getCoordinates(),复用 Envelope 与 GeometryFactory,流式写出大结果。 - 线程安全边界:GeometryFactory 与 PreparedGeometry 可共享;Geometry 只读安全、修改不安全;索引查询安全、插入不安全;OverlayNG/Noder/LineIntersector 等实例必须线程私有。
- JMH 是 JVM 微基准标准;JFR 与 async-profiler 用于定位热点;两者结合才能既知道“哪里慢”又知道“优化多少”。
- OverlayNG 调优的核心是减少参与 noding 的线段:包络预筛、前置精度约简/简化、分块;调试开关不要留在生产。
- 分块并行能同时解决内存与耗时,但要注意边界 buffer、结果拼接去重、并行流的收集安全与内存上限。
- 缓存要有界、可失效、只读;PreparedGeometry 等内部索引不适合跨进程传输。
20.15 相关资源
- 系列目录:JTS 教程目录
- 空间索引体系:第 13 章(STRtree/HPRtree/Quadtree 原理与 API)
- 叠加分析:第 09 章(OverlayNG 架构与操作成本)
- 线网处理与 Noding:第 14 章(noding 是 overlay 的主要成本)
- 几何化简:第 12 章(简化容差与拓扑保持)
- 有效性与 Coverage 质检:第 17 章(前置修复与覆盖率算法)
- 精度模型与数值鲁棒性:第 18 章(精度策略对性能的影响)
- 坐标系与工程陷阱:第 19 章(单位错误导致的“假性能问题”)
- 综合实战:第 24 章(性能清单在完整项目中的落地)
- JTS 官方 Javadoc:https://locationtech.github.io/jts/javadoc/
- JMH 官方文档:https://openjdk.org/projects/code-tools/jmh/
- async-profiler:https://github.com/async-profiler/async-profiler