main函数初探

17 篇文章 0 订阅
11 篇文章 0 订阅

题外话

刚学java的同学肯定都知道main方法是一个程序的入口,为我们创建了一个主线程,作为一个老油条了,今天学习springboot项目启动时发现也是通过main方法启动的,于是就触发了我的好奇心,main函数是怎么被执行的呢?被执行之前虚拟机帮我们做了些什么呢?

想了解springboot启动流程和autoconfig的同学请看这里:

  1. SpringBoot之@EnableAutoConfiguration注解
  2. springboot autoconfig 自动配置原理实现

main函数的类是怎么被虚拟机识别加载至内存的

java编译成class文件后,虚拟机是怎么解析class文件并且初始化我们的实体类的呢? 这时候提到jvm类加载机制和双亲委派模型了
在这里插入图片描述
1)Bootstrap ClassLoader

负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类

2)Extension ClassLoader

负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包

3)App ClassLoader

负责记载classpath中指定的jar包及目录中class

4)Custom ClassLoader

属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

那么我们自己在项目里面写的代码是由哪个加载器加载的呢?

双亲委派
双亲委派模式要求除了顶层的启动类加载器之外,其余的类加载器都应该有自己的父类加载器,但是在双亲委派模式中父子关系采取的并不是继承的关系,而是采用组合关系来复用父类加载器的相关代码。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    // 增加同步锁,防止多个线程加载同一类
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else { // ExtClassLoader没有继承BootStrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                // AppClassLoader去我们项目中查找是否有这个文件,如有加载进来
                // 没有就到用户自定义ClassLoader中加载。如果没有就抛出异常
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

工作原理
如果一个类收到了类加载的请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行,如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最后到达顶层的启动类加载器,如果弗雷能够完成类的加载任务,就会成功返回,倘若父类加载器无法完成任务,子类加载器才会尝试自己去加载,这就是双亲委派模式。就是每个儿子都很懒,遇到类加载的活都给它爸爸干,直到爸爸说我也做不来的时候,儿子才会想办法自己去加载。

优势
采用双亲委派模式的好处就是Java类随着它的类加载器一起具备一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类的时候,就没有必要子类加载器(ClassLoader)再加载一次。其次是考虑到安全因素,Java核心API中定义类型不会被随意替换,假设通过网路传递一个名为java.lang.Integer的类,通过双亲委派的的模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字类,发现该类已经被加载,并不会重新加载网络传递过来的java.lang.Integer.而之际返回已经加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在calsspath路径下自定义一个名为java.lang.SingInteger?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器,最终会通过系统类加载器加载该类,但是这样做是不允许的,因为java.lang是核心的API包,需要访问权限,强制加载将会报出如下异常。

java.lang.SecurityException:Prohibited package name: java.lang

想了解更多关于jvm类加载机制和双亲委派模型可以参考:

  1. 深入理解JVM类加载机制
  2. JVM内存结构、垃圾收集算法、垃圾收集器、字节码文件结构、类加载机制

jvm的运行机制

开始进入正题,让我们一起看看底层C++代码是怎么一步一步执行到main函数的吧

main()方法:

int main(int argc, char *argv[])
{
//#ifdef DEBUG
    sync_wcout::set_switch(true);
//#endif
    if (argc != 2) {
        std::wcerr << "argc is not 2. please re-run." << std::endl;
        exit(-1);
    }
    wstring program = utf8_to_wstring(std::string(argv[1]));
    std::ios::sync_with_stdio(true);		// keep thread safe?
    std::wcout.imbue(std::locale(""));
    std::vector<std::wstring> v{ L"automan_jvm", L"1", L"2" };
    automan_jvm::run(program, v);
}

从源码中,我们可以看到,它从程序启动时获取参数,并进行监测。 目前该程序要求的是只能有两个参数。 但是实际上还有许多参数是可以在这里配置的 ,参数设置请看这里 JVM参数设置
run()方法:
然后,它以相关的参数,调用了 jvm的 run方法。 代码如下:


void automan_jvm::run(const wstring & main_class_name, const vector<wstring> & argv)
{
    //todo: 这里注册了一个交互信号,信号处理程序又是一个 无限循环,需要注意
    //todo: 用 raise 生成信号
    signal(SIGINT, SIGINT_handler);
    automan_jvm::main_class_name() = std::regex_replace(main_class_name, std::wregex(L"\\."), L"/");
    automan_jvm::argv() = const_cast<vector<wstring> &>(argv);
    vm_thread *init_thread;
    automan_jvm::lock().lock();
    {
        automan_jvm::threads().push_back(vm_thread(nullptr, {}));
        init_thread = &automan_jvm::threads().back();
    }
    automan_jvm::lock().unlock();
    init_native();
    HANDLE gc_tid;
    gc_tid= (HANDLE)(_beginthreadex(nullptr, 0, reinterpret_cast<unsigned int (*)(void *)>(GC::gc_thread), NULL, 0, NULL));
    gc_thread() = gc_tid;
    // go!
    init_thread->launch();		// begin this thread.
}

注意到代码的第二行,它注册了一个交互信号,此时的环境仍然是本地线程,即该线程为根线程,不受jvm的管理。信号处理程序实际上是一个 gc任务,这也是为什么我们时常听到java的gc触发,既有jvm管理的部分,也有本地的部分。 它的代码如下:

void SIGINT_handler(int signo)
{
    // re-use gc bit to stop-the-world,but won't trigger GC。
    while (true) {
        bool gc;
        GC::gc_lock().lock();
        {
            gc = GC::gc();
        }
        GC::gc_lock().unlock();
        if (gc) {
            continue;
        } else {
            GC::gc_lock().lock();
            {
                GC::gc() = true;
            }
            GC::gc_lock().unlock();
            // FIXME: I don't know whether it is safe... only a solution for dead lock of wind_jvm::num_lock...
            automan_jvm::num_lock().unlock();		// It's only a patch.
            GC::detect_ready();
            GC::gc() = false;		// set back
            BytecodeEngine::main_thread_exception();		// exit
        }
    }
}

我们观察到,该方法实际上是一个无限循环,但是这个线程又是根线程,所以我最开始有一些疑惑。 现在回头来看时,我注意到: GC::detect_ready(),该方法实际上也是无限循环的,但是,它会主动的释放cpu。 我们看看这个方法:


void GC::detect_ready()
{
    while (true) {
        LockGuard lg(gc_lock());
        int total_ready_num = 0;
        int total_size;
        ThreadTable::get_lock().lock();
        {
            total_size = ThreadTable::get_thread_table().size();
            for (auto & iter : ThreadTable::get_thread_table()) {
                thread_state state = std::get<2>(iter.second)->state;
                if (state == Waiting || state == Death/*iter.second == false && iter.first->vm_stack.size() == 0*/) {
                    total_ready_num ++;
                } else {
                    break;
                }
            }
        }
        ThreadTable::get_lock().unlock();
        ThreadTable::print_table();		// delete
        if (total_ready_num == total_size) {		// over!
            return;
        }
        Sleep(1);
    }
}

可以看到,它实际上在管理 jvm内部的线程。 根据线程状态,进行线程的回收。 它的退出条件是: jvm的内部所有线程均为活跃线程。 同时,它没有互斥量进行阻塞,可见它的活跃程度是很高的。 换言之,一旦程序触发了退出信号,jvm内部的线程维护,几乎时刻在运行。

让我们回到 信号处理程序上,它最后调用了: BytecodeEngine::main_thread_exception(),它的代码及作用如下:


void BytecodeEngine::main_thread_exception(int exitcode)		// dummy is use for BytecodeEngine::excute / SIGINT_handler.
{
    automan_jvm::lock().lock();
    {
        for (auto & thread : automan_jvm::threads()) {
            WaitForSingleObject(_all_thread_wait_mutex,INFINITE);
            thread_state state = thread.state;
            ReleaseMutex(_all_thread_wait_mutex);
            if (state == Death) {					// pthread_cancel SIGSEGV bug sloved:
                continue;
            }
            if (thread.tid != GetCurrentThreadId()) {
                HANDLE _handle =OpenThread(THREAD_ALL_ACCESS,FALSE,GetCurrentThreadId());
                WaitForSingleObject(_handle,INFINITE);
                CloseHandle(_handle);
                //todo: 当线程执行完后,清理。
                cleanup(nullptr);
            } else {
                thread.state = Death;
            }
        }
    }
    automan_jvm::lock().unlock();
    GC::cancel_gc_thread();
    automan_jvm::end();
    exit(exitcode);
}

可以看到,它实际上是等待当前jvm中,所有的线程执行完毕,回收相关的资源。 然后回收gc线程,调用jvm的end方法收尾,最后退出整个程序。

因此,我们可以说: jvm启动之初,挂载了一个信号处理程序,该程序负责所有的收尾工作,一旦接收到特定信号,整个程序完成,然后退出。 但是这里我一点不懂,它设计成了 while(true)的方式,但是实际上该代码块只能被执行一次,这个问题留待以后有缘再回答吧。

ok,如今我可以回退退退到: jvm的run方法那里,继续解读。

信号处理程序注册完后,之后的代码功能依次是:

1.将传入的参数保存到jvm中;

2.在jvm的线程表中,插入了一个方法和参数均为空的线程,注意了,该线程将被称为初始化线程(init_thread),并且该线程不受 jvm的管控,它是本地线程象征性的放入 jvm的线程表。

3.初始化本地方法,实际上就是将本地的库地址进行缓存,本质上将本地的方法缓存起来,缓存的内容包括native包下的所有类的核心方法。

4.开启一个gc线程,同时该gc线程并不会放入 jvm的线程表中,而是单独的存储在jvm中。也就是jvm可以直接操纵该线程。 此外,这个gc线程本身也是一个真正意义上的线程,它才生成之后,将会根据信号,阻塞式的进行垃圾清理。它与上面注册的那个信号处理程序有些不同,我们可以看到其源码:


unsigned *GC::gc_thread(void *)
{
    // init `cond` and `mutex` first:
    gc_cond = CreateEvent(NULL,FALSE,FALSE,NULL);
    gc_cond_mutes=CreateMutex(NULL,FALSE,NULL);
    while (true) {
        WaitForSingleObject(gc_cond_mutes,INFINITE);
        //todo: 这里会等待gc条件,该线程具有跟进程一样长的生命周期
        WaitForSingleObject(gc_cond,INFINITE);
        ReleaseMutex(gc_cond_mutes);
        detect_ready();
        system_gc();
    }
}

它会阻塞式的接收处理信号,每次任务,将会首先处理线程回收,然后处理资源回收,也就是system_gc()的作用,考虑到这里主线不是讨论gc,所以暂时先不看gc的细节。

5.初始化线程调用launch()操作,进行java程序的启用,需要注意,当前的初始化线程(init_thread),也就是根线程。

launch()方法:
该方法可以说是关键了,内容很细也很多,考虑到本文的主线任务,将略写本方法,提一提它的功能作用即可,其代码如下:


void vm_thread::launch(InstanceOop *cur_thread_obj)
{
    // start one thread
    p.thread = this;
    p.arg = &const_cast<std::list<Oop *> &>(arg);
    p.cur_thread_obj = cur_thread_obj;
    if (cur_thread_obj != nullptr) {		// if arg is not nullptr, must be a thread created by `start0`.
        p.should_be_stop_first = true;
    }
    bool inited = automan_jvm::inited();
    //todo: 实际上这个线程是用于初始化的
   HANDLE cur_handle = (HANDLE)(_beginthreadex(NULL, 0, scapegoat, &p, 0, NULL));
    this->tid = GetThreadId(cur_handle);		// save to the vm_thread.
    if (!inited) {		// if this is the main thread which create the first init --> thread[0], then wait.
        //todo: 阻塞执行 tid线程,tid执行完后才往后执行
        WaitForSingleObject(cur_handle,INFINITE);
        GC::signal_all_patch();
        int remain_thread_num;
        while(true) {
            automan_jvm::num_lock().lock();
            {
                remain_thread_num = automan_jvm::thread_num();
            }
            automan_jvm::num_lock().unlock();
 
            assert(remain_thread_num >= 0);
            if (remain_thread_num == 0) {
                break;
            }
            //让出CPU调度
            Sleep(0);
        }
        GC::cancel_gc_thread();
        automan_jvm::end();
#ifdef DEBUG
        sync_wcout{} << pthread_self() << " run over!!!" << std::endl;		// delete
#endif
    }
}

从代码中可以看出,它首先将 init_thread与jvm进行了绑定,然后获取jvm的初始化状态。当然了首次运行时,此时其肯定未被初始化。 之后它将开启另一个线程,注意注意了,这是继 gc线程后,本程序开启的第二个线程。 这个线程实际上将是我们在java端调用mian方法的那个线程。同时,它也会做很多的工作,在本文中,我先暂不讨论,后文会专门的讨论。

之后,init_thread将会阻塞在此,直到java的mian线程结束。 之后init_thread会做如下工作:

1.唤醒所有阻塞的线程。 其代码如下:


void GC::signal_all_patch()
{
    while(true) {
        gc_lock().lock();
        if (!GC::gc()) {		// if not in gc, signal all thread is okay.
            signal_all_thread();
            break;
        }
        gc_lock().unlock();
    }
    gc_lock().unlock();


void signal_all_thread()
{
   int size = automan_jvm::thread_num();
    for (int i = 0; i < size; ++i) {
        SetEvent(_all_thread_wait_cond);
        //todo: 这里通过释放 CPU 达到broadst的目的
        Sleep(0);
    }
}

2.判断当前jvm的线程数量,当线程数量为0的时候,退出循环。

3.取消gc线程,以及 调用 jvm的end进行收尾。 整个程序代码执行完毕,退出。 这里我需要提一下,windows中,主线程退出,则子线程也会立即退出,无论子线程是否执行完毕(linux则不会)。 所以,这里面对jvm 的稳健性有要求,如果jvm错误的判断当前系统中的线程数,则会造成一个不可预见的后果。 (注意,当java的主线程执行完毕后,jvm实际上进入了一个预退出状态,此时对线程的管理是十分活跃的,正如 信号处理程序中的那样!我不知道这是整个demo本身的原因,还是说发行版的jvm就是这样设计的。)

总结:
目前,我们知道,整个jvm的原生阶段(不考虑java中新开线程的影响),包含了三个线程,即初始化线程,gc线程,java的主线程。

程序的正常退出包括两个途径:

1.java的mian线程执行完毕后,且jvm的其它线程均执行完毕,则整个程序会因为代码执行完毕而退出。

2.触发了退出的信号,我查了下信号量: SIGINT, 它好像是 “通过ctrl+c对当前进程发送结束信号”,这就说得通了。

在代码中, 我找到有主动发出这个信号的地方,位于runtime/thread中,但是pthread中,是给单个线程发送信号,在windows中,我暂未找到相关的api,因此就是粗略处理的: 其代码如下:


void ThreadTable::kill_all_except_main_thread(DWORD main_tid)
{
    for (auto iter : get_thread_table()) {
        if (iter.first == main_tid)	continue;
        else {
            //这里相当于触发gc
          DWORD ret = raise(SIGINT);
            if (ret!=0) {
                assert(false);
            }
        }
    }
}

从它的方法名称,以及实现来看,它应该是要 关闭除了当前线程之外的其它所有 jvm线程。可是一旦触发信号后,实际上会导致程序整体退出。 我想这可能是 跟 main_exception_thread有关,那个方法会根据当前的线程,而定点关闭线程。 同时整个代码块是线程安全的。 这样就是说,当SIGINT信号走到了 取消gc线程的时候,那么所有的线程一定是关闭了的。 bingo!!

目前尚未验证,但是我想应该是这样的。 只是不知道这里并非根据线程去触发 信号,应该是需要进一步完善的。

目前为止,整个jvm的行为算是粗略的分析完了

jvm的启动细节1—launch

节上文,jvm的launch方法的内容详细讲述一下。 在vm的launch中,有如下方法块:


....
HANDLE cur_handle = (HANDLE)(_beginthreadex(NULL, 0, scapegoat, &p, 0, NULL));
    this->tid = GetThreadId(cur_handle);		// save to the vm_thread.
    if (!inited) {		// if this is the main thread which create the first init --> thread[0], then wait.
        //todo: 阻塞执行 tid线程,tid执行完后才往后执行
        WaitForSingleObject(cur_handle,INFINITE);
....

其中第一行代码开辟的这个线程,便是java中的mian线程,也就是java中的主线程。 它接收命令的参数去执行任务。 其中,scapegoat方法 的代码如下:


//线程的任务
unsigned scapegoat (void *pp) {
    temp *real = (temp *)pp;
//	if (real->cur_thread_obj != nullptr) {		// so the ThreadTable::get_thread_obj may be nullptr.		// I add all thread into Table due to gc should stop all threads.
    ThreadTable::add_a_thread(GetCurrentThreadId(), real->cur_thread_obj, real->thread);		// the cur_thread_obj is from `java/lang/Thread.start0()`.
//	}
    if (real->should_be_stop_first) {		// if this thread is a child thread created by `start0`: should stop it first because of gc's race.
        // it will be hung up at the `global pthread_cond`. and will be wake up by `signal_all_thread()`.
        wait_cur_thread_and_set_bit(&real->the_first_wait_executed, real->thread);
    }
    real->thread->start(*real->arg);
    return 0;
};

通过代码不难察觉,它首先将当前的线程加入了jvm的线程表中,进行管理(注意,此时jvm线程表中,实际上管理的线程有两个了,一个是init_thread,它是本地线程的抽象,另一个就是 mian线程,也就是当前加入的这个线程)。 接着,当前线程(mian线程) 将会调用 start()方法,将命令行的参数一并传递。 start的源码如下:


void vm_thread::start(list<Oop *> & arg)
{
    if (automan_jvm::inited() == false) {
        assert(method == nullptr);			// if this is the init thread, method will be nullptr. this thread will get `main()` automatically.
        assert(arg.size() == 0);
        automan_jvm::inited() = true;			// important!
        //todo: 这里是执行 main 方法  ,重要
        vm_thread::init_and_do_main();		// init global variables and execute `main()` function.
    } else {
        // [x] if this is not the thread[0], detach itself is okay because no one will pthread_join it.
        //todo: 这里分离子线程
//        CloseHandle(tid);
//        pthread_detach(pthread_self());
        assert(this->vm_stack.size() == 0);	// check
        assert(arg.size() == 1);				// run() only has one argument `this`.
        this->vm_stack.push_back(StackFrame(method, nullptr, nullptr, arg, this));
        this->execute();
        automan_jvm::num_lock().lock();
        {
            automan_jvm::thread_num() --;
            assert(automan_jvm::thread_num() >= 0);
        }
        automan_jvm::num_lock().unlock();
    }
    WaitForSingleObject(_all_thread_wait_mutex,INFINITE);
    this->state = Death;
    ReleaseMutex(_all_thread_wait_mutex);
}

它会根据jvm是否初始化而判定当前线程的start是去引导和启动main方法,还是一般的线程。 注意我们在学习java的时候,实例化线程我们需要重写run(){}方法,这个run方法里面写的实际上是线程的任务,而线程的启动,是由一个start0本地方法,即jvm调用的。 调用之后,会来到这里这个代码块。 它将走else下面这个代码逻辑,里面实际上就是执行了run方法。 run方法执行完后,会将当前的线程状态改为 death. 之后这个线程便会在特定的时期被gc给回收掉。

扯远了,我们还是看看main线程的操作吧。 它将会调用 vm_thread::init_and_do_main()方法。 这个方法就比较长,我将分块展示。

1.初始化Class,用于类的映射

java_lang_class::init();		// must init !!!
        auto class_klass = BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/Class");
        java_lang_class::fixup_mirrors();	// only [basic types] + java.lang.Class + java.lang.Object

首先,调用java_lang_class::init()方法,它的作用是将 使用标识符与 (核心)类进行隐射,代码如下:

void java_lang_class::init() {		// must execute this method before jvm!!!
    auto & delay_mirrors = get_single_delay_mirrors();
    // basic types.
    delay_mirrors.push(L"I");
    delay_mirrors.push(L"Z");
    delay_mirrors.push(L"B");
    delay_mirrors.push(L"C");
    delay_mirrors.push(L"S");
    delay_mirrors.push(L"F");
    delay_mirrors.push(L"J");
    delay_mirrors.push(L"D");
    delay_mirrors.push(L"V");	// void...
    delay_mirrors.push(L"[I");
    delay_mirrors.push(L"[Z");
    delay_mirrors.push(L"[B");
    delay_mirrors.push(L"[C");
    delay_mirrors.push(L"[S");
    delay_mirrors.push(L"[F");
    delay_mirrors.push(L"[J");
    delay_mirrors.push(L"[D");
    // set state
    state() = Inited;
}

然后,使用BootstrapClassLoader去加载 Class类,注意注意,我们看下BoostrapClassLoader的源码:

class BootStrapClassLoader : public ClassLoader {
private:
    JarLister jl;
private:
    BootStrapClassLoader() {}
    BootStrapClassLoader(const BootStrapClassLoader &);
    BootStrapClassLoader& operator= (const BootStrapClassLoader &);
    ~BootStrapClassLoader() {}
public:
    static BootStrapClassLoader & get_bootstrap() {
        static BootStrapClassLoader bootstrap;
        return bootstrap;
    }	// singleton
    Klass *loadClass(const wstring & classname, ByteStream * = nullptr, MirrorOop * = nullptr,
                     bool = false, InstanceKlass * = nullptr, ObjArrayOop * = nullptr) override;
    void print() override;
    void cleanup() override;
};

从接口中,我们能够判断两条消息:

get_bootstrap调用会得到一个单例对象;
BootstrapClassLoader有一个成员变量JarLister,首次调用时,会触发它的构造方法,我们去看看它的构造方法:

2.初始化BootstrapClassloader以及加载Class:

//todo: 修改 rjd 为windows 的路径
JarLister::JarLister() : rjd(L"")
{
    pwd = utf8_to_wstring(getProgramDir());
    rjd = RtJarDirectory(pwd);
    wstring rtjar_folder;
#if (defined (__APPLE__))
    rtjar_folder = utf8_to_wstring(pt.get<std::string>("path.mac"));
#elif (defined (__linux__))
    rtjar_folder = utf8_to_wstring(pt.get<std::string>("path.linux"));
#else
    //todo: 这里配置 windows的 rt路径
    rtjar_folder = utf8_to_wstring("C:\\Program Files\\Java\\jdk1.8.0_161\\jre\\lib\\");
#endif
    rtjar_pos =L"\""+ rtjar_folder + L"rt.jar"+L"\"";
    // copy lib/currency.data to ./lib/currency.data ......
    wstringstream ss;
    int status = system(wstring_to_utf8(ss.str()).c_str());
    if (status == -1) {  	// http://blog.csdn.net/cheyo/article/details/6595955 [shell 命令是否执行成功的判定]
        std::cerr << "system error!" << endl;
    }
    bool success = this->getjarlist(rtjar_pos);
    if (!success)	exit(-1);
    ifstream f(wstring_to_utf8(this->rtlist), std::ios_base::in);
    std::string s;
    while(!f.eof()) {
        f >> s;		// 这里有一个细节。因为最后一行仅仅有个回车,所以会读入空,也就是 s 还是原来的 s,即最后一个名字被读入了两遍。使用其他的方法对效率不好,因此在 add_file 中解决了。如果检测到有,忽略。
        if (!Filter::filt(utf8_to_wstring(s))) {
            this->rjd.add_file(StringSplitter(utf8_to_wstring(s)));
        }
    }
}

通过代码可以分析,它做了这样的事情:

  1. 或许到当前环境的 rt.jar,这个文件时jvm的核心jar包,我配置的是我本机环境的rt.jar,它的版本号是: jdk_1.8_161。同时,因为原来这里使用了配置的方式,但是需要以来boost,我就给替换了,直接手写死的。

  2. 调用 getjartlist方法,该方法马上详述。

  3. 调用rjd的add_file方法。 也将会详述。

    下面是getjarlist方法:

/*===---------------- JarLister --------------------*/
bool JarLister::getjarlist(const wstring & rtjar_pos) const
{
    wstringstream cmd;
    cmd << L"jar tf " << rtjar_pos << L" > " << this->rtlist;
    int status =  system(wstring_to_utf8(cmd.str()).c_str());
    if (status == -1) {
        exit(-1);
    }
    // TODO: judge whether mkdir is exist?
    if (0==access(wstring_to_utf8(uncompressed_dir).c_str(),F_OK)) {	// 如果存在
        return true;
    }
    cmd.str(L"");
    cmd << L"mkdir " << uncompressed_dir;
    status = system(wstring_to_utf8(cmd.str()).c_str());
    if (status == -1) {
        exit(-1);
    }
    cmd.str(L"");
    std::wcout << "unzipping rt.jar from: [" << rtjar_pos << "] ... please wait.\n";
    cmd << L"unzip " << rtjar_pos << L" -d " << uncompressed_dir ;
    status = system(wstring_to_utf8(cmd.str()).c_str());
    if (status == -1) {  	// http://blog.csdn.net/cheyo/article/details/6595955 [shell 命令是否执行成功的判定]
        std::cerr << "system error!" << endl;
        exit(-1);
    } else {
        if (status) {
            if (0 ==status) {
                std::wcout << "unzipping succeed.\n";
                return true;
            }
            else {
                std::cerr << "Your rt.jar file is not right!" << endl;
            }
        } else {
            std::cerr << "other fault reasons!" << endl;
        }
    }
    return false;
}

它做了这样的事情:

1.通过jar tf 将rt.jar保存的所有类 保存至某一特定文件中;

2.将rt.jar解压至某一个特定文件夹中;(注意,这个文件夹将会作为是否解压的标准,在同一进程中,最多只可能被加压一次。 我当前版本加压出来有 2999个类,如果解压信息输出到控制台的话,还是要费点时间。但是我想在发行版中,一个环境下的jvm,应该只被解压一次)。

接着是rtjarDirectory的addfile()方法:

void RtJarDirectory::add_file(StringSplitter && ss)
{
    if (ss.counter() == 0) {		// 仅仅在第一次的时候做检查,看文件到底存不存在
        if (this->find_file(std::move(ss)) == true) return;
        else ss.counter() = 0;
    }
    const wstring& target = ss.result()[ss.counter()];
    if (ss.counter() == ss.result().size() - 1) {	// next will be the target, add.
        subdir->insert(make_shared<RtJarDirectory>(target));
    } else {	// dir.
        auto next_dir = findFolderInThis(target);
        ss.counter() += 1;
        if (next_dir != nullptr) {
            (*next_dir).add_file(std::move(ss));	// delegate to the next level dir.
        } else {	// no next_dir, we should create.
            // this level's `subdir` can't be nullptr :)
            subdir->insert(make_shared<RtJarDirectory>(target));
            next_dir = findFolderInThis(target);
            assert(next_dir != nullptr);
            (*next_dir).add_file(std::move(ss));
        }
    }
}

它会将 保存的所有的类,通过根据全限定名称(包名+类名)的形式,进行分割,最终将 类名(去除了包名)的名称 保存进一个智能指针。 这个结构颇为复杂,我没太看懂,它的定义是这样的:

 shared_ptr<set<shared_ptr<RtJarDirectory>,shared_RtJarDirectory_compare>> subdir; //sub directory

我暂且先理解为它保存了所有的类名称在内存中吧,注意到我获取的类的条目有一万多:

  BootstrapClassLoader初始化完成后,就该去loadClass了,以加载Class为例:
// add lock simply
    LockGuard lg(system_classmap_lock);
    assert(jl.find_file(L"java/lang/Object.class")==1);
    wstring target = classname + L".class";
    if (jl.find_file(target)) {
        if (system_classmap.find(target) != system_classmap.end()) {	// has been loaded
            return system_classmap[target];
        } else {	// load
            // parse a ClassFile (load)
            ifstream f(wstring_to_utf8(jl.get_sun_dir() + L"/" + target).c_str(), std::ios::binary);
            if(!f.is_open()) {
                std::wcerr << "wrong! --- at BootStrapClassLoader::loadClass" << std::endl;
                exit(-1);
            }
#ifdef DEBUG
            sync_wcout{} << "===----------------- begin parsing (" << target << ") 's ClassFile in BootstrapClassLoader..." << std::endl;
#endif
            ClassFile *cf = new ClassFile;
            ClassFile_Pool::put(cf);
            f >> *cf;
#ifdef DEBUG
            sync_wcout{} << "===----------------- parsing (" << target << ") 's ClassFile end." << std::endl;
#endif
            // convert to a MetaClass (link)
            InstanceKlass *newklass = new InstanceKlass(cf, nullptr);
            system_classmap.insert(make_pair(target, newklass));
#ifdef KLASS_DEBUG
            BootStrapClassLoader::get_bootstrap().print();
	        MyClassLoader::get_loader().print();
#endif
            return newklass;
        }
        //todo:equals to starts with
    }
.........

上面为loadClass代码片段,可以看到第二行,它首先判断了java.lang.Object.class,通过前面的那个智能指针,显然Object是在里面的。(但是,此时Object并未加载。)这也印证了我前天文章中所说的,Object的唯一性是必须要首先保证的。 同时,加载成功时,会首先 实例一个ClassFile放入 类池,这个类对象保存的是 字节码二进制流!!! 接着,实例化一个 实例类对象,将该类对象保存进入 system_classmap。

3.设置Object,以及基本类型的单例映像:
该步骤是通过 Class完成的。

java_lang_class::fixup_mirrors();	// only [basic types] + java.lang.Class + java.lang.Object

该方法将 8个基本类型,和void 以Mirror实例的形式放入了一个缓存。

......
switch (name[0]) {
                case L'I':case L'Z':case L'B':case L'C':case L'S':case L'F':case L'J':case L'D':case L'V':{	// include `void`.
                    // insert into.
                    MirrorOop *basic_type_mirror = ((MirrorKlass *)klass)->new_mirror(nullptr, nullptr);
                    basic_type_mirror->set_extra(name);			// set the name `I`, `J` if it's a primitve type.
                    get_single_basic_type_mirrors().insert(make_pair(name, basic_type_mirror));
                    break;
                }
                default:{
                    assert(false);
                }
            }
......

4.加载String及Thread:

 // load String.class
        auto string_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/String"));
 
 
        // 1. create a [half-completed] Thread obj, using the ThreadGroup obj.(for currentThread(), this must be create first!!)
        auto thread_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/Thread"));
InstanceOop *init_thread = thread_klass->new_instance();
        BytecodeEngine::initial_client(thread_klass, *this);		// first <clinit>!
        // inject!!
        //todo: 注意这里的 threadid 的来源,可能要修改
        init_thread->set_field_value(THREAD L":eetop:J", new LongOop((uint64_t)GetCurrentThreadId()));
        //todo: 这里的线程优先级还没有绑定到 thread句柄上, 通过 setThreadPriority
        init_thread->set_field_value(THREAD L":priority:I", new IntOop(5));
        //todo: 这里通过 openthread 根据当前线程的id 获取到线程句柄  注意,当前线程的句柄不能关闭
        ThreadTable::add_a_thread(GetCurrentThreadId(), init_thread, this);

加载完Thread之后,会生成一个Thread的实例对象。之后会进行类的初始化,注意这是jvm中第一次类的初始化,它会一直向上初始化直到Object。

设置相关属性后,放入jvm的线程表中。 注意了,此时有在线程表中,就有了两个个线程了。 但是注意这个线程跟第二个线程的id号是一致的,同时,它并不是真正意义上的线程,只是一个抽象。 在线程表的插入中有如下代码:

 if (get_thread_table().insert(make_pair(tid, make_tuple(get_thread_table().size(), _thread, t))).second == false) {	// 如果原先已经插入了的话,那么就复用原先的 thread_no.
        number = std::get<0>(get_thread_table()[tid]);
    }

也就是说,the_whole_world的线程表中的线程数仍然是两个。

5.加载并实例化ThreadGroup:

 // 2. create a [System] ThreadGroup obj.
        auto threadgroup_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/ThreadGroup"));
        InstanceOop *init_threadgroup = threadgroup_klass->new_instance();
        BytecodeEngine::initial_client(threadgroup_klass, *this);		// first <clinit>!
        {
            std::list<Oop *> list;
            list.push_back(init_threadgroup);	// $0 = this
            // execute method: java/lang/ThreadGroup.<init>:()V --> private Method!!
            Method *target_method = threadgroup_klass->get_this_class_method(L"<init>:()V");
            assert(target_method != nullptr);
            this->add_frame_and_execute(target_method, list);
        }
// 3. INCOMPLETELY create a [Main] ThreadGroup obj.
        InstanceOop *main_threadgroup = threadgroup_klass->new_instance();
        {
            init_thread->set_field_value(THREAD L":group:Ljava/lang/ThreadGroup;", main_threadgroup);
        }
        assert(this->vm_stack.size() == 0);
 
        BytecodeEngine::initial_client(((InstanceKlass *)class_klass), *this);
        ((InstanceKlass *)class_klass)->set_static_field_value(L"useCaches:Z", new IntOop(false));

注意,这里除了 类初始化之外,还初始化了一个 ThreadGroup对象。 实例化的threadGroup为主线程组。

6.加载并初始化System:

// 3. load System class
        auto system_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/System"));
        system_klass->set_state(Klass::KlassState::Initializing);
//		BytecodeEngine::initial_clinit(system_klass, *this);
        auto InputStream_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/io/InputStream"));
        BytecodeEngine::initial_client(InputStream_klass, *this);
        auto PrintStream_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/io/PrintStream"));
        BytecodeEngine::initial_client(PrintStream_klass, *this);
        auto SecurityManager_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/SecurityManager"));
        BytecodeEngine::initial_client(SecurityManager_klass, *this);

System用于设置相关的属性,并且完成 当前程序的标准输入流,输出流,错误流的绑定。 windows上,每个程序的标准输入输出和错误流就是控制台。

7.加载并实例化Perf 和 LauncherHelper:

  auto Perf_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"sun/misc/Perf"));
    Perf_klass->set_state(Klass::KlassState::Initializing);				// ban Perf.
    auto PerfCounter_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"sun/misc/PerfCounter"));
    PerfCounter_klass->set_state(Klass::KlassState::Initializing);		// ban PerfCounter.
 
    auto launcher_helper_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"sun/launcher/LauncherHelper"));
    BytecodeEngine::initial_client(launcher_helper_klass, *this);
    Method *load_main_method = launcher_helper_klass->get_this_class_method(L"checkAndLoadMain:(ZILjava/lang/String;)Ljava/lang/Class;");

