JVM 就是 Java 虚拟机, 是⽤来运⾏我们平时所写的 Java 代码的。 优点是它会⾃动进⾏内存管理和垃圾回收, 缺点是⼀旦发⽣问题, 要是不了解 JVM 的运⾏机制, 就很难排查出问题所在。
JVM运行流程
程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能
JVM 运行时数据区(内存布局)

堆和方法区是共享的。
1.堆
堆的作用:程序中创建的所有对象都在保存在堆中。
堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象会放入老生代。新生代还有 3 个区域:一个 Endn + 两个 Survivor(S0/S1)。

2.方法区
方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。
GC的时候,方法区叫永久代(jdk1.7),元空间(JDK1.8)
1.7-》属于Java进程内存
1.8-》-----系统内存
3.Java虚拟机栈
1. 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量。
2. 操作栈:每个方法会生成一个先进后出的操作栈。操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
3. 动态链接:指向运行时常量池的方法引用。假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
4. 方法返回地址:PC 寄存器的地址
(1)栈的生命周期和线程相同:创建线程,就创建栈,销毁线程,就销毁这个线程对应的栈
(2)线程执行某个方法,就创建该方法栈帧(入栈) ,方法返回,就出栈
4.本地方法栈
本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用的
5.程序计数器
程序计数器的作用:用来记录当前线程执行的行号的。
OOM (内存溢出) :
指存放数据的大小,超出该区域的内存大小
运行时数据区域中,除了程序计数器,其他都可能发生OOM
内存泄露:
线程生命周期太长,导致始终引用一些不使用的数据 (这些数据就没法gc垃圾回收),随着使用时间越来越长,不使用的垃圾就越来越多,可用空间越来越少一最后可能导致OOM
GC (垃圾回收) :堆,方法区;其他没有
线程共享的区域存在,私有的不存在:栈是线程销毁才回收,栈帧是某次方法调用后,返回才回收
类加载的过程
1.加载
加载class字节码到java进程的内存中,在堆中创建类对象
2.验证
验证class字节码的规范(是否符合jvm规范)
3.准备
为static修饰的类变量分配内存,并设置初始值(0或null).
4.解析
把常量池中的符号引用,替换为直接引用(初始化常量的过程)
了解:
符号引用: class文件(字节码) private static intx = 123;此时进程还没有启动,就无法表示变量x指向1 23值(本质是指向内存地址)
5.初始化
类的初始化---执行静态变量的赋值,静态代码块。
加载机制-双亲委派模型(JDK默认机制)
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载
(遵循双亲委派机制的类加载,类加载器不直接加载,而是委派给父类加载器,以此类推,达到从上到下进行类加载的方式)
优点:1. 避免类的重复加载2. 避免Java的核心API被篡改
缺点:不够灵活,无法加载不知道的类
类加载的过程, 就是把 class 文件装载到 JVM 内存中, 装载完成以后就会得到一个 Class 对象, 我们就可以使用 new 关键字来实例化这个对象。
加载器:包含4种,从上到下:
BootStrap ClassLoader 启动类载器
ExtClassLoader 扩展类加载器
AppClassLoader 系统/应用类加载器
自定义类加载器
GC垃圾回收
发生在堆(主要)、方法区(很少)
垃圾回收,是回收堆中的对象=>对应=右边的对象
判断是否是垃圾的算法
1.引用计数算法
一个对象被引用一次,计数器+1,如果计数器=0,就表示是垃圾,可以回收
缺陷:无法解决循环引用问题=> 因此,JVM没有采取这个算法
2.可达性分析算法
通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",
当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。

