feat: 新增 java run,按 JDK 版本自动套用调优参数运行 Java 应用
- 自动识别 JDK 8/11/17/21 与宿主机 CPU、可用内存;容器内以 cgroup 限额为准;堆默认取可用内存 50%,-Xms 与 -Xmx 同值 - 按版本分支生成参数:JDK 8 用 -Xloggc 一组,JDK 9+ 用 -Xlog:; 堆超过 30g 时自动省略压缩指针参数 - 支持 --jdk/--java/--heap/--xms/--env/--gc-log/--heap-dump-on-oom/ --print-flags/--dry-run;jar 之后的参数原样交给应用 - 前台运行:stdio 直连、退出码透传、SIGINT/SIGTERM 转发给 JVM - BuiltinRequest 增加 Stdin,使内置命令可以读取调用方输入 - 修正帮助中的用法行重复(自定义 Usage 不再追加通用透传尾巴) - 文档:新增 README.java.md,主 README 补充导航与示例
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// javaTuning is everything BuildJavaFlags needs to describe one JVM launch.
|
||||
type javaTuning struct {
|
||||
JDK string // normalised release: "8", "11", "17", "21"
|
||||
CPUs int // usable CPUs, 0 when unknown
|
||||
Memory int64 // usable memory in bytes, 0 when unknown
|
||||
Heap string // -Xmx value; empty means "compute from Memory"
|
||||
Initial string // -Xms value; empty means "same as -Xmx"
|
||||
Env string // used for -Dconfig.file=application-<Env>.yaml
|
||||
GCDir string // directory for GC logs; empty means no GC log
|
||||
HeapDump bool // add -XX:+HeapDumpOnOutOfMemoryError
|
||||
}
|
||||
|
||||
// javaHeapRatio is the fraction of available memory used for the heap when the
|
||||
// user does not pick a size: half leaves room for metaspace, thread stacks,
|
||||
// direct buffers and the OS page cache.
|
||||
const javaHeapRatioPercent = 50
|
||||
|
||||
// compressedOopsLimit is the largest heap for which compressed oops help; JVM
|
||||
// warns and effectively disables them beyond this, so kopsc stops emitting the
|
||||
// flags at 30 GiB (the commonly recommended ceiling, below the 32 GiB limit).
|
||||
const compressedOopsLimit = int64(30) << 30
|
||||
|
||||
// BuildJavaFlags renders the ordered java arguments for one launch.
|
||||
//
|
||||
// The order mirrors a hand-written command line so `--print-flags` output can
|
||||
// be pasted into a Dockerfile or systemd unit unchanged: JVM ergonomics first,
|
||||
// then heap, then GC and logging, then application properties.
|
||||
func BuildJavaFlags(t javaTuning) []string {
|
||||
flags := []string{"-server"}
|
||||
|
||||
// Heap: -Xms and -Xmx are equal to keep the heap from resizing at runtime.
|
||||
heapBytes, heapText := resolveHeap(t)
|
||||
initialText := t.Initial
|
||||
if initialText == "" {
|
||||
initialText = heapText
|
||||
}
|
||||
if heapText != "" {
|
||||
flags = append(flags, "-Xmx"+heapText)
|
||||
}
|
||||
if initialText != "" {
|
||||
flags = append(flags, "-Xms"+initialText)
|
||||
}
|
||||
|
||||
flags = append(flags,
|
||||
"-Xss256k",
|
||||
"-XX:MaxDirectMemorySize="+directMemory(heapBytes),
|
||||
)
|
||||
|
||||
// Compressed oops only make sense while the heap fits under the limit.
|
||||
if heapBytes <= 0 || heapBytes <= compressedOopsLimit {
|
||||
flags = append(flags, "-XX:+UseCompressedOops", "-XX:+UseCompressedClassPointers")
|
||||
}
|
||||
|
||||
flags = append(flags,
|
||||
"-XX:+SegmentedCodeCache",
|
||||
"-XX:+UseG1GC",
|
||||
"-XX:+ExplicitGCInvokesConcurrent",
|
||||
)
|
||||
|
||||
if jdkMajor(t.JDK) >= 9 {
|
||||
flags = append(flags, "-XX:+PrintCommandLineFlags")
|
||||
if t.GCDir != "" {
|
||||
flags = append(flags, "-Xlog:gc*,safepoint:"+gcLogPath(t.GCDir)+":time,uptime:filecount=100,filesize=50M")
|
||||
}
|
||||
} else {
|
||||
// JDK 8 has no -Xlog:; it uses -Xloggc plus its own rotation options.
|
||||
// Emitting -Xlog: on JDK 8 makes the JVM refuse to start.
|
||||
flags = append(flags, "-verbose:gc", "-XX:+PrintCommandLineFlags", "-XX:+PrintGCDetails", "-XX:+PrintGCDateStamps")
|
||||
if t.GCDir != "" {
|
||||
flags = append(flags,
|
||||
"-Xloggc:"+gcLogPath(t.GCDir),
|
||||
"-XX:+UseGCLogFileRotation",
|
||||
"-XX:NumberOfGCLogFiles=100",
|
||||
"-XX:GCLogFileSize=50M",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
flags = append(flags, "-Djava.security.egd=file:/dev/./urandom")
|
||||
|
||||
if t.HeapDump {
|
||||
flags = append(flags, "-XX:+HeapDumpOnOutOfMemoryError")
|
||||
}
|
||||
|
||||
flags = append(flags, "-Dconfig.file="+configFile(t.Env))
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
// configFile renders the Spring-style config property: --env wins, then the
|
||||
// APP_ENV environment variable, then "prod".
|
||||
func configFile(env string) string {
|
||||
if env == "" {
|
||||
env = os.Getenv("APP_ENV")
|
||||
}
|
||||
if env == "" {
|
||||
env = "prod"
|
||||
}
|
||||
return "application-" + env + ".yaml"
|
||||
}
|
||||
|
||||
// resolveHeap decides the heap size: an explicit value wins, otherwise half of
|
||||
// the available memory.
|
||||
func resolveHeap(t javaTuning) (int64, string) {
|
||||
if t.Heap != "" {
|
||||
return parseJVMSize(t.Heap), t.Heap
|
||||
}
|
||||
if t.Memory <= 0 {
|
||||
return 0, ""
|
||||
}
|
||||
// Multiply first: dividing by 100 up front truncates, turning 8 GiB into
|
||||
// 3.5g because the remainder is lost before the ratio is applied.
|
||||
bytes := t.Memory * javaHeapRatioPercent / 100
|
||||
return bytes, formatJVMSize(bytes)
|
||||
}
|
||||
|
||||
// directMemory returns MaxDirectMemorySize as a quarter of the heap (the JVM's
|
||||
// own default), at least 64 MiB so small heaps still work with NIO.
|
||||
func directMemory(heapBytes int64) string {
|
||||
if heapBytes <= 0 {
|
||||
return "256m"
|
||||
}
|
||||
quarter := heapBytes / 4
|
||||
if quarter < 64<<20 {
|
||||
quarter = 64 << 20
|
||||
}
|
||||
return formatJVMSize(quarter)
|
||||
}
|
||||
|
||||
// gcLogPath joins the GC directory and the standard file name.
|
||||
func gcLogPath(dir string) string {
|
||||
return strings.TrimRight(dir, `/\`) + "/gc.log"
|
||||
}
|
||||
|
||||
// jdkMajor parses the normalised release into a number; "8" when unknown so
|
||||
// the conservative JDK 8 flag set is used.
|
||||
func jdkMajor(jdk string) int {
|
||||
n, err := strconv.Atoi(strings.TrimSpace(jdk))
|
||||
if err != nil {
|
||||
return 8
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// parseJVMSize converts a user-supplied size ("2g", "512m", "1024k", "1073741824")
|
||||
// into bytes. An unparsable value returns 0.
|
||||
func parseJVMSize(value string) int64 {
|
||||
value = strings.TrimSpace(strings.ToLower(value))
|
||||
if value == "" {
|
||||
return 0
|
||||
}
|
||||
multiplier := int64(1)
|
||||
switch value[len(value)-1] {
|
||||
case 'k':
|
||||
multiplier, value = 1<<10, value[:len(value)-1]
|
||||
case 'm':
|
||||
multiplier, value = 1<<20, value[:len(value)-1]
|
||||
case 'g':
|
||||
multiplier, value = 1<<30, value[:len(value)-1]
|
||||
case 't':
|
||||
multiplier, value = 1<<40, value[:len(value)-1]
|
||||
}
|
||||
n, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
||||
if err != nil || n <= 0 {
|
||||
return 0
|
||||
}
|
||||
return n * multiplier
|
||||
}
|
||||
|
||||
// formatJVMSize renders bytes the way -Xmx expects: "512m", "2g". Sizes are
|
||||
// rounded down to whole mebibytes (or gibibytes when large enough) so the
|
||||
// printed flag stays short.
|
||||
func formatJVMSize(bytes int64) string {
|
||||
if bytes <= 0 {
|
||||
return ""
|
||||
}
|
||||
const (
|
||||
mib = int64(1) << 20
|
||||
gib = int64(1) << 30
|
||||
)
|
||||
if bytes >= gib {
|
||||
if bytes%gib == 0 {
|
||||
return fmt.Sprintf("%dg", bytes/gib)
|
||||
}
|
||||
// Round down to 0.5g so "6.4g" becomes "6.5g"-free output (6.4g),
|
||||
// keeping the flag short while never asking for more than available.
|
||||
halfG := int64(1) << 29
|
||||
halves := bytes / halfG
|
||||
if halves%2 == 0 {
|
||||
return fmt.Sprintf("%dg", halves/2)
|
||||
}
|
||||
return fmt.Sprintf("%.1fg", float64(halves)/2)
|
||||
}
|
||||
return fmt.Sprintf("%dm", bytes/mib)
|
||||
}
|
||||
Reference in New Issue
Block a user