Ассоциации гибернации, использующие слишком много памяти

У меня есть таблица «класс», которая связана с таблицами «ученик» и «учителя». «Класс» связан с несколькими учениками и учителями через внешние ключевые отношения.

Когда я использую ассоциации спящего режима и извлекаю большое количество объектов (пробовал для 5000), я вижу, что он занимает в 4 раза больше памяти, чем если бы я просто использовал держатели внешнего ключа. Что-то не так с ассоциацией спящего режима?

Могу ли я использовать любой профилировщик памяти, чтобы выяснить, что использует слишком много памяти?

Вот как выглядит схема:

class(id,className) 

student(id,studentName,class_id)
teacher(id,teacherName,class_id)

class_id is foreign key..

Случай # 1 - ассоциации гибернации

1) в Entity класса отображены ученики и учителя как:

@Entity
@Table(name="class")
public class Class {

private Integer id;
private String className;

private Set<Student> students = new HashSet<Student>();
private Set<Teacher> teachers = new HashSet<Teacher>();

@OneToMany(fetch = FetchType.EAGER, mappedBy = "classRef")
@Cascade({ CascadeType.ALL })
@Fetch(FetchMode.SELECT)
@BatchSize(size=500)
public Set<Student> getStudents() {
    return students;
}

2) у студентов и преподавателей класс отображается как:

@Entity
@Table(name="student")
public class Student {

private Integer id;
private String studentName;
private Class classRef;

@ManyToOne
@JoinColumn(name = "class_id")
public Class getClassRef() {
    return classRef;
}

Используемый запрос:

sessionFactory.openSession().createQuery("from Class where id<5000");

Однако для этого требовалось огромное количество памяти.

Случай № 2 - Удалить ассоциации и получить отдельно

1) Нет сопоставления в сущности класса

@Entity
@Table(name="class")
public class Class {

private Integer id;
private String className;

2) Только местозаполнитель для внешнего ключа в учениках, учителях

@Entity
@Table(name="student")
public class Student {

private Integer id;
private String studentName;
private Integer class_id;

Использованные запросы:

sessionFactory.openSession().createQuery("from Class where id<5000");
sessionFactory.openSession().createQuery("from Student where class_id = :classId");
sessionFactory.openSession().createQuery("from Teacher where class_id = :classId");

Примечание. Показан только имп. часть кода. Я измеряю использование памяти извлеченными объектами через библиотеку JAMM.

Я также попытался пометить запрос как readOnly в случае №1, как показано ниже, что не сильно улучшает использование памяти; совсем немного. Так что решение не в этом.

    Query query = sessionFactory.openSession().
            createQuery("from Class where id<5000");

    query.setReadOnly(true);
    List<Class> classList = query.list();
    sessionFactory.getCurrentSession().close();

Ниже приведены снимки дампа кучи, отсортированные по размеру. Похоже, что Entity, поддерживаемый спящим режимом, создает проблему.

Снимок Heapdump для программы ассоциаций гибернации  Снимок дампа кучи для программы ассоциаций гибернации

Снимок дампа кучи для выборки с использованием отдельных сущностей  Снимок дампа кучи для получения с использованием отдельных объектов


person nikel    schedule 10.02.2016    source источник
comment
В случае № 2, я думаю, что запросы должны быть from Student where class_id < 5000"); вместо from Student where class_id = :classId");, чтобы отразить случай № 1 с отдельными запросами. То же самое относится и к запросу выбора Teacher. Можете ли вы опубликовать наблюдения памяти с этими изменениями?   -  person Madhusudana Reddy Sunnapu    schedule 21.03.2016
comment
Что ж, все извлеченные классы попадают в коллекцию, а затем я перебираю эту коллекцию, внутри которой я выполняю запрос от Student, где class_id =: classId) Так что это то же самое. Я не понимаю, почему это может быть причиной этого анализа памяти.   -  person nikel    schedule 23.03.2016
comment
Я согласен. Я хотел убедиться, что правильно понял постановку проблемы.   -  person Madhusudana Reddy Sunnapu    schedule 23.03.2016


Ответы (7)


Вы выполняете выборку EAGER с аннотацией ниже. Это, в свою очередь, приведет к получению всех учащихся, даже если вы не получите доступ к getStudents(). Сделайте его ленивым, и он будет получать только при необходимости.

Из

@OneToMany(fetch = FetchType.EAGER, mappedBy = "classRef")

To

   @OneToMany(fetch = FetchType.LAZY, mappedBy = "classRef")
