全部產品
Search
文件中心

Artificial Intelligence Recommendation:Java SDK

更新時間:Mar 21, 2026

當使用者希望把Feature Generator(FG)整合到Java服務中,則需要一個Java SDK去完成特徵的變換。本文介紹Feature Generator工具Java SDK的使用方法。

使用限制

  • Java SDK 目前只支援在linux-x86_64macosx-arm64平台上運行

使用Java SDK

Maven專案為例,介紹如何使用Java SDK

1. 下載FG SDK的jar包到本地位置,如 /path/to/feature_generator-${version}-${platform}.jar

platform

FG SDK Jar package

linux-x86_64

fg-linux-x86_64.jar

macosx-arm64

fg-macosx-arm64.jar

注意事項:目前FG預設採用std::hash做hash分箱,不同的平台上C++標準庫中std::hash的實現是不同的,因此最終的分箱結果是不一致的。

如果需要分箱結果的跨平台一致性,可以通過設定環境變數USE_FARM_HASH_TO_BUCKETIZE=true來實現,具體參考FG全域配置的文檔。

2. 使用mvn install:install-file安裝到本地倉庫

mvn install:install-file \
  -Dfile=/path/to/feature_generator-${version}-${platform}.jar \
  -DgroupId=com.aliyun.pai \
  -DartifactId=feature_generator \
  -Dversion=${version} \
  -Dclassifier=${platform} \
  -Dpackaging=jar

注意:需要替換上述命令中的${version}為實際的版本號碼;${platform}要替換為實際的平台名稱。

3. 在你的pom.xml中添加依賴

<dependencies>
<dependency>
    <groupId>com.aliyun.pai</groupId>
    <artifactId>feature_generator</artifactId>
    <version>${version}</version>
    <classifier>linux-x86_64</classifier>
</dependency>
</dependencies>

4. 建立fg.json檔案

Demo樣本:

{
  "features": [
    {
      "feature_name": "query_word",
      "feature_type": "id_feature",
      "value_type": "String",
      "expression": "user:query_word",
      "default_value": "",
      "combiner": "mean",
      "need_prefix": false,
      "is_multi": true
    },
    {
      "feature_name": "query_match",
      "feature_type": "lookup_feature",
      "map": "user:query_token",
      "key": "item:title",
      "needDiscrete": false,
      "needKey": false,
      "default_value": "0",
      "combiner": "sum",
      "need_prefix": false,
      "value_type": "double"
    },
    {
      "feature_name": "goods_id",
      "feature_type": "id_feature",
      "value_type": "String",
      "expression": "item:goods_id",
      "default_value": "-1024",
      "combiner": "mean",
      "need_prefix": false,
      "value_dimension": 1
    },
    {
      "feature_name": "filter_type",
      "feature_type": "id_feature",
      "value_type": "String",
      "expression": "item:filter_type",
      "default_value": "-1024",
      "combiner": "mean",
      "need_prefix": false,
      "value_dimension": 1
    },
    {
      "feature_name": "day_h",
      "feature_type": "id_feature",
      "value_type": "int64",
      "expression": "user:day_h",
      "default_value": "0",
      "combiner": "mean",
      "need_prefix": false,
      "value_dimension": 1
    },
    {
      "feature_name": "week_day",
      "feature_type": "id_feature",
      "value_type": "int64",
      "expression": "user:week_day",
      "default_value": "0",
      "combiner": "mean",
      "need_prefix": false,
      "value_dimension": 1
    },
    {
      "feature_name": "city",
      "feature_type": "id_feature",
      "value_type": "String",
      "expression": "user:city",
      "default_value": "-1024",
      "combiner": "mean",
      "need_prefix": false,
      "value_dimension": 1
    },
    {
      "feature_name": "province",
      "feature_type": "id_feature",
      "value_type": "String",
      "expression": "user:province",
      "default_value": "-1024",
      "combiner": "mean",
      "need_prefix": false,
      "value_dimension": 1
    },
    {
      "feature_name": "country",
      "feature_type": "id_feature",
      "value_type": "String",
      "expression": "user:country",
      "default_value": "-1024",
      "combiner": "mean",
      "need_prefix": false,
      "value_dimension": 1
    },
    {
      "feature_name": "is_new_user",
      "feature_type": "id_feature",
      "value_type": "int64",
      "expression": "user:is_new_user",
      "default_value": "-1024",
      "combiner": "mean",
      "need_prefix": false,
      "value_dimension": 1
    },
    {
      "feature_name": "focus_author",
      "feature_type": "id_feature",
      "value_type": "String",
      "expression": "user:focus_author",
      "separator": ",",
      "default_value": "-1024",
      "combiner": "mean",
      "need_prefix": false,
      "is_multi": true
    },
    {
      "feature_name": "title",
      "feature_type": "id_feature",
      "value_type": "String",
      "expression": "item:title",
      "default_value": "-1024",
      "combiner": "mean",
      "need_prefix": false,
      "is_multi": true
    }
  ],
  "reserves": [
    "request_id",
    "user_id",
    "is_click",
    "is_pay"
  ]
}

