存储大字节数组至DB时触发OOM,需优化Hibernate日志输出
问题场景与错误信息
将总大小2GB的字节数组(分为1.7GB和300MB两部分)存入数据库时,触发如下内存溢出错误:
java.lang.OutOfMemoryError: Requested array size exceeds VM limit at java.base/java.util.Arrays.copyOf(Arrays.java:3745) at java.base/java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:169) at java.base/java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:772) at java.base/java.lang.StringBuilder.append(StringBuilder.java:248) at java.base/java.util.Arrays.toString(Arrays.java:4988) at org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor.extractLoggableRepresentation(PrimitiveByteArrayTypeDescriptor.java:63) at org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor.extractLoggableRepresentation(PrimitiveByteArrayTypeDescriptor.java:26) at org.hibernate.type.descriptor.sql.BasicBinder.bind(BasicBinder.java:70) at org.hibernate.type.descriptor.converter.AttributeConverterSqlTypeDescriptorAdapter$1.bind(AttributeConverterSqlTypeDescriptorAdapter.java:88) at org.hibernate.type.AbstractStandardBasicType.nullSafeSet(AbstractStandardBasicType.java:280) at org.hibernate.type.AbstractStandardBasicType.nullSafeSet(AbstractStandardBasicType.java:275) at org.hibernate.type.AbstractSingleColumnStandardBasicType.nullSafeSet(AbstractSingleColumnStandardBasicType.java:39) at org.hibernate.persister.entity.AbstractEntityPersister.dehydrate(AbstractEntityPersister.java:2879) at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3176) at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3699) 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.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:50) at org.hibernate.internal.SessionImpl.autoFlushIfRequired(SessionImpl.java:1418) at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1504) at org.hibernate.query.internal.AbstractProducedQuery.doList(AbstractProducedQuery.java:1561) at org.hibernate.query.internal.AbstractProducedQuery.list(AbstractProducedQuery.java:1529) at org.hibernate.query.Query.getResultList(Query.java:168) at org.hibernate.query.criteria.internal.compile.CriteriaQueryTypeQueryAdapter.getResultList(CriteriaQueryTypeQueryAdapter.java:76)
相关实体与数据库定义
实体类
@Table(name = "FILE_DATA") public class FileData { @Id @Column(name= "ID") private Long id; @Convert(converter = CustomArrayCompressionConverter.class) @Column(name = "CONTENT") private byte[] content; }
数据库表结构
TABLE [FILE_DATA] ( [ID] [numeric](10) NOT NULL PRIMARY KEY, [CONTENT] [varbinary](max) NOT NULL )
问题分析
错误根源是PrimitiveByteArrayTypeDescriptor.extractLoggableRepresentation方法会将超大字节数组转换为字符串用于日志输出,这个转换过程需要额外的内存空间,即使JVM堆内存设为14GB也无法处理2GB数组的转字符串操作。仅通过Log4j正则过滤日志输出无法阻止这个转换操作,依然会触发OOM。
已知自定义UserType的方案不可行,因为当前实体使用了自定义的AttributeConverter。
可行解决方案
方案1:修改Hibernate日志级别,跳过日志生成
直接将Hibernate中负责参数绑定日志的类的日志级别调整为OFF或ERROR,从根源上避免触发extractLoggableRepresentation方法。
在日志配置文件(如log4j2.xml、logback.xml)中添加:
<!-- 针对BasicBinder类关闭日志,避免触发字节数组转字符串 --> <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="OFF"/>
或者只针对PrimitiveByteArrayTypeDescriptor关闭日志:
<logger name="org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor" level="OFF"/>
方案2:自定义TypeDescriptor替换默认实现
通过Hibernate的TypeDescriptorRegistry替换默认的PrimitiveByteArrayTypeDescriptor,重写extractLoggableRepresentation方法,避免转换大数组:
- 创建自定义的ByteArrayTypeDescriptor:
public class CustomPrimitiveByteArrayTypeDescriptor extends PrimitiveByteArrayTypeDescriptor { @Override public String extractLoggableRepresentation(byte[] value) { if (value == null) { return super.extractLoggableRepresentation(value); } // 替换为自定义日志内容,避免转换整个数组 return String.format("Huge byte array (size: %d bytes)", value.length); } }
- 在Hibernate初始化时注册自定义TypeDescriptor:
// 获取SessionFactory的TypeDescriptorRegistry SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class); TypeDescriptorRegistry typeDescriptorRegistry = sessionFactory.getTypeHelper().getTypeDescriptorRegistry(); // 替换默认的PrimitiveByteArrayTypeDescriptor typeDescriptorRegistry.registerDescriptor(byte[].class, new CustomPrimitiveByteArrayTypeDescriptor());
如果使用Spring Boot,可以在@PostConstruct方法中执行上述注册逻辑。
方案3:调整Hibernate配置,禁用参数绑定日志
修改Hibernate配置,关闭参数绑定的日志输出:
在application.properties或hibernate.cfg.xml中添加:
# 禁用Hibernate的参数绑定日志 hibernate.show_sql=false hibernate.format_sql=false hibernate.use_sql_comments=false
注意:这个方案会关闭所有SQL相关日志,如果需要保留其他SQL日志,优先选择方案1。
内容的提问来源于stack exchange,提问作者Dark Knight