person Ashraff Ali Wahab    schedule 10.02.2016
comment
То же самое делается в случае № 2 без использования утилит гибернации. Это занимает в 4 раза меньше памяти ... - person nikel; 10.02.2016
comment
Ленивая загрузка - ключевая функция спящего режима, используйте ее, если вам действительно не нужны коллекции (дочерний объект) одновременно с загрузкой родительских объектов. - person Mitul Sanghani; 21.03.2016
comment
Я хочу все коллекции. Я не понимаю, насколько ленивая загрузка решает проблему, в обоих случаях я получаю все дочерние объекты. Только в режиме гибернации требуется слишком много памяти, и в этом проблема. Так что ленивая загрузка не решит проблему. - person nikel; 23.03.2016
comment
Активная выборка для @OneToMany - не лучшая практика, поскольку она устанавливает активную выборку в качестве поведения по умолчанию для этой связи для всего приложения. Сделайте это ленивым в классе сущности и используйте `FetchMode` (присоединиться / выбрать) в вашем Criteria, если вам действительно нужно активное извлечение. - person Diablo; 23.03.2016
comment
@Diablo: я знаю, что ленивая загрузка не загружает все за один раз и, следовательно, уменьшает объем памяти. Но проблема в случае №1, я загружаю все, и это занимает меньше памяти, чем в случае №2. Поскольку оба они загружают практически одни и те же данные, заметной разницы быть не должно. Надеюсь, это проясняет причину, по которой я не принял это как ответ - person nikel; 25.03.2016

Когда Hibernate загружает Class объект, содержащий OneToMany отношения, он заменяет коллекции своей собственной версией. В случае Set используется PersistentSet. Как видно на grepcode, этот PersistentSet объект содержит довольно много вещей, большая часть которых унаследована от AbstractPersistentCollection, чтобы помочь Hibernate управлять и отслеживать вещи, особенно грязные проверки.

Среди прочего, PersistentSet содержит ссылку на сеанс, логическое значение для отслеживания того, инициализирован ли он, список операций в очереди, ссылку на объект Class, которому он принадлежит, строку, описывающую его роль (не знаю, для чего именно, просто используя имя переменной здесь), строку uuid фабрики сеанса и многое другое. Самая большая проблема с памятью среди множества - это, вероятно, моментальный снимок неизмененного состояния набора, который, как я ожидал, сам по себе примерно удвоит потребление памяти.

В этом нет ничего плохого, Hibernate просто делает больше, чем вы думали, и более сложными способами. Это не должно быть проблемой, если у вас не очень мало памяти.

Кстати, обратите внимание, что когда вы сохраняете новый объект Class, о котором Hibernate ранее не знал, Hibernate заменит простые объекты HashSet, которые вы создали, новыми объектами PersistentSet, сохраняя исходный HashSet, заключенный внутри PersistentSet в его поле set. Все Set операции будут перенаправлены в упакованный HashSet, а также будут запускать PersistentSet грязное отслеживание и логику очередей и т. Д. Помня об этом, вы не должны сохранять и использовать какие-либо внешние ссылки на Set из перед сохранением, а вместо этого должны получить новый ссылку на экземпляр PersistentSet Hibernate и используйте его, если вам нужно внести какие-либо изменения (в набор, а не в учащихся или учителей в нем) после первоначального сохранения.

person Douglas    schedule 21.03.2016
comment
Хм .. Я очень надеюсь, что есть способ отключить его. Я думаю, что мы можем работать без этих функций в большинстве наших случаев. Это вызывает беспокойство, поскольку у нас есть такой случай с более чем 3L объектами, и он поглощает слишком много памяти. - person nikel; 21.03.2016
comment
@nikel Какие это особенности? Обнаружение изменений? Вам это всегда нужно, если вы не выполняете операции только для чтения, и в этом случае может помочь установка флага только для чтения в сеансе или запросе. - person Douglas; 21.03.2016
comment
Да ... неизмененное состояние, грязная проверка и т. Д ... попробую установить только чтение и посмотрим, что произойдет ... - person nikel; 22.03.2016
comment
Это происходит только в отношениях «один-ко-многим» или всегда? Если так, то это произойдет и в случае №2. Я действительно пытался установить readOnly в случае №1, не очень помогает, просто очень небольшое улучшение. Я обновляю фактический вопрос, чтобы показать внесенные мной изменения. - person nikel; 23.03.2016
comment
@nikel Это происходит с полями / отношениями типов коллекций - так, один-ко-многим или многие-ко-многим. Чтобы сделать его однонаправленным, многие к одному (Class, как в случае №2, Student, как в случае №1), нужно использовать примерно ту же память, что и в случае №2, как есть, я думаю, возможно, немного меньше из-за отсутствия дублирования идентификатора номер в памяти. Кроме того, я не уверен, будет ли это иметь какое-либо значение, но пробовали ли вы сделать сеанс, а не запрос, доступным только для чтения? И уверены ли вы, что тестовый запрос только для чтения является первой загрузкой сущностей, поэтому нет возможности извлечения из кэшированных копий, не предназначенных только для чтения? - person Douglas; 23.03.2016
comment
Я уверен, что запрос только для чтения загружает объект в самый первый раз. Я использую две автономные java-программы для случая №1 и случая №2 для анализа. Похоже, что hibernate v ‹3.5 не поддерживает session.setDefaultReadOnly (), поэтому я попытаюсь обновить свои программы, чтобы использовать эту версию гибернации ... - person nikel; 25.03.2016

