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默认为内存的地址,最后我们发现它打印的都是[email protected],也就是说多个JVM进程返回的内存地址都是一样的,这说明每个进程都有单独的地址空间。
实际上每个进程自己都会维护一个虚拟的内存(参考下图),虚拟存储让每个进程以为自己独占整个内存空间,这样的好处是每个进程都拥有一直的虚拟地址空间,简化了内存管理,进程不需要和其他进程竞争内存空间,因为它是独占的,这也保护了各自进程不会被其他进程破坏。每个进程在申请内存的时候,会维护虚拟内存和物理内存的映射关系,避免其他进程占用自己的内存。
而这个虚拟内存空间可能会超过物理内存,当超过物理内存的时候,可能会发生数据溢出,从而存储到磁盘上。页表保存了虚拟地址和物理地址的映射(如下图),页表是一个数组,每一个元素为一个页的映射关系,这个映射关系可能是和主存的,也可能是和磁盘的,页表存储在主存中,也可能存储在缓冲区,我们将存储在高速缓冲区中的页称为TLAB。
Java反射获取对象属性
参考下图,它首先要获取这个属性相对对象初始位置的偏移量,如果你持有这个对象的引用,你就能获取到这个对象在虚拟内存中的起始地址,然后我们根据属性的偏移量,就可以获取这个属性的虚拟的内存地址,之后再查询页表就可以获取物理的内存的起始地址,接着再根据这个属性的类型取对应长度的数据。写入也是一样的道理,属性相对对象初始位置的偏移量,在加载这个class的时候就确认好了,它是和class绑定的,那么如果一个对象就一个属性,如果不压缩的话那么除了对象头占128位,这个属性的偏移量可能就是128,如果有多个属性JVM会对属性进行重排序和内存对齐,保证对象占用的大小是8的倍数,另一个作用就是保证一个属性的值,都在一个CPU缓存行中,不然一个属性值会一部分在缓存行A中,一部分在缓存行B中。
关键点:
1.解释执行和即时编译的区别
2.CPU是如何访问内存里的数据
3.JVM里面是如何获取某个对象的属性值
原文地址:https://www.cnblogs.com/aaronzheng/p/12327555.html