diff --git a/bin/as.sh b/bin/as.sh index 054fbff92..66b5c0d1d 100755 --- a/bin/as.sh +++ b/bin/as.sh @@ -229,6 +229,64 @@ check_permission() } +# 验证候选 Java,成功后才更新 JAVA_HOME 和对应的启动参数。 +try_java_home() +{ + local java_home="$1" + [ -n "$java_home" ] || return 1 + if [ ! -x "$java_home/bin/java" ]; then + echo "[WARN] Skip JAVA_HOME '$java_home': bin/java is missing or not executable." >&2 + return 1 + fi + + local version_output + if ! version_output=$("$java_home/bin/java" -version 2>&1); then + echo "[WARN] Skip JAVA_HOME '$java_home': java -version failed." >&2 + return 1 + fi + + local java_version= line version + local version_pattern='version[[:space:]]+"([^"]+)"' + while IFS= read -r line; do + if [[ "$line" =~ $version_pattern ]]; then + version=${BASH_REMATCH[1]} + [[ "$version" == 1.* ]] && version=${version#1.} + if [[ "$version" =~ ^([0-9]+)($|[-._+]) ]]; then + java_version=$((10#${BASH_REMATCH[1]})) + break + fi + fi + done <<< "$version_output" + + if [ -z "$java_version" ] || [ "$java_version" -lt 1 ]; then + echo "[WARN] Skip JAVA_HOME '$java_home': unrecognized Java version." >&2 + return 1 + fi + + java_home=$(rreadlink "$java_home") || return 1 + local boot_classpath= candidate + # 仅 Java 8 及以下需要 tools.jar;保留从 JRE 子目录向父 JDK 查找的行为。 + if [ "$java_version" -lt 9 ]; then + local java_homes=("$java_home" "$java_home/.." "$java_home/../..") + for candidate in "${java_homes[@]}"; do + if [ -x "$candidate/bin/java" ] && [ -f "$candidate/lib/tools.jar" ]; then + java_home=$(rreadlink "$candidate") || return 1 + boot_classpath="-Xbootclasspath/a:$java_home/lib/tools.jar" + break + fi + done + if [ -z "$boot_classpath" ]; then + echo "[WARN] Skip JAVA_HOME '$java_home': Java $java_version requires a JDK with lib/tools.jar." >&2 + return 1 + fi + fi + + JAVA_HOME=$java_home + BOOT_CLASSPATH=$boot_classpath + return 0 +} + + # reset arthas work environment # reset some options for env reset_for_env() @@ -239,73 +297,38 @@ reset_for_env() mkdir -p "${ARTHAS_LIB_DIR}" \ || exit_on_err 1 "create ${ARTHAS_LIB_DIR} fail." - # if env define the JAVA_HOME, use it first - # if is alibaba opts, use alibaba ops's default JAVA_HOME - [ -z "${JAVA_HOME}" ] && [ -d /opt/taobao/java ] && JAVA_HOME=/opt/taobao/java + local configured_java_home="${JAVA_HOME}" + JAVA_HOME= + BOOT_CLASSPATH= - if [[ (-z "${JAVA_HOME}") && ( -e "/usr/libexec/java_home") ]]; then - # for mac - JAVA_HOME=`/usr/libexec/java_home` + # 有效的显式配置优先;只有当前来源不可用时才查找下一个来源。 + if ! try_java_home "$configured_java_home" && [ -d /opt/taobao/java ]; then + try_java_home /opt/taobao/java + fi + + if [ -z "${JAVA_HOME}" ] && [ -x /usr/libexec/java_home ]; then + local mac_java_home + mac_java_home=$(/usr/libexec/java_home 2>/dev/null) && try_java_home "$mac_java_home" fi if [ -z "${JAVA_HOME}" ]; then - # try to find JAVA_HOME from java command - local JAVA_COMMAND_PATH=$( rreadlink $(type -p java) ) - JAVA_HOME=$(echo "$JAVA_COMMAND_PATH" | sed -n 's/\/bin\/java$//p') - fi - - # iterater through candidates to find a proper JAVA_HOME at least contains tools.jar which is required by arthas. - if [ ! -d "${JAVA_HOME}" ]; then - JAVA_HOME_CANDIDATES=($(ps aux | grep java | grep -v 'grep java' | awk '{print $11}' | sed -n 's/\/bin\/java$//p')) - for JAVA_HOME_TEMP in ${JAVA_HOME_CANDIDATES[@]}; do - if [ -f "${JAVA_HOME_TEMP}/lib/tools.jar" ]; then - JAVA_HOME=`rreadlink "${JAVA_HOME_TEMP}"` - break - fi - done - fi - - if [ -z "${JAVA_HOME}" ]; then - exit_on_err 1 "Can not find JAVA_HOME, please set \$JAVA_HOME bash env first." - fi - - # maybe 1.8.0_162 , 11-ea - local JAVA_VERSION - - local IFS=$'\n' - # remove \r for Cygwin - local lines=$("${JAVA_HOME}"/bin/java -version 2>&1 | tr '\r' '\n') - for line in $lines; do - if [[ (-z $JAVA_VERSION) && ($line = *"version \""*) ]] - then - local ver=$(echo $line | sed -e 's/.*version "\(.*\)"\(.*\)/\1/; 1q') - # on macOS, sed doesn't support '?' - if [[ $ver = "1."* ]] - then - JAVA_VERSION=$(echo $ver | sed -e 's/1\.\([0-9]*\)\(.*\)/\1/; 1q') - else - JAVA_VERSION=$(echo $ver | sed -e 's/\([0-9]*\)\(.*\)/\1/; 1q') + local java_command_path + java_command_path=$(type -p java) + if [ -n "$java_command_path" ] && java_command_path=$(rreadlink "$java_command_path"); then + case "$java_command_path" in + */bin/java) try_java_home "${java_command_path%/bin/java}" ;; + esac fi - fi - done - - # when java version less than 9, we can use tools.jar to confirm java home. - # when java version greater than 9, there is no tools.jar. - if [[ "$JAVA_VERSION" -lt 9 ]];then - # possible java homes - javaHomes=("${JAVA_HOME%%/}" "${JAVA_HOME%%/}/.." "${JAVA_HOME%%/}/../..") - for javaHome in ${javaHomes[@]} - do - toolsJar="$javaHome/lib/tools.jar" - if [ -f $toolsJar ]; then - JAVA_HOME=$( rreadlink $javaHome ) - BOOT_CLASSPATH=-Xbootclasspath/a:$( rreadlink $toolsJar ) - break - fi - done - [ -z "${BOOT_CLASSPATH}" ] && exit_on_err 1 "tools.jar was not found, so arthas could not be launched!" fi + if [ -z "${JAVA_HOME}" ]; then + local candidate + while IFS= read -r candidate; do + try_java_home "$candidate" && break + done < <(ps aux | awk '$11 ~ /\/bin\/java$/ { sub(/\/bin\/java$/, "", $11); print $11 }') + fi + + [ -n "${JAVA_HOME}" ] || exit_on_err 1 "Can not find a usable JAVA_HOME; see the candidate errors above." echo "[INFO] JAVA_HOME: ${JAVA_HOME}" # reset CHARSET for alibaba opts, we use GBK diff --git a/boot/src/test/java/com/taobao/arthas/boot/AsScriptTest.java b/boot/src/test/java/com/taobao/arthas/boot/AsScriptTest.java index 2dd1a5df0..20056b410 100644 --- a/boot/src/test/java/com/taobao/arthas/boot/AsScriptTest.java +++ b/boot/src/test/java/com/taobao/arthas/boot/AsScriptTest.java @@ -40,6 +40,10 @@ public class AsScriptTest { private Path javaCalls; private Path curlCalls; private Path download; + private Path javaHome; + private Path platformJavaHome; + private Path macJavaHome; + private Path discoveryCalls; private Map environment; @Before @@ -50,6 +54,9 @@ public class AsScriptTest { javaCalls = temporaryFolder.newFile("java-calls").toPath(); curlCalls = temporaryFolder.newFile("curl-calls").toPath(); download = temporaryFolder.newFile("download.zip").toPath(); + discoveryCalls = temporaryFolder.newFile("discovery-calls").toPath(); + platformJavaHome = temporaryFolder.getRoot().toPath().resolve("platform-jdk"); + macJavaHome = temporaryFolder.getRoot().toPath().resolve("mac-java-home"); createArchive(true); // 只暴露启动器所需命令,避免宿主机的 telnet、lsof 和 Java 进程影响结果。 @@ -61,7 +68,16 @@ public class AsScriptTest { Files.createSymbolicLink(commands.resolve(command), Paths.get(new String(output, StandardCharsets.UTF_8).trim())); } writeExecutable(commands.resolve("telnet"), "exit 0\n"); - writeExecutable(commands.resolve("ps"), "case \"$*\" in *user=*) id -u -n;; *) echo 1234;; esac\n"); + writeExecutable(commands.resolve("ps"), + "case \"$*\" in\n" + + " *user=*) id -u -n;;\n" + + " aux)\n" + + " printf 'ps\\n' >> \"$AS_TEST_DISCOVERY_CALLS\"\n" + + " if [[ -n $AS_TEST_PROCESS_JAVA_HOME ]]; then\n" + + " printf 'user 1234 0 0 0 0 ? Sl 00:00 0:00 %s/bin/java example.Main\\n' \"$AS_TEST_PROCESS_JAVA_HOME\"\n" + + " fi;;\n" + + " *) echo 1234;;\n" + + "esac\n"); writeExecutable(commands.resolve("curl"), "printf '%s\\n' \"$*\" >> \"$AS_TEST_CURL_CALLS\"\n" + "output=\n" @@ -77,12 +93,7 @@ public class AsScriptTest { + " cp \"$AS_TEST_DOWNLOAD\" \"$output\"\n" + "fi\n"); - Path javaHome = temporaryFolder.newFolder("jdk").toPath(); - Files.createDirectory(javaHome.resolve("bin")); - writeExecutable(javaHome.resolve("bin/java"), - "if [[ $1 == -version ]]; then echo 'openjdk version \"17.0.2\"' >&2; exit 0; fi\n" - + "printf '%s\\n' \"$*\" >> \"$AS_TEST_JAVA_CALLS\"\n"); - writeExecutable(javaHome.resolve("bin/jps"), "echo '1234 example.Main'\n"); + javaHome = createJavaHome(temporaryFolder.getRoot().toPath().resolve("jdk"), "17.0.2", false); Path project = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); while (project != null && !Files.isRegularFile(project.resolve("bin/as.sh"))) { @@ -90,6 +101,9 @@ public class AsScriptTest { } assertTrue("Cannot locate bin/as.sh", project != null); String script = new String(Files.readAllBytes(project.resolve("bin/as.sh")), StandardCharsets.UTF_8); + // 隔离固定平台路径,保留完整 Java 初始化和启动流程。 + script = script.replace("/opt/taobao/java", platformJavaHome.toString()) + .replace("/usr/libexec/java_home", macJavaHome.toString()); int mainCall = script.lastIndexOf("\nmain \"${@}\""); assertTrue("Cannot locate launcher entry point", mainCall >= 0); // 显式设置 umask,避免宿主设置影响权限断言;旧版本的 /tmp 仍隔离到测试目录。 @@ -108,9 +122,172 @@ public class AsScriptTest { environment.put("AS_TEST_DOWNLOAD", download.toString()); environment.put("AS_TEST_JAVA_CALLS", javaCalls.toString()); environment.put("AS_TEST_CURL_CALLS", curlCalls.toString()); + environment.put("AS_TEST_DISCOVERY_CALLS", discoveryCalls.toString()); environment.put("AS_TEST_TMP_DIR", temporaryFolder.newFolder("downloads").getAbsolutePath()); } + @Test + public void invalidExplicitJavaHomeFallsBackToPlatform() throws Exception { + Path fallback = createJavaHome(platformJavaHome, "11.0.27.26-AJDK", false); + Path missing = temporaryFolder.getRoot().toPath().resolve("missing-jdk"); + Path empty = temporaryFolder.newFolder("empty-jdk").toPath(); + Path nonExecutable = createJavaHome(temporaryFolder.getRoot().toPath().resolve("non-executable"), "17", false); + Files.setPosixFilePermissions(nonExecutable.resolve("bin/java"), PosixFilePermissions.fromString("rw-r--r--")); + for (Path invalid : Arrays.asList(missing, empty, nonExecutable)) { + environment.put("JAVA_HOME", invalid.toString()); + String output = runHelp(0); + assertJavaHome(output, fallback); + assertTrue(output, output.contains("bin/java is missing or not executable")); + } + assertEquals("", read(discoveryCalls)); + } + + @Test + public void validExplicitJavaHomeAvoidsFallbackDiscovery() throws Exception { + Path fallback = createJavaHome(platformJavaHome, "11", false); + useMacJava(fallback); + Files.createSymbolicLink(commands.resolve("java"), fallback.resolve("bin/java")); + environment.put("AS_TEST_PROCESS_JAVA_HOME", fallback.toString()); + String output = runHelp(0); + assertJavaHome(output, javaHome); + assertFalse(output, output.contains("[WARN]")); + assertEquals("", read(discoveryCalls)); + } + + @Test + public void platformJavaTakesPrecedenceOverMacAndPath() throws Exception { + environment.put("JAVA_HOME", ""); + Path fallback = createJavaHome(platformJavaHome, "11", false); + useMacJava(javaHome); + Files.createSymbolicLink(commands.resolve("java"), javaHome.resolve("bin/java")); + assertJavaHome(runHelp(0), fallback); + assertEquals("", read(discoveryCalls)); + } + + @Test + public void macJavaTakesPrecedenceOverPath() throws Exception { + environment.put("JAVA_HOME", ""); + useMacJava(javaHome); + Path pathJava = createJavaHome(temporaryFolder.getRoot().toPath().resolve("path-jdk"), "21", false); + Files.createSymbolicLink(commands.resolve("java"), pathJava.resolve("bin/java")); + assertJavaHome(runHelp(0), javaHome); + assertEquals("mac\n", read(discoveryCalls)); + } + + @Test + public void failedMacLookupFallsBackToPath() throws Exception { + environment.put("JAVA_HOME", ""); + writeExecutable(macJavaHome, "exit 1\n"); + Files.createSymbolicLink(commands.resolve("java"), javaHome.resolve("bin/java")); + assertJavaHome(runHelp(0), javaHome); + } + + @Test + public void pathWithSpacesAndSymlinkWorksWithoutOptionalPlatformPaths() throws Exception { + environment.put("JAVA_HOME", ""); + Path path = temporaryFolder.newFolder("path with spaces").toPath(); + Files.createSymbolicLink(path.resolve("java"), javaHome.resolve("bin/java")); + environment.put("PATH", path + File.pathSeparator + commands); + String output = runHelp(0); + assertJavaHome(output, javaHome); + assertFalse(output, output.contains("[WARN]")); + assertEquals("", read(discoveryCalls)); + } + + @Test + public void processFallbackAcceptsModernJavaWithoutToolsJar() throws Exception { + environment.put("JAVA_HOME", temporaryFolder.getRoot().toPath().resolve("missing-jdk").toString()); + Path processJava = createJavaHome(temporaryFolder.getRoot().toPath().resolve("process-jdk"), "11", false); + environment.put("AS_TEST_PROCESS_JAVA_HOME", processJava.toString()); + assertJavaHome(runHelp(0), processJava); + assertEquals("ps\n", read(discoveryCalls)); + } + + @Test + public void failedVersionCommandIsRejectedEvenWithRecognizableOutput() throws Exception { + Path fallback = createJavaHome(platformJavaHome, "11", false); + writeJava(javaHome, "openjdk version \"17.0.2\"", 42); + String output = runHelp(0); + assertJavaHome(output, fallback); + assertTrue(output, output.contains("java -version failed")); + } + + @Test + public void invalidVersionsFallBackInsteadOfAssumingJava8() throws Exception { + Path fallback = createJavaHome(platformJavaHome, "11", false); + for (String version : Arrays.asList("", "unrecognized vendor output", "openjdk version \"0\"", + "openjdk version \"invalid\"")) { + writeJava(javaHome, version, 0); + String output = runHelp(0); + assertJavaHome(output, fallback); + assertTrue(output, output.contains("unrecognized Java version")); + } + } + + @Test + public void modernJavaVersionsDoNotNeedToolsJar() throws Exception { + for (String version : Arrays.asList("9", "11.0.27.26-AJDK", "11-ea", "17.0.15", "21.0.7", "25")) { + writeJava(javaHome, "openjdk version \"" + version + "\"\r\nOpenJDK Runtime Environment\r", 0); + assertJavaHome(runHelp(0), javaHome); + } + } + + @Test + public void java8AndNestedJreUseJdkToolsJarWithSpaces() throws Exception { + Path jdk = createJavaHome(temporaryFolder.getRoot().toPath().resolve("jdk8 with spaces"), "1.8.0_402", true); + Path jre = createJavaHome(jdk.resolve("jre"), "1.8.0_402", false); + Path nestedJre = createJavaHome(jre.resolve("nested"), "1.8.0_402", false); + Path arthas = cache("4.3.5"); + for (Path candidate : Arrays.asList(jdk, jre, nestedJre)) { + Files.write(javaCalls, new byte[0]); + environment.put("JAVA_HOME", candidate.toString()); + environment.put("BOOT_CLASSPATH", "-Xbootclasspath/a:/stale/tools.jar"); + assertJavaHome(run(0, "--arthas-home", arthas.toString(), "--attach-only", "1234"), jdk); + String calls = read(javaCalls); + assertTrue(calls, calls.contains("-Xbootclasspath/a:" + jdk.resolve("lib/tools.jar"))); + assertFalse(calls, calls.contains("/stale/tools.jar")); + } + } + + @Test + public void java8WithoutUsableParentJdkFallsBack() throws Exception { + Path fallback = createJavaHome(platformJavaHome, "11", false); + Path parent = temporaryFolder.newFolder("jdk8-without-java").toPath(); + Files.createDirectories(parent.resolve("lib")); + Files.createFile(parent.resolve("lib/tools.jar")); + Path jre = createJavaHome(parent.resolve("jre"), "1.8.0_402", false); + environment.put("JAVA_HOME", jre.toString()); + String output = runHelp(0); + assertJavaHome(output, fallback); + assertTrue(output, output.contains("requires a JDK with lib/tools.jar")); + } + + @Test + public void modernJavaClearsInheritedBootClasspathBeforeAttach() throws Exception { + environment.put("BOOT_CLASSPATH", "-Xbootclasspath/a:/stale/tools.jar"); + Path arthas = cache("4.3.5"); + assertJavaHome(run(0, "--arthas-home", arthas.toString(), "--attach-only", "1234"), javaHome); + assertFalse(read(javaCalls), read(javaCalls).contains("-Xbootclasspath")); + } + + @Test + public void inheritedBootClasspathCannotMakeInvalidJavaUsable() throws Exception { + environment.put("JAVA_HOME", temporaryFolder.getRoot().toPath().resolve("missing-jdk").toString()); + environment.put("BOOT_CLASSPATH", "-Xbootclasspath/a:/stale/tools.jar"); + String output = runHelp(1); + assertTrue(output, output.contains("Can not find a usable JAVA_HOME")); + assertFalse(output, output.contains("tools.jar was not found, so arthas could not be launched!")); + } + + @Test + public void unknownVersionReportsSelectionFailureBeforeDownloadOrAttach() throws Exception { + writeJava(javaHome, "unrecognized vendor output", 0); + String output = runHelp(1); + assertTrue(output, output.contains("unrecognized Java version")); + assertTrue(output, output.contains("Can not find a usable JAVA_HOME")); + assertFalse(output, output.contains("tools.jar was not found, so arthas could not be launched!")); + } + @Test public void choosesNewestNumericCachedVersion() throws Exception { cache("4.3.9"); @@ -325,6 +502,10 @@ public class AsScriptTest { command.addAll(Arrays.asList(arguments)); Path output = temporaryFolder.newFile().toPath(); ProcessBuilder builder = new ProcessBuilder(command).directory(temporaryFolder.getRoot()); + for (String variable : Arrays.asList("BOOT_CLASSPATH", "BASH_ENV", "ENV", "JAVA_TOOL_OPTIONS", "_JAVA_OPTIONS", + "JDK_JAVA_OPTIONS")) { + builder.environment().remove(variable); + } builder.environment().putAll(environment); builder.redirectErrorStream(true).redirectOutput(output.toFile()); Process process = builder.start(); @@ -337,6 +518,47 @@ public class AsScriptTest { return text; } + private String runHelp(int expectedExit) throws Exception { + String output = run(expectedExit, "--help"); + assertEquals("Java selection must not invoke download commands", "", read(curlCalls)); + assertEquals("Java selection must not invoke attach or client commands", "", read(javaCalls)); + if (expectedExit == 0) { + assertTrue(output, output.contains("Usage:")); + } + return output; + } + + private void assertJavaHome(String output, Path expected) throws IOException { + assertTrue(output, output.contains("[INFO] JAVA_HOME: " + expected.toRealPath() + "\n")); + } + + private Path createJavaHome(Path home, String version, boolean tools) throws IOException { + Files.createDirectories(home.resolve("bin")); + writeJava(home, "openjdk version \"" + version + "\"", 0); + writeExecutable(home.resolve("bin/jps"), "echo '1234 example.Main'\n"); + if (tools) { + Files.createDirectories(home.resolve("lib")); + Files.createFile(home.resolve("lib/tools.jar")); + } + return home.toRealPath(); + } + + private void writeJava(Path home, String versionOutput, int exitCode) throws IOException { + writeExecutable(home.resolve("bin/java"), + "if [[ $1 == -version ]]; then\n" + + " printf '%s\\n' '" + versionOutput.replace("'", "'\\''") + "' >&2\n" + + " exit " + exitCode + "\n" + + "fi\n" + + "printf '%s\\n' \"$*\" >> \"$AS_TEST_JAVA_CALLS\"\n"); + } + + private void useMacJava(Path home) throws IOException { + environment.put("AS_TEST_MAC_JAVA_HOME", home.toString()); + writeExecutable(macJavaHome, + "printf 'mac\\n' >> \"$AS_TEST_DISCOVERY_CALLS\"\n" + + "printf '%s\\n' \"$AS_TEST_MAC_JAVA_HOME\"\n"); + } + private void assertHome(String output, Path home) { assertTrue(output, output.contains("Arthas home: " + home + "\n")); } diff --git a/site/docs/doc/install-detail.md b/site/docs/doc/install-detail.md index 10f10763a..12e6567bf 100644 --- a/site/docs/doc/install-detail.md +++ b/site/docs/doc/install-detail.md @@ -37,6 +37,22 @@ curl -L https://arthas.aliyun.com/install.sh | sh 也可以执行`./as.sh -h`来获取更多参数信息。 +`as.sh` 按以下顺序查找可用的 Java,选中后停止查找: + +1. 环境变量 `JAVA_HOME`。 +2. `/opt/taobao/java`(目录存在时)。 +3. macOS 的 `/usr/libexec/java_home` 返回的目录。 +4. `PATH` 中的 `java`,解析软链接后确定安装目录。 +5. 运行中 Java 进程的安装目录(需要进程列表中包含完整的 `/bin/java` 路径)。 + +每个候选目录都需要包含可执行的 `bin/java`,并且 `java -version` 执行成功、版本可以识别。Java 8 及以下还需要 `lib/tools.jar`;如果 `JAVA_HOME` 指向 JRE 子目录,脚本会向上查找最多两级,选择包含 `tools.jar` 和可执行 `bin/java` 的 JDK。Java 9 及以上不需要 `tools.jar`。 + +有效的 `JAVA_HOME` 始终优先。已配置的目录失效时,脚本会输出原因并继续查找;全部候选不可用时,会在下载或 attach 之前退出。可以为单次启动指定 Java: + +```bash +JAVA_HOME="/path/to/jdk" ./as.sh +``` + ## 全量安装 最新版本,点击下载:[![](https://img.shields.io/maven-central/v/com.taobao.arthas/arthas-packaging.svg?style=flat-square "Arthas")](https://arthas.aliyun.com/download/latest_version?mirror=aliyun) diff --git a/site/docs/en/doc/install-detail.md b/site/docs/en/doc/install-detail.md index 08a6b11d3..f2cedc2bf 100644 --- a/site/docs/en/doc/install-detail.md +++ b/site/docs/en/doc/install-detail.md @@ -29,6 +29,22 @@ The command above will download the bootstrap script `as.sh` to the current dire You can enter its interactive interface by executing `as.sh`, or execute `as.sh -h` for more help information. +`as.sh` searches for a usable Java installation in the following order and stops at the first valid candidate: + +1. The `JAVA_HOME` environment variable. +2. `/opt/taobao/java`, if the directory exists. +3. The directory returned by `/usr/libexec/java_home` on macOS. +4. The `java` command on `PATH`, resolving symbolic links to locate its installation directory. +5. The installation directories of running Java processes, when the process list contains their full `/bin/java` paths. + +Each candidate must contain an executable `bin/java`, and `java -version` must succeed and report a recognizable version. Java 8 and earlier also require `lib/tools.jar`. If `JAVA_HOME` points to a JRE subdirectory, the script searches up to two parent directories for a JDK containing both `tools.jar` and an executable `bin/java`. Java 9 and later do not require `tools.jar`. + +A valid `JAVA_HOME` always takes precedence. If a configured installation is invalid, the script reports the reason and continues searching. If no candidate is usable, it exits before downloading or attaching. To select Java for a single invocation: + +```bash +JAVA_HOME="/path/to/jdk" ./as.sh +``` + ## Full installation Latest Version, Click To Download: [![](https://img.shields.io/maven-central/v/com.taobao.arthas/arthas-packaging.svg?style=flat-square "Arthas")](https://arthas.aliyun.com/download/latest_version)