Что касается огромного потребления памяти, которое вы замечаете, одна из возможных причин состоит в том, что Hibernate Session необходимо поддерживать состояние каждого entity загруженного файла в виде _ 3_, т. е. один дополнительный объект EntityEntry для каждого загруженного entity. Это необходимо для механизма автоматической грязной проверки спящего режима на этапе очистки, чтобы сравнить текущее состояние объекта с его исходным состоянием (тем, которое хранится как EntityEntry).

Обратите внимание, что этот EntityEntry отличается от объекта, к которому мы получаем доступ в коде нашего приложения, когда вызываем session.load/get/createQuery/createCriteria. Это внутреннее для спящего режима и хранится в кэше первого уровня.

Цитирование из javadocs для EntityEntry:

Нам нужна запись, чтобы сообщить нам все о текущем состоянии объекта по отношению к его постоянному состоянию. Предупреждение реализации. Hibernate должен создать большое количество экземпляров этого класса, поэтому нам нужно позаботиться о его влиянии на потребление памяти.

Один из вариантов, предполагающий, что целью является только чтение и итерация данных, а не внесение каких-либо изменений в эти объекты, вы можете рассмотреть возможность использования _ 8_ вместо Session.

Преимущество, указанное в Javadocs для сеанса без сохранения состояния :

Сеанс без сохранения состояния не реализует кеш первого уровня, не взаимодействует с каким-либо кешем второго уровня, а также не реализует транзакционную обратную запись или автоматическую грязную проверку.

Без автоматической грязной проверки Hibernate не нужно создавать EntityEntry для каждого объекта загруженного entity, как это было в предыдущем случае с Session. Это должно снизить нагрузку на использование памяти.

Сказано, что у него есть собственный набор ограничений, упомянутых в Javadoc для StatelessSession.

Одно ограничение, которое стоит выделить, - это не ленивая загрузка коллекций. Если мы используем StatelessSession и хотим загрузить связанные collections, мы должны либо join fetch их использовать HQL, либо EAGER получить с помощью Criteria.

Другой связан с second level cache, где он не взаимодействует ни с каким кешем второго уровня, если таковой имеется.

Так что, учитывая, что у него нет накладных расходов на кеш первого уровня, вы можете попробовать с Stateless Session и посмотреть, соответствует ли это вашим требованиям и помогает ли также снизить потребление памяти.

person Madhusudana Reddy Sunnapu    schedule 23.03.2016
comment
Имеет смысл. Но похоже, что сеансы без сохранения состояния не могут работать с коллекциями. Это не срабатывает с ошибкой ниже: коллекции не могут быть получены сеансом без сохранения состояния - person nikel; 25.03.2016
comment
@nikel Это одно из ограничений StatelessSession. Как я уже упоминал в ответе, вы сможете это преодолеть - если мы используем StatelessSession и хотим загрузить связанные коллекции, мы должны либо join fetch их, используя HQL, либо EAGER fetch, используя Criteria - person Madhusudana Reddy Sunnapu; 25.03.2016
comment
Обновил мой вопрос, чтобы показать данные кучи. Похоже, EntityEntry определенно играет роль. Попробую ваше предложение по использованию критериев .. - person nikel; 25.03.2016

Да, вы можете использовать профилировщик памяти, например visualvm или yourkit, чтобы узнать, что занимает так много памяти. Один из способов - получить дамп кучи, а затем загрузить его в один из этих инструментов.

Однако вам также необходимо сравнивать яблоки с яблоками. Ваши вопросы по делу № 2 sessionFactory.openSession().createQuery("from Student where class_id = :classId"); sessionFactory.openSession().createQuery("from Teacher where class_id = :classId");

