使用太多内存的Hibernate关联

时间:2016-02-10 19:13:45

标签: java hibernate one-to-many memory-profiling heap-profiling

我有一张桌子" class"这与表格#34;学生"和#34;老师"。 A"班级"通过foriegn关键关系链接到多个学生和教师。

当我使用hibernate关联并获取大量实体(尝试5000)时,我发现它占用的内存比使用外键占位符的内存多4倍。 在休眠关联中有什么问题吗?

我可以使用任何内存分析器来找出使用过多内存的内容吗?

这是架构的方式:

class(id,className) 

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

class_id is foreign key..

案例#1 - Hibernate协会

1)在Class 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");

注意 - 仅显示imp。部分代码。我正在通过JAMM库测量获取实体的内存使用情况。

我还尝试在下面的情况#1中将查询标记为readOnly,这不会极大地提高内存使用率;只是一点点。所以这不是解决方案。

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

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

以下是按大小排序的heapdump快照。看起来像hibernate维护的实体正在创建问题..

Hapnate关联程序的Heapdump快照 Snapshot of Heapdump for hibernate associations program

heapdump的快照,用于使用单独的实体进行提取 Snapshot of heapdump for fetching using separate entities

7 个答案:

答案 0 :(得分:7)

您正在使用以下注释进行EAGER提取。即使您没有访问getStudents(),也可以获取所有学生。让它变得懒惰,只在需要时才会获取。

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

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

答案 1 :(得分:3)

当Hibernate加载包含Class关系的OneToMany实体时,它会用自己的自定义版本替换这些集合。对于Set,它使用PersistentSet。从grepcode可以看出,这个PersistentSet对象包含很多东西,其中大部分都是从AbstractPersistentCollection继承的,以帮助Hibernate管理和跟踪事物,特别是脏检查。

除其他外,PersistentSet包含对会话的引用,用于跟踪其是否已初始化的布尔值,排队操作列表,对拥有的Class对象的引用它,一个描述其角色的字符串(不知道究竟是什么,只是通过这里的变量名称),会话工厂的字符串uuid等等。该批次中最大的内存占用可能是该组未修改状态的快照,我希望它本身可以大约加倍内存消耗。

这里没有任何问题,Hibernate只是做得比你意识到的更多,并且以更复杂的方式。除非你内存严重不足,否则它不应该成为一个问题。

顺便提一下,当你保存一个Hibernate以前不知道的新Class对象时,Hibernate将用新的HashSet对象替换你创建的简单PersistentSet对象,存储原始HashSet包含在其PersistentSet字段中的set内。所有Set操作都将转发到包装HashSet,同时还会触发PersistentSet脏跟踪和排队逻辑等。考虑到这一点,您不应该保留并使用任何外部引用来自保存之前的Set,而应该获取对Hibernate的PersistentSet实例的新引用,并在需要进行任何更改时使用它(对于集合,而不是对学生或教师在初始保存之后。)

答案 2 :(得分:2)

关于你注意到的巨大内存消耗,一个潜在的原因是Hibernate Session必须保持每个entity的状态,它已经加载了EntityEntry对象的形式,即一个额外的对象,EntityEntry,用于每个加载的entity。这是在刷新阶段hibernate自动脏检查机制所需要的,以比较实体的当前状态与其原始状态(存储为EntityEntry的状态)。

请注意,当我们调用EntityEntry时,此session.load/get/createQuery/createCriteria与我们在应用程序代码中访问的对象不同。这是休眠内部并存储在第一级缓存中。

引用EntityEntry的javadocs

  

我们需要一个条目来告诉我们关于对象当前状态的所有信息   关于其持久状态实现警告:Hibernate   需要实例化此类的大量实例,   因此,我们需要注意它对内存消耗的影响。

一个选项,假设目的只是读取和遍历数据而不对这些实体执行任何更改,您可以考虑使用StatelessSession而不是Session

