JNI 跨线程调用时局部引用的陷阱_跨线程调用函数
myzbx 2025-09-01 09:52 5 浏览
一、背景
近期在项目中遇到一个困扰了2天的问题,虽然知道问题的原因,但始终不明所以,解决的方式也是囫囵吞枣。问题的起因是,项目中封装了一个统一的JNI 回调函数,将Native层C++ 调用Java 的函数统一接管,做过JNI 的都知道,Native调用Java 回调函数时需要考虑跨线程的情况,即在JNI 线程调用和不在JNI 线程调用的情况(有人喜欢把这类线程叫作子线程)。在JNI 线程时操作起来比较简单,直接调用即可。但不在JNI 线程时需要先对相关对象附在JNI 线程中,否则会闪退。
本文的问题在调用时已经考虑了线程的附加与分离,但依然报下述crash :
//(局部引用非法)
java_vm_ext.cc:598] JNI DETECTED ERROR IN APPLICATION: JNI ERROR (app bug): jobject is an invalid local reference: 0x7a38ba2035 (reference outside the table: 0x7a38ba2035)
java_vm_ext.cc:598] JNI DETECTED ERROR IN APPLICATION: JNI ERROR (app bug): jobject is an invalid local reference: 0x7a38ba2035 (reference outside the table: 0x7a38ba2035)
java_vm_ext.cc:598] in call to GetObjectClass
2025-08-04 10:18:43.917 32203-7932 n.collaboration com.tran.collaboration A runtime.cc:708] Runtime aborting...
runtime.cc:708] Dumping all threads without mutator lock held
runtime.cc:708] All threads:
runtime.cc:708] DALVIK THREADS (26):
runtime.cc:708] "main" prio=10 tid=1 Native
runtime.cc:708] | group="" sCount=1 ucsCount=0 flags=1 obj=0x72adc9e8 self=0xb40000784b84df50
runtime.cc:708] | sysTid=32203 nice=-10 cgrp=top-app sched=0/0 handle=0x7a3a0844f8
runtime.cc:708] | state=S schedstat=( 807140063 37525466 241 ) utm=68 stm=12 core=6 HZ=100
二、原因分析
分析这个问题之前,先将问题涉及的代码贴出来,这样好讲。
统一的回调函数:
JavaObjCtx.h
//
// Created by Rambo.liu on 2025/8/1.
// java object 管理器
#ifndef INPUTSHARE_JAVAOBJCTX_H
#define INPUTSHARE_JAVAOBJCTX_H
#include <jni.h>
class JavaObjCtx {
public:
JNIEnv *env;
//类对象
jclass clazz;
//构造器
jmethodID constructor;
//实例对象
jobject instanceObj;
//方法ID
jmethodID methodId;
JavaVM *jvm;
/**
* java 对象
* @param _jvm
* @param _env
* @param clzz 类 需要包名 如:com/tran/share/input/Injection
* @param methodName 方法名
* @param methodSign 方法签名
*/
JavaObjCtx(JavaVM *_jvm,JNIEnv *_env,const char* clzz,const char* methodName,const char* methodSign);
~JavaObjCtx();
/**
* 调用函数
* @param obj 实例对象,需要注意跨线程调用,
* 注意:obj 在跨线程调用时,当第一次在 JNI 线程调用时工作正常,但第二次在非 JNI 线程调用时,原始局部引用 jobject 已经失效。
* 解决方案为:在首次获取 Java 对象时,直接转换为全局引用
* 第二次调用时 obj 打印不为空但实际已失效,是 JNI 局部引用机制的一个典型陷阱
* 当 JNI 方法返回时,整个局部引用表会被自动清空,但 C++ 变量 obj 仍保留原地址值
* 类似「野指针」现象:指针地址非 NULL,但指向的内容已无效
* @param methodID
* @param ...
*/
void callVoidMethod(jobject obj, jmethodID methodID, ...);
bool isReferenceValid(JNIEnv* env, jobject obj);
};
#endif //INPUTSHARE_JAVAOBJCTX_H
JavaObjCtx.cpp
//
// Created by Rambo.liu on 2025/8/1.
//
#include "JavaObjCtx.h"
#include "Log.h"
JavaObjCtx::JavaObjCtx(JavaVM *_jvm,JNIEnv *_env, const char *clzz, const char *methodName,
const char *methodSign) {
LOGI("class = %s methodId = %s methodSign = %s", clzz, methodName, methodSign);
env = _env;
jvm = _jvm;
//先找类对象
jclass injectionCls = env->FindClass(clzz);
if (injectionCls == nullptr) {
LOGE("找不到 %s 类对象", clzz);
return;
}
//找到类对象,先初始化,获取类对象实例,使用默认的构造方法
jmethodID _constructor = env->GetMethodID(injectionCls, "<init>", "()V");
if (nullptr == _constructor) {
LOGW("can't constructor injectionCls");
return;
}
//构造injectionCls 实例对象
jobject injectionClsObject = env->NewObject(injectionCls, _constructor);
if (nullptr == injectionClsObject) {
LOGW("can't new injectionClsObject");
env->DeleteLocalRef(injectionCls);
return;
}
//查找要调用的方法
jmethodID _methodId = env->GetMethodID(injectionCls, methodName,
methodSign);
if (_methodId == nullptr) {
LOGV("onStatus 未找到,直接释放类对象与实例对象");
env->DeleteLocalRef(injectionCls);
env->DeleteLocalRef(injectionClsObject);
return;
}
clazz = injectionCls;
instanceObj = injectionClsObject;
constructor = _constructor;
methodId = _methodId;
}
JavaObjCtx::~JavaObjCtx() {
LOGI("JavaObjCtx::~JavaObjCtx()析构");
if (env) {
if (clazz) {
env->DeleteLocalRef(clazz);
}
}
}
void JavaObjCtx::callVoidMethod(jobject obj, jmethodID methodID, ...) {
bool attached = false;
if (!env) {
LOGE("JNIEnv is null!");
return;
}
if (!obj) {
LOGE("Java object is null!");
return;
}
// 检查线程是否已附加
if (jvm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_EDETACHED) {
if (jvm->AttachCurrentThread(&env, nullptr) == JNI_OK) {
attached = true;
LOGI("运行在非主线程中,附加成功");
} else {
LOGE("此函数运行在非创建JNI线程中,附加JVM 失败!!");
return;
}
} else {
LOGI("函数已运行在创建JNI 线程中,无需附加");
}
LOGD("跨线程地址: %p", obj); // 地址相同
LOGD("是否有效: %d", isReferenceValid(env, obj)); // 必为false
va_list args;
va_start(args, methodID);
env->CallVoidMethodV(obj, methodID, args);
va_end(args);
// 检查调用是否产生异常
if (env->ExceptionCheck()) {
LOGD("Exception occurred while calling method");
env->ExceptionDescribe();
env->ExceptionClear();
}
if (attached) {
jvm->DetachCurrentThread();
}
}
bool JavaObjCtx::isReferenceValid(JNIEnv *env, jobject obj) {
if (!obj){
LOGI("obj 已经为空");
return false;
}
jclass objClass = env->GetObjectClass(obj); // 关键检查
if (env->ExceptionCheck() || !objClass) {
env->ExceptionClear();
LOGI("obj 已经为空");
return false;
}
return true;
}
出现问题的场景为:
- Socket 初始时针对Socket初始化各种失败的场景将结果回调给java 层做业务交互。这类场景都在JNI 线程中调用 callVoidMethod。调用的代码如下:
//回调函数
std::function<void(CONNECTED_STATUS status)> _connectCallBack;
//状态改变时 触发回调
void setStatus(CONNECTED_STATUS status) {
_status = status;
LOGI("80----------- status = %d",status);
if(_connectCallBack) {
_connectCallBack(status);
}
}
void start(std::string &ip, const int port) {
if (getStatus() == CONNECTED_STATUS::CONNECTING) {
LOGI("正在连接中...");
return;
} else if (getStatus() == CONNECTED_STATUS::CONNECTED) {
LOGI("已连接成功,无需要重复连接!!!");
return;
}
setStatus(CONNECTED_STATUS::CONNECTING);
_ip = ip;
_port = port;
// 1. 若已有线程在运行,先终止并清理
if (_connect_thread && _connect_thread->joinable()) {
_connect_thread->join(); // 等待旧线程结束
_connect_thread.reset(); // 释放线程资源
}
_is_connected = false;
_stop = true;
//第二次创建线程调用socket connect
std::future<bool> connectFuture = commit(
std::bind(&SocketClient::connectServer, this, ip, port));
_is_connected = connectFuture.get();
if (_is_connected) {
_conn_cond.notify_one();
std::packaged_task<bool()> task([this, ip, port]() {
LOGI("连接成功,服务器地址:%s 端口:%d", ip.c_str(), port);
_reconnect_count = 1;
_stop = false;
setStatus(CONNECTED_STATUS::CONNECTED);
initEventLoop();
read();
return _is_connected;
});
_connect_thread = std::make_unique<std::thread>(std::move(task));
}
}
- 第二次在Socket 连接服务器时调用,这次是开了一个线程来调用Socket 的connect,(非JNI 线程调用)callVoidMethod。(注 上述代码的红色的注释部分)
- 外部调用时两个调用对象为同一个,调用部分代码为:
std::shared_ptr<SocketClient> _socketClient;
std::unique_ptr<JavaObjCtx> _javaObjCtx;
extern "C"
JNIEXPORT void JNICALL
testSocket(JNIEnv *env, jclass thiz, jstring ip, jint port) {
jboolean isCopy;
const char * szIp = env->GetStringUTFChars(ip, &isCopy);
if(!_socketClient) {
_socketClient = std::make_shared<SocketClient>();
}
std::string s(szIp);
if(!_javaObjCtx) {
_javaObjCtx = std::make_unique<JavaObjCtx>(g_jvm,env,"com/tran/share/input/Injection","onStatus",
"(I)V");
}
//收到回调时由此统一向java 回吐
_socketClient->setConnectStatusCallBack([&env](CONNECTED_STATUS status)->void {
_javaObjCtx->callVoidMethod(_javaObjCtx->instanceObj, _javaObjCtx->methodId, status);
});
_socketClient->start(s , port);
env->ReleaseStringUTFChars(ip, szIp);
}
Java 层的回调函数:
package com.tran.share.input;
import android.util.Log;
import java.io.DataOutputStream;
/**
* author rambo.liu
* date 2025/7/15 11:04
* Version: 1.0
* Description: 事件注入器
*/
public class Injection {
private final static String TAG = "Injection";
static {
System.loadLibrary("inputShare");
}
public void onStatus(int code) {
Log.i(TAG,"58--------收到来自Native 的回调 code = "+code);
}
}
业务需求很简单,但问题却不小。在JNI 对象中明明考虑了跨线程的场景,而且对相关的对象都做判空处理,
callVoidMethod 中判空代码
void JavaObjCtx::callVoidMethod(jobject obj, jmethodID methodID, ...) {
bool attached = false;
//判空
if (!env) {
LOGE("JNIEnv is null!");
return;
}
//判空
if (!obj) {
LOGE("Java object is null!");
return;
}
// 检查线程是否已附加
if (jvm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_EDETACHED) {
if (jvm->AttachCurrentThread(&env, nullptr) == JNI_OK) {
attached = true;
LOGI("运行在非主线程中,附加成功");
} else {
LOGE("此函数运行在非创建JNI线程中,附加JVM 失败!!");
return;
}
} else {
LOGI("函数已运行在创建JNI 线程中,无需附加");
}
va_list args;
va_start(args, methodID);
env->CallVoidMethodV(obj, methodID, args);
va_end(args);
// 检查调用是否产生异常
if (env->ExceptionCheck()) {
LOGD("Exception occurred while calling method");
env->ExceptionDescribe();
env->ExceptionClear();
}
if (attached) {
jvm->DetachCurrentThread();
}
}
为毛还报 "java_vm_ext.cc:598] JNI DETECTED ERROR IN APPLICATION: JNI ERROR (app bug): jobject is an invalid local reference: 0x7a38ba2035 (reference outside the table: 0x7a38ba2035) (局部引用非法)"
产生这个问题的根本原因:
当第一次在 JNI 线程调用时工作正常,但第二次在非 JNI 线程调用时,原始局部引用 jobject 已经失效。跨线程传递未保护的局部引用导致的。
- 第一次调用(JNI 线程)
- 局部引用 obj 有效,因为仍在原始 JNI 上下文生命周期内
- 第二次调用(非 JNI 线程)
- 局部引用 obj 已随第一次 JNI 调用结束被自动释放
- 直接使用会导致 invalid local reference 错误
这里还有一个问题需要搞清楚,不然没法理解:
既然已经失效了,按照常规理解,指针指向的内存应该也已经被释放了,callVoidMethod方法中判空应该会被阻断,但上述代码并非如此?何解?
第二次调用时callVoidMthod obj 打印不为空,其实是忽略了一个C/C++ 中的常见问题,即野指针问题。虽然指针指向的内存释放了,但指针并未指空,导致了该问题出现。这也是 JNI 局部引用机制的一个典型陷阱。以下是深度解析:
- 打印显示非空但实际无效
- C++ 层的 jobject 本质上是一个指针(类似 void*),打印时只是显示内存地址值
- 局部引用被释放后,指针地址不会自动置零,但 JVM 内部已将其标记为无效
- JNI 局部引用表机制
jobject obj = env->NewLocalRef(someObj); // 添加到当前线程的局部引用表
- 当 JNI 方法返回时,整个局部引用表会被自动清空,但 C++ 变量 obj 仍保留原地址值
- 类似「野指针」现象:指针地址非 NULL,但指向的内容已无效
为了验证上述分析,可以使用如下代码测试:
extern "C" JNIEXPORT void JNICALL
Java_com_example_Test_printRef(JNIEnv* env, jobject thiz, jobject javaObj) {
jobject localRef = env->NewLocalRef(javaObj);
LOGD("第一次地址: %p", localRef); // 输出类似 0x7a38ba2039
// 模拟跨线程传递(错误做法!)
std::thread([=]{
LOGD("第二次地址: %p", localRef); // 地址相同但已失效!
}).join();
}
输出结果:
第一次地址: 0x7a38ba2039
第二次地址: 0x7a38ba2039 // 相同地址但实际已失效
三、解决方案
既然局部引用已经 失效,但就转换为全局引用,将传递给callMethodId 方法的jobject 参数转换为实全局引用,调用的地方改为如下:
std::shared_ptr<SocketClient> _socketClient;
std::unique_ptr<JavaObjCtx> _javaObjCtx;
JavaVM *g_jvm = nullptr;
jobject g_globalObj = nullptr;
extern "C"
JNIEXPORT void JNICALL
testSocket(JNIEnv *env, jclass thiz, jstring ip, jint port) {
jboolean isCopy;
const char * szIp = env->GetStringUTFChars(ip, &isCopy);
if(!_socketClient) {
_socketClient = std::make_shared<SocketClient>();
}
std::string s(szIp);
if(!_javaObjCtx) {
_javaObjCtx = std::make_unique<JavaObjCtx>(g_jvm,env,"com/tran/share/input/Injection","onStatus",
"(I)V");
if (g_globalObj) env->DeleteGlobalRef(g_globalObj);
//全局引用
g_globalObj = env->NewGlobalRef(_javaObjCtx->instanceObj);
}
_socketClient->setConnectStatusCallBack([&env](CONNECTED_STATUS status)->void {
//jobject 改为全局引用
_javaObjCtx->callVoidMethod(g_globalObj, _javaObjCtx->methodId, status);
});
_socketClient->start(s , port);
env->ReleaseStringUTFChars(ip, szIp);
}
//不用的时候记得释放全局指针
JNIEXPORT void JNI_OnUnload(JavaVM *vm, void *reserved) {
LOGV("卸載---------如果有全局引用需要將其移除");
JNIEnv* env;
vm->GetEnv((void**)&env, JNI_VERSION_1_6);
if (g_globalObj) {
env->DeleteGlobalRef(g_globalObj);
g_globalObj = nullptr;
}
}
除了jobject 这类参数有此问题之外,还有一个对象在跨线程使用的过程中也容易忽视-- JNIEnv *env。在跨线程使用过程中需要注意以下几点:
- 线程安全
JNIEnv* 是线程私有对象,不能跨线程传递。如果在新线程中使用 env,必须先附加到 JVM 并获取当前线程的 env:按如下方式获取当前线程的JNIEnv 指针
// 在 C++ 新线程中获取 JNIEnv* 的正确方式
JNIEnv* env = nullptr;
// 假设 vm 是全局的 JavaVM* 指针(通过 JNI_OnLoad 获取)
int ret = vm->AttachCurrentThread(&env, nullptr);
if (ret != JNI_OK || env == nullptr) {
LOGD("Failed to attach thread");
return;
}
// 使用 env 前检查有效性
if (env->ExceptionCheck()) { ... }
// 线程结束时分离
vm->DetachCurrentThread();
- 生命周期:env 仅在当前 JNI 调用或附加的线程生命周期内有效,不要将其保存为全局变量长期使用。
在上述代码JavaObjCtx.h 中刚好把JNIEnv 当作成员变量了:
//
// Created by Rambo.liu on 2025/8/1.
// java object 管理器
#ifndef INPUTSHARE_JAVAOBJCTX_H
#define INPUTSHARE_JAVAOBJCTX_H
#include <jni.h>
class JavaObjCtx {
public:
//成员变量
JNIEnv *env;
//类对象
jclass clazz;
//构造器
jmethodID constructor;
//实例对象
jobject instanceObj;
//方法ID
jmethodID methodId;
JavaVM *jvm;
};
#endif //INPUTSHARE_JAVAOBJCTX_H
源文件JavaObjCtx.cpp 中构造器中给JNIEnv* env赋值:
JavaObjCtx::JavaObjCtx(JavaVM *_jvm,JNIEnv *_env, const char *clzz, const char *methodName,
const char *methodSign) {
LOGI("class = %s methodId = %s methodSign = %s", clzz, methodName, methodSign);
//给成员变量赋值
env = _env;
jvm = _jvm;
//先找类对象
......
}
这在跨线程调用函数:
void JavaObjCtx::callVoidMethod(jobject obj, jmethodID methodID, ...) {
bool attached = false;
if (!env) {
return;
}
if (!obj) {
return;
}
// 检查线程是否已附加
if (jvm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_EDETACHED) {
if (jvm->AttachCurrentThread(&env, nullptr) == JNI_OK) {
attached = true;
//LOGI("运行在非主线程中,附加成功");
} else {
//LOGE("此函数运行在非创建JNI线程中,附加JVM 失败!!");
return;
}
} else {
//LOGI("函数已运行在创建JNI 线程中,无需附加");
}
va_list args;
va_start(args, methodID);
env->CallVoidMethodV(obj, methodID, args);
va_end(args);
// 检查调用是否产生异常
if (env->ExceptionCheck()) {
LOGD("Exception occurred while calling method");
env->ExceptionDescribe();
env->ExceptionClear();
}
if (attached) {
jvm->DetachCurrentThread();
}
}
上述代码中使用的 env 由于是类的成员变量(而非局部变量),但 JNIEnv* 是线程私有的(每个线程有自己独立的 JNIEnv*),不能作为类成员跨线程共享。理由如下:
- 当在新线程中调用此函数时,成员变量 env 可能仍是其他线程的无效值(甚至 nullptr)。
- 即使通过 GetEnv 或 AttachCurrentThread 获取了当前线程的 env,若未正确更新成员变量,后续使用的仍是旧的无效 env。
如果在跨线程调用时,env 为成员变量同时又没有随线程更新,就会出现下面的crash 情况:
JNI 中 2025-08-04 19:15:16.227 8925-10816 libc com.tran.collaboration A Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 10816 (Thread-2), pid 8925 (n.collaboration)
Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 是 JNI 开发中常见的内存访问错误,本质是 C/C++ 代码尝试访问无效的内存地址(这里是 0x0,即空指针 NULL),导致操作系统触发段错误(Segmentation Fault)。
错误核心原因
SIGSEGV 是 Linux 系统的 “段错误” 信号,SEGV_MAPERR 表示程序尝试访问的内存地址未被映射到合法的内存空间,而 fault addr 0x0 明确指出:代码中存在对空指针(NULL)的解引用操作(比如访问 NULL->field 或 *(NULL))。
修复方案
核心原则:JNIEnv* 应作为局部变量使用,每次通过 JavaVM* 获取当前线程的有效实例,并严格检查所有指针的有效性。完整方案见文首的JavaObjCtx.h 代码
四、小结
场景 | 正确做法 |
需要跨线程使用 | 必须用 NewGlobalRef 转换 |
临时局部引用 | 确保不跨越 JNI 方法调用 |
调试检查 | 用 GetObjectClass 检测有效性 |
技术类比
JNI 概念 | 类似 C++ 概念 | 关键区别 |
局部引用 | 栈指针 | 自动管理生命周期 |
全局引用 | new 分配的对象 | 需手动释放 |
弱全局引用 | weak_ptr | 可能被 GC 回收 |
绝对禁忌
// 错误!跨线程传递局部引用
void dangerousCall(jobject obj) {
std::thread([=]{
// 即使 obj 地址非空,实际已失效
someJNICall(obj);
}).detach();
}
记住:在 JNI 中,指针非空 ≠ 引用有效!必须通过 JVM 机制验证。
相关推荐
- 半导体行业术语缩写词典总结-JKL_半导体词汇缩写表
-
作为半导体行业新人来说,最痛苦的莫过于各种缩写词术语了,有的缩写词一样但是会有不同的解释。这里作者给大家整理了部分术语词典,后面会按照更新顺序一一分享出来。废话不多说,直接开始,如有遗漏,欢迎大家在评...
- JD.com Deepens Push Into Embodied Intelligence With Investment in Sensor Maker PaXiniTech
-
ToraOne,thesecond-generationmultidimensionaltactilehumanoidrobotdevelopedbyPaXiniTechTMTPOS...
- Hong Kong's Consumer Market Becomes New Battleground for Chinese Mainland Internet Giants
-
AI-generatedimageTMTPOST--StrollthroughthestreetsofHongKongtoday,anditmightfeellikey...
- http2解决了哪些问题_简述http2的优点
-
HTTP/2(最初称为SPDY)是HTTP协议的第二个主要版本,它在HTTP/1.1的基础上进行了重大改进,旨在解决其在性能和效率方面的诸多瓶颈。以下是HTTP/2主要解决的问题:队头阻...
- China's economy stays strong and vital amid pressure
-
Peoplevisitthe4thChina-CEECExpo&InternationalConsumerGoodsFairinNingbo,eastChina's...
- JD.com Makes $2.4 Billion Bid for Ceconomy in Bold Push to Build a Global Retail Empire
-
TMTPOST--JD.comhasunveiledplanstoacquireGermany’sCeconomyAG—theparentofEurope’sleading...
- 深入剖析 Java 中的装饰器设计模式:原理、应用与实践
-
在Java软件开发的广阔天地里,设计模式犹如璀璨星辰,照亮我们构建高效、可维护系统的道路。今天,让我们聚焦于其中一颗闪耀的星——装饰器设计模式,深入探究它的奥秘,看看如何利用它为我们的代码赋予...
- 组合模式应用-适配器模式_适配器组件
-
写在前面Hello,我是易元,这篇文章是我学习设计模式时的笔记和心得体会。如果其中有错误,欢迎大家留言指正!该部分为各模式组合使用,涉及代码较多,熟能生巧。内容回顾定义适配器模式是一种结构型设计模式,...
- OOM (Out Of Memory) 故障排查指南
-
1.确认OOM类型首先需要确认是哪种类型的OOM:JavaHeapOOM:Java堆内存不足NativeMemoryOOM:本地内存不足MetaspaceOOM:元空间内存不足Contai...
- 刷完这49题,面试官当场给Offer!Java程序员必备指南
-
1.问题:如果main方法被声明为private会怎样?答案:能正常编译,但运行的时候会提示”main方法不是public的”。2.问题:Java里的传引用和传值的区别是什么?答案:传引用是指传递的是...
- C#编程基础(看这一篇就够了)_c#编程入门与应用
-
C#及其开发环境简介C#概述C#是一个现代的、通用的、面向对象的编程语言,由微软(Microsoft)开发,经Ecma和ISO核准认可。它由AndersHejlsberg和他的团队在.NET框架开发...
- 说一下JDK的监控和 线上处理的一些case
-
一句话总结JDK监控常用工具包括JConsole、VisualVM、JMC等,用于实时查看内存、线程、GC状态。线上常见问题处理:内存泄漏通过heapdump分析对象引用链;频繁GC可调整-Xmx/...
- JavaScript深拷贝极简指南:3种方法解决嵌套与循环引用难题
-
为什么需要深拷贝?首先我们看看浅拷贝,point指向的是同一个地址,这时我们修改obj2.point的属性时,obj1的point属性也会被修改再看看深拷贝,point指向的是不同地址,这时我们修改o...
- Java 25 在 JEP 519 中集成了紧凑对象头
-
作者|ANMBazlurRahman译者|刘雅梦策划|丁晓昀Java25通过JEP519将紧凑对象头作为产品特性进行了集成,在不需要更改任何代码的情况下,为开发人员提供了...
- 每日一练 Python 面试题(1)_python每日一记
-
以下是5道Python基本语法相关的面试题,涵盖变量、运算符、数据结构、函数和异常处理等核心概念:1.变量与作用域题目:以下代码的输出是什么?解释原因。x=10deffunc():...
- 一周热门
- 最近发表
-
- 半导体行业术语缩写词典总结-JKL_半导体词汇缩写表
- JD.com Deepens Push Into Embodied Intelligence With Investment in Sensor Maker PaXiniTech
- Hong Kong's Consumer Market Becomes New Battleground for Chinese Mainland Internet Giants
- http2解决了哪些问题_简述http2的优点
- China's economy stays strong and vital amid pressure
- JD.com Makes $2.4 Billion Bid for Ceconomy in Bold Push to Build a Global Retail Empire
- 深入剖析 Java 中的装饰器设计模式:原理、应用与实践
- 组合模式应用-适配器模式_适配器组件
- OOM (Out Of Memory) 故障排查指南
- 刷完这49题,面试官当场给Offer!Java程序员必备指南
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)
- JS Loop For (32)