Android未捕获异常监控原理

news2025/7/18 7:32:12

背景

  • 本文仅探讨java层的未捕获异常的监控
  • 为什么我们自己的异常捕获总是比 Bugly 收到的信息少?

Android未捕获异常的监控与收集

Java层未捕获异常监控的基本实现

先看看Java层未捕获异常监控的运行过程:

public class MyUncaughtExceptionHandler implements UncaughtExceptionHandler {
	private static UncaughtExceptionHandler oriHandler;
	private static volatile boolean disable = false;

	public static void disable() {
		disable = true;
	}

	public static void register() {
		if (!disable) {
		    // 1.保存原有的UncaughtExceptionHandler实例
			oriHandler = Thread.getDefaultUncaughtExceptionHandler();
			// 2.设置自定义的UncaughtExceptionHandler实例,替代原有的
			Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
		}
	}

	private MyUncaughtExceptionHandler() {}

	public void uncaughtException(Thread thread, Throwable ex) {
		// 3.收到未捕获异常时,进行相应的业务处理
		handleCrash(ex);
		// 4.将异常信息继续丢回给原有的UncaughtExceptionHandler
		if (oriHandler != null) {
			oriHandler.uncaughtException(thread, ex);
		}
	}
}

提问:有没有可能某些第三方SDK甚至应用自己私吞异常,不向下传递?
对于这个猜测的证伪,需要从源码层面分析,下面来看看。

Framework 对未捕获异常的处理流程分析

应用启动流程中会执行到 RumtimeInit.commonInit() 中:

// RuntimeInit.java
protected static final void commonInit() {
    if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");

    /*
     * set handlers; these apply to all threads in the VM. Apps can replace
     * the default handler, but not the pre handler.
     */
    // LoggingHandler 与 KillApplicationHandler 都是 UncaughtExceptionHandler 的实现类
    // UncaughtExceptionPreHandler 无法被应用替换,只有系统能用
    LoggingHandler loggingHandler = new LoggingHandler();
    RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
    // defaultUncaughtExceptionHandler 是可以被应用替换掉的
    Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
    ...
}

可见,系统在应用启动时,同时注册了 LoggingHandler 与 KillApplicationHandler 两个 UncaughtExceptionHandler 的实现类,用于监听未捕获异常。为什么这么做呢,我们分别分析两个实现类的流程:

先分析 RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler) :

// RuntimeHooks.java
public static void setUncaughtExceptionPreHandler(
        Thread.UncaughtExceptionHandler uncaughtExceptionHandler) {
        // 直接调用Thread.setUncaughtExceptionPreHandler()
    Thread.setUncaughtExceptionPreHandler(uncaughtExceptionHandler);
}
// Thread.java
// Thread 同时提供了UncaughtExceptionPreHandler的set/get方法
public static void setUncaughtExceptionPreHandler(UncaughtExceptionHandler eh) {
    uncaughtExceptionPreHandler = eh;
}

// getUncaughtExceptionPreHandler() 方法会被 Thread.dispatchUncaughtException(Throwable e) 调用
public static UncaughtExceptionHandler getUncaughtExceptionPreHandler() {
    return uncaughtExceptionPreHandler;
}

// 分发异常
public final void dispatchUncaughtException(Throwable e) {
    // BEGIN Android-added: uncaughtExceptionPreHandler for use by platform.
    // 分发给 UncaughtExceptionPreHandler
    Thread.UncaughtExceptionHandler initialUeh =
            Thread.getUncaughtExceptionPreHandler();
    if (initialUeh != null) {
        try {
            initialUeh.uncaughtException(this, e);
        } catch (RuntimeException | Error ignored) {
            // Throwables thrown by the initial handler are ignored
        }
    }
    // END Android-added: uncaughtExceptionPreHandler for use by platform.
    // 分发给 UncaughtExceptionHandler(就是应用可以自行注册的UEH)
    getUncaughtExceptionHandler().uncaughtException(this, e);
}

