2010-10-05 116 views
2

我有一个Java应用程序,除其他外,每小时发送到我们的Active Directory服务器,并拉下所有帐户的列表,并将它们转储到数据库中;这项工作是通过一个每小时产生一个新的线程完成的,数据库接口是通过Hibernate完成的。该线程的run方法(基本上是这个线程做的唯一的事情)看起来像这样:休眠内存堆错误

public void run() { 
    try { 
     Thread.sleep(3600000); //we run once an hour, so we sleep for an hour 
     Thread newHourlyRunThread = new Thread(new HourlyRunThread()); 
     newHourlyRunThread.start(); 
     LDAPNewUsersReport report = new LDAPNewUsersReport(); 
     Calendar calendar = Calendar.getInstance(); 
     calendar.set(0, 0, 0, 0, 0); //We tell the report to look for everything from 12AM Jan 1 0 AD, which should be sufficient to find all created AD objects. 
     report.runReport(calendar.getTime(), new Date()); 
     HashSet<LDAPEntry> allEntries = report.getAllEntries(); 
     Iterator it = allEntries.iterator(); 
     while (it.hasNext()) { 
      ContactParser.parseContact((LDAPEntry) it.next()); 
     } 
} 

从ContactParser相关的方法如下:

public static void parseContact(LDAPEntry entry) { 
    Contact chosenContact = null; 
    Session session = HibernateUtil.getSessionFactory().getCurrentSession(); 
    session.beginTransaction(); 

    List contacts = session.getNamedQuery("ContactByCanonicalName").setString(0, entry.getDN()).list(); 
    Iterator it = contacts.iterator(); 
    if (it.hasNext()) { 
     chosenContact = (Contact) it.next(); 
     chosenContact = ContactParser.fillContactFields(chosenContact, entry); 
    } else { 
     chosenContact = ContactParser.fillContactFields(new Contact(), entry); 
    } 
    session.saveOrUpdate(chosenContact); 
    session.getTransaction().commit(); 
} 

private static Contact fillContactFields(Contact chosenContact, LDAPEntry entry) { 
    chosenContact.setCanonicalName(entry.getDN()); 
    chosenContact.setFirstName(ContactParser.getEntryField(entry, "givenName")); 
    chosenContact.setLastName(ContactParser.getEntryField(entry, "sn")); 
    chosenContact.setUserName(ContactParser.getEntryField(entry, "sAMAccountname")); 
    chosenContact.setEmployeeID(ContactParser.getEntryField(entry, "employeeID")); 
    chosenContact.setMiddleName(ContactParser.getEntryField(entry, "initials")); 
    chosenContact.setEmail(ContactParser.getEntryField(entry, "mail")); 
    if(chosenContact.getFirstSeen() == null){ 
     chosenContact.setFirstSeen(new Date()); 
    } 
    chosenContact.setLastSeen(new Date()); 
    return chosenContact; 
} 

private static String getEntryField(LDAPEntry entry, String fieldName){ 
    String returnString = ""; 
    if(entry.getAttribute(fieldName) != null){ 
     returnString = entry.getAttribute(fieldName).getStringValue(); 
    } 
    return returnString; 
} 

这所有的作品非常漂亮,如果我们只运行一个单一的实例(所以,没有新的线程是事后产生的),但如果我们不止一次运行这个线程(IE,我加快执行到~30秒,以便我可以看到问题),Hibernate报告缺乏堆空间。这看起来不像是一组特别强烈的数据(只有大约6K条目),但是当我们将代码碰到分级错误以准备推进生产时,我看到了同样的错误。在编写高效的线程方面我缺乏经验,而在Hibernate方面缺乏经验,所以如果有人有一个想法,可能会耗尽我们的堆空间(这个应用程序中的另一个主要线程没有同时运行,并且占用了几百千字节的内存),我非常感谢任何建议。

在此先感谢。

+1

您是否尝试过分析该应用程序?另外,请查看'-Xmx'和'-Xms'命令行参数。有一些有用的(和免费的)用于Java的内存分析软件包。 – 2010-10-05 15:00:07

+0

@Dave Jarvis:现在尝试进行配置。至于增加堆大小:理想情况下,我想尽量避免这种情况,因为这最终将在具有许多其他进程的服务器上运行,并且我想成为一个好公民,而不是需要巨大的这是一个小小的应用程序的记忆堆。 – EricBoersma 2010-10-05 15:05:26

回答

0

感谢大家的建议,但事实证明,我们收到的错误实际上是由本地测试和分段之间的配置错误引起的 - 数据库是新的,并且权限未正确配置以允许暂存区域与创建的数据库对话。当使用正确的权限运行时,它就像一个魅力。

我一定会考虑设置Hibernate的批处理设置,并转移到线程调度程序,而不是我当前的黑客攻击系统。

1

Memory Analyzer是一个免费的开源强大的Java堆分析器。我已经多次使用它来识别内存泄漏的来源。有了这个工具,你将能够快速看到休眠是否是一个惩罚;-)