5. 參考如下代碼使用Java API

package org.example;

import com.aliyun.pai.fg.*;
import java.net.URL;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        String filePath = "/path/to/fg.json";
        FgHandler handler = new FgHandler(filePath, 4, false);

        List<String> outputs = new ArrayList<>();
        outputs.add("goods_id");
        outputs.add("is_new_user");
        outputs.add("day_h");
        outputs.add("query_match"); // set output feature_name
        outputs.add("title");
        outputs.add("filter_type");
        outputs.add("city");
        outputs.add("province");
        outputs.add("country");
        outputs.add("focus_author");
        handler.setOutputs(outputs.toArray(new String[0]));

        List<String> expectGoods = Arrays.asList("218687106", "1142068348", "1142068347");
        VariantVectorMap inputs = new VariantVectorMap.Builder()
                .putOptionalString("goods_id", expectGoods)
                .putOptionalInt32("is_new_user", Arrays.asList(0, 1, 0))
                .putOptionalInt32("day_h", Arrays.asList(6, 8, 11))
                .putListString("title", Arrays.asList(
                        Arrays.asList("k2", "k3", "k5"),
                        Arrays.asList("k1", "k2", "k3"),
                        Arrays.asList("k2", "k4")))
                .putMapStringFloat("query_token", Arrays.asList(
                        new HashMap<String, Float>() {{
                            put("k2", 0.8f);
                            put("k3", 0.5f);
                        }},
                        new HashMap<String, Float>() {{
                            put("k1", 0.9f);
                            put("k4", 0.2f);
                        }},
                        new HashMap<String, Float>() {{
                            put("k1", 0.7f);
                            put("k2", 0.3f);
                            put("k4", 0.6f);
                        }}))
                .putOptionalString("filter_type", Arrays.asList(null, "f1", null))
                .putOptionalString("city", Arrays.asList("hangzhou"))
                .putOptionalString("province", Arrays.asList("zhejiang"))
                .putOptionalString("country", Arrays.asList("china"))
                .putOptionalString("focus_author", Arrays.asList("2255010511022,14164467", "10511022,24164467", "550105110,34164467"))
                .build();

        VariantVectorMap results = handler.Process(inputs);
        if (results == null || results.isNull()) {
            System.out.println("fg result is null");
            return;
        }
        inputs.close();  // 釋放native記憶體
        
        System.out.println("result size=" + results.size());
        List<String> features = results.getKeys();
        System.out.println("result features=" + features);
        List<String> goodsIds = results.getList("goods_id");
        System.out.println("goods_ids=" + String.join(", ", goodsIds));

        List<List<String>> titles = results.getList("title");
        System.out.println("titles=" + titles);

        List<Long> dayHours = results.getList("day_h");
        System.out.println("day_h=" + dayHours);

        List<String> filters = results.getList("filter_type");
        System.out.println("filter_type=" + String.join(", ", filters));

        List<List<String>> focus = results.getList("focus_author");
        System.out.println("focus_author=" + focus);

        List<String> citys = results.getList("city");
        System.out.println("city=" + String.join(", ", citys));

        List<String> provinces = results.getList("province");
        System.out.println("provinces=" + String.join(", ", provinces));

        List<String> countrys = results.getList("country");
        System.out.println("country=" + String.join(", ", countrys));

        List<Long> isNewUsers = results.getList("is_new_user");
        System.out.println("is_new_user=" + isNewUsers);

        List<Double> queryMatch = results.getList("query_match");
        System.out.println("query_match=" + queryMatch);
        System.out.println("===========================================================");

        Set<String> itemInputs = handler.GetItemInputNames();
        System.out.println("item side inputs=" + itemInputs);

        Set<String> userInputs = handler.GetUserInputNames();
        System.out.println("user side inputs=" + userInputs);

        Set<String> ctxInputs = handler.GetContextInputNames();
        System.out.println("context side inputs=" + ctxInputs);

        Set<String> reserved = handler.GetReserveColumns();
        System.out.println("reserved columns =" + reserved);

        Set<String> qminputs = handler.GetFeatureInputs("query_match");
        System.out.println("inputs of query_match =" + qminputs);

        String defaultVal = handler.DefaultFeatureValue("query_match");
        System.out.println("default feature value of query_match =" + defaultVal);

        List<String> allFeatures = handler.GetAllFeatureNames();
        System.out.println("all feature names:" + allFeatures);

        Map<String, String> schema = handler.GetTableSchema();
        System.out.println("table schema:" + schema);
        
        results.close();  // 釋放native記憶體
        handler.close();  // 釋放資源
    }
}

