第一章:Golang信号处理不当致服务优雅退出失败:SIGTERM未触发defer cleanup的5种典型模式
Go 程序在容器化或 systemd 环境中常因信号处理疏漏导致 defer 清理逻辑未执行,引发资源泄漏、数据丢失或进程僵死。根本原因在于:defer 仅在函数正常返回或 panic 时执行,而 os.Exit()、runtime.Goexit()、syscall.Kill() 等强制终止方式会绕过 defer 链;更关键的是,若主 goroutine 在收到 SIGTERM 后未阻塞等待清理完成,程序将立即退出。
忽略信号监听,主 goroutine 提前结束
主函数未注册 signal.Notify 或未保持主 goroutine 活跃,导致 main() 返回后进程终止,所有 defer 被跳过:
func main() {
defer cleanup() // ❌ 永远不会执行
http.ListenAndServe(":8080", nil) // 阻塞,但 Ctrl+C 会 panic 并调用 os.Exit(2)
}
✅ 正确做法:显式监听 SIGTERM/SIGINT,并用 sync.WaitGroup 或 <-done 确保主 goroutine 等待清理完成。
使用 os.Exit() 替代正常返回
在信号处理函数中直接调用 os.Exit(0),跳过所有 defer:
signal.Notify(c, syscall.SIGTERM)
go func() {
<-c
cleanup() // ✅ 执行
os.Exit(0) // ❌ defer 全部失效
}()
在非主 goroutine 中启动 cleanup,但无同步机制
cleanup 在子 goroutine 中异步执行,主 goroutine 未等待即退出:
go func() { defer cleanup() }() // ⚠️ 主 goroutine 继续执行并退出,该 defer 不生效
panic 后被 recover 但未重新 panic,导致 defer 链中断
recover 捕获 panic 后未再次 panic 或 return,使 defer 失去触发上下文。
使用 syscall.Kill() 或 exec.Command(“kill”) 发送信号后未等待
父进程发送 SIGTERM 后立即 os.Exit(),子进程尚未响应信号即被系统回收。
常见修复模式对比:
| 场景 | 错误行为 | 推荐方案 |
|---|---|---|
| 容器环境 | CMD ["./app"] + 无信号处理 |
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT); <-c; cleanup(); os.Exit(0) |
| HTTP 服务 | http.Server.Shutdown() 未配合 context.WithTimeout |
使用 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second); server.Shutdown(ctx); defer cancel() |
务必确保:主 goroutine 的生命周期 ≥ cleanup 执行周期,且所有退出路径均通过 return(触发 defer)而非 os.Exit()。
第二章:Go进程信号机制与defer执行时机的底层原理剖析
2.1 Go runtime对SIGTERM/SIGINT的默认捕获与转发机制
Go runtime 不主动捕获或处理 SIGTERM/SIGINT——信号默认交由操作系统传递,进程直接终止,无清理机会。
默认行为本质
- Go 程序启动时,runtime 仅屏蔽
SIGPIPE(避免 panic),其余信号(含SIGINT/SIGTERM)保持默认 disposition(terminate) - 无注册
signal.Notify时,信号直达 OS,触发exit(128 + signal),跳过defer、runtime.GC()和os.Exit()钩子
信号转发路径
// 示例:显式监听 SIGINT(非默认行为)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan // 阻塞等待信号
log.Println("Graceful shutdown initiated")
此代码覆盖默认行为:
signal.Notify将指定信号重定向至 channel,阻止其终止进程。syscall.SIGINT对应 Ctrl+C(值 2),syscall.SIGTERM为kill默认信号(值 15)。
关键差异对比
| 行为 | 未调用 signal.Notify |
调用 signal.Notify 后 |
|---|---|---|
| SIGINT/SIGTERM 响应 | 立即终止 | 送达 channel,可自定义处理 |
| defer 执行 | ❌ 不执行 | ✅ 可在信号处理后触发 |
| GC/资源释放 | 强制中断 | 允许同步完成 |
graph TD
A[OS 发送 SIGTERM] --> B{Go runtime 是否 Notify?}
B -- 否 --> C[OS 终止进程<br>跳过所有 Go 清理]
B -- 是 --> D[信号入 channel<br>程序自主调度处理]
D --> E[执行 defer<br>关闭连接/flush 日志等]
2.2 defer语句在main goroutine退出时的真实执行边界条件
defer 的触发时机本质
defer 并非在 main() 函数返回后才执行,而是在 main goroutine 的栈帧完全展开完毕、运行时准备销毁该 goroutine 前一刻执行——即 runtime.goexit() 被调用前的最后屏障。
关键边界:os.Exit() 的绕过行为
func main() {
defer fmt.Println("defer executed")
os.Exit(0) // ⚠️ 不会触发任何 defer!
}
os.Exit() 直接向操作系统发送信号终止进程,跳过所有 Go 运行时清理逻辑(包括 defer 队列遍历),这是唯一能无条件跳过 defer 执行的路径。
defer 执行的三大必要条件
- ✅
main()函数已返回(显式return或自然结束) - ✅
main goroutine尚未被 runtime 标记为“已完成” - ✅ 未调用
os.Exit()、syscall.Exit()等底层终止函数
| 条件 | 是否触发 defer | 原因 |
|---|---|---|
return 正常退出 |
是 | runtime 执行 defer 队列 |
panic() 未被捕获 |
是 | panic recovery 前必执行 |
os.Exit(1) |
否 | 绕过 runtime 清理流程 |
graph TD
A[main goroutine 开始执行] --> B{main 函数是否返回?}
B -->|是| C[启动 defer 队列逆序执行]
B -->|否| D[继续执行]
C --> E{是否调用 os.Exit?}
E -->|否| F[执行所有 defer]
E -->|是| G[进程立即终止 → defer 跳过]
2.3 os.Exit()、log.Fatal()等强制终止对defer链的破坏性实证分析
defer 的预期执行契约
Go 中 defer 语句注册的函数在当前函数返回前按后进先出(LIFO)顺序调用,构成“清理契约”。但该契约在进程级终止面前失效。
强制终止的三类破坏者
os.Exit(code):立即终止进程,跳过所有 deferlog.Fatal(args...):等价于log.Print(args...) + os.Exit(1)runtime.Goexit():仅终止当前 goroutine,仍执行本 goroutine 的 defer
实证代码对比
func demoDeferBreak() {
defer fmt.Println("defer A")
defer fmt.Println("defer B")
os.Exit(0) // ← 此处退出,A/B 均不打印
}
逻辑分析:
os.Exit(0)调用底层syscall.Exit,绕过 Go 运行时的 defer 栈遍历机制,直接向操作系统发送终止信号。参数code=0表示成功退出,但无 defer 执行机会。
| 终止方式 | 是否执行 defer | 是否触发 panic 恢复 | 适用场景 |
|---|---|---|---|
return |
✅ | ❌ | 正常流程退出 |
os.Exit() |
❌ | ❌ | 紧急退出,不清理资源 |
log.Fatal() |
❌ | ❌ | 日志记录后立即终止 |
graph TD
A[函数开始] --> B[注册 defer A]
B --> C[注册 defer B]
C --> D[调用 os.Exit]
D --> E[内核终止进程]
E --> F[defer 栈被完全跳过]
2.4 signal.Notify + select阻塞模型中goroutine生命周期管理误区
常见误用模式
开发者常将 signal.Notify 与无退出机制的 select{} 搭配,导致 goroutine 永驻内存:
func handleSignals() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
select {} // ❌ 永久阻塞,goroutine 无法回收
}
此处
select{}不含任何 case,是空 select,语义为永久阻塞。即使信号已送达sigs通道,也无对应case <-sigs接收逻辑,信号被丢弃,goroutine 泄漏。
正确生命周期控制
需显式监听通道并触发退出路径:
func handleSignals(done chan struct{}) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
select {
case <-sigs:
fmt.Println("received shutdown signal")
case <-done: // 支持外部协同关闭
fmt.Println("shutdown initiated externally")
}
}
done通道提供可控终止入口;signal.Notify的第二个参数指定信号集,syscall.SIGINT(Ctrl+C)与syscall.SIGTERM(kill 默认)为典型组合。
关键差异对比
| 场景 | 是否可回收 | 信号是否被消费 | 外部干预能力 |
|---|---|---|---|
select{} |
否 | 否 | 无 |
select + sigs/done |
是 | 是 | 支持 |
graph TD
A[启动 goroutine] --> B[注册信号到 channel]
B --> C{select 等待}
C --> D[接收信号 → 清理 → return]
C --> E[接收 done → 清理 → return]
2.5 主goroutine提前return与子goroutine残留导致cleanup丢失的竞态复现
当主 goroutine 在子 goroutine 完成前 return,资源清理逻辑极易被跳过。
复现场景代码
func riskyCleanup() {
done := make(chan struct{})
go func() {
defer close(done) // 模拟耗时IO或延迟释放
time.Sleep(100 * time.Millisecond)
fmt.Println("cleanup executed")
}()
// 主goroutine提前退出,无等待逻辑
return // ⚠️ cleanup可能永不执行
}
该函数启动子 goroutine 执行清理,但主 goroutine 立即 return,无同步机制(如 sync.WaitGroup 或 <-done),导致子 goroutine 成为“孤儿”,其 defer 清理动作无法保证执行。
关键风险点
- 主 goroutine 生命周期结束 ≠ 子 goroutine 自动终止
- Go 不提供跨 goroutine 的自动资源生命周期绑定
| 同步方式 | 是否阻塞主goroutine | 能否确保cleanup执行 |
|---|---|---|
time.Sleep() |
是 | ❌(仅粗略等待) |
<-done |
是 | ✅ |
wg.Wait() |
是 | ✅ |
graph TD
A[main goroutine start] --> B[start sub-goroutine]
B --> C[main returns immediately]
C --> D[main exits]
B --> E[sub runs cleanup]
E -.-> F[但main已退出,进程可能终止]
第三章:生产环境高频失效场景的代码诊断与验证方法
3.1 使用pprof+trace定位未执行defer的goroutine栈快照技术
当 goroutine 因 panic 或提前 exit 未执行 defer 时,其清理逻辑丢失,可能引发资源泄漏。pprof 的 goroutine profile 仅捕获阻塞态栈,而 runtime/trace 可记录全生命周期事件。
启用精细化 trace 捕获
import "runtime/trace"
func main() {
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// ...业务逻辑
}
trace.Start() 启动运行时事件采样(含 goroutine 创建/阻塞/结束),defer trace.Stop() 确保写入完成——但若此处 panic,trace 文件将截断。
关键诊断组合命令
| 工具 | 作用 |
|---|---|
go tool trace trace.out |
可视化 goroutine 执行轨迹 |
go tool pprof -goroutine trace.out |
提取所有 goroutine 栈(含已退出但未 defer 的) |
分析流程
graph TD
A[启动 trace] --> B[触发 panic]
B --> C[trace.Stop 未执行]
C --> D[用 go tool trace 加载不完整 trace.out]
D --> E[在 Goroutines 视图中筛选 'finished' 状态但无 defer 调用栈]
核心技巧:结合 runtime.Stack() 在 init() 中注册 runtime.SetFinalizer 或 recover 钩子,捕获异常退出前的栈快照。
3.2 基于systemd/journald日志时间戳反向推演SIGTERM处理延迟根因
日志时间精度与时钟域差异
journald 默认使用 CLOCK_MONOTONIC 记录日志事件,而应用内 clock_gettime(CLOCK_REALTIME) 获取的 SIGTERM 到达时间属于系统实时时钟域。二者存在纳秒级偏移,需校准。
关键日志提取命令
# 提取服务单元的SIGTERM及后续日志(含精确纳秒时间戳)
journalctl -u myapp.service -o json --since "2024-06-15 10:00:00" \
| jq -r 'select(.MESSAGE | test("Received SIGTERM")) | .__REALTIME_TIMESTAMP'
__REALTIME_TIMESTAMP是微秒级 Unix 时间戳(自纪元起),用于对齐外部监控系统;__MONOTONIC_TIMESTAMP仅适用于内部间隔计算。忽略时钟域差异将导致 ±10–200ms 误判。
反向推演路径
- 步骤1:定位
Received SIGTERM日志时间戳T₁ - 步骤2:查找
Stopping myapp.service或进程退出日志时间戳T₂ - 步骤3:计算差值
Δt = T₂ − T₁,即实际响应延迟
| 字段 | 含义 | 典型值 |
|---|---|---|
T₁ |
systemd 发送 SIGTERM 的精确时刻 | 1718445601234567 μs |
T₂ |
进程调用 exit() 或被 kill 的时刻 |
1718445601890123 μs |
Δt |
应用层 SIGTERM 处理耗时 | 655.556 ms |
graph TD
A[systemd send SIGTERM] --> B[journald 记录 T₁]
B --> C[应用 signal handler 执行]
C --> D[业务清理逻辑阻塞]
D --> E[调用 exit/return]
E --> F[journald 记录 T₂]
3.3 利用go test -race与自定义signal injector进行可重现压力测试
race检测器的精准启用
go test -race 启用Go内置竞态检测器,它在运行时插桩内存访问,记录goroutine ID与操作序号,冲突时输出调用栈。关键参数:
-race:启用竞态检测(仅支持go test和go run)-gcflags="-race":用于go build场景(需配合-ldflags="-race")
go test -race -count=10 -timeout=30s ./pkg/...
此命令执行10轮竞态敏感测试,每轮超时30秒;
-count确保多次运行以暴露偶发竞态,而非单次通过即认为安全。
可控信号注入器设计
为触发特定并发路径,需在临界点注入SIGUSR1信号中断goroutine调度:
// signal_injector.go
func InjectSignal(pid int, sig os.Signal) error {
return syscall.Kill(pid, sig)
}
syscall.Kill绕过Go信号处理层,直接向OS进程发送信号,确保调度器被强制抢占,使竞争窗口稳定复现。
测试流程协同机制
| 组件 | 作用 | 协同方式 |
|---|---|---|
go test -race |
捕获数据竞争事件 | 输出带goroutine ID的堆栈 |
| 自定义injector | 在runtime.Gosched()前注入信号 |
强制调度切换,放大竞态窗口 |
graph TD
A[启动测试] --> B[启动目标goroutine]
B --> C[Injector等待同步点]
C --> D[注入SIGUSR1]
D --> E[调度器抢占并切换]
E --> F[race detector捕获冲突]
第四章:五类典型反模式的重构实践与工程化防护方案
4.1 反模式一:全局变量缓存资源且无显式Close调用的重构路径
问题本质
全局变量持有 *sql.DB 或 *http.Client 等资源时,若未在生命周期结束时显式调用 Close(),将导致连接泄漏、句柄耗尽与 GC 无法回收底层 OS 资源。
典型错误示例
var db *sql.DB // 全局声明
func init() {
db, _ = sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/test")
// ❌ 缺失 db.Close() —— 程序退出前无释放
}
逻辑分析:
sql.Open仅初始化连接池配置,不建立物理连接;db作为全局变量长期存活,其内部连接池持续占用 TCP 连接与内存,且defer db.Close()在init()中无效。关键参数db.SetMaxOpenConns()无法动态调整,加剧资源僵化。
重构路径对比
| 方案 | 生命周期管理 | 可测试性 | 资源隔离性 |
|---|---|---|---|
| 全局单例 + 隐式关闭 | ❌ 无控制点 | ❌ 无法 mock | ❌ 全局污染 |
| 函数级依赖注入 | ✅ 显式 defer | ✅ 单元测试友好 | ✅ 按需创建 |
推荐实践
func NewDB(dsn string) (*sql.DB, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
// ✅ 显式设置并预留 Close 调用入口
db.SetMaxOpenConns(10)
return db, nil
}
逻辑分析:返回实例由调用方负责生命周期(如
defer db.Close()),配合context.WithTimeout可控超时释放,避免“一次分配、永久驻留”。
graph TD
A[服务启动] --> B[NewDB 创建实例]
B --> C[业务逻辑使用]
C --> D[defer db.Close()]
D --> E[连接池清理+OS句柄释放]
4.2 反模式二:HTTP Server graceful shutdown未绑定signal handler的补救策略
当 HTTP 服务未注册 SIGTERM/SIGINT 处理器时,进程会立即终止,导致活跃连接被强制中断。
核心修复:注册标准信号处理器
srv := &http.Server{Addr: ":8080", Handler: mux}
// 启动服务前绑定信号监听
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
go func() {
<-quit
log.Println("Shutting down server...")
if err := srv.Shutdown(context.Background()); err != nil {
log.Fatalf("Server shutdown error: %v", err)
}
}()
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Server failed: %v", err)
}
逻辑分析:
signal.Notify将SIGTERM/SIGINT转为 Go channel 事件;srv.Shutdown()阻塞等待活跃请求完成(默认无超时),需配合context.WithTimeout控制最大等待时间(如context.WithTimeout(context.Background(), 30*time.Second))。
关键参数对照表
| 参数 | 作用 | 推荐值 |
|---|---|---|
srv.ReadTimeout |
读请求超时 | 30s |
srv.WriteTimeout |
写响应超时 | 30s |
context.WithTimeout |
Shutdown 最大等待 | 30s |
生命周期流程
graph TD
A[收到 SIGTERM] --> B[触发 Shutdown]
B --> C{所有连接空闲?}
C -->|是| D[退出进程]
C -->|否| E[等待至 context 超时]
E --> F[强制关闭连接]
4.3 反模式三:第三方库异步启动goroutine绕过主流程控制的拦截与封装
当第三方库(如 redis-go 的 PubSub.Receive() 或 sarama.AsyncProducer)在内部隐式启动 goroutine 并直接向 channel 发送事件时,主调用方完全失去生命周期控制权。
数据同步机制失效风险
主流程无法等待其完成,亦无法注入 context 取消信号:
// 危险示例:库内部 goroutine 脱离主 context 管理
client := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
pubsub := client.PubSub()
pubsub.Subscribe(ctx, "topic") // ctx 仅用于订阅建立,不传递给接收循环
// ⚠️ Receive() 在后台 goroutine 中持续阻塞读取,无法响应 cancel
逻辑分析:
pubsub.Receive()启动独立 goroutine 监听消息,但未将传入ctx透传至该 goroutine 的 select 分支,导致ctx.Done()信号被忽略;参数ctx仅用于初始化握手阶段,无运行时约束力。
对比:可控封装方案
| 方案 | Context 传播 | 可等待性 | 可中断性 |
|---|---|---|---|
| 原生库异步接收 | ❌ | ❌ | ❌ |
封装为 ReceiveWithContext |
✅ | ✅ | ✅ |
graph TD
A[主 goroutine] -->|启动| B[第三方库 Receive]
B --> C[内部 goroutine]
C --> D[无 ctx.Done 检查]
A -.->|无法通知| D
4.4 反模式四:context.WithCancel未与signal context联动导致cleanup被跳过的修复范式
问题本质
当 context.WithCancel 独立创建而未监听 os.Interrupt 或 syscall.SIGTERM 时,进程收到信号后 cancel() 不被触发,defer 清理逻辑被跳过。
典型错误代码
func badCleanup() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 永不执行!
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
<-sig
// missing: cancel() call here → cleanup skipped
}
逻辑分析:cancel() 仅在函数退出时调用(因 defer),但信号阻塞在 <-sig,函数永不返回;defer 失效。ctx 也未参与 signal 传播。
修复范式:信号驱动取消
func fixedCleanup() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // now safe: triggered by signal handler
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
go func() {
<-sig
cancel() // ✅ explicit trigger
}()
select {
case <-time.After(5 * time.Second):
fmt.Println("done")
}
}
参数说明:signal.Notify 注册多信号;goroutine 解耦信号接收与 cancel 调用;select 使 ctx 可被取消感知。
关键对比
| 方案 | cancel 触发时机 | cleanup 是否执行 |
|---|---|---|
| 独立 WithCancel | 函数 return 时 | ❌ 跳过 |
| Signal 绑定 | 信号到达即刻调用 | ✅ 可靠执行 |
graph TD
A[收到 SIGINT] --> B[goroutine 唤起]
B --> C[显式调用 cancel()]
C --> D[ctx.Done() 关闭]
D --> E[所有 <-ctx.Done() 阻塞解除]
E --> F[defer/资源清理执行]
第五章:从SIGTERM到云原生优雅退出的演进思考
信号语义的变迁:从进程终止到生命周期契约
早期Linux服务收到SIGTERM仅执行exit(0),导致TCP连接被RST重置、数据库事务中断、Kafka消费者位点丢失。某电商订单服务曾因未捕获SIGTERM直接退出,在K8s滚动更新时造成0.3%订单状态不一致。现代云原生应用将SIGTERM视为“生命周期协商起点”,而非终止指令——需主动通知上游负载均衡器下线、完成正在处理的HTTP请求、提交未刷盘的日志缓冲区。
Kubernetes中优雅退出的三阶段实践
K8s通过terminationGracePeriodSeconds(默认30秒)与preStop钩子构建退出时间窗。某金融支付网关配置如下:
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "curl -X POST http://localhost:8080/shutdown && sleep 2"]
terminationGracePeriodSeconds: 60
该配置使Pod在SIGTERM后先触发内部停机API,再等待60秒确保gRPC长连接完成流式响应传输,实测将请求失败率从12%降至0.07%。
服务网格场景下的退出协同挑战
当Istio Sidecar注入后,应用容器与Envoy代理形成退出竞态。某微服务集群出现过Envoy已关闭监听端口但应用仍在处理请求的案例。解决方案采用双通道协调机制:
- 应用监听
SIGTERM后向Sidecar发送/healthz?readyz=false - Envoy检测到该探针失败后,立即停止新请求路由并启动连接 draining
- 应用等待Envoy返回
200 OK后再执行业务逻辑清理
| 组件 | 默认draining超时 | 实际观测值 | 关键依赖 |
|---|---|---|---|
| Envoy | 5s | 8.2s | envoy.reloadable_features.enable_drain_listeners |
| Spring Boot | 30s | 42.6s | server.shutdown.grace-period + spring.lifecycle.timeout-per-shutdown-phase |
| Kafka Consumer | 10s | 15.3s | max.poll.interval.ms + session.timeout.ms |
分布式事务的退出边界控制
某跨行转账服务采用Saga模式,在SIGTERM处理中引入退出检查点:
- 检查当前Saga步骤是否处于补偿安全点(如已完成本地扣款且消息已持久化至RocketMQ)
- 若处于非安全点,则强制进入补偿流程并阻塞退出
- 使用Redis分布式锁保证单实例退出时的补偿原子性
该机制使滚动更新期间Saga事务失败率从4.8%降至0.002%。
云原生退出协议的标准化演进
CNCF SIG Apps正推动Container Lifecycle Protocol (CLP)草案,定义退出握手序列:
sequenceDiagram
participant K as Kubernetes API Server
participant P as Pod Container
participant S as Service Mesh Proxy
K->>P: SIGTERM + terminationGracePeriodSeconds=60
P->>S: POST /v1/lifecycle/draining
S->>P: 200 OK + drain_duration=35s
P->>P: execute business cleanup
P->>K: Ready=False (via readiness probe)
Note right of P: Wait for S to complete connection draining
P->>P: exit 0
监控指标驱动的退出优化闭环
生产环境采集container_termination_grace_period_seconds与container_exit_code关联分析,发现某批Java服务因-XX:+UseG1GC导致GC暂停时间超过grace period。通过动态调整JVM参数:
# 根据退出前GC日志自动优化
jstat -gc $PID | awk '{print $6,$7}' | \
while read young old; do
[[ $young -gt 200 ]] && echo "G1MaxNewSizePercent=40" >> /etc/jvm.conf
done 