Java程序main方法执行流程

Java程序main方法执行流程Java程序main方法执行流程当我们编写完java源代码程序后,经过javac编译后,执行java命令执行这个程序时,是怎么一步步的调用到我们程序中的main方法的呢?今天通过查看OpenJdk的源码来揭开它的神秘面纱。java命令是在安装jre/jdk时配置到系统环境路径中去的,执行java命令时会找到bin目录下的java可执行程序,并将我们编译后的java程序类名传递进去就可以执行了。…

大家好,又见面了,我是你们的朋友全栈君。

Java程序main方法执行流程

当我们编写完java源代码程序后,经过javac编译后,执行java命令执行这个程序时,是怎么一步步的调用到我们程序中的main方法的呢?今天通过查看OpenJdk的源码来揭开它的神秘面纱。

java命令是在安装jre/jdk时配置到系统环境路径中去的,执行java命令时会找到bin目录下的java可执行程序,并将我们编译后的java程序类名传递进去就可以执行了。

java可执行程序是由C++编写的,它的内部会启动一个Java虚拟机实例。
虚拟机启动入口函数位于src/java.base/share/native/launcher/main.c。

// src/java.base/share/native/launcher/main.c

// java程序启动入口主函数
JNIEXPORT int main(int argc, char **argv) {
    
    ...
    
    return JLI_Laucher(margc, margv,
                        jargc, (const char**) jargv,
                         0, NULL,
                   VERSION_STRING,
                   DOT_VERSION,
                   (const_progname != NULL) ? const_progname : *margv,
                   (const_launcher != NULL) ? const_launcher : *margv,
                   jargc > 0,
                   const_cpwildcard, const_javaw, 0)
        
}

// src/java.base/share/native/libjli/java.c

JNIEXPORT int JNICALL JLI_Launch(int argc,
                                char** argv,
                                int jargc,
                                const char** jargv,
                                int appclassc,
                                const char** appclassv,
                                const char* fullversion,
                                const char* dotversion,
                                const char* pname,
                                const char* lname,
                                jboolean javaargs,
                                jboolean cpwildcard,
                                jboolean javaw,
                                jint ergo) {
                                
                                
    ...               
    
    return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}


int ContinueInNewThread(InvocationFunction* ifn, 
                        jlong threadStackSize, 
                        int argc, 
                        char **argv, 
                        int mode, 
                        char *what, 
                        int ret) {
    int rslt;
    
    ...
    
    rslt = CallJavaMainInNewThread(threadStackSize, (void*)&args);
    return (ret != 0) ? ret : rslt;
}

//真正调用Java类的main函数入口
int JavaMain(void* _args) {
    JNIEnv *env = 0;
 
    jclass mainClass = NULL;
    //找到main函数所在的类
    mainClass = LoadMainClass(env, mode, what);   
    //获取main函数的参数
    mainArgs = CreateApplicationArgs(env, argv, argc);
    //从类中找到main方法标识
    mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
                                       "([Ljava/lang/String;)V");
    
    //调用main方法                                   
    (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
}

// src/java.base/macosx/native/libjli/java_md_macosx.m
// src/java.base/unix/native/libjli/java_md_solinux.c

int JVMInit(InvocationFunctions* ifn, jlkong threadStackSize, int argc,
            char **argv, int mode, char **what, int ret) {
            
    ...
    
    return continueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}

CallJavaMainInNewThread(jlong stack_size, void* args) {
    
    int rslt;
    
    ...
    
    
    rslt = JavaMain(args);
    
    return rslt;
}

//hotspot/share/prims/jni.cpp

//调用一个main这个静态方法
static void jni_invoke_static(JNIEnv *env, JavaValue* result, jobject receiver, JNICallType call_type, jmethodID method_id, JNI_ArgumentPusher *args, TRAPS) {
    
    JavaCalls::call(result, method, &java_args, CHECK);
}
// hotspot/share/runtime/javaCalls.cpp

void JavaCalls::call(JavaValue* result, const methodHandle& method, JavaCallArguments* args, TRAPS) {

    os::os_exception_wrapper(call_helper, result, method, args, THREAD);
}