Perf作用暂时不清楚,LauncherHelper会用于引导 Launcher然后使用 AppClassLoader去加载用户类。

8.加载动态调用的类:

 // load some useful klass...
    {
        auto methodtype_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/invoke/MethodType"));
        BytecodeEngine::initial_client(methodtype_klass, *this);
        auto methodhandle_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/invoke/MethodHandle"));
        BytecodeEngine::initial_client(methodhandle_klass, *this);
 
    }

9.执行main:
launch流程结束。

jvm的启动细节2—classloader

说到这个古老的话题,那可得追溯到我写文章开始。 那一年春节,我决定看看源码,一上来就猛的 想把那个双亲加载机制给搞明白。 事实上,一直没怎么搞明白,唯一的作用是给了自己学习的动力。 但是今天我想借着这个机会,是可以完全搞懂的。

上午说到,jvm在加载客户类之前,会启动一个LauncherHelper类,代码如下:

 BytecodeEngine::initial_client(launcher_helper_klass, *this);
    Method *load_main_method = launcher_helper_klass->get_this_class_method(L"checkAndLoadMain:(ZILjava/lang/String;)Ljava/lang/Class;");
    // new a String.
    wstring ss = automan_jvm::main_class_name();
    InstanceOop *main_klass = (InstanceOop *)java_lang_string::intern(automan_jvm::main_class_name());
 
    this->vm_stack.push_back(StackFrame(load_main_method, nullptr, nullptr, {new IntOop(true), new IntOop(1), main_klass}, this));
