探究
-
谈谈对JVM的理解?java8虚拟机和之前的变化更新?
-
什么是OOM,什么是栈溢出?怎么分析?
-
JVM的常用调优参数有哪些?
-
内存快照如何抓取,怎么分析Dump文件?
-
谈谈JVM的类加载器? rt-jat ext application
-
百度
-
思维导图 (在线找或者自己动手画)
JVM脑图 | ProcessOn免费在线作图,在线流程图,在线思维导图
jvm | ProcessOn免费在线作图,在线流程图,在线思维导图
![image-20210607220714736](https://i-blog.csdnimg.cn/blog_migrate/1bb674a2057bf110326dc7e88b0e8f90.png)
JVM的位置
![image-20210607221827080](https://i-blog.csdnimg.cn/blog_migrate/886dce0c40d97aa7f20cf91b1659f016.png)
JVM的体系结构
方法调优99%都是在调堆。
![image-20210607231753551](https://i-blog.csdnimg.cn/blog_migrate/57135abcd6c83d4308a1159cd68c80f0.png)
JVM的类加载器
作用:加载class文件。
![image-20210607232918705](https://i-blog.csdnimg.cn/blog_migrate/34f61f4fdd6e62b57319e10aa783d5a6.png)
![image-20210607232612781](https://i-blog.csdnimg.cn/blog_migrate/1c1561ee5666a220b583be889ff6b554.png)
![image-20210607232840319](https://i-blog.csdnimg.cn/blog_migrate/ce8bff2314fc44441e4bd217ce6ddd45.png)
- 虚拟机自带的加载器
- 启动类(根)加载器
- 拓展类加载器
- 应用程序(系统类)加载器
![image-20210607233308060](https://i-blog.csdnimg.cn/blog_migrate/cc8028bb0038c603e0d8c804b6754366.png)
第3个在rt.jar包内,第2个拓展加载器在jre/lib/ext文件夹下
![image-20210607234441502](https://i-blog.csdnimg.cn/blog_migrate/b4dcb549ee8e09ad22842acf3f6bf126.png)
双亲委派机制
定义
当某个类加载器需要加载某个.class
文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
安全:
加载器:APP->EXT->BOOT(最终执行)
若BOOT没有::(最终执行)APP<-EXT<-BOOT
![image-20210607233903396](https://i-blog.csdnimg.cn/blog_migrate/3722c1e3ae48d8d7d714f8d6c2257518.png)
沙箱安全机制
沙箱演进
JDK1.0
![image-20210608093056051](https://i-blog.csdnimg.cn/blog_migrate/b0aacfa440ba99f937b3fe99842ed80b.png)
JDK1.1
![image-20210608093307300](https://i-blog.csdnimg.cn/blog_migrate/14e8bea52bd5a7b640cb3934a3efdbb5.png)
JDK1.2
![image-20210608093528458](https://i-blog.csdnimg.cn/blog_migrate/13d857419c373256dbd0eeb087e45315.png)
JDK1.6
![image-20210608093711865](https://i-blog.csdnimg.cn/blog_migrate/81461f773859cc223f3e7542f39bd1bd.png)
![image-20210608094021332](https://i-blog.csdnimg.cn/blog_migrate/53ac53d67a313da540052950c995e08c.png)
沙箱组件
-
字节码校验器
:确保Java类文件遵循Java语言规范,帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。 (检查时异常) -
类加载器
:虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,他们之间互不可见。在3个方面对沙箱起作用:
- 防止恶意代码去干涉善意代码; //双亲委派机制
- 守护被信任的类库边界; //双亲委派机制
- 将代码归入保护域,确定代码可以进行哪些操作。 //沙箱机制
-
native本地方法接口
public static void main(String[] args) {
new Thread(()->{
System.out.println("Thread start..");
},"my thread name").start();
}
native:凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库->进入本地方法栈->调用本地方法本地接口JNI。
native在内存区域中专门开辟了一块标记区域:Native Method Stack,登记native方法,在最终执行的时候,加载本地方法库中的方法通过JNI接口。
JNI作用:拓展Java的使用,融合不同的编程语言为Java所用。
应用了解即可:Java程序驱动机,管理系统,Robot。
![image-20210608100740973](https://i-blog.csdnimg.cn/blog_migrate/076cecd3bf2146f4d4cf1b0e12bc1aee.png)
PC寄存器
![image-20210608101824268](https://i-blog.csdnimg.cn/blog_migrate/7454a2a17e0b993bdcadb2343aa0468f.png)
方法区
![image-20210608101934488](https://i-blog.csdnimg.cn/blog_migrate/8f394924acaf30f0cf4e8cf838afab39.png)
方法区只存放:static final class 常量池
是堆中占内存比较小的。
![image-20210608102348074](https://i-blog.csdnimg.cn/blog_migrate/9a6bfab91fefa681e5a98f8a659cf471.png)
![image-20210608102444521](https://i-blog.csdnimg.cn/blog_migrate/83502cd801344562cf29ef625b0b3d66.png)
栈
数据结构。
栈–队列;
LIFO:后进先出;
栈:栈内存,主管程序的运行,生命周期和线程同步,线程结束,栈内存释放。
对于栈来说不存在垃圾回收,线程结束栈释放。
栈存放的东西:8大基本类型+对象引用+实例的方法
栈运行原理:栈帧。
程序正在执行的方法一定是在栈顶部。
![image-20210608104054894](https://i-blog.csdnimg.cn/blog_migrate/683ec637b9ce178aea13123d282867ea.png)
栈堆:
![image-20210608104415040](https://i-blog.csdnimg.cn/blog_migrate/3a6c20867e0a16cefba03f8af2c8a602.png)
main方法先执行,后结束。
无限压栈溢出:
![image-20210608103240869](https://i-blog.csdnimg.cn/blog_migrate/b04603d01c7a07abd907f71360641333.png)
面试题:详解JAVA对象实例化过程
1 对象的实例化过程
- 对象的实例化过程是分成两部分:类的加载初始化,对象的初始化
- 要创建类的对象实例需要先加载并初始化该类,main方法所在的类需要先加载和初始化
- 类初始化就是执行方法,对象实例化是执行方法
- 一个子类要初始化需要先初始化父类
2 类的加载过程
![image-20210608105553668](https://i-blog.csdnimg.cn/blog_migrate/ebaa9feb314a7bff51d1679e69fcdc1b.png)
-
类的加载机制:如果没有相应类的class,则加载class到方法区。对应着加载->验证->准备->解析–>初始化阶段
-
加载:载入class对象,不一定是从class文件获取,可以是jar包,或者动态生成的class
-
验证:校验class字节流是否符合当前jvm规范
-
准备:为 类变量 分配内存并设置变量的初始值( 默认值 )。如果是final修饰的对象则是赋值声明值
-
解析:将常量池的符号引用替换为直接引用
-
初始化:执行类构造器( 注意不是对象构造器 ),为 类变量 赋值,执行静态代码块。jvm会保证子类的执行之前,父类的先执行完毕
-
-
其中验证、准备、解析3个部分称为 连接
-
方法由 静态变量赋值代码和静态代码块 组成;先执行类静态变量显示赋值代码,再到静态代码块代码。
3 触发类加载的条件
-
第一次创建类的新对象时, 会触发类的加载初始化和对象的初始化函数执行,这个是实例初始化,其他6个都是类初始化
-
JVM启动时会先加载初始化包含main方法的类
-
调用类的静态方法(如执行invokestatic指令)
-
对类或接口的静态字段执行读写操作(即执行getstatic、putstatic指令);不过final修饰的静态字段的除外(已经赋值,String和基本类型,不包含包装类型),它被初始化为一个编译时常量表达式
- 注意 :操作静态字段时,只有直接定义这个字段的类才会被初始化;如通过其子类来操作父类中定义的静态字段,只会触发父类的初始化而不是子类的初始化
-
调用JavaAPI中的反射方法时(比调用java.lang.Class中的方法(Class.forName),或者java.lang.reflect包中其他类的方法)
-
当初始化一个类时,其父类没有初始化,则需先触发父类的初始化(接口例外)
4对象的实例化过程
-
对象实例化过程其实就是执行类构造函数 对应在字节码文件中的()方法(称之为实例构造器);()方法由 非静态变量、非静态代码块以及对应的构造器组成
- ()方法可以重载多个,类有几个构造器就有几个()方法
- ()方法中的代码执行顺序为:父类变量初始化,父类代码块,父类构造器,子类变量初始化,子类代码块,子类构造器。
-
静态变量,静态代码块,普通变量,普通代码块,构造器的执行顺序:
-
具有父类的子类的实例化顺序如下:
5类加载器和双亲委派规则,如何打破双亲委派规则
-
类加载器
-
通过一个类的全限定名来获取 描述此类的二进制字节流 ,实现这个动作的代码模块称为类加载器
-
任意一个类都需要其加载器和类本身来确定类在JVM的唯一性;每个类加载器都有自己的类名称空间,同一个类class由不同的加载器加载,则被JVM判断为不同的类
-
-
双亲委派模型
-
启动类加载器有C++代码实现,是虚拟机的一部分。负责加载lib下的类库
-
其他的类加载器有java语言实现,独立于JVM,并且继承ClassLoader
-
extention ClassLoader负责加载libext目录下的类库
-
application ClassLoader 负责加载用户路径下(ClassPath)的代码
-
不同的类加载器加载同一个class文件会导致出现两个类。而java给出解决方法是下层的加载器加委托上级的加载器去加载类,如果父类无法加载(在自己负责的目录找不到对应的类),而交还下层类加载器去加载。如下图:
-
-
打破双亲委派模型
- 双亲委派模型并不是一个强制的约束模型,而是java设计者推荐给开发者的类加载实现方式
- 双亲委派模型很好的解决各个类加载基础类的同一问题(越基础的类由越上层的加载器加载),但是基础类总是作为用户代码调用的API,但是如果它的具体实现是下层的代码,此时基础类需要调用下层的代码,则需要打破双亲委派模型
- 如JNDI服务,JNDI的代码有启动类去加载(rt.jar),它需要调用由独立厂商部署在应用程序classpath下的JNDI的SPI(Service Provider Interface)代码。为了解决SPI代码加载问题,java引入了线程上下文类加载器去加载SPI代码。也就是父类加载器请求子类去完成类的加载动作
- 线程上下文类加载器,线程创建时会从父线程继承,如果全局范围没有设置过,则默认设置为application Class Loader
3种JVM
![image-20210608115803298](https://i-blog.csdnimg.cn/blog_migrate/f4ae389b3e28522634add2515882d915.png)
堆
Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。
![image-20210608120220752](https://i-blog.csdnimg.cn/blog_migrate/4fc8cef5aab8b6073e7483be5d65a33b.png)
堆内存中细分三个区域:新生区(伊甸园区),老年区,永久区。
![image-20210608120645639](https://i-blog.csdnimg.cn/blog_migrate/df753e566e5c0d958ecc80fa723fc0eb.png)
GC垃圾回收主要是在伊甸园区和老年区。
假设堆内存满了,OOM,内存爆了
![image-20210608120947371](https://i-blog.csdnimg.cn/blog_migrate/3c84a7077544874d8536ab4680e4e296.png)
JDK8以后,永久区改了名字(元空间)
新生区
- 类诞生和成长甚至死亡的地方;
- 伊甸园区:所有对象都是在这个区new出来的;
- 幸存者区0,幸存者区1。
老年区
![image-20210608121542383](https://i-blog.csdnimg.cn/blog_migrate/51c519f05a88cd7db50a94ed38ed87ec.png)
永久区
关闭JVM才会释放这个内存。
![image-20210608121821439](https://i-blog.csdnimg.cn/blog_migrate/aeddd9fd8788816ac9ce20ab340c67f1.png)
一个启动类,加载了大量的三方jar包。比如Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载,直到内存满,报OOM错误。
![image-20210608122320142](https://i-blog.csdnimg.cn/blog_migrate/b14104ed665e53b5c52c6e84c1a6c4ec.png)
方法区里面还有一小块放常量池。
![image-20210608122443612](https://i-blog.csdnimg.cn/blog_migrate/0cbd3b2dc82a90a47473c04449ebd5b0.png)
测试堆大小
![image-20210608122807180](https://i-blog.csdnimg.cn/blog_migrate/a00b802204fa72aedf0f4ff6e2b5608c.png)
![image-20210608123109614](https://i-blog.csdnimg.cn/blog_migrate/fda418a2a09076790eca8578b22e8041.png)
默认分配总内存是电脑内存的1/4.
初始化内存是1/64.
修改参数
![image-20210608123333087](https://i-blog.csdnimg.cn/blog_migrate/477c8b15446eca1e1b5b0edc2f13af11.png)
![image-20210608123428623](https://i-blog.csdnimg.cn/blog_migrate/be1e18e01f3d90630a8650ad37496a18.png)
OOM报错解决思路
![image-20210608123631150](https://i-blog.csdnimg.cn/blog_migrate/38fb76f572b4c698967556ec617a2392.png)
![image-20210608123847758](https://i-blog.csdnimg.cn/blog_migrate/b5173b567cab6a69a21c41bdc28a6b0d.png)
![image-20210608124002413](https://i-blog.csdnimg.cn/blog_migrate/a4a15e9901b4ad268e5e011efd9bc201.png)
JProfiler
面试问题
![image-20210608154658880](https://i-blog.csdnimg.cn/blog_migrate/abf4c1830ac3072082730c9a408ab3d4.png)
关联IDEA:
![image-20210608155025546](https://i-blog.csdnimg.cn/blog_migrate/47fe66f5d400bb979c71fee2f44165f9.png)
安装:
![image-20210608154803255](https://i-blog.csdnimg.cn/blog_migrate/49caadaf098500aed10730a4f5c4e592.png)
测试:
-Xms设置初始化内存分配大小默认1/64,
-Xmx设置最大分配内存默认1/4
-XX:+PrintGCDetails
-XX:+HeapDumOnOutOfMemoryError
![image-20210608161015634](https://i-blog.csdnimg.cn/blog_migrate/02dc2cf235f8f8171d516be05d741f69.png)
![image-20210608161444523](https://i-blog.csdnimg.cn/blog_migrate/b45b63ac1eb543ba8479bd7a5351f0b2.png)
![image-20210608161621853](https://i-blog.csdnimg.cn/blog_migrate/a7dd31bb8882b56e0b50d2b70c348bac.png)
GC垃圾回收器常用算法
![image-20210608162557156](https://i-blog.csdnimg.cn/blog_migrate/d6d797507ce48ab20f407ecd099a7e4e.png)
![image-20210608162949032](https://i-blog.csdnimg.cn/blog_migrate/c70fce66a9645f6cfb1dd4494d19c50f.png)
GC常见面试题:
- JVM的内存模型和分区,详细到每个区放什么东西?
- 堆里面的分区有哪些?Eden,from,to 老年区,都有什么特点!
- GC算法有哪几种?标记清除算法,标记压缩,复制算法,引用计数器,怎么用的?
- 轻GC和重GC分别在什么时候发生?
![image-20210608163528877](https://i-blog.csdnimg.cn/blog_migrate/76d2ba3268414f686e341d399dde8fda.png)
JVM并不采用这种方式,并不高效。
GC复制算法
新生代用的主要是复制算法:
没有被GC掉的对象就会被复制到下一个区域。to区不能有对象,当GC时先将to区对象复制到from区。
![image-20210608164143864](https://i-blog.csdnimg.cn/blog_migrate/2e8b67ddeec61610537d1fd3c0618df0.png)
![image-20210608164623476](https://i-blog.csdnimg.cn/blog_migrate/eb54855445e1571b459f690233c7ebb0.png)
每次GC过后:Eden区和to区是空的。
好处:没有内存碎片。
坏处:多一个幸存区,浪费一半的空间。
极端情况:对象100%存活就都复制!
最佳使用场景:对象存活度较低的时候。
GC标记清除算法
优点:不需要额外的空间。
缺点:时间成本,两次扫描,产生内存碎片。
![image-20210608165141535](https://i-blog.csdnimg.cn/blog_migrate/8c3ef5f39d0bc8d1d54c4b9e6368afc5.png)
GC标记压缩
对前者的优化。
压缩:防止内存碎片。
多了一个移动成本。
![image-20210608165458357](https://i-blog.csdnimg.cn/blog_migrate/d1c02a59cf50231f45a191bf04f3228b.png)
GC总结
![image-20210608165735641](https://i-blog.csdnimg.cn/blog_migrate/2546f54c06515c8eeace017c3e40b1dd.png)
面试问题:有没有最优的GC算法?没有最好的算法,只有最合适的算法。
所以GC也被称为分代收集算法。
![image-20210608170052866](https://i-blog.csdnimg.cn/blog_migrate/556ba41a7520b484e0a97e0bd850e47f.png)
内存调优调的就是最后标记清除什么时候清除。
JMM
Java内存模型。
定义
![image-20210608170558382](https://i-blog.csdnimg.cn/blog_migrate/83fb53719dff74dcbb7483b92fcac5dd.png)
作用
![image-20210608170919519](https://i-blog.csdnimg.cn/blog_migrate/219a06df98ba87340ae61dce19bcf896.png)
![image-20210608171827202](https://i-blog.csdnimg.cn/blog_migrate/47d231a0e0f200950638b0e6330effaa.png)
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
![image-20210608171254653](https://i-blog.csdnimg.cn/blog_migrate/3b7cf8731bf3b5da9af89da1755ac597.png)
JMM内存模型带来的问题:
- 可见性问题:CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改为2。但这个变更对运行在右边CPU中的线程不可见,因为这个更改还没有flush到主存中:要解决共享对象可见性这个问题,我们可以使用java volatile关键字或者是加锁。
JMM是一个抽象的概念,本质是需要学习线程同步机制:
![image-20210608171622647](https://i-blog.csdnimg.cn/blog_migrate/7f72913c3c2e8049d1e3c902ea46d287.png)
学习三部曲:
- 是什么?
- 有什么用?
- 面试问什么?
![image-20210608172012259](https://i-blog.csdnimg.cn/blog_migrate/2421e110c37e1c18ee055114577f9818.png)