可见,系统会将未捕获异常同时分发给 uncaughtExceptionPreHandler 和 UncaughtExceptionHandler。所以,接下来就要弄明白这两个 UEH 各自做了什么,先看 LoggingHandler :

// RuntimeInit.java
// LoggingHandler 是 RuntimeInit 的内部类
private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
    public volatile boolean mTriggered = false;

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        mTriggered = true;

        // Don't re-enter if KillApplicationHandler has already run
        if (mCrashing) return;

        // mApplicationObject is null for non-zygote java programs (e.g. "am")
        // There are also apps running with the system UID. We don't want the
        // first clause in either of these two cases, only for system_server.
        if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) {
            Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
        } else {
            // 大部分情况都会走到这里
            logUncaught(t.getName(), ActivityThread.currentProcessName(), Process.myPid(), e);
        }
    }
}

public static void logUncaught(String threadName, String processName, int pid, Throwable e) {
    // 生成一段msg
    StringBuilder message = new StringBuilder();
    // The "FATAL EXCEPTION" string is still used on Android even though
    // apps can set a custom UncaughtExceptionHandler that renders uncaught
    // exceptions non-fatal.
    message.append("FATAL EXCEPTION: ").append(threadName).append("\n");
    if (processName != null) {
        message.append("Process: ").append(processName).append(", ");
    }
    message.append("PID: ").append(pid);
    // 打印
    Clog_e(TAG, message.toString(), e);
}

private static int Clog_e(String tag, String msg, Throwable tr) {
    return Log.printlns(Log.LOG_ID_CRASH, Log.ERROR, tag, msg, tr);
}

可见,LoggingHandler 在收到异常回调时,仅仅做了一些系统打印操作。那么,KillApplicationHandler 呢?

// RuntimeInit.java
// KillApplicationHandler 是 RuntimeInit 的内部类
/**
 * Handle application death from an uncaught exception.  The framework
 * catches these for the main threads, so this should only matter for
 * threads created by applications. Before this method runs, the given
 * instance of {@link LoggingHandler} should already have logged details
 * (and if not it is run first).
 */
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
    // 持有 pre-handler(LoggingHandler)的引用,作用是确保 LoggingHandler.uncaughtException() 被触发
    private final LoggingHandler mLoggingHandler;

    public KillApplicationHandler(LoggingHandler loggingHandler) {
        this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
    }

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        try {
            // 确保 LoggingHandler.uncaughtException() 被触发
            ensureLogging(t, e);

            // Don't re-enter -- avoid infinite loops if crash-reporting crashes.
            if (mCrashing) return;
            mCrashing = true;

            /*
             * 如果正在使用 profiler 跟踪代码,就应该停止 profiling,否则杀死应用进程时会导致 
             * profiler 的内存数据丢失,而正确的 stopProfiling,可以保证用户能通过 profiler 跟踪crash
             */
            // Try to end profiling. If a profiler is running at this point, and we kill the
            // process (below), the in-memory buffer will be lost. So try to stop, which will
            // flush the buffer. (This makes method trace profiling useful to debug crashes.)
            if (ActivityThread.currentActivityThread() != null) {
                // 如果应用的UEH拦截了异常,不传回给系统,主要有影响的其实就是这里
                ActivityThread.currentActivityThread().stopProfiling();
            }

            // 弹出崩溃提示框
            // Bring up crash dialog, wait for it to be dismissed
            ActivityManager.getService().handleApplicationCrash(
                    mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
        } catch (Throwable t2) {
            if (t2 instanceof DeadObjectException) {
                // System process is dead; ignore
            } else {
                try {
                    Clog_e(TAG, "Error reporting crash", t2);
                } catch (Throwable t3) {
                    // Even Clog_e() fails!  Oh well.
                }
            }
        } finally {
            // 最终结束进程
            // Try everything to make sure this process goes away.
            Process.killProcess(Process.myPid());
            System.exit(10);
        }
    }

    /**
     * Ensures that the logging handler has been triggered.
     *
     * See b/73380984. This reinstates the pre-O behavior of
     *
     *   {@code thread.getUncaughtExceptionHandler().uncaughtException(thread, e);}
     *
     * logging the exception (in addition to killing the app). This behavior
     * was never documented / guaranteed but helps in diagnostics of apps
     * using the pattern.
     *
     * If this KillApplicationHandler is invoked the "regular" way (by
     * {@link Thread#dispatchUncaughtException(Throwable)
     * Thread.dispatchUncaughtException} in case of an uncaught exception)
     * then the pre-handler (expected to be {@link #mLoggingHandler}) will already
     * have run. Otherwise, we manually invoke it here.
     */
    private void ensureLogging(Thread t, Throwable e) {
        if (!mLoggingHandler.mTriggered) {
            try {
                mLoggingHandler.uncaughtException(t, e);
            } catch (Throwable loggingThrowable) {
                // Ignored.
            }
        }
    }
}

