fix(java): give jdtls memory back when idle and stop it showing as not responding (#1137)

jdtls ran on the parallel collector, which never shrinks the heap while idle, so each server kept its import/build peak committed for as long as it lived. It now runs on G1 with a periodic collection every minute and a 10–30% free-heap band, so memory goes back to the OS once the work is done (a SkyWalking checkout: 1.9 GB → 1.25–1.3 GB idle). A collector set in the JVM options still replaces ours with its tuning; setting either heap-free ratio drops both of ours.

The Equinox launcher gets -nosplash: without it its native library loads AppKit on macOS and every jdtls shows as Not Responding in Activity Monitor.

Part of #1008.
This commit is contained in:
l0ng-ai
2026-11-04 11:40:02 +08:00
committed by GitHub
parent 5e73b1634f
commit fc6a27d4ba
3 changed files with 44 additions and 26 deletions
+42 -24
View File
@@ -105,15 +105,17 @@ fn split_script_args(args: &[String]) -> ScriptArgs {
out
}
/// JVM options jdtls runs with unless the user's set the same thing: a parallel collector
/// tuned to give memory back to the OS (resident memory stays near what is live instead of
/// growing to the maximum heap), a 2 GB heap limit (the JVM's own is a quarter of physical
/// JVM options jdtls runs with unless the user's set the same thing: G1 with a periodic
/// collection while idle and a narrow free-heap band, so the heap shrinks and memory goes
/// back to the OS once an import or a build is over (a parallel collector keeps its peak
/// committed for good), a 2 GB heap limit (the JVM's own is a quarter of physical
/// memory), no JVM logging on stdout (the LSP channel), and no scan for installed JDKs at
/// start (they are passed as `java.configuration.runtimes`).
pub const DEFAULT_VM_ARGS: &[&str] = &[
"-XX:+UseParallelGC",
"-XX:GCTimeRatio=4",
"-XX:AdaptiveSizePolicyWeight=90",
"-XX:+UseG1GC",
"-XX:G1PeriodicGCInterval=60000",
"-XX:MinHeapFreeRatio=10",
"-XX:MaxHeapFreeRatio=30",
"-Dsun.zip.disableMemoryMapping=true",
"-Xmx2G",
"-Xms100m",
@@ -130,6 +132,8 @@ fn option_key(arg: &str) -> Option<String> {
let name = name.split_once('=').map_or(name, |(n, _)| n);
match name {
"MaxHeapSize" | "MaxRAM" | "MaxRAMPercentage" | "MaxRAMFraction" => "Xmx".to_owned(),
// The JVM rejects a minimum above the maximum, so they go together.
"MinHeapFreeRatio" | "MaxHeapFreeRatio" => "XX:HeapFreeRatio".to_owned(),
"InitialHeapSize" | "InitialRAMPercentage" | "InitialRAMFraction" => "Xms".to_owned(),
_ if is_collector(name) => "collector".to_owned(),
_ => format!("XX:{name}"),
@@ -153,8 +157,8 @@ fn is_collector(name: &str) -> bool {
name.starts_with("Use") && name.ends_with("GC") && name.len() > "UseGC".len()
}
/// Our tuning of the parallel collector, meaningless (or rejected) with another one.
const COLLECTOR_TUNING: &[&str] = &["GCTimeRatio", "AdaptiveSizePolicyWeight"];
/// Our tuning of the default collector, meant for it alone.
const COLLECTOR_TUNING: &[&str] = &["G1PeriodicGCInterval", "HeapFreeRatio"];
/// [`DEFAULT_VM_ARGS`] less what `user` (the JVM options already on the command line)
/// sets itself. Choosing a collector drops our collector and its tuning altogether.
@@ -180,7 +184,10 @@ pub fn add_default_vm_args(args: &mut Vec<String>) {
}
/// The JVM command line `bin/jdtls` builds: its system properties and module
/// options, the user's `--jvm-arg`s, then the Equinox launcher with `-data`.
/// options, the user's `--jvm-arg`s, then the Equinox launcher with `-nosplash` and
/// `-data`. Without `-nosplash` the launcher's native library loads AppKit on macOS,
/// which registers the server as an app that never answers events and shows as
/// "Not Responding" in Activity Monitor.
pub fn java_args(install: &JdtlsInstall, java_major: u32, script_args: &[String]) -> Vec<String> {
let parsed = split_script_args(script_args);
let mut args: Vec<String> = Vec::new();
@@ -211,6 +218,9 @@ pub fn java_args(install: &JdtlsInstall, java_major: u32, script_args: &[String]
);
args.extend(parsed.jvm);
args.extend(["-jar".to_owned(), install.launcher.to_string_lossy().into_owned()]);
if !parsed.rest.iter().any(|a| a == "-nosplash") {
args.push("-nosplash".to_owned());
}
if let Some(data) = parsed.data {
args.extend(["-data".to_owned(), data]);
}
@@ -342,6 +352,7 @@ mod tests {
&format!("-javaagent:{agent}"),
"-jar",
&format!(r"{tools}\plugins\org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar"),
"-nosplash",
"-data",
data,
])
@@ -356,7 +367,9 @@ mod tests {
DEFAULT_VM_ARGS.iter().filter(|a| !drop.contains(a)).map(|a| a.to_string()).collect()
}
const PARALLEL: &[&str] = &["-XX:+UseParallelGC", "-XX:GCTimeRatio=4", "-XX:AdaptiveSizePolicyWeight=90"];
const G1: &[&str] =
&["-XX:+UseG1GC", "-XX:G1PeriodicGCInterval=60000", "-XX:MinHeapFreeRatio=10", "-XX:MaxHeapFreeRatio=30"];
const HEAP_FREE: &[&str] = &["-XX:MinHeapFreeRatio=10", "-XX:MaxHeapFreeRatio=30"];
#[test]
fn user_options_override_the_defaults() {
@@ -369,13 +382,15 @@ mod tests {
(&["-XX:MaxRAMPercentage=40"], &["-Xmx2G"]),
(&["-Xms1G"], &["-Xms100m"]),
(&["-XX:InitialHeapSize=256m"], &["-Xms100m"]),
(&["-XX:+UseG1GC"], PARALLEL),
(&["-XX:+UseZGC", "-XX:+ZGenerational"], PARALLEL),
(&["-XX:+UseShenandoahGC"], PARALLEL),
(&["-XX:+UseSerialGC"], PARALLEL),
(&["-XX:-UseParallelGC"], PARALLEL),
(&["-XX:+UseParallelGC"], PARALLEL),
(&["-XX:GCTimeRatio=9"], &["-XX:GCTimeRatio=4"]),
(&["-XX:+UseParallelGC"], G1),
(&["-XX:+UseZGC", "-XX:+ZGenerational"], G1),
(&["-XX:+UseShenandoahGC"], G1),
(&["-XX:+UseSerialGC"], G1),
(&["-XX:-UseG1GC"], G1),
(&["-XX:+UseG1GC"], G1),
(&["-XX:G1PeriodicGCInterval=0"], &["-XX:G1PeriodicGCInterval=60000"]),
(&["-XX:MinHeapFreeRatio=40"], HEAP_FREE),
(&["-XX:MaxHeapFreeRatio=70"], HEAP_FREE),
(&["-Xlog:gc*:file=gc.log"], &["-Xlog:disable"]),
(&["-Dsun.zip.disableMemoryMapping=false"], &["-Dsun.zip.disableMemoryMapping=true"]),
(&["-Daether.dependencyCollector.impl=df"], &["-Daether.dependencyCollector.impl=bf"]),
@@ -389,10 +404,10 @@ mod tests {
#[test]
fn defaults_go_ahead_of_the_users_options_once() {
let user = strings(&["--jvm-arg=-Xmx4G", "-data", "/ws", "--jvm-arg", "-XX:+UseG1GC"]);
let user = strings(&["--jvm-arg=-Xmx4G", "-data", "/ws", "--jvm-arg", "-XX:+UseZGC"]);
let mut args = user.clone();
add_default_vm_args(&mut args);
let dropped: Vec<&str> = PARALLEL.iter().copied().chain(["-Xmx2G"]).collect();
let dropped: Vec<&str> = G1.iter().copied().chain(["-Xmx2G"]).collect();
let expected: Vec<String> =
without(&dropped).into_iter().map(|a| format!("--jvm-arg={a}")).chain(user).collect();
assert_eq!(args, expected);
@@ -413,14 +428,14 @@ mod tests {
let args = java_args(&install, 25, &script);
let jar = args.iter().position(|a| a == "-jar").unwrap();
let jvm = &args[..jar];
for flag in ["-XX:+UseParallelGC", "-XX:GCTimeRatio=4", "-Xms100m", "-Xlog:disable", "-Xmx3G"] {
for flag in ["-XX:+UseG1GC", "-XX:G1PeriodicGCInterval=60000", "-Xms100m", "-Xlog:disable", "-Xmx3G"] {
assert!(jvm.contains(&flag.to_owned()), "{flag}: {jvm:?}");
}
let heap: Vec<&String> = jvm.iter().filter(|a| option_key(a).as_deref() == Some("Xmx")).collect();
assert_eq!(heap, ["-Xmx3G"], "one heap limit, the user's");
assert!(!jvm.iter().any(|a| a == "-Xms1G"), "{jvm:?}");
assert_eq!(args[jar + 1], format!(r"{tools}\plugins\org.eclipse.equinox.launcher_1.7.0.jar"));
assert_eq!(args[jar + 2..], strings(&["-data", r"C:\Users\张 三\ws"]));
assert_eq!(args[jar + 2..], strings(&["-nosplash", "-data", r"C:\Users\张 三\ws"]));
}
#[test]
@@ -439,18 +454,21 @@ mod tests {
assert_eq!(args[0], "-Declipse.application=org.eclipse.jdt.ls.core.id1", "no JDK 24+ flags on 21");
assert!(args.contains(&"-Dosgi.sharedConfiguration.area=/Users/me/Library/Application Support/ai.l0ng.openidea/tools/jdtls/config_mac_arm".to_owned()));
assert_eq!(
args[args.len() - 7..],
args[args.len() - 8..],
strings(&[
"java.base/java.lang=ALL-UNNAMED",
"-Dlog.level=ALL",
"-jar",
"/Users/me/Library/Application Support/ai.l0ng.openidea/tools/jdtls/plugins/org.eclipse.equinox.launcher_1.7.0.jar",
"-nosplash",
"-data",
"/d",
"--extra",
])
);
assert!(!args.iter().any(|a| a.contains("validate-java-version")));
let own = java_args(&install, 21, &strings(&["-data", "/d", "-nosplash"]));
assert_eq!(own.iter().filter(|a| *a == "-nosplash").count(), 1, "{own:?}");
}
#[test]
@@ -526,7 +544,7 @@ mod tests {
assert!(direct.args.iter().any(|a| a == "-Xmx1G"));
let jar = direct.args.iter().position(|a| a == "-jar").unwrap();
assert!(direct.args[jar + 1].ends_with("org.eclipse.equinox.launcher_1.7.0.jar"));
assert_eq!(direct.args[jar + 2..], strings(&["-data", "/ws"]));
assert_eq!(direct.args[jar + 2..], strings(&["-nosplash", "-data", "/ws"]));
let jre = dir.path().join("jre-25");
std::fs::create_dir_all(jre.join("bin")).unwrap();
@@ -640,7 +658,7 @@ mod tests {
assert!(direct.args.iter().any(|a| a == "-Xmx1G"));
let jar = direct.args.iter().position(|a| a == "-jar").unwrap();
assert!(direct.args[jar + 1].ends_with("org.eclipse.equinox.launcher_1.7.0.jar"));
assert_eq!(direct.args[jar + 2..], strings(&["-data", r"C:\ws"]));
assert_eq!(direct.args[jar + 2..], strings(&["-nosplash", "-data", r"C:\ws"]));
// A shim whose descriptor is gone is someone's own wrapper: keep the command.
std::fs::remove_file(shims.join("jdtls.shim")).unwrap();
@@ -297,7 +297,7 @@ export function JavaPage() {
<Row
label="JVM options"
htmlFor={id}
hint="Passed to the JVM running jdtls (java.jdt.ls.vmargs), e.g. -Xmx4g. jdtls runs with -XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Xmx2G -Xms100m by default; an option set here replaces the default for the same setting, and another collector (e.g. -XX:+UseG1GC) replaces the default collector options. A Lombok agent is added automatically: the one installed in Language Servers, else, when the project uses Lombok (its build, an inherited parent POM or Gradle build, or its sources' imports), the newest from the Maven / Gradle caches."
hint="Passed to the JVM running jdtls (java.jdt.ls.vmargs), e.g. -Xmx4g. jdtls runs with -XX:+UseG1GC -XX:G1PeriodicGCInterval=60000 -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=30 -Xmx2G -Xms100m by default; an option set here replaces the default for the same setting, and another collector (e.g. -XX:+UseZGC) replaces the default collector options. A Lombok agent is added automatically: the one installed in Language Servers, else, when the project uses Lombok (its build, an inherited parent POM or Gradle build, or its sources' imports), the newest from the Maven / Gradle caches."
>
<Input
id={id}
+1 -1
View File
@@ -1478,7 +1478,7 @@
"Passed as -Dmaven.repo.local.": "以 -Dmaven.repo.local 传入。",
"Passed as -s (mirrors, servers, proxies).": "以 -s 传入(镜像、服务器、代理)。",
"Passed to rustfmt when reformatting, e.g. --edition 2024": "重新格式化时传递给 rustfmt,例如 --edition 2024",
"Passed to the JVM running jdtls (java.jdt.ls.vmargs), e.g. -Xmx4g. jdtls runs with -XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Xmx2G -Xms100m by default; an option set here replaces the default for the same setting, and another collector (e.g. -XX:+UseG1GC) replaces the default collector options. A Lombok agent is added automatically: the one installed in Language Servers, else, when the project uses Lombok (its build, an inherited parent POM or Gradle build, or its sources' imports), the newest from the Maven / Gradle caches.": "传递给运行 jdtls 的 JVM (java.jdt.ls.vmargs),例如 -Xmx4g。jdtls 默认使用 -XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Xmx2G -Xms100m 运行;此处设置的选项会替换同一设置的默认值,指定其他垃圾回收器(例如 -XX:+UseG1GC)时会替换默认的回收器选项。会自动添加 Lombok 代理:优先使用在“语言服务器”中安装的版本;否则当项目使用 Lombok(构建文件、继承的父 POM 或 Gradle 构建、源码中的 import)时,使用 Maven / Gradle 缓存中最新的版本。",
"Passed to the JVM running jdtls (java.jdt.ls.vmargs), e.g. -Xmx4g. jdtls runs with -XX:+UseG1GC -XX:G1PeriodicGCInterval=60000 -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=30 -Xmx2G -Xms100m by default; an option set here replaces the default for the same setting, and another collector (e.g. -XX:+UseZGC) replaces the default collector options. A Lombok agent is added automatically: the one installed in Language Servers, else, when the project uses Lombok (its build, an inherited parent POM or Gradle build, or its sources' imports), the newest from the Maven / Gradle caches.": "传递给运行 jdtls 的 JVM (java.jdt.ls.vmargs),例如 -Xmx4g。jdtls 默认使用 -XX:+UseG1GC -XX:G1PeriodicGCInterval=60000 -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=30 -Xmx2G -Xms100m 运行;此处设置的选项会替换同一设置的默认值,指定其他垃圾回收器(例如 -XX:+UseZGC)时会替换默认的回收器选项。会自动添加 Lombok 代理:优先使用在“语言服务器”中安装的版本;否则当项目使用 Lombok(构建文件、继承的父 POM 或 Gradle 构建、源码中的 import)时,使用 Maven / Gradle 缓存中最新的版本。",
"Pop stash": "弹出储藏",
"Preview Source": "预览源代码",
"Reinstate index": "恢复索引",