Java SDK 使用文檔

1. 類概述

FgHandler 用於根據配置(config_json)對輸入特徵進行處理,並輸出處理後的特徵結果。

主要能力:

  • 通過配置初始化特徵處理器(可指定線程數、是否只做 bucketize 等)。

  • 處理輸入特徵:Process(VariantVectorMap inputs) -> VariantVectorMap

  • 設定輸出特徵集合:setOutputs(...)

  • 查詢配置中的輸入/輸出/特徵元資訊(feature names、schema、預設值、維度等)。

  • 支援顯式釋放 native 資源:close()(非常重要)

2. 快速開始

2.1 初始化

提供四種構造方式(最終都會調用 native allocate):

// 1) 僅配置
FgHandler handler = new FgHandler(configJsonOrPath);
// 2) 配置 + 線程數
FgHandler handler = new FgHandler(configJsonOrPath, threadNum);
// 3) 配置 + 線程數 + 是否僅 bucketize
FgHandler handler = new FgHandler(configJsonOrPath, threadNum, bucketizeOnly);
// 4) 配置 + 線程數 + 是否僅 bucketize + config 是否為檔案路徑
FgHandler handler = new FgHandler(configJsonOrPath, threadNum, bucketizeOnly, isCfgPath);

參數語義:

  • config_json:配置內容或配置路徑(由 is_cfg_path 決定)

  • thread_num:native 內部處理線程數(size_t)

  • bucketize_only:是否只執行 bucketize(可通過 IsOnlyBucketize() 查詢)

  • is_cfg_path:true 表示 config_json 參數是設定檔路徑;false 表示配置 JSON 文本

2.2 設定輸出特徵

可通過兩種方式設定輸出:

handler.setOutputs("feature_a", "feature_b");
// 或者使用 String[] 數組作為參數
List<String> outputs = new ArrayList<>();
outputs.add("goods_id");
outputs.add("is_new_user");
outputs.add("title");
outputs.add("city");
outputs.add("province");
outputs.add("country");
handler.setOutputs(outputs.toArray(new String[0]));

或使用底層介面:

StrVector outs = new StrVector();
outs.push_back("feature_a");
outs.push_back("feature_b");
handler.setOutputs(outs);

注意:如果不設定輸出,則預設輸出配置的所有特徵。

2.3 執行處理

VariantVectorMap inputs = new VariantVectorMap();
// TODO: 按你的 VariantVectorMap 約定填充 inputs
VariantVectorMap outputs = handler.Process(inputs);
if (outputs == null || outputs.isNull()) {
   System.out.println("fg output is null");
   inputs.close();
   return;
}
// TODO: 從 outputs 讀取結果