最后,让我们再多看一眼 ActivityThread.currentActivityThread().stopProfiling() ,确认一下它到底是不是如推测的那样,是控制 Profiler 的:

// ActivityThread.java
/**
 * Public entrypoint to stop profiling. This is required to end profiling when the app crashes,
 * so that profiler data won't be lost.
 *
 * @hide
 */
public void stopProfiling() {
    if (mProfiler != null) {
        mProfiler.stopProfiling();
    }
}

// Profiler 是 ActivityThread 的内部类
static final class Profiler {
    ...
    public void stopProfiling() {
        if (profiling) {
            profiling = false;
            // traceview 等工具做代码跟踪时,都会用到的方法
            Debug.stopMethodTracing();
            if (profileFd != null) {
                try {
                    profileFd.close();
                } catch (IOException e) {
                }
            }
            profileFd = null;
            profileFile = null;
        }
    }
}

至此,Framework 处理未捕获异常的完整流程就分析完了。

最后做个总结
  • 系统会同时注册两个 UncaughtExceptionHandler 实例:LoggingHandler 和 KillApplicationHandler
LoggingHandlerKillApplicationHandler
注册为pre-handlerhandler
可替换仅可被系统使用,不能被替换可被应用替换
作用在 Logcat 中输出崩溃日志1.确保 LoggingHandler 被触发
2.停止 Profiler(如果正在 profiling 的话)
3.弹出崩溃提示框
4.结束进程

完整的未捕获异常处理流程

Android未捕获异常处理流程.png

回到最初的“猜测”

基于以上分析,文初的“猜测”基本上不存在可能性,原因如下:

  • 异常的传递是必须的,最终要传回给系统进行处理,如果私吞,应用会停留在前台,但无法响应任何操作,这不符合大部分产品的设计理念

当然,你可以私吞异常,并且自行杀死进程,以避免应用停留前台,如下操作:

	public void uncaughtException(Thread thread, Throwable ex) {
		handleCrash(ex);
		System.exit(10);
		// 不往下传递异常信息
//		if (oriHandler != null) {
//			oriHandler.uncaughtException(thread, ex);
//		}
	}

但作为sdk,私吞异常还会导致应用自己的UEH也捕获不到异常,会被应用投诉,得不偿失!而作为应用,如此操作会导致无法使用第三方异常监控工具,也会导致 profiler 失效,影响自己内部调试。

所以说,私吞异常是一个损人不利己的行为,甚至是害人害己。

更可靠的推测

崩溃处理流程中的一切异步操作,均有失败的风险

新的优化方向:异步 → 同步

  • 上传失败:数据安全,下次启动时会上传
  • 缓存DB失败:数据丢失,本次/下次均不会上传

因此,至少要将“缓存DB”改为同步操作,才能保证缓存成功

问题:uncaughtException()是在主线程触发的,而我们禁止主线程I/O?

Bugly 源码分析

  • crashreport:4.1.9

反编译bugly,验证其异常系统的核心实现:

// com.tencent.bugly.proguard.av.java
/**
 * 注册监控
 */