无状态会话Javadocs引用的优势:

  

无状态会话也不实现第一级缓存   与任何二级缓存交互,也不实现   事务性后写或自动脏检查

没有自动脏检查,Hibernate不需要为加载EntityEntry的每个实体创建entity,就像之前使用Session的情况一样。这可以减少内存利用率的压力。

说,它确实有自己的一组限制,如StatelessSession javadoc文档中所述。

值得强调的一个限制是,它不会延迟加载集合。如果我们使用StatelessSession并希望加载关联的collections,我们应join fetch使用HQLEAGER使用Criteria获取。

另一个与second level cache相关,它与任何二级缓存(如果有)都不会互动。

因此,考虑到它没有任何第一级缓存的开销,您可能需要尝试使用Stateless Session,看看它是否符合您的要求,并有助于减少内存消耗。 / p>

答案 3 :(得分:0)

是的,您可以使用内存分析器(如visualvm或yourkit)来查看占用大量内存的内存。一种方法是获取堆转储,然后将其加载到其中一个工具中。

但是,您还需要确保将苹果与苹果进行比较。您的问题是#2 sessionFactory.openSession().createQuery("from Student where class_id = :classId"); sessionFactory.openSession().createQuery("from Teacher where class_id = :classId");

仅为一个班级选择学生和老师,而在#1的情况下,您选择的方式更多。您需要改为使用<= :classId

此外,每个班级需要一名学生和一名教师记录,这有点奇怪。教师可以教授多个班级,学生可以在多个班级教授。我不知道你要解决的确切问题,但如果学生确实可以参加很多课程而且老师可以教授多个课程,你可能需要以不同的方式设计你的表格。

答案 4 :(得分:0)

尝试@Fetch(FetchMode.JOIN),这只生成一个查询而不是多个选择查询。还要查看生成的查询。我更喜欢使用Criteria而不是HQL(只是一个想法)。

要进行性能分析,请使用 visualvm jconsole 等免费软件。 yourkit 适用于高级分析,但它不是免费的。我猜它有一个跟踪版本。

您可以使用应用程序的 heapdump 并使用任何内存分析器工具对其进行分析,以检查是否存在任何内存泄漏。

顺便说一句,我不确定当前场景的内存使用情况。

答案 5 :(得分:0)

可能原因是从学生到班级和班级到学生的双向链接。当你获取A类(id 4500)时,Class对象必须是水合的,反过来这必须去拉动所有与这个类相关的Student对象(和教师)。发生这种情况时,每个学生对象都必须保湿。这导致学生参与的每个班级的获取。所以,虽然你只想要A级,但你最终会得到:

获取A类(id 4900) 参考3名学生A,B,C返回A班。 学生A已参考A,B(身份证5500) B级需要保湿 B班参考学生C,D 学生C需要补水 学生C仅参考A类和B类 学生C保湿完成。 学生D需要补水 学生D仅参考B类 学生B补水完成 B级水合作用完成 学生B需要补水(从原班级负荷A级)

等......随着热切的提取,这一直持续到所有链接都水合为止。关键是你可能最终得到了你并不真正想要的内存类。或者其身份不低于5000.

这可能会变得更快。

此外,您应该确保覆盖hashcode和equals方法。否则,您可能会在内存和设备中获得冗余对象。

改进的一种方法是改变LAZY加载,就像其他人提到的那样或打破双向链接。如果您知道每个班级只会访问学生,那么请不要让学生回到课堂。对于学生/班级的例子,有双向链接是有意义的,但也许可以避免。

答案 6 :(得分:0)

正如你所说“我想要”所有“收藏品”。所以懒惰加载无济于事。 你需要每个实体的每个领域吗?在这种情况下,使用投影来获得您想要的位。见when to use Hibernate Projections。 或者考虑使用全脂版本扩展的极简主义的Teacher-Lite和Student-Lite实体。