JVM入门(狂神说)
**视频地址:**https://www.bilibili.com/video/BV1iJ411d7jS
1、JVM 常问面试题
- 请你谈谈你对JVM的理解?
- Java8虚拟机和之前的变化更新?
- 什么是OOM?什么是栈溢出StackOverFlowError? 怎么分析?
- JVM 的常用调优参数?
- 内存快照如何抓取,怎么分析Dump文件?
- 谈谈JVM中,类加载器你的认识?
2、什么是JVM
JVM是Java Virtual Machine(Java虚拟机)的缩写。
虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
简单来说JVM是用来解析和运行Java程序的。
**Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。**一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
JVM的位置
三种JVM:
- Sun公司:HotSpot 用的最多(HostSpot Java HotSpot™ 64-Bit Server VM (build 25.101-b13, mixed mode))
- BEA:JRockit
- IBM:J9VM
我们学习的是:HotSpot
3、JVM体系结构
jvm调优:99%都是在调方法区和堆,大部分时间调堆。 JNI(java native interface)本地方法接口
4、类加载器
作用:加载Class文件! new Student()其实就是创建了一个Student类的实例,具体实例在堆里,引用变量名(内存地址)在栈里
public class Car {
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
System.out.println(car1.hashCode());
System.out.println(car2.hashCode());
System.out.println(car3.hashCode());
Class<? extends Car> aClass1 = car1.getClass();
Class<? extends Car> aClass2 = car2.getClass();
Class<? extends Car> aClass3 = car3.getClass();
System.out.println(aClass1.hashCode());
System.out.println(aClass2.hashCode());
System.out.println(aClass3.hashCode());
}
}
所有的对象都是同一个模版,这个模版类就是Class Loader对类进行加载后的结果,即上图中的Car Class。
类加载器是有等级的
- 虚拟机自带的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
更上层的加载器发现没有找到,并不代表不存在!
5、双亲委派机制
类的加载流程
1、类加载器收到类加载的请求
2、将这个请求一直委托给上层加载器处理,直到启动类加载器(Boot)
3、启动类加载器检查是否能够加载当前这个类,如果可以加载就结束,使用当前的加载器,否则就抛出异常,通知子加载器去加载。
4、重复第三步,所以我们自定义的类几乎都在AppClassLoader(应用程序加载器)中。
概念
当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。很像啃老。
例子
当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过(findLoadedClass),如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
作用
1、防止重复加载同一个.class,通过委托向上层问一问,如果上层加载过了就不再加载了,保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
比如:如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被BootstrapClassLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
6、沙箱安全机制
组成沙箱的基本组件:
-
字节码校验器(bytecode verifier):确保Java类文件.Class遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
-
类装载器(class loader):其中类装载器在3个方面对Java沙箱起作用
- 它防止恶意代码去干涉善意的代码;
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
- 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
- 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,
包括:
-
安全提供者
-
消息摘要
-
数字签名 keytools https(需要证书)
-
加密
-
鉴别
7、Native
凡是带了native 关键字的,说明java的作用范围达不到了,得回去调用底层C语言的库!
凡是带了native 关键字的方法会进入本地方法栈,其它的是java栈
JNI:Java Native Interface(本地方法接口)
调用本地方法接口(JNI)作用:
扩展java的使用,融合不同的编程语言为java所用
java诞生的初衷是融合C/C++程序,C、C++横行,想要立足,必须要有调用C、C++的程序~
它在内存区域中专门开辟了块标记区城: Native Method Stack
Native Method Stack(本地方法栈):
登记native方法,在执行引擎(Execution Engine)执行的时候。通过JNI 加载**本地方法库(Native Libraies)**中的方法。
在企业级应用中少见,与硬件有关应用:java程序驱动打印机,系统管理生产设备等,掌握即可
8、PC寄存器
程序计数器: Program Counter Register:
每个线程都有一个程序计数器,是线程私有的,就是一个指针, 指向方法区中的方法字节码 ( 用来存储指向下一条指令的地址, 即将要执行的指令代码 ), 在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。在多线程中,会记录每一个线程执行到的位置,使得线程在重新获得处理器时,可以继续执行!
9、方法区
Method Area方法区:
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
如:static,final,,Class(类模板), 常量池
面试题:一张白纸,画出对象实例化过程的内存图。(主要是考对JVM的理解)
10、栈
- 栈:数据结构
程序 = 数据结构 + 算法
程序 = 框架 + 业务逻辑~ :淘汰!
- 栈:先进后出,后进先出:类似一个桶,先放进去的东西最后才能拿出来!
队列:先进后出(FIFO):类似一个管
喝多了吐就是栈,吃多了拉就是队列
为什么Main()先执行后结束?因为一开始main()先压入栈!
- 栈内存,主管程序的运行,生命周期和线程同步
线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题
一旦线程结束,栈就Over!
-
栈存放:8大基本类型+对象引用+实例的方法
栈运行原理:栈帧(局部变量表+操作数栈)每调用一个方法都有一个栈帧
栈满了 main()无法结束,会抛出错误:栈溢出 StackOverflowError
栈运行原理:
栈 + 堆 + 方法区:交互关系
new 一个对象实际上就是在堆中放了一个具体的实例,在栈中放了一个引用
11、堆
Heap:一个JVM只有一个堆内存,堆的大小是可以调节的。
类加载器读取了类文件后,一般会把什么东西放到堆中?
类,方法,常量,变量~,保存所有引用类型的真实对象
堆内存细分为三个区域:
- 新生区(伊甸园区) Young/new
- 养老区 Old
- 永久区 Perm
GC垃圾回收,主要在伊甸园区和养老区~
假设内存满了,报错OOM,就是堆内存不够!OutOfMemoryError:Java heap space
在JDK8以后,永久存储区改了个名字(元空间)
11.1、新生区
- 类诞生和成长的地方、甚至死亡
- 伊甸园区,所有的对象都是在伊甸园区new出来的
- 幸存者区(0,1)
真理:经过研究,99%的对象都是临时对象!直接被清理了
11.2、老年区:
- 新生区剩下来的,轻GC杀不死了。
11.3、永久区
这个区域常驻内存,用来存放JDK自身携带的Class对象,Interface元数据,存储的是java运行时的一些环境或类信息,该区域不存在垃圾回收GC。关闭虚拟机就会释放这个内存。
- jdk1.6之前:永久代,常量池在方法区
- jdk1.7:永久代,但是慢慢退化了(去永久代)常量池在堆中
- jdk1.8之后:无永久代,常量池在元空间
永久区满可能的情况:一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直到内存满,就会出现OOM。
方法区又称非堆(non-heap),本质还是堆,只是为了区分概念。
元空间逻辑上存在,物理上并不存在。
**拓展:**https://www.jianshu.com/p/30e8ff0f7dd9
1.JDK 1.7及以前,Java 类信息、常量池、静态变量都存储在 Perm(永久代)里。类的元数据和静态变量在类加载的时候分配到 Perm,当类被卸载的时候垃圾收集器从 Perm 处理掉。
2.JDK 1.8 的对 JVM 架构的改造将类元数据放到本地内存中,另外,将常量池和静态变量放到 Java 堆里。HotSopt VM 将会为类的元数据明确分配和释放本地内存。在这种架构下,类元信息就突破了原来 -XX:MaxPermSize 的限制,所以PermSize的配置也是无效的,现在可以使用更多的本地内存。这样就从一定程度上解决了原来在运行时生成大量类的造成经常 Full GC 问题,如运行时使用反射、代理等
11.4、JVM调优
//面试题:报OOM怎么办?
/*
1.尝试扩大堆内存,如果还报错,说明有死循环代码 或垃圾代码
2.分析内存,看一下哪个地方有问题(专业工具)
*/
修改运行时的堆内存:
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
11.5、Jprofiler
在一个项目中,突然出现了OOM故障,该如何排除,研究为什么出错~
-
能够看到代码第几行出错:内存快照分析工具,MAT,Jprofiler
-
Debug,一行行分析代码!
MAT,Jprofiler作用:
- 分析Dump内存文件,快速定位内存泄漏;
- 获得堆中的数据
- 获得大的对象(大厂面试)
- …
//-Xms 设置初始化内存分配大小 默认1/64
//-Xmx 设置最大分配内存,默认1/4
//-XX:+PrintGCDetails 打印GC垃圾回收信息
//-XX:+HeapDumpOnOutOfMemoryError oom DUMP
//-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
public class Demo03 {
byte[] array = new byte[1*1024*1024]; //1m
public static void main(String[] args) {
ArrayList<Demo03> list = new ArrayList<>();
int count = 0;
try {
while (true){
list.add(new Demo03()); //不停地把创建对象放进列表
count = count + 1;
}
} catch (Exception e) {
System.out.println("count: "+count);
e.printStackTrace();
}
}
}
具体使用:https://www.bilibili.com/video/BV1iJ411d7jS?p=9
12、GC:垃圾回收及常用算法
JVM在进行GC时,并不是对三个区域统一回收。大部分时候,回收都是新生代~
- 新生代
- 幸存区(from,to)
- 老年区
GC两类:轻GC(普通的GC),重GC(全局GC)
关于GC面试题:
- JVM的内存模型和分区~详细到每个分区放什么?
- 堆里面的分区有哪些?Eden, from, to, 老年区,说说它们的特点!
- GC算法有哪些?怎么用的?标记清除法,标记整理,复制算法,分代收集法。引用计数法。
- 轻GC与重GC分别在什么时候发生?
12.1、引用计数法
一般JVM不用,大型项目对象太多了,这种方法很low!
12.2、复制算法
- -XX:MaxTenuringThreshold=15 设置进入老年代的存活次数条件
https://www.bilibili.com/video/BV1iJ411d7jS?p=11
- 优点:没有内存的碎片,内存效率高
- 缺点:浪费了内存空间(一个幸存区永远是空的);假设对象100%存活,复制成本很高。
复制算法最佳使用场景:对象存活度较低的时候,新生区~。
12.3、标记清除法
- 优点:不需要额外空间,优化了复制算法。
- 缺点:两次扫描,严重浪费时间;会产生内存碎片。
12.4、标记压缩法
对标记清除法的再优化!
- 标记->清除->压缩
**标记清除压缩!**再优化
- 每标记清除几次就压缩一次,或者内存碎片积累到一定程度就压缩。
12.5、总结
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记压缩算法=标记清除算法>复制算法
难道没有最优算法吗?
答案:无,没有最好的算法,只有合适的算法(GC也被称为分代收集算法)。
- 年轻代:存活率低,用复制算法。
- 老年代:存活率高,区域大,用标记-清除-压缩。
参考和研究:《深入理解Java虚拟机》
13、JMM
应该如何去学习一个东西?
1.什么是JMM? 百度
java内存模型 Java Memory Model
2.它是做什么的? 查官方、博客、学习视频
作用:缓存一致性协议,用于定义数据读写的规则。
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的私有变量存储在主内存中, 每个线程都有一个私有的本地变量。
解决共享对象可见性这个问题:volilate
3.如何学会它?
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
- 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
- 不允许read和load、store和write操作之一单独出现
- 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
对于程序员来说,学习新东西是常态!
学会总结,及时分析!