void JavaCalls::call_helper(JavaValue* result, const methodHandle& method, JavaCallArguments* args, TRAPS) {

    //字节码解释器入口函数地址
    address entry_point = method->from_interpreted_entry();
    if (JvmtiExport::can_post_interpreter_events() && thread->is_interp_only_mode()) {
        entry_point = method->interpreter_entry();
    }
    
    ...
    
    通过call_stub->entry_point->method的调用链,完成Java方法的调用
    StubRoutines::call_stub()(
        (address)&link,//call_stub调用完后,返回值通过link指针带回来
        // (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
        result_val_address,          // see NOTE above (compiler problem)
        result_type,
        method(),
        entry_point,
        parameter_address,
        args->size_of_parameters(),
        CHECK
      );
      
      result = link.result();//获取返回值
}
//  hotspot/share/runtime/stubRoutines.hpp

// 将_call_stub_entry指针转换为CallStub类型,并执行该指针对应的函数
// 这个_call_stub_entry指针是通过stubGenerator类在初始化生成的,
// 这个stubGernerator负责为将要执行的方法创建栈帧,其实现区分不同CPU平台
static CallStub call_stub() {
    return CAST_TO_FN_PTR(CallStub, _call_stub_entry);
}

// Calls to Java
typedef void (*CallStub)(
    address   link,
    intptr_t* result,
    BasicType result_type,
    Method* method,
    address   entry_point,
    intptr_t* parameters,
    int       size_of_parameters,
    TRAPS
);

这里以x86_32平台为例进行说明_call_stub_entry的创建:

// hotspot/cpu/x86/stubGenerator_x86_32.cpp

class StubGenerator: public StubCodeGenerator {
    public:
    //构造函数
  StubGenerator(CodeBuffer* code, bool all) : StubCodeGenerator(code) {
    if (all) {
      generate_all();
    } else {
      generate_initial();
    }
  }
  
  //初始化
  void generate_initial() {
    
    ...
    
    //创见CallStub实例,并赋值给StubRoutines::_call_stub_entry
    StubRoutines::_call_stub_entry =
      generate_call_stub(StubRoutines::_call_stub_return_address);
    
    ...
  }
  
}


//创建方法调用的栈帧
address generate_call_stub(address& return_address) {
    
    //创建栈帧、参数入栈等
    
    ...
    
    __ movptr(rbx, method);           // 保存方法指针到rbx中
    __ movptr(rax, entry_point);      // get entry_point
    __ mov(rsi, rsp);                 // set sender sp
    
    //调用rax寄存器存储的解释器入口函数,这里解释器入口函数就是entry_point指针指向的函数
    //解释器就会开始从method指针指向的位置开始执行字节码
    __ call(rax);   
    
    ...
}

下面看一下解释器的入口函数的实现,从前面可以知道解释器入口函数是从method中获取到的。

// hotspot/share/oops/method.hpp

//该方法被内联了,即获取成员变量_from_interpreted_entry的值。
address from_interpreted_entry() const;

inline address Method::from_interpreted_entry() const {
  return Atomic::load_acquire(&_from_interpreted_entry);
}

// _from_interpreted_entry是在link_mehtod函数被赋值的。
void Mehthd

那么_from_interpreted_entry是在什么时候被赋值的呢?在链接方法时。

// hotspot/share/oops/method.cpp

void Method::link_method(const methodHandle& h_method, TRAPS) {
    
    ...
    
    if (!is_shared()) {
        //终于和字节码解释器勾搭上了
        //根据h_method的类型取出对应的解释器入口函数
        address entry = Interpreter::entry_for_method(h_method);
        set_interpreter_entry(entry);
    }
    
    ...
}
// hotspot/share/interpreter/abstractInterpreter.hpp

//解释器入口函数数组
static address  _entry_table[number_of_method_entries];

static MethodKind method_kind(const methodHandle& m);
static address entry_for_kind(MethodKind k){ 
     return _entry_table[k];
}

//从methodHandle中取出方法类型MethodKind,并根据方法类型从_entry_table取出对应的解释器入口函数地址
static address entry_for_method(const methodHandle& m){
    return entry_for_kind(method_kind(m));
}

//虚拟机启动时通过该函数填充_entry_table数组
static void set_entry_for_kind(MethodKind k, address e);