FgHandler.Process(inputs) 一次處理一批樣本(batch),其輸入輸出類型都是 VariantVectorMap,含義是:

  • VariantVectorMap:Map<String, VariantVector>(key 為欄位/特徵名)

  • VariantVector:一個“按列儲存”的批資料容器(列向量),支援多種嵌套形態:

    • Optional 標量:List<T>(長度 = batch size;可包含 null)

    • List 序列:List<List<T>>(外層長度 = batch size)

    • Map 特徵:List<Map<K,V>>(外層長度 = batch size)

    • Matrix 特徵:List<List<List<T>>>(外層長度 = batch size,每個樣本一個二維矩陣)

  • Process函數的返回結果可能為null,表示有執行失敗的特徵變換,使用之前需要檢查是否為空白。

3. 輸入與輸出資料結構說明

3.1 VariantVectorMap(輸入/輸出容器)

3.1.1 基本 API

  • 構造:

    • new VariantVectorMap()

    • VariantVectorMap.fromJavaMap(Map<String, VariantVector>)

    • new VariantVectorMap.Builder()...build()

  • 寫入:

    • put(String key, VariantVector value)

    • putAll(Map<String, VariantVector>)

  • 讀取:

    • VariantVector get(String key)

    • boolean contains(String key)

    • List<String> getKeys()

    • long size()

    • Map<String, VariantVector> toJavaMap()(把當前 map 中的索引值轉成 Java Map)

  • 釋放:

    • clear() 清空元素

    • close() 釋放native記憶體,必須調用,否則會有記憶體流失問題

3.1.2 推薦:Builder 構造 inputs

VariantVectorMap.Builder 提供大量 putXXX 方法:

常見(標量 optional,對應一個batch):

VariantVectorMap inputs = new VariantVectorMap.Builder()
    .putOptionalInt64("user_id", Arrays.asList(1001L, 1002L))
    .putOptionalString("item_id", Arrays.asList("i1", "i2"))
    .putOptionalDouble("score", Arrays.asList(0.1, null)) // 支援 null
    .build();

單值便捷(batch=1):

VariantVectorMap inputs = new VariantVectorMap.Builder()
    .putSingleOptionalInt64("user_id", 1001L)
    .putSingleOptionalString("item_id", "i1")
    .build();

序列(List 類型,batch 內每個樣本一個 list):

VariantVectorMap inputs = new VariantVectorMap.Builder()
    .putListInt64("clicked_item_ids",
        Arrays.asList(
            Arrays.asList(11L, 12L),
            Arrays.asList(21L)
        )
    )
    .build();

Map 特徵(batch 內每個樣本一個 map):

VariantVectorMap inputs = new VariantVectorMap.Builder()
    .putMapStringFloat("user_dense",
        Arrays.asList(
            Map.of("age", 18.0f, "lvl", 3.0f),
            Map.of("age", 25.0f)
        )
    )
    .build();

Matrix(每個樣本一個二維矩陣):

VariantVectorMap inputs = new VariantVectorMap.Builder()
    .putMatrixFloat("image_emb",
        Arrays.asList(
            Arrays.asList( // sample1 matrix
                Arrays.asList(0.1f, 0.2f),
                Arrays.asList(0.3f, 0.4f)
            ),
            Arrays.asList( // sample2 matrix
                Arrays.asList(1.0f, 2.0f)
            )
        )
    )
    .build();

3.1.3 getList:按型別安全方式讀取(輸出側常用)

VariantVectorMap.getList(key) 會根據 VariantVector.getType() 自動選擇註冊的 TypeReference 做轉換。

樣本:讀取輸出 feature_x(假設是 OPTIONAL_FLOAT):

List<Float> xs = out.getList("feature_x");   // 實際返回 List<Float>

或讀取 LIST_INT64(返回 List<List>):

List<List<Long>> seq = out.getList("seq_feature");

注意:

  • getList 傳回型別是 <T> List<T>,編譯期無法強約束;類型不符會在運行時拋 IllegalArgumentException("Type mismatch...")。

  • TYPE_REGISTRY 已預註冊了所有你定義的類型常量到 TypeReference 的映射。

建議做法:先擷取輸出表的Schema資訊,再根據schema決定getList函數的傳回值類型。

