JVM内存模型
JVM (Java Virtual Machine)规范里定义了虚拟机运行时的内存模型。虚拟机规范把方法区描述堆的一个逻辑部分,可以在jdk8之前的永久代(PermSize)中,也可以在jdk8的元空间(Metaspace)中。用于存放已经被加载的类信息、常量、静态变量,JIT编译后的代码等数据。
JDK7把符号引用移到了native memory,字符串常量池放入堆中,方法区StringTable只保留字符串的引用。
JDK8移除了永久代(PermGen),替换为元空间(Metaspace),class metadata转移到了natvie memory。
名词解释
- JIT : 即时编译器 just-in-time compiler 是一个可以把Java的字节码(包括需要被解释的指令程序)转换成可以直接发送给处理器的指令的程序。
- 操作数栈 : 又称为表达式栈(Expression Stack),用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
- 运行时常量池 : JVM为每个已加载的类型维护一个常量池,包含字面量和符号引用
- 字面量 : 包含文本字符串、被声明为final的变量(只是对象的引用地址)、基本类型。
- 符号引用 : 类的完全限定名、字段与方法的符号引用
JMM
Java Memory Model(JMM) 是Java内存模型,它是一种虚拟机的规范来实现平台一致性,是对特定的内存或高速缓存进行读写访问过程的抽象。
JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
内存可见性问题
线程1频繁读取主内存,效率比较低,可能会被JVM优化成直接读工作内存;线程2修改了主内存的值,但是由于线程1读取的是工作内存,无法读取到线程2修改后的值。
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
volatile保证可见性
带有volatile修饰的变量在被翻译成机器指令时,会加上lock前缀,lock前缀会引发以下事项:
- 写入操作会强制将处理器缓存行的数据写回到系统主内存
- 缓存行回写到主内存后会导致其它处理器中该缓存失效
volatile变量在修改时时,会在所有工作内存中清除这个变量;这样其他线程读取变量的时候,就会从主内存中读取到了新的值。
volatile保证有序性
为了性能优化,JVM会在不改变数据依赖性的情况下,允许编译器和处理器对指令序列进行重排序,而有序性问题指的就是程序代码执行的顺序与程序员编写程序的顺序不一致,导致程序结果不正确的问题。而加了volatile修饰的共享变量,则通过内存屏障解决了多线程下有序性问题。
类加载流程
类加载共经过四个步骤:
- 编译:将java代码编译成字节码(语法分析、语义分析、注解处理、生成.class文件)
- 加载:通过类加载器将字节码文件装载到JVM中,并进行验证和初始化(是否符合jvm规范、初始化静态变量和静态代码块、将符号引用转换为直接引用)
- 解释:将字节码逐行解释称机器码(JIT会进行热点检测(方法调用&回边计数器、抽样)来直接将方法或循环体进行一次性编译实现复用)
- 执行:调用操作系统执行程序指令
语法分析:用词法分析产生的字符序列,构建一棵“语法树”。即将单词序列组合成各类词法短语,如程序、语句、表达式等。来检测语法正确。
语义分析:编译过程的一个逻辑阶段,审查有无语义错误(如类型检查),为代码生成阶段收集类型信息。
装载:通过类加载器装载到JVM中,使用双亲委派机制解决类版本冲突问题。
连接:对class进行验证(是否符合java规范和jvm规范)、为类变量分配内存空间并为其赋默认值、将符号引用转换为直接引用。
初始化:对类的静态变量赋予正确的初始值(执行java编译器生成的()方法,它把类的静态变量和static代码块合并一起)。在加载一个类之前,虚拟机总是会尝试先加载其父类,也就是说父类的static代码库一定在子类前执行。
字节码解释器:将字节码解释成操作系统可识别的指令。
即时编译器(JIT):在解释代码时会进行热点分析,如果是热点代码则触发JIT编译(下次执行无需重新编译)
- 方法调用计数器在方法入口时进行判断,且一般有半衰期(超过一定时间没调用,会减少已统计的调用次数)
- 回边计数器在循环开始前判断循环体的重复次数
双亲委派机制
类加载器加载类时,会先把请求委托给父加载器去完成,依次向上,如果最终都没加载,再自己加载。主要是为了解决以下问题:
- 安全性:Java核心类库由最顶级的Bootstrap加载器加载,保障了核心类库的安全性,避免恶意代码修改核心类库带来的潜在风险(修改核心类库来达到恶意申请内存、fork线程、调用API等目的)。
- 提高效率:加载前先找父加载器,如果有的话则直接返回了,避免重复加载,提高了效率,减少了资源消耗。
如何打破双亲委派机制
自定义类加载器继承自ClassLoader,重写loadClass方法,不优先调用父类的loadClass方法去加载类(loadClass -> findClass -> defineClass)。
- tomcat为了解决不同web应用class冲突的问题,它定义了 一个WebAppClassLoader,优先加载当前web应用目录下的类,找不到才调用父类