zoukankan      html  css  js  c++  java
  • Java代码是怎么运行的

      Java代码执行步骤

      编译

      Java文件通过JVM的编译器编译成字节码文件,有了字节码,JVM的类加载器就开始加载字节码文件。

            

      解释器

      解释器会将字节码转换成汇编指令,然后在转换成CPU可以识别的机器指令(下图是汇编指令转成机器码的案例)。解释器是软件实现的,他将字节码转换成汇编指令,可以实现同一份Java字节码在不同的硬件上运行,而将汇编指令转换成机器指令由硬件直接实现,所以他的速度会更快。JVM为了提升运行效率,会将某些热点代码,一次全部编译成机器指令再执行。也就是和解释执行对应的即时编译,即时编译的机器码存放在一个叫codecache的缓存的地方,这块内存属于堆外内存,如果这块内存不够了,那么即时编译器将不会再进行编译,可能造成程序运行变慢,这也是排查性能问题变慢的一个点。

      

      内存与CPU的配合

      代码转换成了指令以后,指令必须要有上下文环境,这些环境包括:指令寄存器、数据寄存器、栈空间等内存资源(执行机制如下图)。程序被加载进内存以后,指令就在内存中了,指令的指针寄存器IP,指向内存中的下一条待执行指令的地址,CPU的控制单元根据IP寄存器的指向将主存中的指令装载到指令寄存器,这个指令寄存器也就是一个存储设备,不过他集成在CPU内部,指令从主存到达CPU后,只是一串010101的二进制串,还需要通过译码器进行解码,解码后根据运算类型,在从主存中获取操作数,并调用运算单元进行计算。 

           

      我们的数据主要是存储在内存上,然而CPU的计算速度比主存的存取速度快很多倍,所以在两者之间会有多级高级缓存。例如当CPU有个指令是取主存上某一个值,CPU会根据这个值在主存上的位置,去判断是否已经在高速缓存中,如果没有就会去主存中取,取完再放在高速缓存中,这个地方会涉及到一个知识点,就是去主存上读取的时候,并不会仅仅读取一个值,而是把一段长度的值都拿出来并缓存,因为他会假设你既然读了某个位置的值,而这个位置相邻的值也会被读取,就像我们用SQL去查询id=800这行记录的时候,虽然它返回了id=800这行记录,实际上它去读这行记录的时候,把这行记录所在的数据页上的所有数据都放内存里面了,可能下次你去查询id=801行的那条记录的时候,直接就命中缓存,就不用去磁盘去查了,所以我们知道一个缓存行可能缓存了多个字段的值,如果某个进程改了其中一个值,就会导致一整个缓存都会失效,那么这个缓存行上的其他值也会重新从内存读取,所以一些对内存要求比较高的应用,就想规避掉这种情况,比如它们会用对象填充的方式,让某个字段的值可以独占一整个缓存行。

       指令执行

      CPU一通上电就会不停的读取指令、运算,周而复始。CPU会给系统运行中的每一个进程都分配一个时间片,在这个时间片内执行对应的进程的指令,过了这个时间片就执行其他的进程,一个进程内的指令执行的顺序,靠每个指令执行完,再去指向下一个指令的位置。当然一个进程内的某些操作,也会主动放弃CPU的执行权限,比如等待IO的操作(如下图),所以为了让一个进程内的指令可以更高效的执行,我们可以让某个线程在等待IO的时候,其他线程能够获取到CPU的执行权限,并继续执行,如果你的任务都是计算型的任务,基本不会有主动释放CPU的情况,那么在单核机器上就没必要开多线程,如果有大量的IO操作那么多线程的效果就会比较好。

       内存的分配

      一个JVM启动就会产生一个进程,虽然多个进程会共享一个物理内存,但是每个进程都会拥有自己独立的内存空间,当我们同时启动多个JVM并执行如下代码(下图),将会打印这个对象的hashCode,hashCode默认为内存的地址,最后我们发现它打印的都是Java.lang.Object@4fca772d,也就是说多个JVM进程返回的内存地址都是一样的,这说明每个进程都有单独的地址空间。

      

       实际上每个进程自己都会维护一个虚拟的内存(参考下图),虚拟存储让每个进程以为自己独占整个内存空间,这样的好处是每个进程都拥有一直的虚拟地址空间,简化了内存管理,进程不需要和其他进程竞争内存空间,因为它是独占的,这也保护了各自进程不会被其他进程破坏。每个进程在申请内存的时候,会维护虚拟内存和物理内存的映射关系,避免其他进程占用自己的内存。

      

     而这个虚拟内存空间可能会超过物理内存,当超过物理内存的时候,可能会发生数据溢出,从而存储到磁盘上。页表保存了虚拟地址和物理地址的映射(如下图),页表是一个数组,每一个元素为一个页的映射关系,这个映射关系可能是和主存的,也可能是和磁盘的,页表存储在主存中,也可能存储在缓冲区,我们将存储在高速缓冲区中的页称为TLAB。 

     Java反射获取对象属性

      参考下图,它首先要获取这个属性相对对象初始位置的偏移量,如果你持有这个对象的引用,你就能获取到这个对象在虚拟内存中的起始地址,然后我们根据属性的偏移量,就可以获取这个属性的虚拟的内存地址,之后再查询页表就可以获取物理的内存的起始地址,接着再根据这个属性的类型取对应长度的数据。写入也是一样的道理,属性相对对象初始位置的偏移量,在加载这个class的时候就确认好了,它是和class绑定的,那么如果一个对象就一个属性,如果不压缩的话那么除了对象头占128位,这个属性的偏移量可能就是128,如果有多个属性JVM会对属性进行重排序和内存对齐,保证对象占用的大小是8的倍数,另一个作用就是保证一个属性的值,都在一个CPU缓存行中,不然一个属性值会一部分在缓存行A中,一部分在缓存行B中。

     关键点:

      1.解释执行和即时编译的区别

      2.CPU是如何访问内存里的数据

      3.JVM里面是如何获取某个对象的属性值

     

  • 相关阅读:
    进制
    流程控制
    运算符
    格式化输出
    数据结构-树的遍历
    A1004 Counting Leaves (30分)
    A1106 Lowest Price in Supply Chain (25分)
    A1094 The Largest Generation (25分)
    A1090 Highest Price in Supply Chain (25分)
    A1079 Total Sales of Supply Chain (25分)
  • 原文地址:https://www.cnblogs.com/aaronzheng/p/12327555.html
Copyright © 2011-2022 走看看