JVM探究
Posted 轻舟一曲
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM探究相关的知识,希望对你有一定的参考价值。
探究
-
谈谈对JVM的理解?java8虚拟机和之前的变化更新?
-
什么是OOM,什么是栈溢出?怎么分析?
-
JVM的常用调优参数有哪些?
-
内存快照如何抓取,怎么分析Dump文件?
-
谈谈JVM的类加载器? rt-jat ext application
-
百度
-
思维导图 (在线找或者自己动手画)
JVM脑图 | ProcessOn免费在线作图,在线流程图,在线思维导图
jvm | ProcessOn免费在线作图,在线流程图,在线思维导图
![](https://image.cha138.com/20221202/2019af3ff6654f87a8b5d59aebde757e.jpg)
JVM的位置
![](https://image.cha138.com/20221202/603afd8b92334f9799db98fdc44c930a.jpg)
JVM的体系结构
方法调优99%都是在调堆。
![](https://image.cha138.com/20221202/427bb2f154604b898510ec82df24a861.jpg)
JVM的类加载器
作用:加载class文件。
![](https://image.cha138.com/20221202/2d99299b04e04554be6b3f5ca645a3d7.jpg)
![](https://image.cha138.com/20221202/c45aaa5f661c495ba458bceb41ea98de.jpg)
![](https://image.cha138.com/20221202/9ae04969fc7e4ab7aedb0bde67e92454.jpg)
- 虚拟机自带的加载器
- 启动类(根)加载器
- 拓展类加载器
- 应用程序(系统类)加载器
![](https://image.cha138.com/20221202/a38cdbe315174323970a8e9c5b93aa77.jpg)
第3个在rt.jar包内,第2个拓展加载器在jre/lib/ext文件夹下
![](https://image.cha138.com/20221202/19308e059f944877ade3f6345110be75.jpg)
双亲委派机制
定义
当某个类加载器需要加载某个.class
文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
安全:
加载器:APP->EXT->BOOT(最终执行)
若BOOT没有::(最终执行)APP<-EXT<-BOOT
![](https://image.cha138.com/20221202/65756fc0b33a4319bc9d701c708c3613.jpg)
沙箱安全机制
沙箱演进
JDK1.0
![](https://image.cha138.com/20221202/84dc451cd4ba44d587ba14ed38e26267.jpg)
JDK1.1
![](https://image.cha138.com/20221202/4bb5b64d5f594eef9b564f03aecb148f.jpg)
JDK1.2
![](https://image.cha138.com/20221202/696d301a482f4006ab580dede1008bd1.jpg)
JDK1.6
![](https://image.cha138.com/20221202/818ca31c63614743a2b5e6e7c1fbd043.jpg)
![](https://image.cha138.com/20221202/a4d91ec2ee57436ba8f5e7a31b98eb56.jpg)
沙箱组件
-
字节码校验器
:确保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。
![](https://image.cha138.com/20221202/04516868dd9a4e4e935652e5ed4ce83d.jpg)
PC寄存器
![](https://image.cha138.com/20221202/e95c753f5da749958719ce1a4646d94e.jpg)
方法区
![](https://image.cha138.com/20221202/53ae495f1cef486f92bf484e88295b8b.jpg)
方法区只存放:static final class 常量池
是堆中占内存比较小的。
![](https://image.cha138.com/20221202/657ccca3c80742a6817ecaa22aaa0c59.jpg)
![](https://image.cha138.com/20221202/64922cd973da47b88ef269c30e85135d.jpg)
栈
数据结构。
栈–队列;
LIFO:后进先出;
栈:栈内存,主管程序的运行,生命周期和线程同步,线程结束,栈内存释放。
对于栈来说不存在垃圾回收,线程结束栈释放。
栈存放的东西:8大基本类型+对象引用+实例的方法
栈运行原理:栈帧。
程序正在执行的方法一定是在栈顶部。
![](https://image.cha138.com/20221202/ccd655693a7c4e469c2b68de53d9506b.jpg)
栈堆:
![](https://image.cha138.com/20221202/eecd16e855b64ba4a7f4f696b62c55fe.jpg)
main方法先执行,后结束。
无限压栈溢出:
![](https://image.cha138.com/20221202/96f847cdf3b54135b4a26a0ab2bcf3fb.jpg)
面试题:详解JAVA对象实例化过程
1 对象的实例化过程
- 对象的实例化过程是分成两部分:类的加载初始化,对象的初始化
- 要创建类的对象实例需要先加载并初始化该类,main方法所在的类需要先加载和初始化
- 类初始化就是执行方法,对象实例化是执行方法
- 一个子类要初始化需要先初始化父类
2 类的加载过程
![](https://image.cha138.com/20221202/4df94803963049e3b76685ca9ae11102.jpg)
-
类的加载机制:如果没有相应类的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
![](https://image.cha138.com/20221202/5ff98ebd1d7747b28ffc1c676d3a5e12.jpg)
堆
Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。
![](https://image.cha138.com/20221202/c605d764e9684e259dd53eb2b004f905.jpg)
堆内存中细分三个区域:新生区(伊甸园区),老年区,永久区。
![](https://image.cha138.com/20221202/caaad095c2f24af4ac2153e9b91bba5d.jpg)
GC垃圾回收主要是在伊甸园区和老年区。
假设堆内存满了,OOM,内存爆了
![](https://image.cha138.com/20221202/f50275b56670490ea5cb4d26ad0338b9.jpg)
JDK8以后,永久区改了名字(元空间)
新生区
- 类诞生和成长甚至死亡的地方;
- 伊甸园区:所有对象都是在这个区new出来的;
- 幸存者区0,幸存者区1。
老年区
![](https://image.cha138.com/20221202/fc039dba471e4ad7ae6c96b88ce8f248.jpg)
永久区
关闭JVM才会释放这个内存。
![](https://image.cha138.com/20221202/799265b2264b42e5b27ad8014daad786.jpg)
一个启动类,加载了大量的三方jar包。比如Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载,直到内存满,报OOM错误。
![](https://image.cha138.com/20221202/2947909a2cbf45bbb4a2278921816a4d.jpg)
方法区里面还有一小块放常量池。
![](https://image.cha138.com/20221202/f4c010adfc61452e8f390689f9c0ef78.jpg)
测试堆大小
![](https://image.cha138.com/20221202/5dbbbf5ccaa84e0eba2524386a49a100.jpg)
![](https://image.cha138.com/20221202/1e56e80f857640d5b733a66dfe3c0d66.jpg)
默认分配总内存是电脑内存的1/4.
初始化内存是1/64.
修改参数
![](https://image.cha138.com/20221202/357ffd708eeb420ab9cd96489abf3093.jpg)
![](https://image.cha138.com/20221202/204e30fbbdc147aa94089a180d688879.jpg)
OOM报错解决思路
![](https://image.cha138.com/20221202/f3cc5ebdc779497caaaa8f7e836dc34c.jpg)
![](https://image.cha138.com/20221202/6a59f25a5f334f978350fcb6761a57dc.jpg)
![](https://image.cha138.com/20221202/f7cdd343386b4ae6872c3c5bd40c1077.jpg)
JProfiler
面试问题
![](https://image.cha138.com/20221202/0442ee7cf9e54cde8a255c8df4f941d7.jpg)
关联IDEA:
![](https://image.cha138.com/20221202/fcdfef9972fa4a6cbe55ebac85466a72.jpg)
安装:
![](https://image.cha138.com/20221202/29d327faee584998ab6aea4a7f6bf485.jpg)
测试:
-Xms设置初始化内存分配大小默认1/64,
-Xmx设置最大分配内存默认1/4
-XX:+PrintGCDetails
-XX:+HeapDumOnOutOfMemoryError
![](https://image.cha138.com/20221202/4ea24953cd844d47a644a7b12dbdbb3b.jpg)
![](https://image.cha138.com/20221202/f235b9aef1d34f599ac2f883c91f8940.jpg)
![](https://image.cha138.com/20221202/473c14679b764a2b8950bf9184f174eb.jpg)
GC垃圾回收器常用算法
![](https://image.cha138.com/20221202/7c7606bfd44146d8a2233285e24ead25.jpg)
![](https://image.cha138.com/20221202/93d592539d004435b6ce34f1847cf892.jpg)
GC常见面试题:
- JVM的内存模型和分区,详细到每个区放什么东西?
- 堆里面的分区有哪些?Eden,from,to 老年区,都有什么特点!
- GC算法有哪几种?标记清除算法,标记压缩,复制算法,引用计数器,怎么用的?
- 轻GC和重GC分别在什么时候发生?
![](https://image.cha138.com/20221202/b7a37ba79cf7429e8df8c3addc718206.jpg)
JVM并不采用这种方式,并不高效。
GC复制算法
新生代用的主要是复制算法:
没有被GC掉的对象就会被复制到下一个区域。to区不能有对象,当GC时先将to区对象复制到from区。
![](https://image.cha138.com/20221202/ccf16f692b824bf3a010002b978342e7.jpg)
![](https://image.cha138.com/20221202/7431be9165be49b1af7986a797885786.jpg)
每次GC过后:Eden区和to区是空的。
好处:没有内存碎片。
坏处:多一个幸存区,浪费一半的空间。
极端情况:对象100%存活就都复制!
最佳使用场景:对象存活度较低的时候。
GC标记清除算法
优点:不需要额外的空间。
缺点:时间成本,两次扫描,产生内存碎片。
![](https://image.cha138.com/20221202/906838208fc24c258728269e34cdeb40.jpg)
GC标记压缩
对前者的优化。
压缩:防止内存碎片。
多了一个移动成本。
![](https://image.cha138.com/20221202/535b9352d373433985acb0cfd1c3f7b0.jpg)
GC总结
![](https://image.cha138.com/20221202/ecc4fa43fcfe4ff39753e66b6cc4791c.jpg)
面试问题:有没有最优的GC算法?没有最好的算法,只有最合适的算法。
所以GC也被称为分代收集算法。
![](https://image.cha138.com/20221202/c7d007b929e047d287c6be9ac1932af9.jpg)
内存调优调的就是最后标记清除什么时候清除。
JMM
Java内存模型。
定义
![](https://image.cha138.com/20221202/99663a5ef54444b2b497a28480f973a4.jpg)
作用
![](https://image.cha138.com/20221202/f36ef4534cd24623bf33e741ddcb6968.jpg)
![](https://image.cha138.com/20221202/67920513f62a4bcdb9bf0c8f6da95db4.jpg)
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
![](https://image.cha138.com/20221202/8c91a845744842a190c8f805a931cee2.jpg)
JMM内存模型带来的问题:
- 可见性问题:CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改为2。但这个变更对运行在右边CPU中的线程不可见,因为这个更改还没有flush到主存中:要解决共享对象可见性这个问题,我们可以使用java volatile关键字或者是加锁。
JMM是一个抽象的概念,本质是需要学习线程同步机制:
![](https://image.cha138.com/20221202/85b4a063ae054faf8f80e1189d31ddc2.jpg)
学习三部曲:
- 是什么?
- 有什么用?
- 面试问什么?
![](https://image.cha138.com/20221202/d12cf26f320e4a09a7a122969650ca81.jpg)
以上是关于JVM探究的主要内容,如果未能解决你的问题,请参考以下文章