那么到底是什么时候调用的set_entry_for_kind函数来初始化的呢?

在文章开头说过,launcher/main.c中的main函数是java程序的启动函数,在main函数中调用了JLI_Launcher函数,在JLI_Launcher会调用LoadJavaVM函数加载虚拟机的动态链接库,并找到创建虚拟机的入口函数JNI_CreateJavaVM存储到结构体InvocationFunctions中。
这个结构体InvocationFunctions会一直当做参数传递到JavaMain函数中。
之后再JavaMain函数中,会根据JNI_CreateJavaVM虚拟机创建函数来初始化虚拟机,此时已经是在一个新的线程中运行了。

下面看一下具体的调用流程:

// src/java.base/share/native/libjli/java.c

JNIEXPORT int JNICALL JLI_Launch(int argc,
                                char** argv,
                                int jargc,
                                const char** jargv,
                                int appclassc,
                                const char** appclassv,
                                const char* fullversion,
                                const char* dotversion,
                                const char* pname,
                                const char* lname,
                                jboolean javaargs,
                                jboolean cpwildcard,
                                jboolean javaw,
                                jint ergo) {
                                
                                
    ...               
    
    //Java虚拟机动态链接库的路径
    char jvmpath[MAXPATHLEN];
    //
    InvocationFunctions ifn;
    
    //从参数中读取虚拟机运行环境所需的配置
    CreateExecutionEnvironment(&argc, &argv,
                               jrepath, sizeof(jrepath),
                               jvmpath, sizeof(jvmpath),
                               jvmcfg,  sizeof(jvmcfg));
    
    ifn.CreateJavaVM = 0;
    ifn.GetDefaultJavaVMInitArgs = 0;
    
    //加载Java虚拟机动态链接库,并找到创建虚拟的函数JNI_CreateJavaVM
    //这里会区分不同平台和CPU位数,但大体上就是使用dlopen和dlsym这个两个系统调用来实现
    if (!LoadJavaVM(jvmpath, &ifn)) {
        return(6);
    }
    ...
    
    return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}

下面以macos平台为例看一下LoadJavaVM的实现:

// src/java/base/macosx/native/libjli/java_md_macosx.m

jboolean LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{
    //动态链接库文件句柄
    void *libjvm;
    
    //判断是否是静态编译,使用dlopen函数打开动态链接库
#ifndef STATIC_BUILD
    libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL);
#else
    libjvm = dlopen(NULL, RTLD_FIRST);
#endif

    //使用dlsym函数根据函数符号找到对应函数地址
    //这里一共获取了三个函数JNI_CreateJavaVM、JNI_GetDefaultJavaVMInitArgs、JNI_GetCreatedJavaVMs
    //如果有任何一下缺失都会返回错误,如果成功则将三个函数存储到InvocationFunctions结构体中。
    ifn->CreateJavaVM = (CreateJavaVM_t)
        dlsym(libjvm, "JNI_CreateJavaVM");
        
    ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)
        dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");
        
    ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)
    dlsym(libjvm, "JNI_GetCreatedJavaVMs");
    
    return JNI_TRUE;
}

从动态链接库中找到创建虚拟机的入口函数后,会把InvocationFunctions结构体作为参数一路传递到JavaMain函数中,并在其中发起调用。

// src/java.base/share/native/libjli/java.c

int JavaMain(void* _args) {
    //将参数强制转换为JavaMainArgs类型
    JavaMainArgs *args = (JavaMainArgs *)_args;
    //从参数取出InvocationFunctions结构体
    InvocationFunctions ifn = args->ifn;
    
    JavaVM *vm = 0;
    JNIEnv *env = 0;
    
    ...
    
    //初始化Java虚拟机
    if (!InitializeJVM(&vm, &env, &ifn)) {
        JLI_ReportErrorMessage(JVM_ERROR1);
        exit(1);
    }
    
    ...
}

static jboolean InitializaJVM(JavaVM **pwm, JNIENV **penv, InvocationFunctions *ifn) {
    JavaVMInitArgs args;
    jint r;

    memset(&args, 0, sizeof(args));
    args.version  = JNI_VERSION_1_2;
    args.nOptions = numOptions;
    args.options  = options;
    args.ignoreUnrecognized = JNI_FALSE;
    
    //调用JNI_CreateJavaVM函数创建虚拟机,该函数内部会转调JNI_CreateJavaVM_inner函数
    r = ifn->CreateJavaVM(pvm, (void **)penv, &args);
    JLI_MemFree(options);
    return r == JNI_OK;
}
// hotspot/share/prims/jni.cpp

