性能优化|记一次线上OOM问题处理
概述
最近线上监控发现 OOM 涨幅较大,因此去尝试定位和修复这个问题,在修复了一些内存泄漏和大对象占用问题后, OOM 依旧未达到正常标准,在这些新上报的 hprof 文件中,发现几乎所有 case 中都有个叫 FinalizerReference 的对象,数量巨多,内存占用高居首位,因此判断它就是引起本次 OOM 上涨的罪魁祸首。

ReferenceQueue
首先前置了解下 ReferenceQueue 引用队列是个啥,简言之就是用来存放 Reference 对象的队列,当 Reference 对象所引用的对象被 GC 回收时,该 Reference 对象就会被加入到引用队列 ReferenceQueue 中。即:
valqueue= ReferenceQueue<Bean>()
valbean= Bean()
valreference= SoftReference(bean, queue)
复制代码上述代码中创建了一个 bean 强引用,以及一个 reference 软引用,当 bean 被回收时,该软引用 reference 对象会被放到 queue 队列中,后续该 reference 对象需要开发者自行处理(如从队列中 poll 等)。
Leakcanary 检测内存泄漏的原理就是应用的 ReferenceQueue 引用队列,以 Activity 为例:
监听 Activity 的生命周期。
在 onDestory 的时候,通过 ReferenceQueue 来创建对应 Actitity 的 Refrence 引用对象。
一段时间后,从 ReferenceQueue 中读取,如果有这个 Actitity 的 Refrence,那么说明这个 Activity 的 Refrence 已经被回收;如果 ReferenceQueue 没有这个 Actitity 的 Refrence, 那就说明出现了内存泄漏。
关于 Reference 引用,网上一搜很多文章,大家估计比较清楚了,就不再赘述。以前做过一些相关的笔记,有兴趣可以看看 JVM垃圾回收-引用。
FinalizerReference
首先介绍一下 Finalizer 对象,它指的是在其 Java 类中复写了 finalize() 方法,且该方法非空的对象,有的地方称呼这种类叫 f 类,我们也这样叫它。在类加载过程中会把加载的 Java 类标记为是否 f 类。
FinalizerReference概述
现在开始看看 FinalizerReference 是个啥玩意,知己知彼,才能知道怎么去修它。这个 FinalizerReference 是用来协助 FinalizerDaemon 线程处理对象的 finalize() 工作的,一次性出现了三个名词,我们先从 FinalizerReference 看起。
看看它的部分代码:
// java/lang/ref/FinalizerReference.javapublicfinalclassFinalizerReference<T> extendsReference<T> {// This queue contains those objects eligible for finalization.publicstaticfinal ReferenceQueue<Object> queue = newReferenceQueue<Object>();// This list contains a FinalizerReference for every finalizable object in the heap.// Objects in this list may or may not be eligible for finalization yet.privatestatic FinalizerReference<?> head = null;// The links used to construct the list.private FinalizerReference<?> prev;private FinalizerReference<?> next;publicstaticvoidadd(Object referent) {FinalizerReference<?> reference = newFinalizerReference<Object>(referent, queue);synchronized (LIST_LOCK) {reference.prev = null;reference.next = head;if (head != null) {head.prev = reference;}head = reference;}}publicstaticvoidremove(FinalizerReference<?> reference) {synchronized (LIST_LOCK) {FinalizerReference<?> next = reference.next;FinalizerReference<?> prev = reference.prev;reference.next = null;reference.prev = null;if (prev != null) {prev.next = next;} else {head = next;}if (next != null) {next.prev = prev;}}}
}
复制代码FinalizerReference 中有两个静态方法: add 和 remove 方法。它创建了一个 FinalizerReference 类型的链表(表头用静态变量 FinalizerReference.head 表示),其中每个 FinalizerReference 对象都使用 ReferenceQueue 类型的静态变量 queue 来创建,这样当对应的对象 Object referent 被回收后,该 FinalizerReference 会被放入 ReferenceQueue 中。
FinalizerReference.add
上述 FinalizerReference.add 方法是虚拟机调用的,当创建对象的时候,如果发现该类是 f 类,就会调用 FinalizerReference.add 方法创建 FinalizerRefence 对象,并将其加入到 head 链表中。
// art/runtime/mirror/class-alloc-inl.htemplate<bool kIsInstrumented, Class::AddFinalizer kAddFinalizer, bool kCheckAddFinalizer>
inline ObjPtr<Object> Class::Alloc(Thread* self, gc::AllocatorType allocator_type){CheckObjectAlloc();gc::Heap* heap = Runtime::Current()->GetHeap();bool add_finalizer;switch (kAddFinalizer) {case Class::AddFinalizer::kUseClassTag:// 这里判断是不是 f 类add_finalizer = IsFinalizable();break;case Class::AddFinalizer::kNoAddFinalizer:add_finalizer = false;DCHECK_IMPLIES(kCheckAddFinalizer, !IsFinalizable());break;}// Note that the `this` pointer may be invalidated after the allocation.ObjPtr<Object> obj = heap->AllocObjectWithAllocator<kIsInstrumented, /*kCheckLargeObject=*/false>(self, this, this->object_size_, allocator_type, VoidFunctor());if (add_finalizer && LIKELY(obj != nullptr)) {// 如果是 f 类,则调用 AddFinalizerReference 方法heap->AddFinalizerReference(self, &obj);// ...}return obj;
}// art/runtime/gc/heap.ccvoidHeap::AddFinalizerReference(Thread* self, ObjPtr<mirror::Object>* object){ScopedObjectAccess soa(self);ScopedLocalRef<jobject> arg(self->GetJniEnv(), soa.AddLocalReference<jobject>(*object));jvalue args[1];args[0].l = arg.get();InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_FinalizerReference_add, args);// Restore object in case it gets moved.*object = soa.Decode<mirror::Object>(arg.get());
}
复制代码上面的 AddFinalizerReference 方法会调用到 Java 层的 FinalizerReference.add() 静态方法,就完成了 add 的操作。
FinalizerReference.remove
我们前面提到,当 Reference 对象所引用的对象被 GC 回收时,该 Reference 对象就会被加入到引用队列 ReferenceQueue 中,所以当 f 类的对象发生 gc 时,会将其对应的 FinalizerReference 对象加入到 FinalizerReference.queue 队列里。而 remove 的时机与 FinalizerDaemon 守护线程有关,我们看看这个线程的源码:
// libcore/libart/src/main/java/java/lang/Daemons.java// private static abstract class Daemon implements RunnableprivatestaticclassFinalizerDaemonextendsDaemon {privatestaticfinalFinalizerDaemonINSTANCE=newFinalizerDaemon();privatefinal ReferenceQueue<Object> queue = FinalizerReference.queue;@OverridepublicvoidrunInternal() {// ...while (isRunning()) {try {// Use non-blocking poll to avoid FinalizerWatchdogDaemon communication when busy.FinalizerReference<?> finalizingReference = (FinalizerReference<?>)queue.poll();// ...doFinalize(finalizingReference);} catch (InterruptedException ignored) {} catch (OutOfMemoryError ignored) {}}}
}
复制代码在 FinalizerDaemon.runInternal 方法中会通过 FinalizerReference.queue(ReferenceQueue引用队列) 的 poll/remove 方法拿到 queue 中的 Reference 引用,接着执行 doFinalize() 方法,该方法会调用 Finalizer 对象的 finalize() 方法:
privatevoiddoFinalize(FinalizerReference<?> reference) {FinalizerReference.remove(reference);Objectobject= reference.get();reference.clear();try {object.finalize();} catch (Throwable ex) {// The RI silently swallows these, but Android has always logged.System.logE("Uncaught exception thrown by finalizer", ex);} finally {// Done finalizing, stop holding the object as live.finalizingObject = null;}
}
复制代码首先将获取到的 FinalizerReference 对象从 FinalizerReference.head 链表中移除,接着通过 reference.get() 方法得到 Java 对象,并执行其 finalize() 方法。
小结
FinalizerReference 的主要作用是协助 FinalizerDaemon 守护线程来执行 Finalizer 对象的 finalize() 方法。
在 f 类创建对象时,调用 FinalizerReference.add() 方法创建一个 FinalizerReference 对象(使用 ReferenceQueue 队列)并加入到 head 链表里。
在 f 类的对象被回收后,其对应的 FinalizerReference 对象会被加入到 ReferenceQueue 队列(FinalizerReference.queue)里。
FinalizerDaemon 守护线程从 FinalizerReference.queue 队列中取出 FinalizerReference 对象,并执行其对应 f 类对象的 finalize() 方法。
ReferenceQueueDaemon
上面看完了 FinalizerDaemon 守护线程,这里再看看 ReferenceQueueDaemon 守护线程,上面说到在创建引用对象 Reference 的时候可以关联一个 ReferenceQueue 队列,当被引用对象被 gc 回收时,该 reference 对象就会被加入到其创建时关联的队列去,这个加入队列的操作就是由 ReferenceQueueDaemon 守护线程完成的。
privatestaticclassReferenceQueueDaemonextendsDaemon {@OverridepublicvoidrunInternal() {while (isRunning()) {Reference<?> list;try {synchronized (ReferenceQueue.class) {if (ReferenceQueue.unenqueued == null) {progressCounter.incrementAndGet();do {ReferenceQueue.class.wait();} while (ReferenceQueue.unenqueued == null);progressCounter.incrementAndGet();}list = ReferenceQueue.unenqueued;ReferenceQueue.unenqueued = null;}} catch (InterruptedException e) {continue;} catch (OutOfMemoryError e) {continue;}ReferenceQueue.enqueuePending(list, progressCounter);}}
}
复制代码具体源码感兴趣的话可以自己去看看,这里就不贴太多源码了。
FinalizerWatchdogDaemon
这里再补充一下 FinalizerWatchdogDaemon 守护线程,它跟 FinalizerDaemon 以及 ReferenceQueueDaemon 线程是一起 start 的。我们再看一下上面 FinalizerDaemon 和 ReferenceQueueDaemon 线程的 runInternal() 方法:
// ReferenceQueueDaemon@OverridepublicvoidrunInternal() {FinalizerWatchdogDaemon.INSTANCE.monitoringNeeded(FinalizerWatchdogDaemon.RQ_DAEMON);while (isRunning()) {Reference<?> list;try {synchronized (ReferenceQueue.class) {if (ReferenceQueue.unenqueued == null) {progressCounter.incrementAndGet();FinalizerWatchdogDaemon.INSTANCE.monitoringNotNeeded(FinalizerWatchdogDaemon.RQ_DAEMON);do {ReferenceQueue.class.wait();} while (ReferenceQueue.unenqueued == null);progressCounter.incrementAndGet();FinalizerWatchdogDaemon.INSTANCE.monitoringNeeded(FinalizerWatchdogDaemon.RQ_DAEMON);}list = ReferenceQueue.unenqueued;ReferenceQueue.unenqueued = null;}} catch (InterruptedException e) {continue;} catch (OutOfMemoryError e) {continue;}ReferenceQueue.enqueuePending(list, progressCounter);FinalizerWatchdogDaemon.INSTANCE.resetTimeouts();}
}// FinalizerDaemon@OverridepublicvoidrunInternal() {FinalizerWatchdogDaemon.INSTANCE.monitoringNeeded(FinalizerWatchdogDaemon.FINALIZER_DAEMON);while (isRunning()) {try {FinalizerReference<?> finalizingReference = (FinalizerReference<?>)queue.poll();if (finalizingReference != null) {finalizingObject = finalizingReference.get();} else {finalizingObject = null;FinalizerWatchdogDaemon.INSTANCE.monitoringNotNeeded(FinalizerWatchdogDaemon.FINALIZER_DAEMON);finalizingReference = (FinalizerReference<?>)queue.remove();finalizingObject = finalizingReference.get();FinalizerWatchdogDaemon.INSTANCE.monitoringNeeded(FinalizerWatchdogDaemon.FINALIZER_DAEMON);}doFinalize(finalizingReference);} catch (InterruptedException ignored) {} catch (OutOfMemoryError ignored) {}}
}
复制代码上面的 monitoringNotNeeded 方法会休眠线程,停止 timeout 计时,而 monitoringNotNeeded 会唤醒 FinalizerWatchdogDaemon 守护线程。看看 FinalizerWatchdogDaemon 的源码,我加了一些注释:
privatestaticclassFinalizerWatchdogDaemonextendsDaemon {// Single bit values to identify daemon to be watched.// 监控的两种类型staticfinalintFINALIZER_DAEMON=1;staticfinalintRQ_DAEMON=2;@OverridepublicvoidrunInternal() {while (isRunning()) {// sleepUntilNeeded 内部会调用 wait() 方法,不贴了if (!sleepUntilNeeded()) {continue;}finalTimeoutExceptionexception= waitForProgress();if (exception != null && !VMDebug.isDebuggerConnected()) {// 抛出异常 Thread.currentThread().dispatchUncaughtException(exception)timedOut(exception);break;}}}// 去掉 whichDaemon 的监控,进入 waitprivatesynchronizedvoidmonitoringNotNeeded(int whichDaemon) {activeWatchees &= ~whichDaemon;}// 添加 whichDaemon 的监控, notify 线程privatesynchronizedvoidmonitoringNeeded(int whichDaemon) {intoldWatchees= activeWatchees;activeWatchees |= whichDaemon;if (oldWatchees == 0) {// 调用 notify 唤醒notify();}}// 判断是否添加了指定 whichDaemon 类型的监控privatesynchronizedbooleanisActive(int whichDaemon) {return (activeWatchees & whichDaemon) != 0;}// 调用 isActive 判断开启监控的类型,通过 Thread.sleep() 方法休眠指定时间// 休眠结束后返回 TimeoutException 异常,若途中监控的逻辑执行完成则表示未超时,返回 null// 等待时间: VMRuntime.getFinalizerTimeoutMs// If the FinalizerDaemon took essentially the whole time processing a single reference,// or the ReferenceQueueDaemon failed to make visible progress during that time, return an exception.private TimeoutException waitForProgress() {finalizerTimeoutNs = NANOS_PER_MILLI * VMRuntime.getRuntime().getFinalizerTimeoutMs();// ...}
}
复制代码上面一些细节代码就不贴出了, FinalizerWatchdogDaemon 主要用来监控两种类型的执行时长: FINALIZER_DAEMON 和 RQ_DAEMON。当执行超时时就会抛出 TimeOutException 异常,因此不要在 finalize() 方法中做耗时操作。
这个超时时间在 AOSP 中定义为 10s, 国内厂商可能会修改这个值, 据说有的改为了 60 s, 有兴趣的同学可以自己解包看看:
RUNTIME_OPTIONS_KEY (unsigned int, FinalizerTimeoutMs, 10000u)
复制代码OOM排查
通过线上 hprof 文件排除了大对象和内存泄漏的可能,接着在 hprof 中发现存在大量的 X(用 X 表示业务上的某个对象) 对象堆积,这些对象对应的 Java 类是一个与 Native 层有关的类,其重写了 finalize() 方法,线下无法复现堆积大量 X 对象的路径。
可能有某个业务场景的代码逻辑使用不当造成了 X 对象的疯狂创建,导致 FinalizerDaemon 线程来不及回收
由于这个 X 对象的创建场景比较多,因此无法通过代码 review 来定位到有问题的创建代码,采取了一些监控手段后依旧无果。
用治标不治本的方式,显式调用系统 gc 和 runFinalization 方法
在定位不到具体的问题场景后,因为猜测是创建了大量的 X 对象导致 FinalizerDaemon 线程回收不及时,因此分别在主线程和子线程中显式调用系统 gc 和 runFinalization 方法来手动回收 X 对象。
结果:子线程调用无效果, OOM 未下降;主线程调用发生了 ANR。
查看 ANR 堆栈后,发现 ANR 的位置都在另外一个 f 对象的 finalize() 方法中调用到的一行 Native 代码,因此问题就明了了,是因为在某个 finalize() 方法中调用到的这行代码卡死了(逻辑问题导致死锁),因此阻塞了 FinalizerDaemon 线程的执行,进而导致对象堆积。另外本应发生的 TimeoutException 异常被我们的异常处理框架捕获了,因此未曾暴露出来。
总结
Java 中并没有析构函数, finalize() 实现了类似析构函数的概念,可以在对象被回收前做一些回收性的操作,在 JNI 开发中可能被用来进行 Native 内存的释放。关于 f 类使用不当会造成的影响:
FinalizerDaemon 守护线程的优先级不高,在 CPU 紧张的情况下调度可能会被影响,所以可能无法及时回收 f 类对象。
finalize 对象由于 finalize() 的引用,它变成了一个强引用,即使没有其他显式的强引用了,它也还是无法立即被回收。
因此 f 对象至少需要 2 次 gc 才可能被回收,第一次 gc 时会将 f 对象加入到 ReferenceQueue 中,而等到 finalize() 方法执行完毕后的下一次 gc 里才有可能回收它。由于守护线程的优先级低,这期间可能已经发生过多次 gc 了,长时间未回收的话又可能会导致 f 类在资源紧张时进入到老年代,从而引起老年代的 gc 甚至是 full gc。
因此尽量不要重载 finalize() 方法,而是通过一些逻辑上的接口去释放内存,如果一定要重载它的话,也不要让一些需要频繁创建的对象或者大对象去通过 finalize() 释放,不然总有一天会出现问题的。
与此相关的守护线程有四个,感兴趣的同学可以深入看看相关源码:
// libcore/libart/src/main/java/java/lang/Daemons.javaprivatestaticfinal Daemon[] DAEMONS = newDaemon[] {HeapTaskDaemon.INSTANCE,ReferenceQueueDaemon.INSTANCE,FinalizerDaemon.INSTANCE,FinalizerWatchdogDaemon.INSTANCE,
};前段时间整理收集的关于Android性能优化的知识脑图总结和学习手册文档!既能够夯实底层原理、性能调优等核心技术点,又能够掌握普通开发者,难以触及的架构设计方法论。那你在工作中、团队里、面试时,也就拥有了同行难以复制的核心竞争力。需要完整版的朋友,可后台私信【性能优化】!!

