JVM是Java Virtual Machine的简称,意为Java虚拟机
虚拟机是指通过软件模拟一个具有完整硬件功能,运行在一个完全隔离的环境中的完整计算机系统,
常见的虚拟机有JVM,VMwave,Virtual Box
JVM和其他两个虚拟机的区别是VMwave,Virtual Box都是通过软件模拟物理CPU指令集,物理系统中有很多寄存器;而JVM是通过软件模拟Java字节码的指令集,只保留了PC寄存器,其他的寄存器都进行了裁剪
首先程序执行之前要把Java代码转译为字节码文件(.class文件),JVM用类加载器将字节码文件存储到运行时数据区,由于字节码文件是JVM一套指令规范,所以还需要执行引擎调用本地方法接口将字节码文件转换为底层指令,进而由CPU去执行相关指令.
JVM运行时数据区域也叫内存布局,有5部分组成:程序计数器,虚拟机栈,本地方法栈,堆,方法区.
程序计数器是用来记录当前线程执行到的行号.程序计数器是内存中最小的区域,用来指示吓一条应该执行的指令的地址
Java虚拟机栈的生命周期和线程相同.Java虚拟机栈用于描述Java方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,用以存储局部变量表,操作数栈,动态链接和方法返回地址
- 局部变量表:存放了基本数据类型和对象引用.局部变量表的大小是在编译期间决定的.简单来说存放的就是局部变量(八大数据类型以及对象引用)和方法参数
- 操作数栈:每个方法会生成一个先进后出的操作栈
- 动态链接:指向运行时常量池的方法引用
- 方法返回地址:PC寄存器的地址
本地方法栈和Java虚拟机栈类似,只不过是Java虚拟机栈是给JVM使用的,而本地方法栈是给本地方法使用的.
程序中创建的所有对象都存储在堆中.
堆中分为两个区域:新生代和老生代.新生代中放新建的对象,对象经过一定GC次数之后依然存活则放入老生代.新生代中又分为三部分为:Eden和两个Survivor.GC时会将Eden中存活的对象放入到一个未使用的Survivor中,然后清除掉Eden和正在使用的Survivor中的对象.
方法区用来存储被JVM加载的类信息,常量,静态变量,即时编译器编译后的代码等数据.
方法区在JDK7时被称作是永久代,JDK8中叫做元空间.
JDK8元空间中内存变成了本地内存,不再受JVM最大内存的参数影响;字符串常量由元空间移到了堆中
运行时常量池是方法区的一部分,存放字面量和符号引用(内存中符号地址变成了内存中的真是地址)
- 字面量:字符串(JDK8移到了堆中),final常量
- 符号地址:类和结构的完全限定名,字段的名称和描述符,方法的名称和描述符.
线程私有:JVM多线程是通过线程轮流切换并分配处理器执行时间的方式实现的.因此为了在切换线程后能够恢复到正确的执行位置,每条线程都需要拥有自己的程序计数器,类似于程序计数器这样的内存区域属于线程私有.
程序计数器,Java虚拟机栈,本地方法栈是线程私有的,堆,方法区是线程共享的
加载阶段(loading),主要完成3个部分的工作:
JVM真正开始执行java中的代码,也就是执行类构造器的过程,真正对类对象初始化.如’static int val = 123;',val在准备阶段被赋予大小以及赋予默认值0,在初始化阶段被赋予初始值123.
从Java虚拟机方面看,类加载器可以被分为两种:一种是启动类加载器(BoostrapClassLoader),是虚拟机自身的一部分;另一种就是其他的类加载器,独立存在于虚拟机外部.
而从开发者角度,为了更细致的进行类加载,保留了三层类加载器,双亲委派的类加载架构器.
双亲委派模型由3部分组成:启动类加载器(BootstrapClassLoader),应用程序类加载器(ExtensionClassLoader)和自定义类加载器(ApplicationClassLoader).其中启动类加载器负责加载JDK中lib目录下的核心类库,即家在标准库中的类;应用程序类加载器负责加载jdk目录中扩展的类;自定义类加载器负责加载当前项目中的类,加载的等级依次降低.而双亲委派模型则是按照加载等级从高到低加载,当父加载器加载完后仍无法加载到需要的类,才会在本加载器中查找加载.
双亲委派模型类加载过程:
尽管双亲委派模型有很多优点,但在一些场景下也存在一定的问题,如Java中SPI(Server Provider Interface)机制下的JDBC实现
JDBC中的DriveManger类中的实现类loadInitialDrivers()是由线程上下文加载器加载的(属于ApplicationClassLoader),并没有向上去委派其父类类加载器,造成这种现象的原因是其父类类加载器只能加载指定路径下的类,而该实现类是由开发者实现的,其父类类加载器加载不到.
给对象增加一个引用计数器,当指向该对象时,引用计数器加1,当计数器值变为0时,该对象死亡,可以被回收
事实上,JVM并没有采用这种方式去判断一个对象是否应该被回收,原因是这种方式无法解决循环引用的问题.
class Test{
Test test;
public static void main(String[] args){
//t1的计数器值为1,
Test t1 = new Test();
//t2的计数器值为1
Test t2 = new Test();
//t2的计数器值为2
t1.instance = t2;
//t2的计数器值为2
t2.instance = t1;
//t1,t2的计数器的值变为1,但此时应该回收t1,t2
t1 = null;
t2 = null;
}
}
可达性分析是JVM采用的一种判断垃圾对象的算法.它是通过一系列称为"GC Roots"的对象作为起始点,然后从该点开始以类似dfs的方式搜索内存空间,能够搜索到的对象被作上标记,全部搜索完后,没有标记的对象即为垃圾对象.
栈上的局部变量,方法区的静态成员指向的对象,常量池中引用的对象等区域中的对象通常被作为GC Roots.
缺陷:
复制算法是为了解决标记-清除算法效率低的缺陷.主要是在为对象开辟内存时开辟对象大小的两倍内存,对象存放在其中一块内存中,当进行垃圾回收时,将仍活着的对象移动到另一块没有使用过的区域,而将已经使用过的内存区域全部回收.这样解决了回收内存离散的问题,且操作简单高效,缺点就是内存空间利用率低.新生代就是依据这种 方式回收垃圾的.
与标记-清除算法相似,只是在清除时不是直接将内存空间直接回收掉,而是将存活的对象向内存一端移动,然后回收掉待回收的内存区域.
通过区域划分,在不同区域实施不同的垃圾回收策略.JVM采用的就是分代算法.JVM将堆中的对象分为新生代和老年代.在新生代中,存放的是新创建的对象,每次都会死去大量对象,所以采用复制算法;在老年代中,对象存活率较高(在新生代中熬过15次GC后的对象),空间比较少,需要采用标记-整理算法.
在新生代中,分为三块区域:Eden(伊甸区),Survivor From区,Survivor To区.新创建的对象会首先放入Eden区,当Eden区满后,会将Eden区中存活的对象移入Survivor From区,回收Eden区的内存;当Eden区再次满后,会将Eden区和Survivor From区中存活的对象移入Survivor To区,回收Eden区和Survivor From区的内存.重复这个过程,当在新生代熬过15(由JVM参数MAXTenuringThreshold参数决定)次GC扫描后存活的对象就会放入老年代.
CMS收集器是一个老年代收集器,是一种获取最短回收停顿时间为目标的收集器.
CMS收集器是基于"标记-清除"算法实现的.整个过程分为4个步骤
G1垃圾回收器用于heap memory比较大的情况下,将内存区域分成很多小块的region块,然后并行的对其进行垃圾回收.给这些region进行了不同的标记(新生代,老年代),再扫描的时候,一次扫若干个region(尽管一轮GC不能完全扫描完,分多次来扫描),对于业务线程的影响是比较小的. G1收集器可以将STW停顿时间优化到1ms
上述垃圾收集器的核心思路是化整为零.
JVM提供了一种Java内存模型(Java Memory Model,JMM),用以屏蔽掉各种硬件和操作系统的内存访问差异.
JMM的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存中和从内存中读取变量.此处的变量包括实例字段,静态字段和构成数组对象的元素,不包括局部变量和形参.
JMM规定了所有变量都存储到主内存中,每条线程拥有自己的工作内存,工作内存中保存了该线程使用的变量的主内存的拷贝,线程对变量的操作在工作内存中完成.不同的线程不能直接访问其他线程工作内存中的变量,而需要主内存去传递对应的变量值.
为了实现主内存和工作内存之间的交互,JMM定义了8种操作.且保证这8种操作都是原子的,不可分的.
1. lock:作用于主内存的变量,表示该变量被一个线程独占.
2. unlock:作用于主内存的变量,将一个lock态的变量释放出来.处于unlock状态的变量才可以被其他线程锁定.
3. read:作用于主内存的变量,它将一个主内存中的变量传输到工作内存中.
4. load:作用于工作内存的变量,把read操作之后的变量放入到工作内存的变量副本中.
5. use:作用于工作内存的变量,把一个工作内存中变量的值传递给执行引擎.
6. assign:作用域工作内存的变量,把执行引擎中的值传递给工作内存中对应的变量
7. store:作用于工作内存的变量,将工作内存中变量的值传递给主内存.
8. write:作用于主内存的变量,将store操作后的值放入到主内存的变量中.
JMM的三大特性
1. 原子性
2. 可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改.
3. 有序性:如果在本线程内观察,所有操作是有序的;如果在线程中观察另外一个线程,所有的操作是无序的.
因此,并发地执行程序,只有同时保证原子性,可见性和有序性才能保证程序正常运行,三者缺一不可.