Map<String, String> schema = handler.GetTableSchema();
System.out.println("table schema:" + schema);

3.2 VariantVector(列向量與類型系統)

VariantVector 是“單列”資料,必須設定 type 並按該類型組織內部資料。

3.2.1 支援類型一覽(按常量)

  • Optional(允許 null,外層即 batch):

    • OPTIONAL_STRING / INT32 / INT64 / FLOAT / DOUBLE

    • Java 表示:List<T>(長度 = batch;元素可為 null)

  • List(每條樣本一段序列):

    • LIST_STRING / INT32 / INT64 / FLOAT / DOUBLE

    • Java 表示:List<List<T>>(外層長度 = batch)

  • Map(每條樣本一個 map):

    • MAP_STRING_*:List<Map<String, V>>

    • MAP_INT32_*:List<Map<Integer, V>>

    • MAP_INT64_*:List<Map<Long, V>>

  • Matrix(每條樣本一個 2D 矩陣):

    • MATRIX_FLOAT / MATRIX_INT64 / MATRIX_STRING

    • Java 表示:List<List<List<T>>>

3.2.2 推薦構造方式:靜態方法 fromXXX

你通常不需要手寫底層 Builder(type).withXXXData(),直接用:

  • VariantVector.fromOptionalInt64(List<Long> values)

  • VariantVector.fromListFloat(List<List<Float>> values)

  • VariantVector.fromMapStringInt32(List<Map<String,Integer>> maps)

  • VariantVector.fromMatrixFloat(List<List<List<Float>>> matrices) 等。

樣本(手動構造再放入 map):

VariantVector userIds = VariantVector.fromOptionalInt64(Arrays.asList(1L, 2L, null));
VariantVectorMap inputs = new VariantVectorMap();
inputs.put("user_id", userIds);

通過這種方式構建的對象擁有自己的 native 記憶體,使用完之後需要調用close()釋放native記憶體

還有一種借用記憶體(borrowed)模式的對象,比如:

VariantVectorMap results;
// build results
VariantVector v = results.get("goods_id");

這種類型的對象,本質上是一種視圖,底層的記憶體與VariantVectorMap對象共用,當VariantVectorMap對象被釋放時,該對象就變成“懸空”狀態,不能夠再被使用。

borrowed 類型的VariantVector對象,使用完之後不需要調用close(),注意不要單獨長期儲存,生命週期依賴所屬的VariantVectorMap對象

3.2.3 讀取:toXXXList

拿到一個 VariantVector vv 後,可按類型調用轉換方法:

VariantVector vv = out.get("feature_x");
if (vv.getType() == VariantVector.OPTIONAL_DOUBLE) {
    List<Double> vals = vv.toOptionalDoubleList();
}

也可用 VariantVectorMap.getList(key) 自動分發(見上文 3.1.3)。

3.2.4 輔助方法:size / isEmpty / totalElementCount

  • vv.size():

    • Optional:返回 batch size(nullFlags 長度)

    • 其他:返回 sizes[0](外層元素數,通常=batch size)

  • vv.isEmpty():根據 type 檢查底層資料是否為空白

  • vv.totalElementCount():對 list/map/matrix 計算嵌套展開後的總元素量(主要用於調試/檢查)

4. 元資訊查詢介面

這些介面主要用來瞭解配置內容與特徵依賴。

4.1 查詢輸入欄位集合

Set<String> itemInputs = handler.GetItemInputNames();
Set<String> userInputs = handler.GetUserInputNames();
Set<String> ctxInputs  = handler.GetContextInputNames();

4.2 查詢所有特徵名 / 保留列 / 特殊集合

List<String> allFeatures = handler.GetAllFeatureNames();
Set<String> reserveCols  = handler.GetReserveColumns();
Set<String> userSide     = handler.GetUserSideFeatures();
Set<String> seqFeatures  = handler.GetSequenceFeatures();

4.3 查詢特徵依賴與預設值/維度/bucketizer