public final synchronized void registerUeh() {
    if (this.j >= 10) {
        BuglyLog.a("java crash handler over %d, no need set.", new Object[]{Integer.valueOf(10)});
        return;
    }
    this.enable = true;
    Thread.UncaughtExceptionHandler uncaughtExceptionHandler;
    if ((uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()) != null) {
        String str1 = getClass().getName();
        String str2 = uncaughtExceptionHandler.getClass().getName();
        // 注册过的Bugly监控不再注册,避免重复注册
        if (str1.equals(str2)) {
            return;
        }
        // 当前的UEH是系统默认的,缓存起来(系统默认的在高版本中是RuntimeInit$KillApplicationHandler,bugly未做适配)
        if ("com.android.internal.os.RuntimeInit$UncaughtHandler".equals(uncaughtExceptionHandler.getClass().getName())) {
            BuglyLog.a("backup system java handler: %s", new Object[]{uncaughtExceptionHandler.toString()});
            this.sysDefUeh = uncaughtExceptionHandler;
            this.custUeh = uncaughtExceptionHandler;
        } else {
            // 当前的UEH是应用自定义的
            BuglyLog.a("backup java handler: %s", new Object[]{uncaughtExceptionHandler.toString()});
            this.custUeh = uncaughtExceptionHandler;
        }
    }
    // 设置Bugly的UEH实例,替代原有的
    Thread.setDefaultUncaughtExceptionHandler(this);
    this.j++;
    BuglyLog.a("registered java monitor: %s", new Object[]{toString()});
}
// com.tencent.bugly.proguard.av.java
public final void uncaughtException(Thread paramThread, Throwable paramThrowable) {
    synchronized (i) {
        handleException(paramThread, paramThrowable, true, null, null, this.d.Q);
        return;
    }
}

/**
 * 异常处理
 *
 * @param paramThread
 * @param paramThrowable
 * @param uncaughtException
 * @param paramString
 * @param paramArrayOfbyte
 * @param paramBoolean2
 */
public final void handleException(Thread paramThread, Throwable paramThrowable, boolean uncaughtException, String paramString, byte[] paramArrayOfbyte, boolean paramBoolean2) {
    // uncaughtException = true
    if (uncaughtException) {
        BuglyLog.e("Java Crash Happen cause by %s(%d)", new Object[]{paramThread.getName(), Long.valueOf(paramThread.getId())});
        // 已处理过
        if (isHandled(paramThread)) {
            BuglyLog.a("this class has handled this exception", new Object[0]);
            // 有系统默认的UEH,交给系统处理
            if (this.sysDefUeh != null) {
                BuglyLog.a("call system handler", new Object[0]);
                this.sysDefUeh.uncaughtException(paramThread, paramThrowable);
            } else {
                // 自行结束进程
                exit();
            }
        }
    } else {
        BuglyLog.e("Java Catch Happen", new Object[0]);
    }
    // 核心处理
    try {
        // 监控被禁用,结束
        if (!this.enable) {
            BuglyLog.c("Java crash handler is disable. Just return.", new Object[0]);
            return;
        }
        // 没有设置StrategyBean
        if (!this.c.b()) {
            BuglyLog.d("no remote but still store!", new Object[0]);
        }
        // crash report被禁用,且设置了StrategyBean,则本地打印crash日志,结束
        if (!this.c.getStrategy().enableCrashReport && this.c.b()) {
            BuglyLog.e("crash report was closed by remote , will not upload to Bugly , print local for helpful!", new Object[0]);
            as.printLocal(uncaughtException ? "JAVA_CRASH" : "JAVA_CATCH", ap.a(), this.d.d, paramThread.getName(), ap.a(paramThrowable), null);
            return;
        }
        CrashDetailBean crashDetailBean;
        // 打包crash数据,生成CrashDetailBean,如果失败就退出
        if ((crashDetailBean = makeCrashDetailBean(paramThread, paramThrowable, uncaughtException, paramString, paramArrayOfbyte, paramBoolean2)) == null) {
            BuglyLog.e("pkg crash datas fail!", new Object[0]);
            return;
        }
        as.printLocal(uncaughtException ? "JAVA_CRASH" : "JAVA_CATCH", ap.a(), this.d.d, paramThread.getName(), ap.a(paramThrowable), crashDetailBean);
        // 保存到本地数据库,并返回操作结果(推断如果失败,不再执行上传)
        if (!this.b.saveDb(crashDetailBean, uncaughtException)) {
            // 根据BuglyStrategy的设置,决定是否要立即上传
            this.b.uploadOnNecessary(crashDetailBean, uncaughtException);
        }
        if (uncaughtException) {
            // 内部只是一个日志打印的逻辑
            this.b.a(crashDetailBean);
        }
    } catch (Throwable throwable) {
        if (!BuglyLog.a((Throwable) (paramString = null))) {
            paramString.printStackTrace();
        }
    } finally {
        // uncaughtException = true
        if (uncaughtException) {
            // 有应用自定义的UEH,且 它没有在处理当前异常,就传给它处理
            if (this.custUeh != null && targetUehNotHandling(this.custUeh)) {
                BuglyLog.e("sys default last handle start!", new Object[0]);
                this.custUeh.uncaughtException(paramThread, paramThrowable);
                BuglyLog.e("sys default last handle end!", new Object[0]);
            // 没有自定义的,但有系统默认的UEH,就传给系统处理
            } else if (this.sysDefUeh != null) {
                BuglyLog.e("system handle start!", new Object[0]);
                this.sysDefUeh.uncaughtException(paramThread, paramThrowable);
                BuglyLog.e("system handle end!", new Object[0]);
            // 都没有,就自行结束进程
            } else {
                BuglyLog.e("crashreport last handle start!", new Object[0]);
                exit();
                BuglyLog.e("crashreport last handle end!", new Object[0]);
            }
        }
    }
}

