Tomcat 7+在停止/重新部署时存在内存泄漏问题。Spring Data,JPA,Hibernate,MySQL

10

当停止/重新部署应用程序时,我遇到了Tomcat内存泄漏问题。它说:以下Web应用程序已停止(重新加载,未部署),但是它们之前运行时加载的类仍然存在于内存中,从而导致内存泄漏(请使用分析工具进行确认):/test-1.0-SNAPSHOT。

MySQL连接器驱动程序位于Tomcat/lib文件夹中。我可以在Tomcat 7/8两个版本中复现此问题。还尝试了使用"net.sourceforge.jtds.*"驱动程序进行MS SQL数据库操作,但没有帮助。

请查找以下项目文件。该项目仅在数据库中创建1个表格。

build.gradle

group 'com.test'
version '1.0-SNAPSHOT'

apply plugin: 'java'
apply plugin: 'war'
sourceCompatibility = 1.8
repositories {
    mavenCentral()
}
dependencies {
    compile group: 'org.hibernate', name: 'hibernate-entitymanager', version: '5.2.10.Final'
    compile group: 'org.springframework.data', name: 'spring-data-jpa', version: '1.11.4.RELEASE'
    compile group: 'org.springframework', name: 'spring-webmvc', version: '4.3.9.RELEASE'
    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
    providedCompile group: 'mysql', name: 'mysql-connector-java', version: '5.1.6'
    compile group: 'commons-dbcp', name: 'commons-dbcp', version: '1.4'
}

ApplicationConfig.java

@Configuration
@Import({JPAConfiguration.class})
@EnableWebMvc
public class ApplicationConfig {}

JPAConfiguration.java

@Configuration
@EnableJpaRepositories("com.test.dao")
@EnableTransactionManagement
public class JPAConfiguration {

    @Bean
    public EntityManagerFactory entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        factory.setPackagesToScan("com.test.model");
        factory.setDataSource(restDataSource());
        factory.setJpaPropertyMap(getPropertyMap());
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    @Bean(destroyMethod = "close")
    public DataSource restDataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/test");
        dataSource.setUsername("test");
        dataSource.setPassword("test");
        return dataSource;
    }

    private Map<String, String> getPropertyMap() {
        Map<String, String> hibernateProperties = new HashMap<>();
        hibernateProperties.put("hibernate.hbm2ddl.auto", "update");
        hibernateProperties.put("hibernate.show_sql", "true");
        hibernateProperties.put("hibernate.format_sql", "true");
        hibernateProperties.put("hibernate.dialect", "org.hibernate.dialect.MySQL5InnoDBDialect");
        return hibernateProperties;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory());
        return txManager;
    }

}

TestRepository.java

@Repository
public interface TestRepository extends JpaRepository<TestEntity, Long> {}

TestEntity.java

@Entity
@Table(name = "ent")
public class TestEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String descript;
    //equals, hashcode, toString, getters, setters
}

AppInitializer.java

public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    private WebApplicationContext rootContext;

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{ApplicationConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return null;
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

}

命令

jmap -histo <tomcat_pid>

在Tomcat停止后,项目结构只显示了2个项:

com.test.config.dao.JPAConfiguration$$EnhancerBySpringCGLIB$$792cb231$$FastClassBySpringCGLIB$$45ff499c
com.test.config.dao.JPAConfiguration$$FastClassBySpringCGLIB$$10104c1e

有人对解决这个问题有什么想法或建议吗?


这个链接可能会有帮助 https://stackoverflow.com/questions/40040289/java-spring-application-has-memory-leak-system-non-heap-memory-increases-consta - hrdkisback
首先停止覆盖onStartupcreateRootApplicationContext,这样你会创建一个几乎没有用处的额外上下文并删除该字段。你试图变得太聪明了,而且这会干扰生命周期。我还建议删除不需要的setDriverClassName行,因为JDBC可以根据URL基本上找出驱动程序。 - M. Deinum
感谢回复。@M.Deinum,我删除了onStartupcreateRootApplicationContext方法,但没有帮助。Tomcat仍然显示有关内存泄漏的消息。尝试删除setDriverClassName但出现异常org.hibernate.engine.jdbc.spi.SqlExceptionHelper.logExceptions Cannot create JDBC driver of class "" for connect URL "jdbc:mysql://localhost:3306/test" - Eugene Gotovko
GitHub代码库,包含源代码 https://github.com/egotovko/tomcat-leak - Eugene Gotovko
@MạnhQuyếtNguyễn,1)使用提供的源代码创建一个war文件。2)然后在Tomcat管理器http://localhost:8080/manager/html中部署这个war文件。3)然后点击“Undeploy”来卸载已部署的应用程序。4)在Tomcat管理器中点击“Find leaks”。您将看到我在描述中提供的消息。完成这些步骤后,您可以在tomcat进程的线程转储中看到来自此应用程序的类。 - Eugene Gotovko
显示剩余2条评论
2个回答

6

这个小项目中有2个内存泄漏问题:

  1. MySQL jdbc驱动程序存在问题。

我们需要添加 ContextLoaderListener 来注销 jdbc 驱动程序:

监听器:

