11

我有一个表“类”,它链接到表“学生”和“教师”。一个“班级”通过外键关系链接到多个学生和教师。

当我使用休眠关联并获取大量实体(尝试 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");

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

我还尝试将查询标记为 readOnly 在 case #1 中,如下所示,这并不能极大地提高内存使用率;只是很少。所以这不是解决办法。

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

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

以下是按大小排序的堆转储快照。看起来由休眠维护的实体正在造成问题..

用于休眠关联程序的 Heapdump 快照 用于休眠关联程序的 Heapdump 快照

使用单独实体获取的堆转储快照 使用单独实体获取的堆转储快照

4

7 回答 7

7

您正在使用以下注释进行 EAGER 提取。这将反过来获取所有学生,甚至您无需访问getStudents(). 让它变得懒惰,它只会在需要时获取。

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

   @OneToMany(fetch = FetchType.LAZY, mappedBy = "classRef")
于 2016-02-10T19:17:53.307 回答
4

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

除其他外,PersistentSet包含对会话的引用、用于跟踪它是否已初始化的布尔值、排队操作的列表、对Class拥有它的对象的引用、描述其角色的字符串(不确定它到底是为了什么,只是去这里的变量名)、会话工厂的字符串 uuid 等等。其中最大的内存消耗可能是该集合的未修改状态的快照,我希望它本身大约会增加一倍的内存消耗。

这里没有错,Hibernate 只是做的比你想象的要多,而且方式更复杂。除非您的内存严重不足,否则这应该不是问题。

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

于 2016-03-21T00:29:57.903 回答
2

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

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

引用EntityEntry的javadocs :

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

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

引用Javadocs for Stateless Session 的优势:

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

由于没有自动脏检查,Hibernate 不需要像之前的情况那样EntityEntry为每个加载的实体创建. 这应该可以减少内存利用率的压力。entitySession

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

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

另一个与second level cache它不与任何二级缓存(如果有的话)交互的地方有关。

因此,鉴于它没有任何一级缓存的开销,您可能想尝试一下Stateless Session,看看它是否符合您的要求并有助于减少内存消耗。

于 2016-03-23T16:15:15.020 回答
0

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

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

只为一堂课选择学生和老师,而在第 1 种情况下,您可以选择更多。你需要<= :classId改用。

另外,一个班级需要一个学生和一个老师的记录有点奇怪。一个老师可以教多个班级,一个学生可以上多个班级。我不知道您要解决的确切问题,但是如果确实一个学生可以参加许多课程并且老师可以教多个课程,那么您可能需要以不同的方式设计您的表格。

于 2016-03-21T18:12:01.813 回答
0

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

对于分析,请使用诸如visualvmjconsole之类的免费软件。yourkit适合高级分析,但它不是免费的。我想它有一个跟踪版本。

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

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

于 2016-03-23T09:58:45.330 回答
0

其原因可能是从学生到班级和班级到学生的双向链接。当您获取 Class A (id 4500) 时,必须为 Class 对象添加水合,然后这必须拉动与该类关联的所有 Student 对象(可能还有教师)。发生这种情况时,必须为每个学生对象补水。这会导致获取学生所在的每个班级。因此,尽管您只想要 A 类,但您最终会得到:

获取 A 类 (id 4900) 返回 A 类,参考 3 个学生,学生 A、B、C。学生 A 参考了 A、B 类 (id 5500) B 类需要补水 B 类参考学生 C、D 学生C 需要补水 学生 C 仅参考 A 类和 B 学生 C 补水完成。学生 D 需要补水 学生 D 仅参考 B 类 学生 B 补水完成 B 类补水完成 学生 B 需要补水(来自原始班级负荷 A 类)

等等......通过急切的获取,这将一直持续到所有链接都被水合为止。关键是您最终可能会在内存中得到您实际上并不想要的类。或者id不小于5000的。

这可能会很快变得更糟。

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

改进的一种方法是更改​​为其他人提到的延迟加载或中断双向链接。如果您知道您只能访问每个班级的学生,那么就没有学生返回班级的链接。对于学生/班级示例,具有双向链接是有意义的,但也许可以避免。

于 2016-03-23T13:05:29.243 回答
0

正如您所说,“我想要”所有“收藏品”。所以延迟加载无济于事。您需要每个实体的每个字段吗?在这种情况下,使用投影来获得您想要的位。查看何时使用 Hibernate Projections。或者,考虑使用全脂版本扩展的极简 Teacher-Lite 和 Student-Lite 实体。

于 2016-03-23T14:23:18.880 回答