JVM(虚拟机):
-
通俗来说,JVM其实就是一个软件,也叫做虚拟机,每一个JVM实例对应一个JAVA进程,当我们执行java **.class其实就是启动了一个JAVA进程,此时一个JVM实例诞生,当该进程关闭时,JVM实例随之消亡。如果一个计算机运行多个java进程,每个java进程都有它自己的jvm实例。
-
jvm执行引擎实例则是对应所属java进程的线程
Class文件:
-
字节码文件,是由.java编译而来的二进制文件,class文件是一组以8位字节为基础单位的二进制流。其实这里的字节码文件,并不是真正的机器语言的01文件。
-
class文件其实是特殊的二进制文件,用UltraEdit打开确实是0和1,用hex这样的16进制工具打开为16进制,会出现包括有CAFEBABE等内容,如果class文件如果是二进制,那么计算机直接就能运行了,为什么还需要jvm呢。
类加载和执行流程:
1)加载过程: 首先用类加载器将.class文件加载到JVM的方法栈中去(类加载器获得数据流)
- 搜索:先找到.class文件
- 转换:将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,然后将字节流转为byte[]
- 放置:ClassLoader(这里有个双亲委派原则)读取了这个byte[]以后,Classloder可以找到这个byte[]中描述类的格式的类信息。当Classoader读取到类信息了以后,通过类加载器以一个类的全限定类名来把这个类需要支持的jar包加载到内存中去,同时ClassLoader就可以知道要如何去拆分这个类了,这个类里面有多少属性,常量,以及方法,以及方法的格式(参数个数,参数顺序,返回值类型,引用名称),将byte[]放在方法栈中。
2)链接:
- 验证:通过类信息描述,来验证当前的JVM内存中所具有的元素,是不是可以支撑这个类型的创建(如果缺少jar包就会抛出一个的异常),所有的方法都只是一个流程,并不能真正的执行,所以,JVM会将这个流程记录在方法区
- 初始化:当需要初始化内容的时候会初始化,并将内容放在常量池中,对应的static方法也会别保存在对应的方法区中。
3)使用:
- 使用过程:分为创建对象并使用,和直接使用(static),如果创建对象并使用,那么这个使用的方法,就是一个动态的方法,方法的起始点,是从栈开始的,(所有的方法的起始,是从main这个方法。也就是说,当前的线程中,所具有的所有方法,属性,都是因为mian这个方法被加载到栈中,然后栈中的操作数过程,调用了其他方法,那么就会生成新的栈帧,就会先执行,得到结果以后,之前的栈帧再执行)
4)回收: gc(垃圾回收机制)
5)卸载
JVM内存模型:
一个java的线程里包含虚拟机栈,本地方法栈,程序计数器,堆(共用一个),方法区。其中虚拟机栈,本地方法栈,程序计数器,线程独有。
Threads(线程栈):每个线程都有自己的栈,当多线程同步的时候,我们加入同步锁,其实锁住的是线程栈中的程序计数器。
程序计数器:记录当前运行时的位置,其实就是程序运行的行号。
虚拟机栈: 存放Java方法的内存模型。每个方法被执行的时候都会创建一个栈帧(局部变量区,操作数栈,动态连接,方法的返回地址),栈帧里保存的的就是一个方法执行的必要参数和必要条件。
本地方法栈:是虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。
方法区:存放所有的方法的流程,和所有常量的内容,永久代(java 1.7)是在JVM内存模型中的,而元数据区(java 1.8)是在物理内存中的,所以元数据区根本不会溢出。
堆:存放的是所有对象,所谓的对象其实就是里面所有方法地址的集合而已,对象中放的就是地址。
指令重排序和解决办法
指令重排序是啥:
cpu ——>寄存器 ——> 内存
指令重排序的原因:CPU的执行速度过快,所以代码需要优化尽量根上cpu的执行速度。
编译的指令重排序:在编译的过程中,JVM调整指令执行顺序
运行时期的指令重排序:在执行的过程中,高速缓存也会指令重排序,
指令重排序的问题:多线程下,如果指令重排序的话,可能会改变最终结果的一致性。
指令重排序解决:
cpu硬件层(内存屏障)
-
阻止屏障两侧指令重排序
-
强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
java中解决:
-
使用volatile(强制高速缓冲的更新):
cpu会不停检测总线中该变量的变化,如果该变量一旦变化了,由于这个嗅探机制,其它cpu会立马将该变量的cpu缓存数据清空掉,重新的去从主内存拿到这个数据。
-
synchronize(变成同步):
同一时刻,只能有一个线程操作,所以就保证数据最终的一致性