Android性能分析与优化实战进阶手册
Android性能分析与优化实战进阶手册目录及内容展示
第一章:Android性能优化概述
第二章:卡顿优化
第三章:启动速度优化
第四章:内存优化
第五章:布局优化
第六章:线程优化
第七章:电量优化
第八章:稳定性优化

相关文章:
性能优化|记一次线上OOM问题处理
概述最近线上监控发现 OOM 涨幅较大,因此去尝试定位和修复这个问题,在修复了一些内存泄漏和大对象占用问题后, OOM 依旧未达到正常标准,在这些新上报的 hprof 文件中,发现几乎所有 case 中都有个叫 FinalizerReference 的对象&…...
Vue动态粒子特效插件(背景线条吸附动画)
目录 效果图: 一、安装: 二、引入 main.js 文件: 三、使用: 四、属性说明: 效果图: 一、安装: npm install vue-particles --save 二、引入 main.js 文件: import VueParticles…...
【Java 类】002-类、属性、方法、代码块
【Java 类】002-类、属性、方法、代码块 文章目录【Java 类】002-类、属性、方法、代码块一、类1、类与对象2、类的作用3、创建与使用类类结构创建类调用类运行结果4、Java 类的执行过程5、封装、继承、多态、抽象类、内部类、接口、枚举、记录、注解等二、属性1、概述2、类型3…...
Ubuntu Linux 编译安装的基本步骤
文章目录1 基本步骤若报错: No such file or directory2 解压 tar.bz2文件参考:1 基本步骤 解压: tar -zxvf file.tar.gz 进入解压后的文件夹: cd file 源码编译安装 ./configure # ./configmakesudo make install 若报错&…...
day59反刍笔记
1.本地环境安装vue后,没有vue.js文件,只有vue.cjs.js文件, 引用后也无法正常使用,看npm install vue后的文件夹中没有vue.js文件_找不到vue.js_一枝风的博客-CSDN博客 老哥的博客后得到启发,将原本的命令由࿱…...
【阅读笔记】你不知道的Javascript--强制类型转换4
目录强制类型转换基本概念JSONboolean强转归纳其他知识点被诟病的安全使用隐式强转法则抽象关系比较语法表达式变动强制类型转换 基本概念 类型转换发生在静态类型语言的编译阶段; 强制类型转换则发生在动态类型语言的运行时(runtime) JSON…...
华为OD机试真题Python实现【有效子字符串】真题+解题思路+代码(20222023)
有效子字符串 题目 输入两个字符串S和L,都只包含小写字母, S长度 <= 100,L长度 <= 500000, 判断S是否是L的有效子字符串, 判定规则:S中的每个字符在L中都能找到(可以不连续) 且S在L中字符的前后顺序与S中顺序要保持一致 例如: S="ace"是L="abcd…...
上门按摩预约APP源码-东郊到家源码(开发,PHP,平台搭建)
一、什么是上门按摩预约APP源码? 上门按摩预约APP源码是一款家政服务类型的APP,可以帮忙用户在家就能享受按摩的服务。APP源码分两端,一端是用户端,另外一端是技师端。采用的技术,前端是安卓IOS,后端是PHP&…...
STL讲解——模拟实现vector
STL讲解——模拟实现vector vector深度剖析 在STL源码中,发现vector定义的并不是 start、size、capacity,而是start、finish、end_of_storage. 这样就可以得到size()和capacity()。 sizefinish-start capacityend_of_storage-start 扩容可能是本地扩容也…...
各种经典排序算法介绍及实现源码
一,冒泡排序(Bubble Sort) 排序算法是程序员必须了解和熟悉的一类算法,排序算法有很多种,基础的如:冒泡、插入、选择、快速、归并、计数、基数和桶排序等。 冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求,如果不…...
历史大讲堂:这是真·图形化 苹果系统历史回顾(上)
众所周知,米国有个非常牛掰的公司叫苹果,想必大家对这个logo不陌生吧。 目前已发布的苹果产品有iPhone、iPad、iPod等等,简直花样繁多,而且各种功能很好用,我的手机就是一部苹果iPhone X。 等一下,似乎扯远…...
今天女神节,用python画个贺卡送给母亲吧
今天女神节,你给女神妈妈准备了什么祝福呢?如果还没有,那么画个贺卡送给她吧,在你眼里,她是一个什么样的人呢? 是"可爱",“温柔”,“美丽”,“漂亮”…...
【编程基础之Python】11、Python中的表达式
【编程基础之Python】11、Python中的表达式Python中的表达式表达式与运算符算术表达式赋值表达式比较表达式逻辑表达式位运算表达式总结Python中的表达式 在Python中,表达式是由操作数、运算符和函数调用等组成的语法结构,可以进行各种数学运算、逻辑判…...
华为OD机试真题Python实现【乱序整数序列两数之和绝对值最小】真题+解题思路+代码(20222023)
乱序整数序列两数之和绝对值最小 题目 给定一个随机的整数数组(可能存在正整数和负整数)nums, 请你在该数组中找出两个数,其和的绝对值(|nums[x]+nums[y]|)为最小值 并返回这两个数(按从小到大返回)以及绝对值。 每种输入只会对应一个答案。但是,数组中同一个元素不能使用两…...
字符串转换整数 (atoi)(python)
链接: https://leetcode.cn/problems/string-to-integer-atoi 题目描述: 请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C 中的 atoi 函数)。 函数 myAtoi(string s) 的算法…...
洛谷 P1115 最大子段和
题目链接:P1115 最大子段和 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 题目描述 给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大。 输入格式 第一行是一个整数,表示序列的长度 n。 第二行有 n 个整数ÿ…...
【Linux】-- 权限和Shell运行原理
目录 Shell的运行原理 用户切换 su - / su sudo 权限 chmod chown chgrp 八进制方法修改文件属性 目录权限 粘滞位 umask 自定义默认权限 Shell的运行原理 广义上,Linux发行版 Linux内核 外壳程序 Linux 从广义上来理解它是一个操作系统 而从狭义上…...
C++各类设计模式及实现详解
软件领域中的设计模式为开发人员提供了一种使用专家设计经验的有效途径。设计模式中运用了面向对象编程语言的重要特性:封装、继承、多态,真正领悟设计模式的精髓是可能一个漫长的过程,需要大量实践经验的积累。最近看设计模式的书࿰…...
【Linux】进程理解与学习(Ⅰ)
环境:centos7.6,腾讯云服务器Linux文章都放在了专栏:【Linux】欢迎支持订阅🌹相关文章推荐:【Linux】冯.诺依曼体系结构与操作系统进程概念什么是进程?进程是什么?我们打开任务管理器可以看到有…...
认识代码之前,请先认识你自己 |《编程人生》
这是我的湛庐课程《给技术人的职场突围课》 (链接) 的一部分。 这篇文章也是 IT 女神征文活动 的一部分。 《编程人生》是一本优秀程序员的采访集,里面记录了15位世界级编程大师的故事。 我在 发刊词 里面说过,在这个书单课里&am…...
eNSP-Cloud(实现本地电脑与eNSP内设备之间通信)
说明: 想象一下,你正在用eNSP搭建一个虚拟的网络世界,里面有虚拟的路由器、交换机、电脑(PC)等等。这些设备都在你的电脑里面“运行”,它们之间可以互相通信,就像一个封闭的小王国。 但是&#…...
linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...
在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能
下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能,包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...
Python爬虫(二):爬虫完整流程
爬虫完整流程详解(7大核心步骤实战技巧) 一、爬虫完整工作流程 以下是爬虫开发的完整流程,我将结合具体技术点和实战经验展开说明: 1. 目标分析与前期准备 网站技术分析: 使用浏览器开发者工具(F12&…...
【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
Linux离线(zip方式)安装docker
目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1:修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本:CentOS 7 64位 内核版本:3.10.0 相关命令: uname -rcat /etc/os-rele…...
RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...
0x-3-Oracle 23 ai-sqlcl 25.1 集成安装-配置和优化
是不是受够了安装了oracle database之后sqlplus的简陋,无法删除无法上下翻页的苦恼。 可以安装readline和rlwrap插件的话,配置.bahs_profile后也能解决上下翻页这些,但是很多生产环境无法安装rpm包。 oracle提供了sqlcl免费许可,…...
