停止/重新部署时 Tomcat 7+ 内存泄漏。弹簧数据,JPA,休眠,MySQL

Posted

技术标签:

【中文标题】停止/重新部署时 Tomcat 7+ 内存泄漏。弹簧数据,JPA,休眠,MySQL【英文标题】:Tomcat 7+ memory leak on stop/redeploy. Spring Data, JPA, Hibernate, MySQL 【发布时间】:2017-12-16 17:32:10 【问题描述】:

我在停止/重新部署应用程序时遇到了 tomcat 内存泄漏问题。它说 以下 Web 应用程序已停止(重新加载、取消部署),但它们的 以前运行的类仍然加载到内存中,从而导致内存 泄漏(使用分析器确认):/test-1.0-SNAPSHOT

位于 Tomcat/lib 文件夹中的 mysql 连接器驱动程序。 我可以在两者中重现此问题:Tomcat 7/8。还尝试了带有“net.sourceforge.jtds.*”驱动程序的 MS SQL 数据库,但没有帮助。

请在下面找到项目文件。项目仅在 DB 中创建 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

有人有解决此问题的想法或建议吗?

【问题讨论】:

此链接可能会有所帮助***.com/questions/40040289/… 对于初学者来说,停止覆盖 onStartupcreateRootApplicationContext 您正在创建一个几乎无用的附加上下文,并且还删除了该字段。你正试图变得聪明,这会扰乱生命周期。我还建议删除不需要的 setDriverClassName 行,因为 JDBC 几乎能够根据 URL 找出驱动程序。 感谢您的回复。 @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" GitHub 存储库,来源为 github.com/egotovko/tomcat-leak 鉴于上面的 git,我在本地机器上停止 tomcat 时没有发现内存泄漏。你能详细说明一下吗?你做了什么? 【参考方案1】:

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

    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 example 中修改监听器。

    第二个问题是已知的 jboss-logging 库中的内存泄漏 (link)。

在我们从休眠依赖中排除这个库后,内存泄漏已经消失了:

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'

然后从repo 构建jar 并添加到tomcat /lib 文件夹中。

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

【讨论】:

【参考方案2】:

简短的回答 - 希望你也遇到同样的问题......

这两个 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)

以下answer 使我能够解决问题。例如。在tomcat/conf/server.xml 中,查找JreMemoryLeakPreventionListener 行并将其替换为:

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" 
    classesToInitialize="com.mysql.jdbc.Driver" />

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


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

希望以上内容就是您所需要的 - 足以重现和解决针对 https://github.com/egotovko/tomcat-leak 的问题

但是,还有许多其他原因会导致对 Web 应用程序的引用泄露,从而阻止其取消部署。例如。其他仍在运行的线程(Tomcat 擅长警告这些)或来自 Web 应用程序外部的引用。

要正确追踪原因,您可以在堆转储中追踪引用。如果对此不熟悉,您可以从jmap -dump:file=dump.hprof &lt;pid&gt; 获取堆转储,或者直接从jvisualvm 等连接(也包含在JDK 中)。

jvisualvm 中打开堆转储:

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

注意AbandonedConnectionCleanupThread 有一个contextClassLoader,它是Web 应用程序的ParallelWebappClassLoader。 Tomcat 需要能够释放类加载器才能取消部署 Web 应用程序。

一旦您找到了保存引用的内容,通常就是调查如何更好地在 Tomcat 中配置该库,或者其他人可能已经看到了内存泄漏。当有几个参考要清理时,必须重复练习也很常见。

【讨论】:

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

以上是关于停止/重新部署时 Tomcat 7+ 内存泄漏。弹簧数据,JPA,休眠,MySQL的主要内容,如果未能解决你的问题,请参考以下文章

内存泄漏 - Tomcat、Spring MVC

在 Tomcat 中重新部署应用程序时发生内存泄漏

Tomcat如何检测内存泄漏

重新部署时 Google Cloud Pub Sub 内存泄漏(基于 Netty)

Tomcat 停止线程以避免潜在的内存泄漏

Tomcat 8 内存泄漏