@WebListener
public class ContextListener extends ContextLoaderListener {

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        log.info("-= Context started =-");

    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        super.contextDestroyed(sce);
        log.info("-= Context destroyed =-");
        try {
            log.info("Calling MySQL AbandonedConnectionCleanupThread checkedShutdown");
            com.mysql.cj.jdbc.AbandonedConnectionCleanupThread.uncheckedShutdown();

        } catch (Exception e) {
            log.error("Error calling MySQL AbandonedConnectionCleanupThread checkedShutdown {}", e);
        }

        ClassLoader cl = Thread.currentThread().getContextClassLoader();

        Enumeration<Driver> drivers = DriverManager.getDrivers();
        while (drivers.hasMoreElements()) {
            Driver driver = drivers.nextElement();

            if (driver.getClass().getClassLoader() == cl) {

                try {
                    log.info("Deregistering JDBC driver {}", driver);
                    DriverManager.deregisterDriver(driver);

                } catch (SQLException ex) {
                    log.error("Error deregistering JDBC driver {}", driver, ex);
                }

            } else {
                log.info("Not deregistering JDBC driver {} as it does not belong to this webapp's ClassLoader", driver);
            }
        }
    }
}

如果您可以访问tomcat服务器,您可以修改tomcat/conf/server.xml 示例中的监听器。

  1. 第二个问题是在jboss-logging库中已知的内存泄漏(链接)。

我们从hibernate依赖项中排除该库后,内存泄漏问题已经解决:

build.gradle:

group 'com.test'
version '1.0-SNAPSHOT'

apply plugin: 'java'
apply plugin: 'war'
sourceCompatibility = 1.8
repositories {
    mavenCentral()
}
dependencies {
    compile(group: 'org.hibernate', name: 'hibernate-entitymanager', version: '5.2.10.Final') {
        exclude group: 'org.jboss.logging', module: 'jboss-logging'
    }

    compile group: 'org.springframework.data', name: 'spring-data-jpa', version: '1.11.4.RELEASE'

    compile group: 'org.springframework', name: 'spring-webmvc', version: '4.3.9.RELEASE'

    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
    providedCompile group: 'mysql', name: 'mysql-connector-java', version: '8.0.11'
    compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25'
    compile group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.25'
}

存储库构建jar文件,然后将其添加到tomcat的/lib文件夹中。

jboss-logging的问题在Java 9中可能已经被修复(pull request链接)。


5
希望这对你来说是同一个问题...
这两个 `com.test.config.dao.JPAConfiguration$$...CGLIB$$...` 类是由 MySQL 中的 `Abandoned connection cleanup thread` 间接引用的。
20-Jun-2018 21:25:22.987 WARNING [localhost-startStop-1] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [test-1.0-SNAPSHOT] appears to have started a thread named [Abandoned connection cleanup thread] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
 java.lang.Object.wait(Native Method)
 java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
 com.mysql.cj.jdbc.AbandonedConnectionCleanupThread.run(AbandonedConnectionCleanupThread.java:43)

以下答案帮助我解决了问题。例如,在tomcat/conf/server.xml中,查找JreMemoryLeakPreventionListener行,并将其替换为以下内容:
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" 
    classesToInitialize="com.mysql.jdbc.Driver" />

这将强制MySQL JDBC驱动程序及其清理线程在Web应用程序的类加载器之外加载。这意味着清理线程将不会将Web应用程序的类加载器作为其上下文类加载器保留引用。

扩展回答 - 如何追踪您的环境中的泄漏...

希望以上内容对您足够 - 它足以在https://github.com/egotovko/tomcat-leak上重现和解决问题。

然而,还有许多其他导致 Web 应用程序泄漏引用并阻止其卸载的原因。例如,其他线程仍在运行(Tomcat 在这方面很擅长发出警告),或者来自 Web 应用程序外部的引用。

要正确追踪原因,您可以在堆转储中跟踪引用。如果这不熟悉,您可以通过jmap -dump:file=dump.hprof <pid>从中获取堆转储,或通过直接连接到如jvisualvm(也包含在 JDK 中)的工具获取。

使用jvisualvm打开堆转储:

  • 选择堆转储的 Classes 按钮
  • 按名称对类列表进行排序
  • 查找 Web 应用程序中的类 - 例如,在此示例中 com.test.config.dao.JPAConfiguration$$EnhancerBySpringCGLIB$$
  • 这应该显示为实例计数为2左右
  • 双击以在 Instances View 中显示这些内容
  • 在这些实例的 References 窗格中,右键单击并选择 Show Nearest GC Root
  • 例如,在 MySQL 中的那个 Abandoned connection cleanup threadReferencing thread in JVisualVM

请注意,AbandonedConnectionCleanupThread 具有一个 contextClassLoader,它是 Web 应用程序的 ParallelWebappClassLoader。Tomcat 需要能够释放类加载器以卸载 Web 应用程序。

一旦您找到了引用的原因,通常情况下需要调查如何更好地在Tomcat中配置该库,或者可能有其他人已经发现了内存泄漏问题。清理多个引用时,重复这个过程也很常见。

谢谢。使用JreMemoryLeakPreventionListener进行更改可以修复与AbandonedConnectionCleanupThread相关的问题,并允许我们将mysql-connector-java依赖项保留在“提供”的范围内。正如下一个答案中提到的,此应用程序中存在与hibernate-entitymanager依赖项相关的org.jboss.logging泄漏的第二个问题。 - Eugene Gotovko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接