概念
- Java虚拟机就是二进制字节码的运行环境
特点
- 一次编译,到处运行
- 自动内存管理
- 自动垃圾回收管理
JVM的架构模型
java编译器:一、基于栈的指令集架构,二、基于寄存器的指令集架构
区别:
基于栈的指令集的特点
- 设计和实现更简单,适用于资源受限的系统
- 避开了寄存器的分配难题:使用零地址指令方式分配
- 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现
- 不需要硬件支持,可移植性好,更好实现跨平台
基于寄存器的指令集特点
- 典型的应用是x86的二进制指令集:比如android的Dalivk虚拟机
- 指令集架构则完全依赖硬件,可移植性差
- 性能优秀和执行更高效
- 花费更少的指令去完成一项操作
- 大部分情况下,基于寄存器的架构的指令集往往都是以一地址指令、二地址指令、三地址指令为主
JVM的生命周期
虚拟机的启动
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类来完成,这个类是有虚拟机起的具体实现指定的
虚拟机的执行
- 一个运行中的java虚拟机有着一个清晰的任务:执行Java程序
- 程序开始执行时才运行,程序结束时他就停止了
- 执行一个Java程序的时候,真正执行的是一个叫做Java虚拟机的进程
虚拟机的退出
- 程序正常执行结束
- 程序再执行的过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
- 某县城调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作
- 除此之外,JNI规范描述了用JNI Invocation API来加载或卸载 Java虚拟机时,Java虚拟机异常的退出
类加载器的分类
- jvm支持两种类型的加载器,分别为引导类加载器(Bootstrap classload)和自定义加载器
- Java虚拟机规范将所有继承于classloader的类加载器都划分为自定义加载器
启动类加载器(Bootstrap classload)
- 这个类加载使用c/c++实现的,嵌套在jvm内部
- 用来加载Java核心库
- 并不继承classloader,没有父加载器
扩展类加载器(Extension ClassLoader)
- 派生于Classloader类
- 父类加载器为启动加载器
应用程序类加载器(Application classLoader)
- 派生于Classloader类
- 父类加载器为扩展加载器
- 该类加载是程序默认的类加载器,一般来说,Java应用的类都是由它来完成的
双亲委派机制
工作原理
- 1、如果一个类加载器收到了类加载请求,并不会自己先加载,而是把请求委托给父类的加载器去执行
- 2、如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
- 3、如果父类加载器可以完成类加载任务,九成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
优势
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改(如String)
沙箱安全机制
保证对Java核心代码的保护,就是沙箱安全机制
其他
- 在Jvm中表示两个class对象是否为同一个类存在两个必要的条件
1、类的完整类名必须一样,包括包名
2、加载这个类的classloader(指classloader实例对象)必须相同
Java内存管理机制
java虚拟机定义若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是和线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁
如上图,方法区和堆是所有线程共享的数据区
而每个线程都有独立的程序计数器,栈和本地栈
运行时区域
Java虚拟机在执行Java程序代码过程中会把它所管理的内存划分为若干个不同的数据区域。Java虚拟机所管理的内存将会包括以下几个运行时数据区域:
方法区,虚拟机栈(线程),本地方法栈,堆,程序计数器
程序计数器
- 属于线程私有内存
- 程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时候就是通过改变这个计数器的值来选取下一条指令需要执行的字节码指令,跳转,循环,异常处理等都需要这个计数器来处理。
- 在任何一个确定的时刻,一个处理器只会执行一条线程中的命令。因此对于多线程而言,每条线程都需要一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。
- 如果线程正在执行Java方法,这个计数器记录的是正在执行的虚拟机节码指令的地址;如果正在执行的是native方法,这个计数器值内空,指向的是null
- 它是唯一一个在Java虚拟机规范中没有规定任何outotmemoryError情况的区域(方法区和堆都有垃圾回收器,栈没有垃圾回收器但是可能会出现oom)
为什么使用PC寄存器记录当前线程执行地址
- 因为CPU需要不断切换线程,切回来的时候,需要知道从哪儿继续执行
- JVM的字节码解释器需要通过改变PC寄存器的值来明确下一条应该执行什么样字节码指令
Java虚拟机栈
- 属于线程私有内存,生命周期与线程相同
- 每个方法被执行的时候都会创建一个栈帧,每个方法被调用到执行完成的过程,对应一个栈帧在虚拟机中入到出栈的过程
- 栈,对应的是虚拟机栈,或者说虚拟机栈中的局部变量表
- 局部变量表,所需的内存空间在编译期间完成分配。它存放了编译期间可知的基本数据类型,对象引用类型和returnAddress类型(指向一个字节码指令的地址)
- 栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址
栈可能出现的异常
- Java虚拟机规范允许Java栈的大小是固定的或者动态的
固定不变:如果线程请求分配的栈容量超过了Java虚拟机栈允许的最大容量,Java虚拟机将会抛出stackOverflowError异常 - 动态:在尝试扩展的时候无法申请到足够内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将会抛出OOM异常
修改栈的大小
Run->Edit Conifgurations->vm options里面修改(-Xss+数字大小+单位)
局部变量表
- 定义一个数字数组,主要用于存储方法参数定义和定义在方法体内的局部变量,这些数据类型包括:各类基本数据类型,对象引用,以及returnAddress类型
- 局部变量表所需容量的大小是在编译期间确定的
- 由于局部变量表是建立在线程的栈上,是线程私有数据,因此不存在线程安全问题
- 局部变量表中的变量只在当前方法调用中有效,当方法调用结束,随着方法栈帧的销毁,局部变量表也随之销毁
public class Test {
public static void main(String[] args) {
Test test = new Test();
test.compute();
}
int compute() {
int a = 3;
int b = 6;
int c = (a + b)*2;
return c;
}
}
分析:首先我们会new Test()对象,放到堆内存中,随后会走Test 类中(也就是方法区中的类元信息Test.class)的compute方法,此方法会有个程序计数器,进入之后,首先将3加入到操作数栈中,下一步会将a加入到局部变量表中,并将3赋值给a,以此轮推,最后会将compute这个栈中的变量返回地址给main栈中,而compute方法这个寻找的过程也称为动态链接
本地方法栈
- 内存线程私有
- 虚拟机栈执行的是java方法
- 本地方法栈执行的是native方法
堆
- 所有线程共享的一块内存区域,在虚拟机启动时创建
- 主要存放对象实例,垃圾收集器管理的主要区域
- Java堆可以是一块不连续内存空间
-
堆分代
所有新生成的对象首先都是放在年轻代。首先它会进入到Eden区,当Eden区满时,还存活的对象将被复制到Survivor区(两个中其中一个,必须有一个是空闲的),只要移动到To区,里面有个值叫做分代年龄,这个值就会+1,当To满了,GC后还活着的会移动到From区,依次循环,当进行分代年龄到15以后,就会加入到老年代。永久代放的是静态变量和静态常量,类信息等
方法区
- 所有线程共享的一块内存区域。
- 用于存储已被虚拟机加载的类信息,常量,静态变量,即使编译器编译后的代码等数据。
- 回收目标主要针对的是常量池的回收和对类型的卸载
- 运行时常量池,属于方法区的一部分,存放各种字面量和符号引用。
JMM内存模型
概述
- java内存模型简称JMM
-
JMM决定一个线程对共享变量的写入何时对另一个线程可
AB通信过程:
1.线程A把本地内存A中更新过的共享变量刷新到主内存中去。2. 线程B到主内存中去读取线程A之前已更新过的共享变量。
一些方法
- read (读取) :从主内存读取数据
- load (载入) :将主内存读取到的数据写入工作内存
- use(使用) :从工作内存读取数据来计算
- assign (赋值) :将计算好的值重新赋值到工作内存中
- store (存储) :将工作内存数据写入主内存
- write (写入) :将store过去的变量值赋值给主内存中的变量
- lock (锁定) :将主内存变量加锁,标识为线程独占状态
- unlock (解锁) :将主内存变量解锁,解锁后其他线程可以锁定该变量
private static volatile boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("等待数据...");
while (!initFlag) {
}
System.out.println("======终于成功了");
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("准备数据...");
initFlag = true;
System.out.println("准备结束");
}
}).start();
}
这里我加了一个volatile,我们来分析下没有加volatile的过程
首先主内存现在有个变量initFlag值为false,线程会从主内存读到(read)变量initFlag,随后将变量加载到写入线程的工作内存中,再将工作内存变量use进行内存变量的计算,再经过assign将变量赋值到工作内存中,store将工作内存数据写入主内存,write 将store过去的变量值赋值给主内存中的变量,这样主内存的工作变量就发生了变化。
JMM内存缓存不一致
- 总线加锁(性能太低)
cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其它cpu没法去读或写这个数据,直到这个cpu使用完数据释放锁之后其它cpu才能读取该数据 - MESI缓存一致性协议
多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其它cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效
volatile实现原理
底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存并回写到主内存,此操作被称为"缓存锁定”, MESI缓存一致性协议机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存值通过总线回写到内存会导致其他处理器相应的缓存失效。
并发编程;可见性,原子性,有序性
volatile保证可见性和有序性,却不保证原子性,保证原子性需要借助synchronized
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败
public static volatile int num=0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads=new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i]=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
add();
}
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(num);
}
private static synchronized void add() {
num++;
}
如果不加synchronized,其打印的值会小于10000,原因是volatile不是立即保证可见性,它有个写入主内存的过程,其他线程(B)等不及的会自己去拿原本的数据进行++运算,当这个线程(B)之前的线程(A)修改完主内存的值后,B已经运算完,但是此时失效了,就会导致原本++运算无效
垃圾收集器和内存分配策略
概述
- 程序计数器,虚拟机栈,本地方法区这三个区域都随线程而生,随线程而灭,因此不需要考虑
- Java堆和方法区则不一样,我们只有在运行期间才知道会创建哪些对象和其大小,所以内存分配指的就是这一块
算法
目的:判断对象是死是活
引用计数器算法
给对象添加一个引用计数器,每当一个地方引用它时候,计数器值+1,引用失效的时候-1,任何时候为0的对象就不可能会再次被使用根搜索算法
GC Roots为起点,从该节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,表示这个对象不可用。
可作为GC Roots对象的有:
虚拟机栈(栈帧的本地变量表)中的引用对象
方法区中的类静态属性引用的对象
方法区中的常量引用对象
本地方法中JNI中的引用
四种引用
强引用
只要强引用存在,垃圾回收器永远不会回收掉被引用的对象软引用
当系统将要发生内存溢出异常之前,将会把这些对象列进回收范围并进行第二次回收,Java提供提供SoftReference类来实现软引用弱引用
被弱引用关联的对象只能生存到下一次垃圾收集发生之前,Java提供weakReference来实现弱引用虚引用
最弱的一种引用。无法通过虚引用来取得一个对象实例。其作用就是这个对象被收集器回收的时候收到一个通知
finalize()方法
任何一个对象的finalize方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize方法不会再被执行
运行代价高昂,不确定性大,无法保证各个对象调用的顺序
finalize能做的所有工作,使用try-finally都可以做,而且会更好
回收方法区
- 回收主要对象:废弃常量和无用的类
- 废弃常量:定义的字符串或者常量没有被使用和引用
- 无用的类:
1.该类已经被回收了,Java堆中不存在该类的任何引用
2.加载该类的classloader已经回收
3.该类没有被反射使用
垃圾收集算法
标记清除法
1、首先标记所有要回收的对象,在标记完成后统一回收掉所有被标记的对象
2、缺点:标记和清除效率都不高;空间浪费,会产生大量不连续空间复制算法
1、内存按容量划分大小相等两块,每次只使用其中一块。当一块内存用完了,就将存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。
2、主要针对新生代。标记-整理法
1、标记过程和标记清除法一样,但是后续步骤不是对可回收对象直接清除,而是让所有存活的对象都向着一端移动,然后直接清理掉边界以外的内存。
2、主要针对老年代分代收集算法
1、根据对象的存活周期的不同将内存划分为几块。一般是将Java堆分为新生代和老年代
2、新生代使用复制算法
3、老年代使用标记-清除或者标记-整理法