在准备上一篇文章时,我的一个应用程序中出现了一个奇怪的例子,这引起了我的注意。 我将其设计为单独的文章,您目前正在阅读。
本质非常简单:在创建报告并将其写入数据库时,我们会不时收到OOME。 错误是浮动的:在某些数据上,它不断地被复制,而在另一些数据上,它从未被复制。
在研究此类偏差时,动作顺序很清楚:
- 我们在具有类似于prod的设置的隔离环境中启动应用程序,而不会忘记令人垂涎的标志
-XX:+HeapDumpOnOutOfMemoryError
,以便VM在堆满时创建堆堆 - 执行导致跌倒的动作
- 拿得到的演员表开始检查
第一种方法提供了研究所需的材料。 下图打开强制转换来自此处提供的测试应用程序。 要查看完整尺寸,请右键单击图片,然后选择“在新选项卡中打开图像”:

在第一近似中,两个相等的71 MB块清晰可见,最大的块大6倍。
简短地调用链和源代码链有助于点缀所有“”。
前十行就足够了 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.base/java.util.Arrays.copyOf(Arrays.java:3745) at java.base/java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:172) at java.base/java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:538) at java.base/java.lang.StringBuilder.append(StringBuilder.java:174) at com.p6spy.engine.common.Value.quoteIfNeeded(Value.java:167) at com.p6spy.engine.common.Value.convertToString(Value.java:116) at com.p6spy.engine.common.Value.toString(Value.java:63) at com.p6spy.engine.common.PreparedStatementInformation.getSqlWithValues(PreparedStatementInformation.java:56) at com.p6spy.engine.common.P6LogQuery.logElapsed(P6LogQuery.java:203) at com.p6spy.engine.logging.LoggingEventListener.logElapsed(LoggingEventListener.java:107) at com.p6spy.engine.logging.LoggingEventListener.onAfterAnyExecute(LoggingEventListener.java:44) at com.p6spy.engine.event.SimpleJdbcEventListener.onAfterExecuteUpdate(SimpleJdbcEventListener.java:121) at com.p6spy.engine.event.CompoundJdbcEventListener.onAfterExecuteUpdate(CompoundJdbcEventListener.java:157) at com.p6spy.engine.wrapper.PreparedStatementWrapper.executeUpdate(PreparedStatementWrapper.java:100) at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:175) at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3176) at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3690) at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:90) at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604) at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:478) at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:356) at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:39) at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1454) at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:511) at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3290) at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2486) at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:473) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:178) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$300(JdbcResourceLocalTransactionCoordinatorImpl.java:39) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:271) at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:104) at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:532)
该项目将通常的Spring + Hibernate组合用于此类应用程序。 在某些时候,为了研究应用程序在数据库中的运行情况(大多数情况下这是这样做的),DataSource被包装在p6spy中 。 这是一个简单且极为有用的库,旨在拦截和记录数据库查询以及衡量其执行时间。 它的亮点是记录了所有参数进入数据库的查询,即从日志中获取查询可以立即在控制台中执行它,而不必担心参数的转换(Hibernate编写而不是它们?
),这在使用@Convert
或存在类型为Date
/ LocalDate
/ LocalTime
及其派生的字段。 恕我直言,这在开发商血腥的E的经济中极为有用。
这是包含报告的实体的样子:
@Entity public class ReportEntity { @Id @GeneratedValue private long id; @Lob private byte[] reportContent; }
当一个实体仅用于保存/卸载报告时,使用字节数组非常方便,并且大多数开箱即用的xslx / pdf工具都支持以这种形式创建书/文档的功能。
然后发生了一件可怕的,不可预见的事情:Hibernate,字节数组和p6spy的组合变成了定时炸弹,它暂时悄悄地滴答作响,当数据过多时,就会爆炸。
如上所述,在保存实体时,p6spy截获了请求并将其与所有参数一起写入日志。 在这种情况下,它们只有2个:密钥和报告本身。 P6spy开发人员认为,如果参数是字节数组,那么将其转换为十六进制会很好。 在我们使用的3.6.0版本中,这样做是这样的:
注意事项注入两个更改( tyts和tyts )后,代码如下所示(当前版本3.8.2):
private String toHexString(byte[] bytes) { char[] result = new char[bytes.length * 2]; int idx = 0; for (byte b : bytes) { int temp = (int) b & 0xFF; result[idx++] = HEX_CHARS[temp / 16]; result[idx++] = HEX_CHARS[temp % 16]; } return new String(result); }
将来,我们将以该版本为指导,因为它是在演示应用程序中使用的。
结果,类似这样的内容被写入日志
insert into report_entity (report_content, id) values ('6C6F..........7565', 1);
你看,是吗? 如果情况组合失败,则应用程序内存中可能会出现以下情况:
- 报告,以字节数组形式
- 从字节数组派生的字符数组
- 从字符数组获得的字符串从字节数组获得
StringBuilder
,其中包括从字节数组中获得的字符数组中获得的字符串的副本- 一个字符串,其中包含
StringBuilder
内部的数组副本,该字符串包含从字节数组中获取的字符数组中获取的字符串的副本。
在这种情况下,一个由2个类组成的演示应用程序在Java 11(即带有压缩行)上组装并启动后,具有1 GB的堆,您可以放一个只有71 MB的报告!
有两种方法可以解决此问题而不丢掉p6spy:
- 用
java.sql.Clob
替换byte[]
(一般解决方案,因为没有立即加载数据,并且OutputStream
了InputStream
/ OutputStream
麻烦) - 将
excludebinary=true
属性添加到excludebinary=true
文件中(已经在测试应用程序中添加了它,您只需要打开它)
在这种情况下,查询日志简洁明了:
insert into report_entity (report_content, id) values ('[binary]', 1);
播放指南,请参阅README.MD
结论:
- 不变性(特别是行)值得
贵的 贵的 亲爱的 - 如果您的表中包含敏感数据(外观,密码等),则使用p6spy,并且日志不正确,那么...很好,您了解
- 如果您有p6spy并确定它将是永久性/永久性的,那么对于大型实体,有必要查看
@DynamicInsert
/ @DynamicUpdate
。 重点是通过为每个单独的更新/插入创建请求来减少日志量。 是的,这些查询将在每次运行时动态创建,但是如果实体在20个字段中更新1个字段,这种妥协可能会派上用场。 有关更多信息,请参见上述注释的文档。
今天就这些了:)