+0

谢谢,我会给你一个镜头。 – EricBoersma 2010-10-05 15:03:47

3

你可以重写一个ScheduledExecutorService,我怀疑部分问题是你正在创建大量的HourlyRunThread对象,当你只需要一个。

例如这个试验说明了如何安排线程运行每秒10秒

@Test(expected = TimeoutException.class) 
public void testScheduledExecutorService() throws InterruptedException, ExecutionException, TimeoutException { 
    final AtomicInteger id = new AtomicInteger(); 
    final ScheduledExecutorService service = Executors.newScheduledThreadPool(1); 
    service.scheduleAtFixedRate(new Runnable() { 
     public void run() { 
      System.out.println("Thread" + id.incrementAndGet()); 
     } 
    }, 1, 1, TimeUnit.SECONDS).get(10, TimeUnit.SECONDS); 
} 

这使运行时,那里的这个测试创建了其10个第二几乎10K线程你所期望的输出运行时

private static final class HourlyRunThread extends Thread { 
    private static final AtomicInteger id = new AtomicInteger(); 
    private final int seconds; 

    private HourlyRunThread(final int seconds) { 
     super("Thread" + id.incrementAndGet()); 
     this.seconds = seconds; 
    } 

    public void run() { 
     try { 
      Thread.sleep(seconds); 
      if (seconds < 10) { 
       Thread newHourlyRunThread = new Thread(new HourlyRunThread(seconds)); 
       newHourlyRunThread.start(); 
      } 
      // do stuff 
      System.out.println(getName()); 
     } catch (InterruptedException e) { 
     } 
    } 
} 

@Test 
public void testThreading() { 
    final Thread t = new HourlyRunThread(1); 
    t.start(); 
} 
3

它看起来像你正在做批量插入或更新,在这种情况下,你应定期冲洗和清除Hibernate的Session对象,从而使会话级高速缓存不能与更多的空间填满比你分配。

请参阅Hibernate手册中关于Batch Processing的章节,获取有关如何完成此操作的建议。

此外,我强烈建议寻找另一种方法在计划的时间范围内启动您的任务,或者使用Jon Freedman建议的ScheduledExecutorService或使用库如Quartz Scheduler。在启动实际线程来完成工作之前,将线程放置3600000毫秒似乎是处理此问题的一个非常有问题的(和非确定性)方式。

+0

@jon freedman:感谢您对线程启动的建议。我有一种感觉,这不是做这件事的最好方式,我会考虑把这个方向比我现在更好的方向移动。我也非常感谢有关Hibernate批处理的信息,我会看看这是否有所作为。 – EricBoersma 2010-10-05 16:18:21

+0

我还建议考虑在单个事务中每小时运行一次而不是每次更新一次的“任务”的工作 - 这可能有助于整体执行时间,也可能更符合语义上的正确性(也就是说,所有的工作一次成功或者没有任何成功)。这取决于你期望的隔离/原子水平。 – 2010-10-05 16:37:47

0

我意外地为每笔交易创建了一个新的sessionfactory。由于某种原因,GC无法清理那些旧的sessionfactories

始终使用相同的SessionFactory实例解决了我的问题。