//todo: 到这里应该是java层面的类加载器开始生效!!!
 MirrorOop *main_class_mirror = (MirrorOop *)this->execute();
 接着去看看 checkAndLoadMain方法:

在这里插入图片描述

之后通过ClassLoader的loadClass方法,通过查找虚拟表,调用Launcher的AppClassLoader的loadClass方法,再调用之前,将会首先进行AppClassClassloader的初始化(这个初始化过程蛮复杂的):

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

花了较多的精力去看这块的源码,主要原因基于以下几点:

1.windows中由于文件系统路径分隔符的原因,我在调试时就遇到了一个坑,即我明明需要使用 文件系统去加载,但是它却使用了JarLoader去加载。 现在回过头来,知道了其原因: 首先在LauncherHelper中会根据模式选择加载器,其次会判断java.class.path下面的所有配置路径,如果为文件夹,则会匹配为 fileLoader,如果为文件,则会使用默认的loader,而实际上默认的loader,其最终采用的还是Jarloader的方式加载的。(我的问题就出在这!)。

2.AppClassLoader与ExtClassLoader都继承自URLClasspath,因此必须弄明白URLClasspath是在什么时机初始化的,以及相应的参数都是什么。 关于URLClasspath的作用,网上帖子挺多。 它的几个关键方法: loadClass,findClass,defineClass 可以用于验证双亲委派机制的运行流程。

我在调试时,分别给loadClass 和 defineClass添加了锚点, 最终实际上是可以印证双亲委派模型的。 由于当时未截图,因此这里就不再继续操作了。(因为这个过程实在是有些痛苦~,感兴趣的小伙伴可以亲自调试一下)。

另外强调一点就是,以前没有认识到URLClassLoader在java中的重要性,以及URLClassLoader与URLClasspath的关系,这是一个十分有趣的问题。

此外,在AppclassLoader加载类的过程中,它的流将会以匿名内部类的形式给出:
在这里插入图片描述

就到这吧,原计划写的很详细的,但是真写了太细了吧,速度太慢,自己又是个急性子。 后续再补充细节,还需要进一步的学习补充。。。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值