JVM设计与实现-底层原理

摘自虚拟机设计与实现 6.1 什么是线程
由于线程支持多个层级,当讨论到一个线程时,应该指出它位于哪个层级上,一个层级上的单个线程可能包含更高级上的多个线程
现实中没有必要构造太多层级的线程,通常不超过3级。第二级共享第1级的硬件上下文,第3级共享第二级的软件上下文
在Linux设计中,内核线程(软件线程)以M:N映射复用硬件上下文,glibc的用户线程以1:1映射使用内核线程上下文。有些系统在用户线程和内核线程之间使用M:N或者M:1映射,比如GNU Portable Threads 和Windows Fiber。但是这些特性要么不常用,要么只用于特殊情况
注意,在这里进程是一个无关紧要的概念,尽管进程常常和线程相混淆。线程主要是关于执行的控制流,而进程主要是关于内存空间隔离。如果两个先成运行在隔离的内存空间中,可以认为他们是运行在不同的进程中。在Linux内核中,因为所有的任务共享内核内存空间,所以在严格意义上说,在内核级别没有进程,只有内核线程。进程只存在于用户空间,用户空间为每个进程建立了隔离的虚拟内存你空间。在内核上下文中讨论进程也不是错误的,但这里进程实际上是指1:1映射到用户进程的内核线程。

摘自虚拟机设计与实现 8.1 栈展开   
栈展开是指虚拟机枚举目标线程的栈内容的过程,通常涉及识别栈上方法帧的栈帧枚举过程,以及识别每个方法帧内容的栈槽枚举过程。这个过程从栈顶开始,因为这是当前栈指针指向的位置。我们知道栈指针是线程上下文的一部分,而线程上下文可以被线程直接访问。
异常处理需要栈展开,他需要运行时递归的展开栈帧,知道在某个方法内找到catch块,否则他就是未捕获异常,可能需要操作系统来处理,然后控制流从异常抛出点转移到异常处理点。
对象追踪垃圾回收器需要通过站展开找到运行时栈的根引用。调试器需要通过栈展开检查栈内容。方法调用的返回也可以被看做栈展开的一种特殊情况,它展开一个帧并把控制从被调用方转移到调用方,但通常不把这称为栈展开。栈展开通常是指运行时服务,但函数返回通常不涉及运行时,而是返回指令的硬件功能。

GC支持
java代码中支持GC,主要任务是让JIT编译器生成安全点。安全点可能包含一下位置。他们可能触发回收,阻塞线程执行,或者导致线程长时间执行。每个安全点都需要一个GC-map数据结构支持根集枚举,其中存储执行上下文中哪些位置包含引用的相关信息。
1.对象分配点    2.调用点    3.阻塞点    4.循环中    5.异常抛出点


JVM是如何调用Java方法的:JVM是C写的,JAVA字节码编译后是机器码
C语言有函数指针,通过函数指针,C可以将一个变量直接执行一个函数的首地址。C语言被编译时,C函数将被直接编译成机器指令,而这个函数指针将直接只想这段机器指令的首地址。
于是在JVM中,在源代码编译阶段就定义好一段机器指令,然后直接讲一个C函数指针指向这段机器指令的首地址,从而间接实现C语言直接调用机器指令的目的。
其实C语言还有一种办法可以调用汇编指令,那就是内嵌汇编的方式,在Linux操作系统内核中就有大量的这种用法,但是这种用法与JVM要实现的目标稍微有点不同,JVM要实现直接由C语言调用机器指令,而内嵌汇编的方式只实现了这个目标的一般,内嵌汇编的方式只能实现由C语言直接调用汇编指令
在JVM内部也有这么一个函数指针,就是call_stub。这个函数指针正式JVM内部C语言程序与机器指令的分水岭,JVM在调用这个函数之前,主要执行C程序,而JVM通过这个函数指针调用目标函数之后就直接进入了机器指令的领域。

由于物理机器不能识别java程序,也不能直接执行java程序,因此JVM必然要通过自己作为一座桥梁连接到Java程序,并让Java被调用的函数的堆栈能够寄生在JVM的某个函数的堆栈空间中,否则物理机器不会自动为Java方法分配堆栈。
JVM选择CallStub这一函数指针作为JVM内部的C/C++程序与Java程序的分水岭,当JVM启动后,执行完JVM自身的一系列指令后,能够跳转到执行Java程序经翻译后所对应的二进制机器指令,CallStub能够实现机器逻辑指令上的联机额,同时,JVM会调用Java的入口函数main,并将main主函数的入参传递进去,
    因此在分水岭之后,JVM需要为主函数分配堆栈空间,以在主函数中读取入参数据,那么Java函数所需要的堆栈空间分配在哪里呢?答案是明显的,既然CallStub()作为分水岭函数,很自然的JVM将Java函数堆栈空间“寄生”在了CallStub()函数堆栈中。当然,从技术实现的手段而言,
    JVM并非一定要选择寄生这种方式,JVM完全可以另外定义一种算法接口来支持java函数的调用机制,但是JVM并没有,那么如何实现“寄生”,这需要依靠物理机器提供的指令,对CallStub()堆栈进行扩展。
    物理机器提供了扩展堆栈空间的指令:
    sub operand ,%sp
    operand是一个自然数,例如8,16或者其他数值,这条指令表示将堆栈向下扩展一定的空间,如果你写了一个C/C++函数,编译器会自动计算一个函数所需的堆栈大小并分配堆栈空间。CallStub作为JVM内部C/C++和Java的分水岭,他是在JVM启动过程中动态生成的,他会计算堆栈大小并生成对应的分配合适大小堆栈的机器码
    JVM设计与实现-底层原理_第1张图片

JVM设计与实现-底层原理_第2张图片JVM设计与实现-底层原理_第3张图片

 JVM设计与实现-底层原理_第4张图片

 JVM设计与实现-底层原理_第5张图片

 JVM设计与实现-底层原理_第6张图片JVM设计与实现-底层原理_第7张图片

JVM常量池的内存分配:
mutableSpace.cpp -> allocate
函数中通过HeapWord* new_top=obj+size,将permSpace内存区域的Top指针往高地址方向移动了size大小的字节数,完成当前被加载的类所对应的常量池的内存分配。由于JVM的堆区在JVM初始化过程中便已完成了指定大小的空间分配,因此这里并没有真正向操作系统申请内存分配,仅仅从JVM已申请的堆内存中划拨一块空间用于存储常量池结构信息。

内存对齐:

因为不同平台读取基本数据类型的方式不一样,有些硬件必须在偶数位读取short,否则会报错,有些不会报错,但是会读前后段,然后舍头去尾,这对性能的影响很大,所以C/C++通过内存对齐来解决这种问题,比如int类型的起始内存地址必须是4的倍数,所以在C/C++中同一个结构体如果字段顺序不一样,那么占用的内存也会不一样,因为内存对齐的原因
而JVM则是通过字段重排+内存对齐来解决

你可能感兴趣的:(Java从开发到设计,java,架构,linux)