Java 大对象的内存管理比较简单,主要集中在 LargeObjectSpace 模块,主要负责 Java 大对象的内存申请和释放。
结合源码,接下来简单介绍一下 Java 大对象申请流程,整理流程图如下。在此过程中,重点关注 Heap 已申请内存大小的更新过程和内存不足时抛出 OOM 的过程。
在上图我们可以看到,在内存实际申请过程,会首先检测当前对象是否满足大对象,具体条件是:申请对象类型必须是原子类型或者 String 类型,并且要大于 3 个物理页。
inline bool Heap::ShouldAllocLargeObject(ObjPtrmirror::Class c, size_t byte_count) const {
return byte_count >= large_object_threshold_ && (c->IsPrimitiveArray() || c->IsStringClass());
}
如果满足以上条件,则会执行大对象申请流程,从 LargeObjectMapSpace 进行申请。但是在正式申请之前,会再次判断是否内存触顶,计算规则是将 Heap 中已经申请的内存和将要申请的内存进行相加,判断是否超过虚拟机的内存上限(growth_limit_),如果没有超过则说明不会触顶,然后就会直接从 LargeObjectMapSpace 申请一块内存;如果大于 growth_limit_,则说明可能会触顶,此时要先进行 GC,争取释放一些内存,如果多轮 GC 之后仍然满足不了,则抛出 OOM。
在通过以上检测之后,将会通过LargeObjectMapSpace::Alloc 实例化一个 Object 对象,如下图:
结合上图,可以看到 LargeObjectMapSpace 在内存申请过程中如要完成以下工作:
根据申请的内存大小,利用 mem_map 映射与之对应的一块内存;
将映射的内存转换为 mirror::Object 对象;
将该 Object 对象与 mem_map 实例进行关联,并存储到 large_objects 集合;
更新 largeObjectMapSpace 当前内存占用和对象数量,以及累计占用大小和对象数量;
内存释放逻辑:根据传入的对象,先检查是否在 large_objects___集合中,如果不存在则抛出异常;否则同步更新(释放)当前内存状态,并从该集合移除该对象。
在介绍完虚拟机内存空间管理以及 Large Object Space 内存管理方式的相关背景知识后,接下来我们回归到正题,看看我们针对虚拟机是如何改造 Large Object Space 的内存管理策略的。
4. mSponge 实现原理
为了便于更好地理解,我们将整个方案分为 2 个部分进行介绍。
一期方案:主要介绍在 Java 大对象通过 LargeObjectSpace 的内存申请和释放过程中,如何在内存申请和释放过程对其进行改造,以脱离虚拟机对这些对象的内存管理,最后实现 LargeObjectSpace 占用的内存完全脱离虚拟机内存统计。
二期方案:针对一期方案需要在应用运行过程中提前开启,但是线上 99%以上运行过程中可能不会发生 OOM,因此一期方案对系统的侵入有点高。为了优化这个现象,二期方案主要通过监听应用是否发生 OOM,如监测到 OOM 则拦截并同开启一期方案“释放更多可用内存”,然后重试内存申请,以挽救本次 OOM;如果没有发生 OOM,则说明内存状态良好,该方案就不需要开启。显然,这种智能式的开启方式和最大化的内存保障效果将是更加极致的解决方案。
在运行过程中,该方案会随着 LargeObjectSpace 的使用情况动态“吸收”和“释放”虚拟机 Heap 统计内存——“吸收”不希望被虚拟机统计的 LargeObjectSpace 的内存,“释放”已经通过 GC 回收的 Large Object 内存。整个运行过程犹如海绵吸水,故将该方案命名为:Memory Sponge,寓意:内存海绵,简称mSponge。
从上面的大内存申请流程图中可以看到,如果当前内存申请满足 Java 大对象的条件(大于 12K),并在内存申请过程检测是否内存触顶时,“一直”返回 False,则可以通过 LargeObjectMapSpace 直接申请并返回对象实例,则可以绕过这里的内存触顶 OOM 问题。
同时,我们知道LargeObjectMapSpace 内部管理的对象是离散的,不支持虚拟机 GC 过程中连续内存空间的拷贝压缩的特性,因此即使是该空间内存占用过多,导致总内存超过了上限(512M?),但是其它连续内存 Space 的内存阈值仍然保持正常范围,因此不会影响到其他内存空间 GC 同构拷贝压缩能力,也就不会破坏虚拟机的内存管理。
那么如何才能在大对象内存触顶检测过程中绕开现有的检测机制呢?通过调研发现判断内存触顶的关键条件在于虚拟机中管理当前已申请内存 Heap::num_bytes_allocated_对象,即每次内存申请成功和 GC 释放时,都会同步更新该值:
inline mirror::Object* Heap::AllocObjectWithAllocator(Thread* self,
ObjPtrmirror::Class klass, size*t byte_count, AllocatorType allocator, const PreFenceVisitor& pre_fence_visitor) {
…
//实际申请内存过程
…
if (bytes_tl_bulk_allocated > 0) {
size_t num_bytes_allocated_before =
//成功申请之后,需要同步更新虚拟机整体 Heap 内存使用
num_bytes_allocated* . fetch_add ( bytes_tl_bulk_allocated , std :: memory_order_relaxed );
…
}
}
GC 过程中,当每个 Space 释放一定对象和内存之后,会进一步同步到虚拟机的 Heap 对象,同步更新虚拟机整体内存使用,接口如下:
void Heap::RecordFree(uint64_t freed_objects, int64_t freed_bytes) {
…
// Note: This relies on 2s complement for handling negative freed_bytes.
//释放之后,需要同步更新虚拟机整体Heap内存使用
num_bytes_allocated_ . fetch_sub (static_cast< ssize_t >( freed_bytes ), std :: memory_order_relaxed );
…
}
通过上面这个接口我们可以看到,每个 Space 在内存回收后都会更新虚拟机更新整体内存使用情况,那么我们是不是可以在合适的时机人为主动调用该接口,减去 LargeObjectMapSpace 管理的内存值,那么 Heap::num_bytes_allocated_统计的就全部是其他内存 Space 的内存使用了;换而言之通过 LargeObjectMapSpace 申请的内存将会“脱离虚拟机”Heap::num_bytes_allocated_的统计,纳入 Native 层的内存管理。但是这些对象的引用和内存回收机制仍然由虚拟机管理,因此并不会存在内存泄漏的隐患。
LargeObjectMapSpace申请的内存,直接通过 Map 映射到虚拟内存,因此对于 32 位环境应用空间可映射内存在 3G 左右,但虚拟机本身会抢先占用 1G+的地址空间用于管理 Java 内存,因此应用侧实际使用范围在 2G 左右,极端情况下调整后的虚拟机内存理论范围将在 512M~2.5G,至于下限为何是 512M?理论上如果发生 OOM 时虚拟机没有任何大对象,这种情况下,则虚拟机可用内存范围将保持不变,因为我们改变的 Java 大对象的内存可用空间;示意图如下:
上面介绍了该方案的背景知识和实现思路,接下来就要从技术层面考虑如何去实现了。如果在系统层面,直接从源码层面定制,相关改动会轻松很多,但是对应用侧来说,要想兼容不同 Android 版本,只有一条路可走——通过 InlineHook 代理相关接口,在执行过程中魔改相关参数以达到目的。在解决完接口代理问题之后,接下来还有下面几件事情要解决:
虚拟机并没有对外暴露获取 LargeObjectMapSpace 内存的接口,如何才能实时获取当前 Space 已申请的内存大小?
如何在合适的时机同步 Heap::num_bytes_allocated_内存统计,以便于让 LargeObjectMapSpace 的内存"脱离"虚拟机的统计?
如何"跳过"虚拟机在内存释放过程对内存大小一致性校验的问题?
针对第一个问题,尽管 LargeObjectSpace 中提供了获取当前内存大小的接口(LargeObjectSpace::GetBytesAllocated),但是这个接口并没有对外暴露,因此需要通过解析 Libart.so 中的"GetBytesAllocated"符号,以 Android Q 为例,该函数签名符号为:_ZN3art2gc5space16LargeObjectSpace17GetBytesAllocatedEv;并在运行过程中动态获取该符号在内存中的地址。
由于 GetBytesAllocated 是非静态函数,因此在实际调用该接口时,需要知道当前对象的实例化对象,然后通过实例化对象调用该接口即可,在这里,我们通过 inlineHook 代理"LargeObjectMapSpace::Alloc"获取 LargeObjectMapSpace 的实例,LargeObjectMapSpace::Alloc 接口部分源码如下:
mirror::Object* LargeObjectMapSpace::Alloc(Thread* self, size_t num_bytes, size_t* bytes_allocated, size_t* usable_size, size_t* bytes_tl_bulk_allocated) {
std::string error_msg;
MemMap mem_map = MemMap::MapAnonymous(“large object space allocation”,
num_bytes,
PROT_READ | PROT_WRITE,
/low_4gb=/ true,
&error_msg);
…
//申请成功后将当前内存占用+ allocation_size
num_bytes_allocated_ += allocation_size ;
total_bytes_allocated_ += allocation_size;
++ num_objects_allocated_ ; //申请成功后将当前内存数量+1
++total_objects_allocated_;
return obj;
}
在获取 LargeObjectMapSpace 的实例化对象之后,再通过该对象直接调用 GetBytesAllocated即可实时获取当前 LargeObjectMapSpace 的内存大小。
当我们可以实时获取 LargeObjectSpace 的内存使用之后,接下来便是如何从虚拟机 Heap 中“移除”LargeObjectSpace 实际占用的内存了,通过调研发现可以通过解析"Heap::RecordFree"函数符号,并调用Heap::RecordFree 的函数接口,增加或减少 Heap 中我们想要更新的内存大小。
void Heap::RecordFree(uint64_t freed_objects, int64_t freed_bytes) {
…
// Note: This relies on 2s complement for handling negative freed_bytes.
num_bytes_allocated_.fetch_sub (static_cast< ssize_t >( freed_bytes ), std :: memory_order_relaxed );
…
}
当上述条件满足我们可以灵活更新虚拟机 Heap 内存之后,接下来要处理的就是选择一个合适的时机直接“移除”LargeObjectSpace 的内存,并且需要更新记录当前 Heap“移除”的内存大小,经过调研并考虑到及时性和精准性,最终选择了在 LargeObjectSpace 的内存申请和回收过程,对虚拟机 Heap 内存进行动强制更新,以移除虚拟机对 LargeObjectSpace 的内存统计。
在 Large Object 申请过程,如果内存申请成功,则在该 Object 实例化对象返回之前,先强制从虚拟机内存统计中减去该部分内存,接下来虚拟机内部会在返回实例化对象之后,并统计本次新增内存,在这里我们通过先减后加的方式,维持了整个内存水位不变,从而间接地实现了虚拟机“忽略”了本次内存开销。
如果后续 GC 过程中释放了 LargeObjectSpace 中的部分或者全部对象,正常情况下释放的内存会同步同步到 Heap,以便于更新整体使用内存及可用内存,但是从上面的分析中我们知道,其实 LargeObjectSpace 的内存已经不在 Heap 统计之中了,如果从 Heap 中减去释放的这些内存,那么将会导致 Heap 统计的内存偏少,因此需要主动将该部分释放的内存"补偿"回来,避免统计错乱。
通过上述步骤实现了在内存回收过程中对大对象内存管理的改造,改造之后 Heap 统计的内存将不再包含 LargeObjectSpace 管理的内存,从而间接地扩大了其他内存 Space 使用上限;对于 LargeObjectSpace 来说,虚拟机统计到的该内存 Space 一直为 0,但是 LargeObjectSpace 内部并没有内存限制,异常该 Space 的内存使用上限将会显著提升,针对 Android O 以下系统来说,这部分内存 Space 不仅包含 Bitmap,还包含其他大对象。
在适配过程中,发现 Android L 版本之后,虚拟机会在 GC 过程中对释放内存和使用内存进行一次校验。如果发现当前使用内存加上释放内存小于 GC 之前的内存,则会抛出“断言”异常,相关源码如下:
void Heap::GrowForUtilization(collector::GarbageCollector* collector_ran,
uint64_t bytes_allocated_before_gc) {
//GC结束后,再次获取当前虚拟机内存大小
const uint64_t bytes_allocated = GetBytesAllocated();
…
if (!ignore_max_footprint_) {
const uint64_t freed_bytes = current_gc_iteration_.GetFreedBytes() +
current_gc_iteration_.GetFreedLargeObjectBytes() +
current_gc_iteration_.GetFreedRevokeBytes();
//GC之后虚拟机已使用内存加上本次GC释放内存理论上要大于等于GC之前虚拟机使用的内存,如果不满足,则抛出Fatel异常!!!
CHECK_GE ( bytes_allocated + freed_bytes , bytes_allocated_before_gc );
}
…
}
因为我们在内存 GC 过程中,动态调整了 Heap 当前使用内存大小,这可能会导致 gc 结束后再次获取的 Heap 当前使用内存小于实际值,为了不影响校验逻辑,需要代理 Heap::GrowForUtilization 接口,强制将bytes_allocated_before_gc 参数设置为 0,以保证校验恒成立(针对该处调整从后续逻辑和实际测试来看,对后续内存 GC 并无明显影响)。
至此,通过上述思路和技术方案完成了 Android 虚拟机内存统计策略的改造,该方案不仅间接提升了虚拟机其它内存空间运行时的使用上限,也将 LargeObjectSpace 的内存使用上限完全脱离了虚拟机的限制,完全等同于 Native 内存属性进行管理。因此该方案相比 Android 系统 Bitmap 内存管理改造更加彻底,给应用的内存环境带来了极大的改善。
在上文,对内存统计策略改造之后可以很大程度释放 LargeObjectSpace 内存空间以优化 Java OOM 问题,但是进一步思考之后,发现一期方案并不是最优解,因为应用运行过程中很大概率不会发生 OOM,如果能监听到 OOM 时,再启动优化方案,同时再救活本次 OOM,那么这种智能化的按需开启将是极致化的解决方案。
针对上述思考,基于 mSponge 方案一期的设计,决定采用“OOM 探测+按需开启”的策略来完成对内存的按需扩展。即:对内存申请过程进行定向监控,当监听到内存不足即将抛出 OOM 异常时,进行拦截,并激活 mSponge 方案一期内存优化方案(从 Heap 内存统计中移除当前 LargeObjectSpace 使用内存);然后再触发一次内存申请,以保证内存成功申请。按照上述思路,整理方案优化前后对比示意图:
基于上面的思路,我们需要在虚拟机内部监听并拦截 OOM,当监听到第一次 OOM 时主动将 LargeObjectSpace 的内存从 Heap 统计中移除,以增加空闲内存,同时再开启 mSponge 一期优化策略,以保证后续 LargeObjectSpace 的内存变化不会影响 Heap 内存统计;按照这种思路,整体二期方案示意图如下:
二期技术实现主要涉及下面几个流程:监听并判断是否需要拦截 OOM 异常;监听内存分配结果;重试内存申请。具体如下:
监听 OOM 异常:代理 Heap::ThrowOutOfMemoryError,监听内存分配失败时抛出 OOM 的过程,并判断是否需要拦截,如果可拦截。
监听内存分配结果:代理 Heap::AllocateInternalWithGc ,监听本次内存申请过程中,是否发生了 OOM 并被拦截,如果发生 OOM 并被拦截,则再次触发内存申请,以保证内存申请成功。
重试内存申请:通过 AllocateInternalWithGc 再次触发一次内存申请,并在此之前禁止拦截本次内存申请过程中可能抛出的 OOM,如果成功申请,则返回该对象。
通过 inlineHook 代理"Heap::ThrowOutOfMemoryError"并监听该接口,如果该接口被调用则说明,当前内存不足或者没有连续内存,无法满足本次内存需求;并根据调用 AllocateInternalWithGc 代理接口设置的标识"sAllowsSkipThrowOutOfMemoryError",判断是否拦截本次 OOM 异常;实现如下:
void ThrowOutOfMemoryErrorProxy(void* heap, void* self, size_t byte_count, AllocatorType allocator_type){
if(isAllowWork()) {
sFindThrowOutOfMemoryError = true;
if (sAllowsSkipThrowOutOfMemoryError
&& !sForceAllocateInternalWithGc) {
//拦截并跳过本次OutOfMemory,并置标记位
sSkipThrowOutOfMemoryError = true;
//TODO:将LargeObjectSpace内存从Heap中移除
return;
}
sSkipThrowOutOfMemoryError = false;
//如果不允许拦截,则直接调用原函数,抛出OOM异常
ThrowOutOfMemoryErrorOrigin(heap, self, byte_count, allocator_type);
} else{
ThrowOutOfMemoryErrorOrigin(heap, self, byte_count, allocator_type);
}
}
通过 inlineHook 代理"Heap::AllocateInternalWithGc"代理并监听该接口,因为在该接口执行过程中会触发一次或多次 GC,如果依然满足不了本次内存申请,则会抛出 OOM 并返回 NULL;因此可以通过代理该接口可以知道本次内存申请是否成功,以及本次申请过程中是否抛出 OOM 异常;如果返回对象为 NULL,并拦截了 OOM 异常,则设置禁止拦截 OOM 标记之后,调用 Heap::AllocateInternalWithGc 原接口再次进行内存申请,以保证成功申请内存。
原则上通过 mSpnge 方案将 LargeObjectSpace 的内存从 Heap 移除之后,理论上虚拟机可用内存会增加很多,基本能保证本次内存成功申请(极端情况仍会出现内存不足,正常抛出 OOM 即可)。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
进阶学习视频
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
进阶学习视频
[外链图片转存中…(img-szf0rMsV-1713623548384)]
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-W4Kd2wPF-1713623548385)]
里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
文章浏览阅读115次。婴幼儿配方奶粉是作为宝宝在无法母乳喂养时的无奈选择,也是除母乳以外最好的选择,宝妈在选择奶粉时也会格外认真,当看到奶粉中含有香精香料成分时,部分宝妈便不会购买,甚至认为此类成分是对宝宝身体有所危害的,所以坚决抵制购买,那么添加到奶粉中的香精香料到底怎么样,长期吃有没有坏处?我们来看看吧~不同人群,对香精香料有不同看法"宝妈们这样说:前段时间网上一位宝妈的评论吸引了笔者的注意:“我这奶粉买回家一开罐...
文章浏览阅读2.5k次。并不是苹果说的触点问题,而是软布导线被腐蚀断线问题。用铜箔胶带粘一下就好了。不过二手直接买一个咸鱼上也就三百多,也不贵。这个明显是苹果设计缺陷。直接丢了挺可惜的。拆的时候小心点。我的QQ: 13008312096,有空的话可以帮忙有偿代处理。参考链接如下:nullhttps://www.youtube.com/watch?v=buNYHzMZJdk修复好的样子,毫无违和感:..._ipad10.5键盘套没反应
文章浏览阅读1.3w次,点赞3次,收藏13次。网上下载的gif图很多都不透底,而且尺寸时间什么的都不合适,用ps简单修改一下就好了1.抽帧很多gif图帧数很多,抽帧可以大幅压缩gif大小1.导入gif到ps2.勾选动作和时间轴面板这时我们发现帧和图层的隐藏显示是相对应的这样的话我们操作图层就会打乱帧,我们要让所有的图层都显示,但又不干扰帧的显示3.这时我们就要把它转换为视频时间轴4.然后再转换帧>>转换为..._gif抽帧
文章浏览阅读235次。通过本文将了解到以下内容:优先队列的概念优先队列的实现优先队列的应用1.优先队列的概念优先队列是计算机科学中的一类抽象数据类型。优先队列中的每个元素都有各自的优先级,优先级最高的元素最先得到服务;优先级相同的元素按照其在优先队列中的顺序得到服务。优先队列至少需要支持下述操作:a.插入带优先级的元素b.取出具有最高优先级的元素c.查看最高优先级的元素。综合考虑插入和删除的性能 优先队列一般采用堆来实..._优先队列 c++面试
文章浏览阅读281次。windows系统VMware安装Linux虚拟机、配置JDK环境、安装tomcat、安装mysql、安装redis第一部分VMware安装Linux—————————————————————————————————xshell、VMware工具链接:解压之后安装VMware工具xshell不需要安装解压可直接使用..._vm中linux下载安装mysql jdk
文章浏览阅读1k次。# -*- coding: utf-8 -*-import hashlib'''加密解密'''#MD5是最常见的摘要算法,速度很快,生成结果是固定的128 bit字节,# 通常用一个32位的16进制字符串表示。md5 = hashlib.md5()update = md5.update('hhhhhaaa')print(md5.hexdigest())#SHA1的结果是160..._咕咕加密v4
文章浏览阅读10w+次,点赞112次,收藏974次。Ubuntu18.04镜像_ubuntu系统18.04
文章浏览阅读6.7k次,点赞3次,收藏17次。一、Json字符串和Json对象定义:1、Json字符串:所谓字符串:单引号或者双引号引起来,是一个String类型的字符串:如下:var person='{"name":"shily","sex":"女","age":"23"}';//json字符串console.log(person)console.log(person.name)console.log(typeof person) 2、Json对象:最显著的特征:对象的值可以用 “对象.属性” 进行访问,_json字符串数组
文章浏览阅读718次。CentOS 7:ip link set interface_name up 或 ip link set interface_name down。Ubuntu:ifconfig interface_name up 或 ifconfig interface_name down。CentOS 7:编辑 /etc/sysconfig/network-scripts/ifcfg-eth0 文件。Ubuntu:编辑 /etc/network/interfaces 文件。_ubuntu 命令跟centos
文章浏览阅读652次。jpegsr9e windows vs2019生成方法,以及库下载_jpeg library error vs2019
文章浏览阅读647次。总的来说,华为Mate 60/Pro系列手机的高速网速表现引起了广泛的关注,这也是消费者对该系列手机购买热情高涨的一个重要因素。可以看出,华为Mate 60/Pro系列手机的网速表现非常出色,这也是消费者购买该系列手机的一个重要原因。此前,华为Mate 60 Pro的供应量已经增至1500万至1700万台,而最新消息称,华为Mate 60 Pro和Mate 60 Pro+的出货量甚至已上调至2000万台。目前,在中国市场上,手机竞争愈发激烈,不仅华为Mate 60系列,其他品牌的手机也都受到了高温的迎接。_华为mate60pro+核实网络
文章浏览阅读7.1k次。公告: 为响应国家净网行动,部分内容已经删除,感谢读者理解。话题:access怎样利用出生日期计算年龄呀!回答:lt;%set rs = server.createobject("adodb.recordset") curid=request("id") sql = "UPDATE pany SET a_num=a_num+1,day_count=day_count+1 WHERE day_lda..._access出生年份表达式