Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Metaspace OOM after repeated hot deployment of Spring Boot war file in Tomcat #35630

Closed
sywong70g opened this issue May 25, 2023 · 5 comments
Closed
Labels
for: external-project For an external project and not something we can fix

Comments

@sywong70g
Copy link

sywong70g commented May 25, 2023

Environment:

a. OS : Ubuntu 22.04.3
b. JVM : Oracle Java 11.0.18 and Oracle java 17.0.17 (tested both)
c. Tomcat : 9.0.75
d. SpringBoot : 2.7.5
e. Oracle : version 19c
f. Oracle jdbc : com.oracle.database.jdbc:ojdbc11:23.2.0.0

Source code:

Policy.java:

Getter
@Setter
@Entity
@Table(name = "policy_m")
public class Policy {

   @Column(name = "policy_agreement_id")
   private Long policyAgreementId;
   @Id
   @Column(name = "policy_num")
   private String policyNum;
  
}

PolicyRepository.java:

@Repository
public interface PolicyRepository extends JpaRepository<Policy, String> {

   @Query(value = "select * from policy_m where policy_num = :policyNum", nativeQuery = true)
   Policy getPolicyByPolicyNum(@Param("policyNum") String policyNum);

}

PolicyServiceImpl.java:

@Service
public class PolicyServiceImpl implements PolicyService {

   @Autowired
   private PolicyRepository policyRepository;

   public Policy getPolicyByPolicyNum(String policyNum) {
       return policyRepository.getPolicyByPolicyNum(policyNum);
   }
}

DemoController.java:

@RestController
public class DemoController {

   @Autowired
   private PolicyService policyService;

   @GetMapping("/{policyNum}")
   public Policy database(@PathVariable("policyNum") String policyNum) {
       return policyService.getPolicyByPolicyNum(policyNum);
   }
}

Steps to reproduce:

  1. prepare the war file
  2. deploy to the tomcat through the admin console
  3. undeploy the application
  4. repeat step 2 - 3 until it throws MetaSpace OOM

The VisualVM shows the chart:

mybatis_java11_metaspace

At the plateau at the right hand side, the tomcat actually already hanged.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label May 25, 2023
@wilkinsona
Copy link
Member

Tomcat's pretty good at identifying possible memory leaks and logging about them. Is there anything in Tomcat's logs when you undeploy the app? If there isn't (and perhaps even if there is) I think we'll need a sample that reproduces the problem, ideally one that doesn't require an Oracle DB.

@wilkinsona wilkinsona added the status: waiting-for-feedback We need additional information before we can continue label May 25, 2023
@sywong70g
Copy link
Author

I just tested with a helloWorld application without DB connection and actually there is no such issue. I am wondering whether it is about the connection pool. Also I am testing on MySQL with JPA and see if the same issue occurs. If yes, I will send you the sample.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 25, 2023
@wilkinsona wilkinsona added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels May 25, 2023
@quaff
Copy link
Contributor

quaff commented May 26, 2023

It's very likely bug of jdbc driver, you can switch to another database and test.

Also you can try to extend SpringBootServletInitializer and override deregisterJdbcDrivers

	@Override
	protected void deregisterJdbcDrivers(ServletContext servletContext) {
		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		try {
			String className = "com.mysql.cj.jdbc.AbandonedConnectionCleanupThread";
			String methodName = "checkedShutdown";
			if (ClassUtils.isPresent(className, cl)) {
				ClassUtils.forName(className, cl).getMethod(methodName).invoke(null);
			}
		}
		catch (Throwable ex) {
			ex.printStackTrace();
		}
		try {
			String className = "com.mysql.jdbc.AbandonedConnectionCleanupThread";
			String methodName = "checkedShutdown";
			if (ClassUtils.isPresent(className, cl)) {
				ClassUtils.forName(className, cl).getMethod(methodName).invoke(null);
			}
		}
		catch (Throwable ex) {
			ex.printStackTrace();
		}
		super.deregisterJdbcDrivers(servletContext);
		cancelTimers();
		cleanupThreadLocals();
	}

	protected void cancelTimers() {
		try {
			for (Thread thread : Thread.getAllStackTraces().keySet()) {
				if (thread.getClass().getSimpleName().equals("TimerThread")) {
					cancelTimer(thread);
				}
			}
		}
		catch (Throwable ex) {
			ex.printStackTrace();
		}
	}

	private void cancelTimer(Thread thread) throws Exception {
		Object queue = ReflectionUtils.getFieldValue(thread, "queue");
		Method m = queue.getClass().getDeclaredMethod("isEmpty");
		m.setAccessible(true);
		if ((boolean) m.invoke(queue)) {
			// Timer::cancel
			synchronized (queue) {
				ReflectionUtils.setFieldValue(thread, "newTasksMayBeScheduled", false);
				m = queue.getClass().getDeclaredMethod("clear");
				m.setAccessible(true);
				m.invoke(queue);
				queue.notify();
			}
		}
	}

	protected void cleanupThreadLocals() {
		try {
			for (Thread thread : Thread.getAllStackTraces().keySet()) {
				cleanupThreadLocals(thread);
			}
		}
		catch (Throwable ex) {
			ex.printStackTrace();
		}
	}

	private void cleanupThreadLocals(Thread thread) throws Exception {
		if ("JettyShutdownThread".equals(thread.getName())) {
			return; // see https://github.com/eclipse/jetty.project/issues/5782
		}
		for (String name : "threadLocals,inheritableThreadLocals".split(",")) {
			Field f = Thread.class.getDeclaredField(name);
			f.setAccessible(true);
			f.set(thread, null);
		}
	}

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 26, 2023
@wilkinsona wilkinsona added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels May 26, 2023
@sywong70g
Copy link
Author

@quaff you may be right, I use the same source code but connect to MySQL instead of Oracle, there is no such issue.

Thank you for your suggestion, we are testing your code and see if we can solve the issue.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 30, 2023
@wilkinsona wilkinsona added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels May 30, 2023
@philwebb
Copy link
Member

philwebb commented May 31, 2023

Thanks for your help @quaff. I'll close this one for now, but if it turns out to be something in Spring Boot rather than the Oracle driver we can reopen it.

@philwebb philwebb added for: external-project For an external project and not something we can fix and removed status: waiting-for-feedback We need additional information before we can continue status: waiting-for-triage An issue we've not yet triaged labels May 31, 2023
@philwebb philwebb closed this as not planned Won't fix, can't repro, duplicate, stale May 31, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
for: external-project For an external project and not something we can fix
Projects
None yet
Development

No branches or pull requests

5 participants