了解: 4种引用类型
1.强引用:普通的对象及=赋值,如Object o=new Object
gc永远不回收强弓|用对象
2.软引用: SoftReference xxx=new SoftReference(obj)
内存溢出前,会回收这部分对象
3.弱引用:
gc发生时,都会回收
4.虚引用
无法使用,只是gc时,发起一个通知
垃圾回收算法
1.标记清除算法=>老年代回收算法
分为两个步骤:
(1)标记:标记对象
(2)清除:回收垃圾对象
缺陷:
(1)效率:两个阶段效率都不高
(2)内存碎片:清除后,产生大量的内存碎片= >剩余可用空间足够存放某个大对象,但连续空间不足存放,就无法存放
2.标记整理算法=>老年代gc算法
过程: (1) 标记 (2)整理:把存活对象往一端移动, 然后清理掉端外的空间
3.复制算法=>新生代的回收算法
将某个内存区域,划分为两块大小相同的空间,每次只使用其中一个,回收就是把存活对象复制到另一个不用的空间, 清除之前使用的空间
JVM中,新生代的回收算法,是复制算法的优化版本
内存划分为1个E区(Eden)和两个S区(Suvivor, SO, S1),每次使用E区和其中一块S区
E区和S区在jvm中默认的比例是8:1,,,空间利用率就是90%
说明:创建一个对象, 放在新生代,如果放不下,就触发新生代的gc
一个对象在s区中,存活超过15 (默认值),就会进入老年代
当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间
4.分代回收算法=> jvm中,采取的算法
没有具体的算法实现,只是把不同区域,采取不同的垃圾回收策略
具体一-点: 内存(堆)划分为新生代(E区1+ S区2)和老年代,新生代采取复制算法,老年代采取标记清除/标记整理算法
说明:
创建对象,是放在哪块区域?--》新生代:默认创建的对象(非大对象)都进入新生代
老年代:
(1) 大对象:对象占据的空间超出jvm规定的阈值(可以设置,不设置就是默认的)
(2)新生代中年龄超过15的对象: 对象在新生代,每经历- -次新生代的gc,还存活,年龄就+1
(3)新生代gc时,分配担保失败的对象:新生代gc是把8(E区)+ 1(1个S区)中的存活对象复制到1(另-个S区)
新生代对象的特性是朝生夕死,也就是说,大多数情况下,90%空间中存活对象,不足10% (另-块S区能放下)
不是绝对的情况,如果放不下,就进入老年代(这个情况,就是分配担保失
败=>老年代来担保,如果失败,就进入老年代)
什么时候发生gc?
对象进入哪个区域(新生代还是老年代),如果该区域空间不足,就会触发该区域的gc
两个gc的特性:
新生代gc:又叫minor gc,采取复制算法,效率比较高
老年代gc:又叫major gc,采取标记清除/标记整理算法,效率比较差, - -般比新生代gc慢10倍以上
垃圾回收器
1.Serial收集器(了解)
新生代收集器(复制算法)
单线程(单个垃圾回收线程)的方案=>目前的大多数电脑都是支持多线程,所以这个收集器效率不高,也不怎么使用
2.ParNew收集器(了解)
新生代收集器(复制算法)
多线程
搭配CMS (老年代收集器)的方案
3. Parallel Scanvenge收集器(了解)
新生代收集器(复制算法)
吞吐量优先= >适用性能优先的程序
搭配Parallel Old (老年代收集器,也是吞吐量优先的)
了解:这个收集器,具有某些特性: (1) 可控制的吞吐量(2) 自适应的调节策略
4. Serial Old收集器(了解)
老年代收集器(标记整理算法)
单线程
5. Parallel Old收集器(了解)
老年代收集器(标记整理算法)
吞吐量优先
因此,在吞吐量优化的程序,目前只有一种选择:
Parallel Scanvenge + Parallel Old
6.CMS收集器(Concurrent Mark Sweep)
1.老年代收集器
2.标记清除算法
3.用户体验优先= >整体看是并发(垃圾回收线程和用户线程同时执行)的过程,有局部的stw(少许时间是暂停用户线程的)
=>此时,CMS-般是搭配新生代的ParNew收集器
=>表现特性:并发收集,低停顿
4.步骤:分为4个步骤
(1)初识标记:标记GC Roots能直接关联的对象,需要STW
(2)并发标记:进行GC Roots引用链追踪的过程(搜索引用路径)、
(3)重新标记:修复第2个阶段用户线程同时执行时,产生标记变动的记录,需要STW
(4)并发清除:并发清除垃圾
第1, 3个阶段,都需要STW,但耗时比较少
第2, 4个阶段,是并发,耗时也比较长
5.缺陷
(1) CPU比较敏感
用户体验优先,就意味吞吐量稍微低-点(单次停顿时间短,整个停顿时间长- -点) =>CPU利用率下降
(2)浮动垃圾问题(浮动垃圾:第4个阶段用户线程并发执行时产生的垃圾,在此次GC无法回收,称为浮动垃圾)
会出现两个问题:
1)需要预留一部分空间(并发清除阶段用户线程创建的对象)
2)并发模式失败(Concurrent Model Failure)
并发清除阶段用户线程创建的对象超出预留空间大小=>再次触发另- -次的老年代GC
说明: CMS本身就是老年代GC,所以这里就是老年代gc时,又触发- -次老
年代gc,而老年代gc是比较耗时(效率比较低)
方式:采取老年代gc的后备方案: Serial Old收集器进行回收
(3)内存碎片问题
标记清除算法就会带来这个问题;内存碎片只是现象,对gc的影响是:可能导致提前触发gc
说明:所有可用空间足够,但连续的可用空间不足存放大对象
7.G1收集器= >全堆的收集器
说明:使用G1,堆的内存划分,就不是一个新生代(E区1+S区2) 及-个老年代
内存划分方案:把堆划分为多个相同大小的region区,动态分配为E区, S区,或T区(Tenured区, 老年代)
1.老年代收集器
2.全堆收集器= >整体看基于“标记整理算法”,局部看基于“复制算法"
3.用户体验优先
4.步骤
新生代回收:回收多个E区+多个S区,复制存活对象到空的region区(动态指定它为S区)
老年代回收:分为4个阶段
(1)初识标记:和cms类似(标记GC Roots关联的对象,STW), 不同的是,可以和新生代gc同时执行
(2)并发标记:和cms类似,多:优先回收(Garbage First, G1名词的由来)= >直接回收存活率低或几乎没有存活的region
(3)最终标记:和cms类似
(4)筛选回收:筛选存活率低的region回收
用户体验优先:
(1) ParNew+ CMS
(2) G1
JMM(Java内存模型)
不全
为了屏蔽硬件和操作系统内存访问差异
三大特性:
(1)原子性:是指8大字节码指令是原子
(2)可见性:一个线程修改某个共享变量,是否对另- -个线程可见
volatile, synchronized可以保证可见性
(3)有序性:一个线程看自己的代码,都是有序的,看别的线程代码执行,都是无序的