- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
在应用中,我们使用的 SpringData ES的 ElasticsearchRestTemplate 来做查询,使用方式不对,导致每次ES查询时都新实例化了一个查询对象,会加载相关类到元数据中。最终长时间运行后元数据出现内存溢出; 。
问题原因: 类加载过多 ,导致元数据OOM。非类实例多或者大对象问题; 。
排查方式:
查看JVM运行情况,发现元数据满导致内存溢出; 导出内存快照,通过 OQL 快速定位肇事者; 排查对应类的使用场景和加载场景(重点序列化反射场景); 。
06-15 下午正摩肩擦掌的备战着晚上8点。收到预发机器的一个GC次数报警.
【警告】UMP JVM监控
【警告】异步(async采集点:async.jvm.info(别名:jvm监控)15:42:40至15:42:50【xx.xx.xx.xxx(10174422426)(未知分组)】,JVM监控FullGC次数=2次[偏差0%],超过1次FullGC次数>=2次
【时间】2023-06-15 15:42:50
【类型】UMP JVM监控
第一时间诧异了下。该应用主要作用是接MQ消息和定时任务,同时任务和MQ都和线上做了隔离,也没有收到大流量的告警.
先看了下对应JVM监控:
只看上面都怀疑是监控异常(之前用文件采集的时候有遇到过,看CPU确实有波动。但堆基本无涨幅,怀疑非堆。) 。
既然怀疑非堆,我们先通过 jstat 来看看情况 。
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 0.89 3.67 97.49 97.96 854 23.720 958 615.300 639.020
0.00 0.00 0.89 3.67 97.49 97.96 854 23.720 958 615.300 639.020
0.00 0.00 0.89 3.67 97.49 97.96 854 23.720 958 615.300 639.020
0.00 0.00 0.89 3.67 97.49 97.96 854 23.720 958 615.300 639.020
0.00 0.00 0.89 3.67 97.49 97.96 854 23.720 958 615.300 639.020
M列代表了metaspace的使用率,当前已经 97.49% 进一步印证了我们的猜测.
接下来通过 jmap 导出内存快照分析。这里我习惯使用 Visual VM 进行分析.
在这里我们看到有 118588 个类被加载了。正常业务下不会有这么多类.
这里我们走了很多弯路.
首先查看内存对象,根据类的实例数排了个序,试图看看是否是某个或某些类实例过多导致.
这里一般是 排查堆异常 时使用,可以看大对象和某类的实例数,但我们的问题是 类加载过多。非类实例对象多或者大 。这里排除.
后续还尝试了直接使用 Visual VM 的聚合按包路径统计,同时排序。收效都甚微。看不出啥异常来.
这里我们使用 OQL 来进行查询统计.
语句如下:
var packageClassSizeMap = {};
// 遍历统计以最后一个逗号做分割
heap.forEachClass(function (it) {
var packageName = it.name.substring(0, it.name.lastIndexOf('.'));
if (packageClassSizeMap[packageName] != null) {
packageClassSizeMap[packageName] = packageClassSizeMap[packageName] + 1;
} else {
packageClassSizeMap[packageName] = 1;
}
});
// 排序 因为Visual VM的查询有数量限制。
var sortPackageClassSizeMap = [];
map(sort(Object.keys(packageClassSizeMap), function (a, b) {
return packageClassSizeMap[b] - packageClassSizeMap[a]
}), function (it) {
sortPackageClassSizeMap.push({
package: it,
classSize: packageClassSizeMap[it]
})
});
sortPackageClassSizeMap;
执行效果如下:
可以看到, com.jd.bapp.match.sync.query.es.po 下存在 92172 个类。这个包下,不到20个类。这时我们在回到开始查看类的地方。看看该路径下都是些什么类.
这里附带一提,直接根据路径获取对应的类数量:
var packageClassSizeMap = {};
// 遍历统计以最后一个逗号做分割
heap.forEachClass(function (it) {
var packageName = it.name.substring(0, it.name.lastIndexOf('.'));
// 加路径过滤版
if (packageName.indexOf('com.jd.bapp.match.sync.query.es.po')){
if (packageClassSizeMap[packageName] != null) {
packageClassSizeMap[packageName] = packageClassSizeMap[packageName] + 1;
} else {
packageClassSizeMap[packageName] = 1;
}
}
});
sortPackageClassSizeMap;
查询 com.jd.bapp.match.sync.query.es.po 路径下的classes 。
我们可以看到
从上面得到的信息得出是ES相关查询时出现的。我们本地debug查询跟踪下.
这里列下主要排查流程 。
在应用中,我们使用的 SpringData ES的 ElasticsearchRestTemplate 来做查询,主要使用方法 org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate#search 。
重点代码如下:
public <T> SearchHits<T> search(Query query, Class<T> clazz, IndexCoordinates index) {
// 初始化request
SearchRequest searchRequest = requestFactory.searchRequest(query, clazz, index);
// 获取值
SearchResponse response = execute(client -> client.search(searchRequest, RequestOptions.DEFAULT));
SearchDocumentResponseCallback<SearchHits<T>> callback = new ReadSearchDocumentResponseCallback<>(clazz, index);
// 转换为对应类型
return callback.doWith(SearchDocumentResponse.from(response));
}
首先看初始化request的逻辑 。
org.springframework.data.elasticsearch.core.RequestFactory#searchRequest 。
首先是: org.springframework.data.elasticsearch.core.RequestFactory#prepareSearchRequest 。
这里有段代码是对搜索结果的排序处理: prepareSort(query, sourceBuilder, getPersistentEntity(clazz)); 重点就是这里的 getPersistentEntity(clazz) 这段代码主要会识别当前类是否已经加载过,没有加载过则加载到内存中:
@Nullable
private ElasticsearchPersistentEntity<?> getPersistentEntity(@Nullable Class<?> clazz) {
// 从convert上下文中获取判断该类是否已经加载过,如果没有加载过,就会重新解析加载并放入上下文
return clazz != null ? elasticsearchConverter.getMappingContext().getPersistentEntity(clazz) : null;
}
具体加载的实现见: 具体实现见:org.springframework.data.mapping.context.AbstractMappingContext#getPersistentEntity(org.springframework.data.util.TypeInformation<?>) 。
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.MappingContext#getPersistentEntity(org.springframework.data.util.TypeInformation)
*/
@Nullable
@Override
public E getPersistentEntity(TypeInformation<?> type) {
Assert.notNull(type, "Type must not be null!");
try {
read.lock();
// 从上下文获取当前类
Optional<E> entity = persistentEntities.get(type);
// 存在则返回
if (entity != null) {
return entity.orElse(null);
}
} finally {
read.unlock();
}
if (!shouldCreatePersistentEntityFor(type)) {
try {
write.lock();
persistentEntities.put(type, NONE);
} finally {
write.unlock();
}
return null;
}
if (strict) {
throw new MappingException("Unknown persistent entity " + type);
}
// 不存在时,添加该类型到上下文
return addPersistentEntity(type).orElse(null);
}
上述是加载流程。执行查询后,我们还需要进行一次转换。这里就到了使用的地方: org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate#search中 callback.doWith(SearchDocumentResponse.from(response)),
这里这个方法会请求内部的 doWith 方法。实现如下:
@Nullable
public T doWith(@Nullable Document document) {
if (document == null) {
return null;
}
// 获取到待转换的类实例
T entity = reader.read(type, document);
return maybeCallbackAfterConvert(entity, document, index);
}
其中的 reader.read 会先从上下文中获取上述加载到上下文的类信息,然后读取 。
@Override
public <R> R read(Class<R> type, Document source) {
TypeInformation<R> typeHint = ClassTypeInformation.from((Class<R>) ClassUtils.getUserClass(type));
typeHint = (TypeInformation<R>) typeMapper.readType(source, typeHint);
if (conversions.hasCustomReadTarget(Map.class, typeHint.getType())) {
R converted = conversionService.convert(source, typeHint.getType());
if (converted == null) {
// EntityReader.read is defined as non nullable , so we cannot return null
throw new ConversionException("conversion service to type " + typeHint.getType().getName() + " returned null");
}
return converted;
}
if (typeHint.isMap() || ClassTypeInformation.OBJECT.equals(typeHint)) {
return (R) source;
}
// 从上下文获取之前加载的类
ElasticsearchPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(typeHint);
// 获取该类信息
return readEntity(entity, source);
}
读取会走 org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter#readEntity 。
先是读取该类的初始化器: EntityInstantiator instantiator = instantiators.getInstantiatorFor(targetEntity),
是通过该类实现: org.springframework.data.convert.KotlinClassGeneratingEntityInstantiator#createInstance 。
org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator#doCreateEntityInstantiator
/*
* (non-Javadoc)
* @see org.springframework.data.convert.ClassGeneratingEntityInstantiator#doCreateEntityInstantiator(org.springframework.data.mapping.PersistentEntity)
*/
@Override
protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity<?, ?> entity) {
PreferredConstructor<?, ?> constructor = entity.getPersistenceConstructor();
if (ReflectionUtils.isSupportedKotlinClass(entity.getType()) && constructor != null) {
PreferredConstructor<?, ?> defaultConstructor = new DefaultingKotlinConstructorResolver(entity)
.getDefaultConstructor();
if (defaultConstructor != null) {
// 获取对象初始化器
ObjectInstantiator instantiator = createObjectInstantiator(entity, defaultConstructor);
return new DefaultingKotlinClassInstantiatorAdapter(instantiator, constructor);
}
}
return super.doCreateEntityInstantiator(entity);
}
这里先请求内部的: createObjectInstantiator 。
/**
* Creates a dynamically generated {@link ObjectInstantiator} for the given {@link PersistentEntity} and
* {@link PreferredConstructor}. There will always be exactly one {@link ObjectInstantiator} instance per
* {@link PersistentEntity}.
*
* @param entity
* @param constructor
* @return
*/
ObjectInstantiator createObjectInstantiator(PersistentEntity<?, ?> entity,
@Nullable PreferredConstructor<?, ?> constructor) {
try {
// 调用生成
return (ObjectInstantiator) this.generator.generateCustomInstantiatorClass(entity, constructor).newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
获取对象生成实例: generateCustomInstantiatorClass 这里获取类名称,会追加 _Instantiator_ 和对应类的 hashCode 。
/**
* Generate a new class for the given {@link PersistentEntity}.
*
* @param entity
* @param constructor
* @return
*/
public Class<?> generateCustomInstantiatorClass(PersistentEntity<?, ?> entity,
@Nullable PreferredConstructor<?, ?> constructor) {
// 获取类名称
String className = generateClassName(entity);
byte[] bytecode = generateBytecode(className, entity, constructor);
Class<?> type = entity.getType();
try {
return ReflectUtils.defineClass(className, bytecode, type.getClassLoader(), type.getProtectionDomain(), type);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
private static final String TAG = "_Instantiator_";
/**
* @param entity
* @return
*/
private String generateClassName(PersistentEntity<?, ?> entity) {
// 类名+TAG+hashCode
return entity.getType().getName() + TAG + Integer.toString(entity.hashCode(), 36);
}
到此我们元数据中的一堆 拼接了 Instantiator_xxxxx 的类来源就破案了.
对应问题产生的问题也很简单.
// 每次search前 都new了个RestTemplate,导致上下文发生变化,每次重新生成加载
new ElasticsearchRestTemplate(cluster);
这里我们是双集群模式,每次请求时会由负载决定使用那一个集群。之前在这里每次都 new 了一个待使用集群的实例.
内部的上下文每次初始化后都是空的.
请求查询ES 。
初始化ES查询 。
最终长时间运行后元数据空间溢出; 。
1.当时的临时方案是重启应用,元数据区清空,同时临时也可以放大元数据区大小.
2.元数据区的类型卸载或回收,8以后已经不使用了.
3.元数据区的泄漏排查思路:找到加载多的类,然后排查使用情况和可能的加载场景,一般在各种序列化反射场景.
4.快速排查可使用我们的方案。使用OQL来完成.
5.监控可以考虑加载类实例监控和元数据空间使用大小监控和对应报警。可以提前发现和处理.
6.ES查询在启动时对应集群内部初始化一个查询实例。使用那个集群就使用对应的集群查询实例.
VisualVM 下载地址: https://visualvm.github.io/ 。
OQL : Object Query Language 可参看 在VisualVM中使用OQL分析 。
获取路径下类加载数量,从高到低排序 。
var packageClassSizeMap = {};
// 遍历统计以最后一个逗号做分割
heap.forEachClass(function (it) {
var packageName = it.name.substring(0, it.name.lastIndexOf('.'));
if (packageClassSizeMap[packageName] != null) {
packageClassSizeMap[packageName] = packageClassSizeMap[packageName] + 1;
} else {
packageClassSizeMap[packageName] = 1;
}
});
// 排序 因为Visual VM的查询有数量限制。
var sortPackageClassSizeMap = [];
map(sort(Object.keys(packageClassSizeMap), function (a, b) {
return packageClassSizeMap[b] - packageClassSizeMap[a]
}), function (it) {
sortPackageClassSizeMap.push({
package: it,
classSize: packageClassSizeMap[it]
})
});
sortPackageClassSizeMap;
获取某个路径下类加载数量 。
var packageClassSizeMap = {};
// 遍历统计以最后一个逗号做分割
heap.forEachClass(function (it) {
var packageName = it.name.substring(0, it.name.lastIndexOf('.'));
// 加路径过滤版
if (packageName.indexOf('com.jd.bapp.match.sync.query.es.po')){
if (packageClassSizeMap[packageName] != null) {
packageClassSizeMap[packageName] = packageClassSizeMap[packageName] + 1;
} else {
packageClassSizeMap[packageName] = 1;
}
}
});
sortPackageClassSizeMap;
感谢黄仕清和Jdos同学提供的技术支持.
作者:京东零售 王建波 。
来源:京东云开发者社区 。
最后此篇关于一次元数据空间内存溢出的排查记录的文章就讲到这里了,如果你想了解更多关于一次元数据空间内存溢出的排查记录的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我有一个 div(蓝色框),它在父元素(红色框)内的页面上绝对定位,我需要将 overflow-y 设置为隐藏,以便它强制 Y 轴上的溢出内容切掉了,但我希望任何溢出-x 的内容都可见。 HTML:
请参阅以下帖子以获取突出显示我的问题和可能的解决方案的图片: CSS overflow-y:visible, overflow-x:scroll 但是,当您实际移动滚动条时,此策略会中断。在建议的实现
我在搜索中看到过几个类似的问题,但要么没有正确回答问题,要么没有给出答案。所以,我再问一次。 .parent { overflow-y:scroll; overflow-x:visible; wid
我读过这个CSS overflow-x hidden and overflow-y visible (以及很多其他帖子)但我无法在我的具体情况下使用它。 我正在使用 slick-slider并想添加下
我有以下 Spark 作业,试图将所有内容保留在内存中: val myOutRDD = myInRDD.flatMap { fp => val tuple2List: ListBuffer[(St
我有疑问 两个16位的值加上最大值,16位机会不会溢出? 我会详细说明 unsigned short a; unsigned short b; unsigned long c; c=(unsigne
我有这个 HTML 和 CSS,但“溢出:隐藏”标签在 Firefox 中不起作用。这让我感到难过...有人知道为什么它不起作用吗?是因为A标签不支持overflow标签吗? #page_sideba
我正在开发一个程序,用于在 C++ 中分解非常大的数字(20 位或更多),并且正在使用 GMP 来处理溢出问题。我的程序对于大约 10 位或更少的数字运行良好,但是当我向它抛出一个 15 位数字时,它
我创建了一个 Canvas ,并在其中放置了一个StackPanel。 StackPanel是水平的,它接受缩略图图像的列表。 Canvas 具有固定的大小。当我放置的缩略图多于Canvas宽度不能容
当 g_array_append_val() 时会发生什么或 GLib 中的其他附加/前置函数之一,使 GArray 的长度大于 guint (unsigned int) 所能容纳的长度? 文档对此没
overflow-x:hidden 和 overflow:hidden; 有什么区别? 我所知道的是overflow-x:hidden;禁用水平滚动,但当我使用它时,它不仅仅适用于 Firefox,所
我们正在运行 Solr 来索引大量数据,但遇到了一个非常有趣的问题,我无法在任何地方找到任何帮助。 似乎 Solr 使用带符号的 32 位整数来计算索引中当前的文档数。我们刚刚达到了这个数字,我们的
这是我的查询: 从相似性中选择 COUNT(*),其中 T1Similarity = 0 或 T2Similarity = 0 结果如下: Msg 8115, Level 16, State 2, L
int main(void) { char x1 = 0x81; char x2 = 0x1; int a, b; a = x1
我有一个 div,其中的内容通过查询的 append() 定期附加到它。随着内容越来越长,最终会溢出div。我不希望在溢出时出现滚动条,但仍然让内容向上滚动以显示下面的新内容。 这可能吗?当我使用 o
我为 UITextField 创建了一个简单的子类,它按预期工作。我遇到的唯一问题是当文本值变得太大时,它会溢出到清除按钮中。 我似乎无法找到如何仅更改文本的右侧以具有一些填充而不与清除按钮相交的方法
我想要一个包括下拉菜单的粘性导航栏。但是,当我将鼠标悬停在它上面时,下拉菜单没有显示。 如果我删除 overflow: hidden;在无序列表中,当我向下滚动时,导航栏设法保持在顶部,但是导航栏是不
我正在研究一些按钮。我想要一个翻转状态,我在一个 div 的图像中有这个,溢出:隐藏以隐藏不活动的状态。它有时有效,但有时看起来像这样: 最奇怪的是,当我尝试使用 Chrome Web Inspect
基本上,我正在尝试创建一个六边形形状,它内部有一个圆圈,圆圈的多余部分应该被隐藏。演示:https://codepen.io/AskSaikatSinha/pen/jwXNPJ?editors=110
这似乎是一个相当常见且不那么奇特的用例,但我以前没有遇到过。我设置了一支笔,但无法在那里复制它,我正在努力找出原因。 Demo Pen 左侧边栏有一个用于元素列表的自定义滚动窗口,但是虽然设置 ove
我是一名优秀的程序员,十分优秀!