- 自动识别 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 补充导航与示例
205 lines
6.0 KiB
Go
205 lines
6.0 KiB
Go
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)
|
|
}
|