Set<String> deps = handler.GetFeatureInputs("feature_x");
String defaultVal = handler.DefaultFeatureValue("feature_x");
long defaultBucket = handler.DefaultBucketizedFeatureValue("feature_x");
long dim = handler.GetFeatureValueDim("feature_x");
boolean hasBucketizer = handler.HasBucketizer("feature_x");
boolean onlyBucketize = handler.IsOnlyBucketize();

適用情境:

  • 產生輸入準備邏輯(知道某個 feature 需要哪些 raw inputs)

  • 輸入缺失時進行補齊/校正(預設值)

  • 下遊模型需要固定維度時(dim)

  • 決定是否需要額外 bucketize 相關處理(hasBucketizer/onlyBucketize)

4.4 查詢MC輸出表結構(Schema)

使用方式:

Map<String, String> schema = handler.GetTableSchema();
String type = schema.get("feature_x");

5. 資源釋放(必須閱讀)

FgHandler 持有 native 側對象指標,必須在不用時釋放,否則可能造成記憶體泄露。

這是個重量級對象,一般:

  • 初始化一次

  • 重複復用

  • 服務退出時再 close()

正確姿勢

  • 不要每個請求都 new FgHandler()

  • 一個配置對應一個長期複用的 handler

5.1 當FgHandler不再被使用時,必須調用close()函數釋放資源。該函數有如下特性:

  • 安全執行緒:synchronized

  • 等冪:重複調用不會重複釋放

重要:close() 後不要再調用任何 native 方法(包括 Process、查詢介面等),否則可能崩潰(use-after-free)

FgHandler handler = new FgHandler(cfg, 4);
try {
    ...
} finally {
    handler.close();
}

5.2 調試階段可以用try-with-resources

try (FgHandler handler = new FgHandler(cfg, 4)) {
    handler.setOutputs("f1", "f2");
    VariantVectorMap out = handler.Process(in);
}

5.3 正式生產環境不要頻繁建立相同的FgHandler對象

重要:在一個服務內部使用FgHandler時,不要每次請求都建立FgHandler對象,因為該對象的初始化開銷比較大,頻繁建立會影響服務效能。

推薦在服務的初始化函數裡建立全域對象,在服務的退出函數裡調用close()函數;需要建立多個不同的FgHandler對象時,也要遵循同樣的模式。


最推薦的請求處理模式

模式 A:同步請求,最穩

public Result handle(Request req, FgHandler handler) {
    try (VariantVectorMap inputs = buildInputs(req);
         VariantVectorMap results = handler.Process(inputs)) {
        Result r = new Result();
        r.goodsIds = results.get("goods_id").toOptionalStringList();
        r.dayHs = results.get("day_h").toOptionalInt32List();
        r.titles = results.get("title").toListStringList();
        return r;
    }
}

特點:

  • native 資源範圍非常清楚

  • 請求結束即釋放

  • 不容易 OOM

模式 B:服務級單例 handler

public class FgService implements AutoCloseable {
    private final FgHandler handler;
    public FgService(String configPath) {
        this.handler = new FgHandler(configPath, 4, false, true);
    }
    public Result process(Request req) {
        try (VariantVectorMap inputs = buildInputs(req);
             VariantVectorMap results = handler.Process(inputs)) {
            return decode(results);
        }
    }
    @Override
    public void close() {
        handler.close();
    }
}

這是生產裡最推薦的方式。

6. 常見用法樣本

處理前先根據配置準備輸入欄位

FgHandler handler = new FgHandler(cfgJson, 8, false, false);
try {
    // 1) 設定需要的輸出
    handler.setOutputs("feature_a", "feature_b");
    // 2) 瞭解需要準備哪些輸入
    Set<String> itemInputs = handler.GetItemInputNames();
    Set<String> userInputs = handler.GetUserInputNames();
    Set<String> ctxInputs  = handler.GetContextInputNames();
    // 3) 構造 inputs(示意,具體取決於 VariantVectorMap 的 API)
    VariantVectorMap inputs = new VariantVectorMap();
    // inputs.put("user_id", ...)
    // inputs.put("item_id", ...)
    // inputs.put("ts", ...)
    VariantVectorMap outputs = handler.Process(inputs);
    inputs.close();
    // 讀取 outputs...
    outputs.close();
} finally {
    handler.close();
}

針對只做 bucketize