static jint JNI_CreateJavaVM_inner(JavaVM **vm, void **penv, void *args) {
    jint result = JNI_ERR;
    
    //通过Threads的create_vm函数创建虚拟机
    result = Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);
    
    if (result == JNI_OK) {
       JavaThread *thread = JavaThread::current();
        *vm = (JavaVM *)(&main_vm);
        *(JNIEnv**)penv = thread->jni_environment(); 
        post_thread_start_event(thread);
        ThreadStateTransition::transition(thread, _thread_in_vm, _thread_in_native);
    }
    ...
    
    return result;
}

Threads::create_vm函数非常长,里面执行了很多初始化工作。例如

  • 预初始化信息,可以在初始化虚拟机之前预先初始化一些可能用到的信息,如虚拟机版本。不同的CPU平台可以在这里初始化自己定义的信息。
  • 初始化ThreadLocalStorage
  • 初始化输出流模块
  • 初始化os操作系统模块,主要是一些固定配置
  • 初始化系统属性
  • 初始化JDK版本
  • 根据JDK版本初始化特定参数
  • 解析命令行参数
  • 初始化和应用ergonomics,主要是初始化大内存页,根据CPU核心数、内存容量设置虚拟内存页大小、JVM内存参数、GC策略、java8取消永久代新增Metaspace区。
  • 解析完参数后,二次初始化os模块。例如快速线程时钟、Linux信号处理器、最小栈长度、最大文件描述符数量、线程优先级策略等
  • 初始化安全点机制,安全点机制是很重要的概念。安全点是指一些特定的位置,当线程运行到这些位置时,线程的一些状态可以被确定。
  • 初始化输出流日志
  • 加载系统库
  • 初始化全局数据结构。如java基础类型、事件队列、全局锁、JVM性能统计区、大块内存池chunkpool等
  • 创建JavaThread
  • 初始化对象监视器ObjectMonitor,它是Java语言级别的同步子系统
  • 初始化全局模块,这些模块是Hotspot的整体基础,如字节码初始化、类加载器初始化、编译策略初始化、解释器初始化等等。我们前面追踪的_entry_table数组就是在这里面初始化的。
  • 创建VMThread,并开始执行。VMThread用于执行VMOptions
  • 初始化主要JDK类,如String类、System类,Class类、线程/线程组类、Module类,还有其他反射、异常相关的类
  • 初始化jni方法的快速调用
  • 标记虚拟机的基本初始化完成
  • 日志系统的后续配置
  • 元数据区Metaspace的后续配置
  • 初始化jdk信号支持
  • 初始化Attach监听机制,它是JVM提供进程间通信机制,负责接收处理其他进程发送过来的命令
  • 初始化JSR292标准核心类,JSR292标准引入invokedynamic指令以支持调用动态类型语言中的方法,使得在把源码编译成字节码时不需要确定方法的签名。当运行invokedynamic指令时,JVM会通过新的动态链接机制Method Handles,寻找到真实的方法。
  • 第二阶段,初始化模块化系统
  • 第三阶段,初始化安全管理器、设置系统类加载器作为线程上下文的类加载器
  • 启动监听线程WatcherThread,用来模拟时钟中断。

Threads::create_vm函数中做了上面那么多工作,这里为了简单就不讲所有源码都贴出来了。
因为_entry_table数组的填充是在init_globals()函数中调用的,所以只说明一下init_globals()函数的调用路径。

// hotspot/share/runtime/thread.cpp
jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {
    
    ...
    
    //初始化全局模块
    jint status = init_globals();
    
    ...
    
    return JNI_OK;
}


// hotspot/share/runtime/init.cpp
jinit init_globals() {
    ...
    
    //初始化方法处理适配器
    MethodHandles::generate_adapters();
    ...
}