bugly的实现完全符合标准流程,同时通过测试其执行日志,与上述代码中的日志输出也完全一致,Bugly 确实不会私吞异常

Bugly未捕获异常处理流程

Bugly未捕获异常处理流程.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/17521.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

企业虚拟网络管理

随着企业规模的扩大,其网络的规模和复杂性也会成比例地扩展。企业级组织和中小型企业需要大规模网络来满足不断增长的业务需求。然而,大规模网络需要大量的物理组件、定期维护和配置,所有这些都是有代价的。因此,为了规避这些额外…

Spring Boot面试题

什么是 Spring Boot? Spring Boot 是 Spring 开源组织下的子项目,其设计目的是专注于Spring应用的开发,开发人员可以把更多的精力放在业务代码上,而无需过多关注XML的配置,从而简化Spring应用开发,提高开发…

牛客小白月赛61-E

传送门 题意 给你一个长度为n的序列,它有n!个排列方法 问在这n!个排列方法中 逆序对的总数是多少 首先要知道 逆序对数n!/2*(不相等的数字对儿数) 不相等的数组对儿数cn2c_{n}^{2}cn2​-Zcnum[a[i]2c_{n…

前端如何写进度条(上传或者下载文件的时候)

1.需求 在日常开发中,我们经常会遇到上传或者下载文件的需求,以下载为例: 如果后台文件是现成的,浏览器就会在底部出现下载的过程,如果点击下载后,有些业务是需要去打包,然后再下载文件的话&a…

【C++语法难点】 深拷贝和浅拷贝是什么,拷贝构造,拷贝赋值

文章目录1.开始:构造函数1.2 在栈区和堆区创建对象1.3 缺省构造函数1.4 类型转换构造函数1.5 拷贝构造函数1.6 缺省拷贝构造函数(浅拷贝)1.7 深拷贝构造函数 (深拷贝)1.8 拷贝赋值1.开始:构造函数 语法形式 class 类名{类名(形参…

UE5笔记【三】UE5材质Materials

材质:可以将材质看作是StaticMesh上面的绘画。这副绘画Paint是由图层组成的,这些图层形成了所谓的物理基础渲染(Physically Based Rendering OR PBR)。这些PBR的特殊之处在于:几乎可以让我们模拟显示世界中的任何材质。…

「Redis数据结构」QuickList

「Redis数据结构」QuickList 文章目录「Redis数据结构」QuickList一、前言二、概述三、结构四、小结一、前言 在前面一章,我们已经学习了ZipList压缩列表,ZipList虽然节省内存,但也引发了不少问题。 问题1:ZipList虽然节省内存&am…

【Bio】基础生物学 - 细胞 cell

文章目录1. 细胞2. 原核细胞 真核细胞3. 细胞器4. 细胞核5. 动物细胞5.1 细胞质5.2 核糖体5.3 内质网6. 植物细胞6.1 液泡6.2 线粒体6.3 叶绿体Ref1. 细胞 生命系统的结构层次依次为: 细胞 (cell)\blue{\text{细胞 (cell)}}细胞 (cell) →\rightarrow→ 组织 (tiss…

玩转MySQL:程序中的“田氏代齐”,InnoDB为何能替换MyISAM?

引言 MySQL是一款支持拔插式引擎的数据库,在开发过程中你可以根据业务特性,从支持的诸多引擎中选择一款适合的,例如MyISAM、InnoDB、Merge、Memory(HEAP)、BDB(BerkeleyDB)、Example、Federated、Archive、CSV、Blackhole..... 不过虽然各款…

[附源码]java毕业设计全国人口普查管理系统论文

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

LeetCode刷题复盘笔记—一文搞懂746. 使用最小花费爬楼梯(动态规划系列第二篇)

今日主要总结一下动态规划的一道题目,746. 使用最小花费爬楼梯 题目:746. 使用最小花费爬楼梯 题目描述: 给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择…

Linux之关于Nginx

目录 1、什么是Nginx? 1.1、负载均衡:流量分摊​编辑 1. 2、反向代理 :处理外网访问内网问题 1.3、动静分离:判断动态请求还是静态请求,选择性的访问指定服务器 2、Nginx的使用 2.1.Nginx安装 2.1.1 添…

11月27日PMI认证才聚各考点防疫要求,PMP考生必看

11月27日深圳才聚、珠海才聚、东莞才聚、南宁才聚防疫要求及如下: 注意:由于疫情防控影响,以下城市的考试将延期举办,该考点的考生无需做任何操作。 北京、天津、石家庄、廊坊、保定、哈尔滨、大庆、呼和浩特、太原、郑州、兰州…

【王道计算机网络笔记】计算机网络体系结构-计算机网络概述

文章目录计算机网络的概念计算机网络的功能计算机网络的组成计算机网络的分类标准化工作及相关组织相关组织计算机网络的性能指标速率带宽吞吐量时延时延带宽积往返时延RTT利用率计算机网络的概念 计算机网络:是一个分散的、具有独立功能的计算机系统,通…

[附源码]java毕业设计汽车租赁系统

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

绿竹生物获上市“大路条”:融资不搞研发去理财,孔健下什么棋?

11月19日,绿竹生物发布消息称,该公司于2022年11月11日获得中国证监会关于首次公开发行境外上市外资股(H股)及境内未上市股份全流通(即“大路条”)的批复,下一步将根据香港联交所的聆讯进度安排及…

【JavaSE】接口

前言: 作者简介:爱吃大白菜1132 人生格言:纸上得来终觉浅,绝知此事要躬行 如果文章知识点有错误的地方不吝赐教,和大家一起学习,一起进步! 如果觉得博主文章还不错的话,希望三连支持&#xff01…

web课程设计网页规划与设计----公司官网带轮播图 (页面精美 1页)

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材,DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | 公司官网网站 | 企业官网 | 酒店官网 | 等网站的设计与制 | HTML期末大学生网页设计作业,Web大学生网页 HTML:结构 CSS&#…

垃圾回收相关概念概述(宋红康JVM学习笔记)

System.gc() 在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。 然而System.gc()调用附带一个免责声明,无法保证对…

java 基于springBoot上传文件/文件夹使用实例

最近项目中遇到一个文件批量上传的需求,对单个的文件、多文件或者文件夹的方式上传文件都可以满足要求,总结一下使用经验。 案例基于springBoot. 1、文件上传请求 这里postman测试了单文件和多文件的上传,同时测试了文件件方式上传。 postman…