FgHandler handler = new FgHandler(cfgJson, 4, true, false);
try {
    if (!handler.IsOnlyBucketize()) {
        // 配置/構造參數不匹配時可做提示
    }
    ...
} finally {
    handler.close();
}

安全的標準用法

try (FgHandler handler = new FgHandler(configPath, 4, false, true)) {
    handler.setOutputs("goods_id", "day_h", "title");
    try (VariantVectorMap inputs = new VariantVectorMap.Builder()
            .putOptionalString("goods_id", Arrays.asList("1", "2"))
            .putOptionalInt32("day_h", Arrays.asList(6, 8))
            .putListString("title", Arrays.asList(
                    Arrays.asList("a", "b"),
                    Arrays.asList("c", "d")))
            .build();
         VariantVectorMap results = handler.Process(inputs)) {
        List<String> goodsIds = results.get("goods_id").toOptionalStringList();
        List<Integer> dayHs = results.get("day_h").toOptionalInt32List();
        List<List<String>> titles = results.get("title").toListStringList();
        // 用完即止,不把 results / borrowed VariantVector 長時間帶出範圍
    }
}

7. 哪些用法最容易導致 OOM

1. 沒有 close VariantVectorMap

錯誤樣本

for (...) {
    VariantVectorMap inputs = new VariantVectorMap.Builder()...build();
    VariantVectorMap results = handler.Process(inputs);
    // 沒有 close
}

這會導致:

  • Java 對象可能很快變成垃圾,被GC回收

  • 但 native 記憶體不一定及時釋放

  • 高 QPS 下很容易導致分配了大量的對象但是沒有被釋放,最終 OOM 或 native RSS 爆掉

正確樣本

for (...) {
    try (VariantVectorMap inputs = new VariantVectorMap.Builder()...build();
         VariantVectorMap results = handler.Process(inputs)) {
        ...
    }
}

在 try-with-resources 模式下會自動釋放資源。

2. 每個請求都 new 一個 FgHandler

錯誤樣本

for (...) {
    FgHandler handler = new FgHandler(configPath, 4, false, true);
    ...
}

這通常非常重:

  • 反覆載入配置

  • 初始化線程池/內部結構

  • native 資源重複申請

正確樣本

FgHandler handler = new FgHandler(configPath, 4, false, true);
try {
    for (...) {
        ...
    }
} finally {
    handler.close();
}

3. 緩衝 results.get(key) 傳回值

錯誤樣本

VariantVector v = results.get("goods_id");
cache.add(v);   // 錯

因為它是 borrowed:

  • 一旦 results.close(),這些對象就失效

  • 如果你不 close results,那又會導致記憶體一直不釋放

正確樣本

要麼立刻解碼成普通 Java 對象:

List<String> goodsIds = results.get("goods_id").toOptionalStringList();
cache.add(goodsIds);

要麼如果確實要儲存 native 對象,用 getCopy(),並負責後續 close:

VariantVector copy = results.getCopy("goods_id");
try {
    ...
} finally {
    copy.close();
}

但一般不建議緩衝 native copy,除非確實必要。

4. 大量使用 toJavaMap() 後不釋放 value

如果你的 toJavaMap() 現在是返回 owned copy ,那這很容易製造很多 native 對象。

錯誤樣本

Map<String, VariantVector> map = results.toJavaMap();
// 放著不關

正確樣本

  • 除非真有必要,不要這麼幹。

  • 更推薦直接按欄位讀取成 Java 對象。

  • 如果必須這樣做:

Map<String, VariantVector> map = results.toJavaMap();
try {
    ...
} finally {
    for (VariantVector v : map.values()) {
        if (v != null) v.close();
    }
}

5. Builder 產生的中間 VariantVector 長期留在 Java Map 中

例如:

VariantVectorMap.Builder builder = new VariantVectorMap.Builder();
builder.putOptionalString(...);
builder.putOptionalInt32(...);
// builder/data 長期存在

Builder 內部持有很多 VariantVector,這些都是 owned native 對象。

如果 Builder 長期不釋放,也會佔 native 記憶體。

正確姿勢

  • Builder 的生命週期盡量短,只用來構建一次請求。