// hotspot/share/prims/methodHandles.cpp
MethodHandlesAdapterBlob* MethodHandles::_adapter_code = NULL;
void MethodHeandles::generate_adapters() {
    _adapter_code = MethodHandlesAdapterBlob::create(adapter_code_size);
    CodeBuffer code(_adapter_code);
    MethodHandlesAdapterGenerator g(&code);
    g.generate();
}

void MethodHandlesAdapterGenerator::generate() {
    //1.生成通用方法处理适配器
    //2.生成解释器入口点
    
    //这里的MethodKinds是一个枚举类型,声明了虚拟机固有的方法类型
    for (Interpreter::MethodKind mk = Interpreter::method_handle_invoke_FIRST;
    mk <= Interpreter::method_handle_invoke_LAST;
    mk = Interpreter::MethodKind(1 + (int)mk)) {
    vmIntrinsics::ID iid = Interpreter::method_handle_intrinsic(mk);
    StubCodeMark mark(this, "MethodHandle::interpreter_entry", vmIntrinsics::name_at(iid));
    
    //根据方法类型ID生成对应的方法处理解释器入口点,并通过set_entry_for_kind设置到abstractInterpreter.cpp中的_entry_table数组中。
    address entry = MethodHandles::generate_method_handle_interpreter_entry(_masm, iid);
    if (entry != NULL) {
      Interpreter::set_entry_for_kind(mk, entry);
    }
  }
}

// hotspot/share/interpreter/abstractInterpreter.cpp

void AbstractInterpreter::set_entry_for_kind(AbstractInterpreter::MethodKind kind, address entry) {

    //将解释器入口点填充到_entry_table数组中
     _entry_table[kind] = entry;

     update_cds_entry_table(kind);
}

到此就可以总结一下了,当我们通过java命令执行一个应用程序时,首先会先启动虚拟机实例,启动过程中包含了很多初始化工作,这些工作是为java程序提供运行环境的必要条件。在初始化工作中会根据不同的方法类型构建对应解释器入口点,并存储到一个数组_entry_table中。
当初始化工作完成后,会调用java应用程序的入口方法(static void main(String[] args)),然后根据main方法的类型从_entry_table数组中找出对应的解释器入口点,然后就开始解释执行main方法的字节码了。

最后介绍一下JVM中都预定义了哪些方法类型。

// hotspot/share/interpreter/abstractInterpreter.hpp

enum MethodKind {
    //大多数没有声明为native和synchronized方法都属于这种类型
    //在执行之前要将局部变量初始化为0
    zerolocals,                                                 // method needs locals initialization
    zerolocals_synchronized,                                    // method needs locals initialization & is synchronized
    native,                                                     // native method
    native_synchronized,                                        // native method & is synchronized
    //空方法,也单独由特定解释器处理,避免创建无效的栈帧
    empty,                                                      // empty method (code: _return)
    //成员变量的get方法
    accessor,                                                   // accessor method (code: _aload_0, _getfield, _(a|i)return)
    abstract,                                                   // abstract method (throws an AbstractMethodException)
    method_handle_invoke_FIRST,                                 // java.lang.invoke.MethodHandles::invokeExact, etc.
    method_handle_invoke_LAST                                   = (method_handle_invoke_FIRST
                                                                   + (vmIntrinsics::LAST_MH_SIG_POLY
                                                                      - vmIntrinsics::FIRST_MH_SIG_POLY)),
    
