2010-10-05 130 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實例解決了我的問題。