1.引言
1.1背景问题
一个spring boo应用程序每天定时执行一次。
启动时,内存占用800M,结算后占用6G。
执行后,堆内存实际使用1G,但分配的内存达6G。
调整以下JVM参数,只能降到3G,不确定是否延长了处理时间。
-XX:MaxHeapFreeRatio=10
-XX:MinHeapFreeRatio=10
目标是使程序执行后,能够恢复到接近启动时的内存消耗规模。
通过应用程序和参数调整,没有办法实现目标。
只能重启程序。
1.2概念
程序重启通过是否产生新进程开区分2种方式。
热启动:无新进程产生
冷启动:有新进程产生,原进程结束,新进程启动相同程序的进程,包括相同的参数。
冷启动激发分为内部,外部:
。外部重启:通过操作系统,或者资源管理系统执行cron调度策略
程序执行完毕后即退出,由外部系统按照调度策略激活
。应用程序自启:
有直接和间接两种方式。间接是通过中间程序重启,直接则是在原进程结束时重启自身。
直接方式需要在原程序释放资源完毕后执行,避免可能的端口冲突。
重启进程的命令,参数要与原进程一致,并能在运行时获取。
外部重启可以简单地满足需要。 这里仅从技术实现角度探究其它重启Spring boot程序方式:热启动,冷启动-应用程序自启。
1.3热启动
Spring Boot Actuator:
。在程序结束时调用RestartEndpoint.restart,可以重启,但内存并没有降下来
。另起线程执行RestartEndpoint.restart:未验证 。
REST API方式是一种外部调用方式,内部也是RestartEndpoint实现的. 程序内部没有必要使用
Spring DevTools:
。Spring DevTools有重置应用的能力,只能在开发中使用。但其思想应该可以参考。
Spring DevTools 介绍见https://www.jianshu.com/p/b2d4f83aa777
1.4冷启动
。构造启动命令:优雅的方式,避免平台差异和硬编码
。启动时机:在程序任务成功完成后,资源释放后。 利用Runtime.addShutdownHook。
。委托方式:通过中间程序启动
2.热启动
1.Spring Boot Actuator
Spring Boot Actuator的RestartEndpoint 见参考资料[1].
RestartService.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.restart.RestartEndpoint;
import org.springframework.stereotype.Service;
@Service
public class RestartService {
@Autowired
private RestartEndpoint restartEndpoint;
public void restartApp() {
restartEndpoint.restart();
}
}
调用: @Autowired private RestartService restartService;
@Override
public void run(String... var) {
/// 需要重启时调用
restartService.restartApp();
}
application.ymal management: endpoint: restart: enabled: true
pom.xml增加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-cloud-starter</artifactId>
<version></version>
</dependency>
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
下文是另起线程执行RestartEndpoint的restart的例子。
Call Spring actuator /restart endpoint from Spring boot using a java function
@Autowired
private RestartEndpoint restartEndpoint;
...
Thread restartThread = new Thread(() -> restartEndpoint.restart());
restartThread.setDaemon(false);
restartThread.start();
文中说会有异常,提示程序启动了一个线程但未终止,可能存在内存泄漏。
另起线程有必要吗?
非要另起线程,如何消除文中所说的异常? ---创建的线程怎么会没有结束呢?
RestartEndpoint的invoke和restart区别? ----invoke是提问者使用的方式.
Thread默认是用户线程还是deamon线程? ----跟父线程相同?
为什么restartThread.setDaemon(false)?
Spring Boot programmatically restart app
1.added dependecies:
compile("org.springframework.boot:spring-boot-starter-actuator")
compile("org.springframework.cloud:spring-cloud-starter:1.2.4.RELEASE")
2.autowired restartEndpoint:
import org.springframework.cloud.context.restart.RestartEndpoint;
....
@Autowired
private RestartEndpoint restartEndpoint;
1. and invoke:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
restartEndpoint.invoke();
}
});
thread.setDaemon(false);
thread.start();
I need new Thread because spring mvc creates daemon threads for each request
访问的场景: 。这里是在处理web请求 。请求处理线程是deamon的,不能在这个线程上执行restart吗? 。spring mvc为每个请求创建deamon线程? ---不是线程池吗? 。这个怎么用的是restartEndpoint.invoke,而不是restart呢?
2.2 REST API
Spring Boot programmatically restart app
You need to add spring-boot-starter-actuator and Spring cloud dependencies to your application and use /restart endpoint to restart the app. Here is the documentation:
For a Spring Boot Actuator application there are some additional management endpoints:
POST to /env to update the Environment and rebind @ConfigurationProperties and log levels
/refresh for re-loading the boot strap context and refreshing the @RefreshScope beans
/restart for closing the ApplicationContext and restarting it (disabled by default)
/pause and /resume for calling the Lifecycle methods (stop() and start() on the ApplicationContext)
Once done, you can use a REST API call to restart the application (via RestTemplate or curl). This will be cleaner way to restart the app than killing it and re-running it.
3.冷启动
3.1示例
参考资料[2]代码:
在结束程序的位置调用:
Application.restartApplication(()->{});
测试结果如下:
- 原进程结束,有新进程产生,并且占用了端口,但没有控制台输出(在console和IDE下都看不到新进程的存在),Windows资源管理器中也没有该进程ID的进程
- 启动Notepad.exe,窗口打开,正常
没有控制台是因为
ProcessBuilder.start和Runtime.exec方法创建一个本机进程,并返回 Process 子类的一个实例。
创建的子进程没有自己的终端或控制台。
对于windows平台,可以采用cmd /c start运行方式使子进程有自己的控制台。
Run java process in windows console from another java process
ProcessBuilder pb = new ProcessBuilder("cmd", "/k", "java", "-jar", "AnotherApp.jar");
pb.start();
// 修改为
ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "start", "java", "-jar", "AnotherApp.jar");
String cmd[]={"cmd", "/c", "start", "java", "-jar", "AnotherApp.jar"};
Runtime rt=Runtime.getRuntime();
Process p=rt.exec(cmd);
Linux下呢?
---应用关心平台差异不是好事情,为什么java不提供满足此需要的api呢?
**没有必要为了本文初衷去选择这种方案。仅作为技术研究活动进行。 **
3.2委托外部程序
程序结束前,启动另外一个程序,由该程序负责在原程序结束后重新启动。
参考代码: JAVA重启自身程序
这种方式也有参考资料[2]代码同样的问题。
冷启动方式有以下问题:
- 启动的进程没有控制台,输出日志如何处理
- IDE支持:IDE环境下重启后的情况
- 跨平台:避免特定平台的代码
所以,冷启动没有理想的方式。 网上的参考代码都是什么应用场景呢?
4参考资料
[1]Programmatically Restarting a Spring Boot Application
https://www.baeldung.com/java-restart-spring-boot-app
[2]Programmatically Restart a Java Application
https://dzone.com/articles/programmatically-restart-java
[3]Spring Boot programmatically restart app
https://stackoverflow.com/questions/45074443/spring-boot-programmatically-restart-app
[4]Understanding Java Process and Java ProcessBuilder
https://www.developer.com/java/data/understanding-java-process-and-java-processbuilder.html
/**
* Sun property pointing the main class and its arguments.
* Might not be defined on non Hotspot VM implementations.
*/
public static final String SUN_JAVA_COMMAND = "sun.java.command";
/**
* Restart the current Java application
*
* @param runBeforeRestart some custom code to be run before restarting
* @throws IOException
*/
public static void restartApplication(Runnable runBeforeRestart) throws IOException {
try {
// java binary
String java = System.getProperty("java.home") + "/bin/java";
// vm arguments
List<String> vmArguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
StringBuffer vmArgsOneLine = new StringBuffer();
for (String arg : vmArguments) {
// if it's the agent argument : we ignore it otherwise the
// address of the old application and the new one will be in conflict
if (!arg.contains("-agentlib")) {
vmArgsOneLine.append(arg);
vmArgsOneLine.append(" ");
}
}
// init the command to execute, add the vm args
final StringBuffer cmd = new StringBuffer("\"" + java + "\" " + vmArgsOneLine);
// program main and program arguments
String[] mainCommand = System.getProperty(SUN_JAVA_COMMAND).split(" ");
// program main is a jar
if (mainCommand[0].endsWith(".jar")) {
// if it's a jar, add -jar mainJar
cmd.append("-jar " + new File(mainCommand[0]).getPath());
} else {
// else it's a .class, add the classpath and mainClass
cmd.append("-cp \"" + System.getProperty("java.class.path") + "\" " + mainCommand[0]);
}
// finally add program arguments
for (int i = 1; i < mainCommand.length; i++) {
cmd.append(" ");
cmd.append(mainCommand[i]);
}
// execute the command in a shutdown hook, to be sure that all the
// resources have been disposed before restarting the application
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
Runtime.getRuntime().exec(cmd.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
});
// execute some custom code before restarting
if (runBeforeRestart != null) {
runBeforeRestart.run();
}
// exit
System.exit(0);
} catch (Exception e) {
// something went wrong
throw new IOException("Error while trying to restart the application", e);
}
}
使用ProcessBuilder
public void restartApplication()
{
final String javaBin = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
final File currentJar = new File(MyClassInTheJar.class.getProtectionDomain().getCodeSource().getLocation().toURI());
/* is it a jar file? */
if(!currentJar.getName().endsWith(".jar"))
return;
/* Build command: java -jar application.jar */
final ArrayList<String> command = new ArrayList<String>();
command.add(javaBin);
command.add("-jar");
command.add(currentJar.getPath());
final ProcessBuilder builder = new ProcessBuilder(command);
builder.start();
System.exit(0);
}
获取jar文件路径
URL url = Application.class.getProtectionDomain().getCodeSource().getLocation();
System.out.println(url);
在IDE环境下执行(未打包成jar)
输出: file:/E:/workspace/ybt/ybt-clearing/target/classes/
如果打包后运行,则会有包名内容。
这种方式有限制,未考虑:命令参数,不支持IDE环境运行。 没有参考资料[2]考虑的全面。