    //一些固定作用的方法,直接指定特定的解释器入口,提高效率
    java_lang_math_sin,                                         // implementation of java.lang.Math.sin   (x)
    java_lang_math_cos,                                         // implementation of java.lang.Math.cos   (x)
    java_lang_math_tan,                                         // implementation of java.lang.Math.tan   (x)
    java_lang_math_abs,                                         // implementation of java.lang.Math.abs   (x)
    java_lang_math_sqrt,                                        // implementation of java.lang.Math.sqrt  (x)
    java_lang_math_log,                                         // implementation of java.lang.Math.log   (x)
    java_lang_math_log10,                                       // implementation of java.lang.Math.log10 (x)
    java_lang_math_pow,                                         // implementation of java.lang.Math.pow   (x,y)
    java_lang_math_exp,                                         // implementation of java.lang.Math.exp   (x)
    java_lang_math_fmaF,                                        // implementation of java.lang.Math.fma   (x, y, z)
    java_lang_math_fmaD,                                        // implementation of java.lang.Math.fma   (x, y, z)
    java_lang_ref_reference_get,                                // implementation of java.lang.ref.Reference.get()
    java_util_zip_CRC32_update,                                 // implementation of java.util.zip.CRC32.update()
    java_util_zip_CRC32_updateBytes,                            // implementation of java.util.zip.CRC32.updateBytes()
    java_util_zip_CRC32_updateByteBuffer,                       // implementation of java.util.zip.CRC32.updateByteBuffer()
    java_util_zip_CRC32C_updateBytes,                           // implementation of java.util.zip.CRC32C.updateBytes(crc, b[], off, end)
    java_util_zip_CRC32C_updateDirectByteBuffer,                // implementation of java.util.zip.CRC32C.updateDirectByteBuffer(crc, address, off, end)
    java_lang_Float_intBitsToFloat,                             // implementation of java.lang.Float.intBitsToFloat()
    java_lang_Float_floatToRawIntBits,                          // implementation of java.lang.Float.floatToRawIntBits()
    java_lang_Double_longBitsToDouble,                          // implementation of java.lang.Double.longBitsToDouble()
    java_lang_Double_doubleToRawLongBits,                       // implementation of java.lang.Double.doubleToRawLongBits()
    number_of_method_entries,
    invalid = -1
  };
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/146193.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 一个QQ空间的钓鱼盗号过程揭露,大家谨防上当[通俗易懂]

    一个QQ空间的钓鱼盗号过程揭露,大家谨防上当[通俗易懂]1.开端很久没有用过QQ空间了,今天突然QQ弹出一条消息,说我的一个好友留言中提到了我,但是我却也打不开这个链接。于是,我就去她的空间留言板查看。发现第一条留言,是一个二维码扫描之后,进入到一个网页,是标准的QQ空间登录页。这么敏感的信息,当然要慎之又慎!我们识别一下这个二维码,发现!他并不是腾讯的域名。如下http://gallatinboom.com/catfish/xxyy….

  • Python标识符的命名规则,下列哪些是对的?_python标识符不能使用关键字

    Python标识符的命名规则,下列哪些是对的?_python标识符不能使用关键字[快速理解]Python标识符是指变量、函数、类、模块等的名称。例如:a=10中的a是标识符反例:foriin[1,2,3]中的for和in不是标识符,是保留字,i是标识符。Python保留字有特殊的语法功能。选择题以下选项中都可以作为Python标识符的是:选项:A_py99pyBcueba_intCandChinaDstr1else问题解析Python标识符的命名规则:1.标识符的第一个字符必须是字母、下划线,其后的字符可以是字…

  • uniapp背景图片占不满屏幕_vue背景图片加载不出来

    uniapp背景图片占不满屏幕_vue背景图片加载不出来1.手机支持base64(小于40kb会自动转换否则就只能自己动手了2.路径前面加~@如:background:url(“~@/static/img/phb-no2@2x.jpg”);图片大于40k可以自己转化转化base64使用如:background:url(“data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAu4AAAEoCAI…

  • Dreamweaver8的安装

    Dreamweaver8的安装安装步骤:Step1:双击<Dreamweaver8-chs>Step2:单击<下一步>Step3:选中<我接受该许可证协议中的条款>,单击<下一步>按钮Step4:选中<在桌面上创建快捷方式(针对所有用户)>,单击<下一步>Step5:单击<下一步>S…

  • ideal的debug_idea debug怎么用

    ideal的debug_idea debug怎么用Debug介绍Debug设置如上图标注1所示,表示设置Debug连接方式,默认是Socket。Sharedmemory是Windows特有的一个属性,一般在Windows系统下建议使用此设置,相对于Socket会快点。Debug常用快捷键快捷键 介绍 F7 在Debug模式下,进入下一步,如果当前行断点是一个方法,则进入当…

  • datagrip2021激活码【中文破解版】

    (datagrip2021激活码)2021最新分享一个能用的的激活码出来,希望能帮到需要激活的朋友。目前这个是能用的,但是用的人多了之后也会失效,会不定时更新的,大家持续关注此网站~IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.cn/100143.html…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号