выбирайте учеников и учителей только для одного класса, а в случае № 1 вы выбираете гораздо больше. Вместо этого вам нужно использовать <= :classId.

Кроме того, немного странно, что вам нужен один ученик и одна учительская запись на один класс. Учитель может вести более одного класса, а ученик может быть в нескольких классах. Я не знаю, какую именно проблему вы решаете, но если ученик действительно может участвовать во многих классах, а учитель может вести больше, чем в одном классе, вам, вероятно, придется создавать свои таблицы по-другому.

person Peter L    schedule 21.03.2016
comment
все извлеченные классы переходят в коллекцию, а затем я перебираю эту коллекцию, внутри которой я выполняю запрос от Student, где class_id =: classId) Так что это то же самое. Я не понимаю, почему это может быть причиной этого анализа памяти. Что касается ассоциации, у меня один-ко-многим между классом и учениками, учителями. Да, в идеале один ученик может быть во многих классах, но это всего лишь пример использования для демонстрации реальной проблемы, с которой мы сталкиваемся (с той же схемой ассоциации :)) - person nikel; 23.03.2016
comment
Вы пробовали получить дамп кучи, а потом посмотрели? Это может пролить свет на то, какие объекты загружаются. Кстати, это выглядит так: stackoverflow.com/questions/1995080/ - person Peter L; 23.03.2016

Попробуйте @Fetch(FetchMode.JOIN). Это создает только один запрос вместо нескольких запросов выбора. Также просмотрите сгенерированные запросы. Я предпочитаю использовать Criteria, а не HQL (просто мысль).

Для профилирования используйте бесплатные программы, такие как visualvm или jconsole. yourkit хорош для расширенного профилирования, но не бесплатно. Я предполагаю, что есть его трейловая версия.

Вы можете взять heapdump своего приложения и проанализировать его с помощью любых инструментов анализатора памяти, чтобы проверить наличие утечек памяти.

Кстати, я не совсем уверен в использовании памяти для текущего сценария.

person Diablo    schedule 23.03.2016

Вероятно, причина в двунаправленной ссылке от ученика к классу и класса к ученикам. Когда вы получаете Class A (id 4500), объект Class должен быть гидратирован, в свою очередь, он должен пойти и вытащить все объекты Student (и предположительно учителей), связанные с этим классом. Когда это происходит, каждый объект ученика должен быть гидратирован. Это вызывает выборку каждого класса, частью которого является ученик. Итак, хотя вам нужен только класс A, вы получите:

Получить класс A (id 4900) Возвращает класс A со ссылкой на 3 ученика, ученика A, B, C.Студент A имеет ссылку на класс A, B (id 5500) Класс B нуждается в увлажнении Класс B имеет ссылку на учеников C, D Student C требуется гидратация. Студент C имеет отношение только к классу A и B. Студент C. гидратация завершена. Учащийся D нуждается в увлажнении. Учащийся D ссылается только на класс B. Учащийся B гидратация завершена.

и т. д. При активной выборке это продолжается до тех пор, пока все ссылки не будут гидратированы. Дело в том, что вполне возможно, что вы останетесь с классами в памяти, которые вам на самом деле не нужны. Или чей id не менее 5000.

Это могло быстро ухудшиться.

Кроме того, вы должны убедиться, что переопределяете хэш-код и методы равенства. В противном случае вы можете получить избыточные объекты как в памяти, так и в вашем наборе.

Один из способов улучшить - либо перейти на ЛЕНИВУЮ загрузку, как упоминали другие, либо разорвать двунаправленные ссылки. Если вы знаете, что когда-либо будете иметь доступ только к ученикам в классе, тогда у вас нет ссылки от ученика обратно к классу. Для примера ученика / класса имеет смысл иметь двунаправленную ссылку, но, возможно, ее можно избежать.

person blouro    schedule 23.03.2016
comment
Что ж, в моем случае ученик является частью только одного класса, и то же самое относится и к учителю. Я знаю, что это не имеет смысла в сценарии ученик / учитель, но это просто представление о реальной проблеме, с которой мы сталкиваемся ... - person nikel; 25.03.2016

как вы говорите "я хочу" все "коллекции". поэтому ленивая загрузка не поможет. Вам нужно каждое поле каждой сущности? В этом случае используйте проекцию, чтобы получить именно то, что вам нужно. См. когда использовать проекции Hibernate. В качестве альтернативы рассмотрите возможность наличия минималистских сущностей Teacher-Lite и Student-Lite, которые расширяются в полнофункциональных версиях.

person Gerry King    schedule 23.03.2016