Java|Java代码运行的过程是怎样的?( 二 )



实际上 , 我们的数据主要是存放在内存上 , 然而CPU的计算速度比主存的存取速度快很多倍 , 所以在两者之间会有多级高速缓存 。 例如当CPU有个指令是取主存上某一个值 , CPU会先根据这个值在主存上的位置去判断是否已经在高速缓存中 , 如果没有就会去主存取 , 取完再放在高速缓存中 。
这个地方会涉及到一个知识点 , 就是去主存上读取的时候并不会仅仅去读取一个值 , 而是把一段长度的值都拿出来并缓存 , 因为它会假设你既然读了某个位置的值而这个位置相邻的值也会被读取 。 就像我们用SQL去查询id=800这行记录的时候 , 虽然它返回了id=800这行记录 , 实际上它去读这行记录的时候把这行记录所在的数据页上的所有数据都放内存里面了 。 可能下次你去查询id=801行的那条记录的时候直接就命中缓存就不用去磁盘去查了 。

所以我们知道一个缓存行可能缓存了多个字段的值 , 如果某个进程改了其中的一个值就会导致一整个缓存都会失效 , 那么这个缓存行上的其他值也会重新从内存读取 , 所以一些对内存要求比较高的应用就想规避掉这种情况 。 比如它们会用对象填充的方式让某个字段的值可以独占一整个缓存行 。

好 , 有了执行环境我们这段代码什么时候执行呢?我们知道CPU一通电就会不停地取指令、运算 , 周而复始 , 那你可能就会问了 , 什么时候才会执行到我这段代码呢?
实际上CPU给每一个进程都分配了一个时间片 , 在这个时间片内执行对应的进程指令 , 过了这个时间片就执行别的进程 , 一个进程内的指令执行顺序靠每个指令执行完再去指向下一个指令的位置 。 当然一个进程内的某些操作也会主动放弃CPU的执行权限 , 比如等待IO操作 。

所以为了让一个进程内的指令可以更高效地执行 , 我们可以让某个线程在等待IO的时候其他线程能够获取到CPU的执行权限并继续执行 。 如果你的任务都是计算性的任务 , 基本不会主动释放CPU的情况 , 那么在单核机器上就没必要开多线程 , 如果有大量的IO操作 , 那么多线程的效果就会比较好 。
接下来我们分析下代码执行的时候内存是怎么分配的 。 一个JVM启动就会产生一个进程 , 虽然多个进程会共享一个物理内存 , 但是每个进程都会拥有自己独立的内存空间 。

当我们同时启动多个JVM并执行System.out.println(new Object()) , 将会打印这个对象的hashcode , hashcode默认为内存的地址 , 最后我们发现它打印的都是java.lang.Object@4fca772d 。
也就是说多个JVM进程返回的内存地址是一样的 , 这说明每个进程都有单独的地址空间 。

实际上 , 每个进程自己都会维护一个虚拟的内存 , 虚拟存储让每个进程以为自己独占整个内存空间 , 这样的好处是每个进程都拥有一致的虚拟地址空间 。 简化了内存管理 , 进程不需要和其他进程竞争内存空间 , 因为它是独占的 , 这也保护了各自进程不会被其他进程破坏 。
每个进程在申请内存的时候会维护虚拟内存和物理内存的映射关系 , 避免其他进程占用自己的内存 , 而这个虚拟内存空间可能会超过物理内存 , 当超过物理内存的时候可能会发生数据溢出从而存储到磁盘上 。

页表保存了虚拟地址和物理地址的映射 , 页表是一个数组 , 每一个元素为一个页的映射关系 , 这个映射关系可能是和主存的也可能和磁盘的 , 页表存储在主存中也可以存储在缓冲区 。
我们将存储在高速缓冲区中的页表称为TLAB 。 好 , 我们现在知道了JVM进程内的内存地址是怎么和物理内存关联的了 , 那么一行具体的java代码无非就是读取某个属性的值 , 将这个值做运算再将新的值写回某个属性 , 那么我们怎么样才能读取到某个属性的值呢?