JVM详细笔记(包括思维导图和部分常见面试问题)
JVM 是可运行Java代码的假想计算机, 包括一套字节码指令集, 一组寄存器, 一个栈 ,一个垃圾回收 ,堆 和一个存储方法域.
JVM运行在操作系统之上, 与硬件没有直接交互.
一. 简短小结:
- Java跨平台因为有JVM屏蔽了底层操作系统
- Java源码到执⾏的过程,从JVM的⻆度看可以总结为四个步骤:编译->加载->解释->执⾏
- 「编译」经过 语法分析、语义分析、注解处理 最后才⽣成会class⽂件
- 「加载」⼜可以细分步骤为:装载->连接->初始化。装载则把class⽂件装载⾄JVM,连接则校验class信息、分配内存空间及赋默认值,初始化则为变量赋值为正确的初始值。连接⾥⼜可以细化为:验证、准备、解析
- 「解释」则是把字节码转换成操作系统可识别的执⾏指令,在JVM中会有字节码解释器和即时编译器。在解释时会对代码进⾏分析,查看是否为「热点代码」,如果为「热点代码」则触发JIT编译,下次执⾏时就⽆需重复进⾏解释,提⾼解释速度
- 「执⾏」调⽤系统的硬件执⾏最终的程序指令
1. 栈和堆的区别
区别 | 栈 | 堆 |
---|---|---|
功能 | 存局部变量和方法调用 | 存java对象 |
共享 | 线程私有 | 线程共享,应用程序共享 |
异常错误 | StackOverFlowError | OutOfMemoryError |
空间大小 | 小,固定(编译器确定) | 大,不固定(运行期确定) |
物理地址 | 连续,性能快 | 不连续,性能慢 |
生命周期 | 同线程 | 同对象实例 |
可见性 | 线程可见 | 应用程序可见 |
静态变量放方法区, 静态对象在堆
2. 什么时候触发FULLGC
- System.gc
- 旧生代空间不足
- Permanet Generation空间满
- CMS GC时出现promotion failed和concurrent mode failure
- 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
3. 对象分配规则
- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC(少量)
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)(目的避免大量内存拷贝)
- 长期存活的对象进入老年代: 对象1次Minor GC会进入Survivor区年龄计数器+1 阀值15
- 动态判断对象的年龄; survivor同龄对象大小和超过survivor空间一半,不小于该年龄的obj
- 空间分配担保
4. 对象可回收
判断对象是否存活一般有两种方式:
-
引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
-
可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象
:::info
tip:
不可达对象不等价 可回收对象; 不可达对象变为可回收对象至少要经过两次标记过程.
两次标记后仍是可回收对象,则面临回收;
:::
5. 垃圾收集算法
- 标记 -清除算法,“标记-清除”(Mark-Sweep)算法,算法分为“标记”“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。问题:内存碎片
- 复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。问题:内存被压缩一半
- 标记-压缩算法(又称标记整理),(Mark-Compact)标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
- 分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。新生代复制算法,老年代标记复制算法
6. STW & OOPMap & safepoint
- 进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为「Stop The World」。也简称为STW。
- 在HotSpot中,有个数据结构(映射表)称为「OopMap」。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过程中,也会在「特定的位置」生成 OopMap,记录下栈上和寄存器里哪些位置是引用。
这些特定的位置主要在:
1.循环的末尾(非 counted 循环)
2.方法临返回前 / 调用方法的call指令后
3.可能抛异常的位置
- 这些位置就叫作「安全点(safepoint)。」 用户程序执行时并非在代码指令流的任意位置都能够在停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。
7. tomcat 类加载机制
当 tomcat启动时,会创建几种类加载器: Bootstrap 引导类加载器 加载 JVM启动所需的类,以及标准扩展类(位于 jre/lib/ext 下) System 系统类加载器 加载 tomcat 启动的类,比如bootstrap.jar,通常在 catalina.bat 或者 catalina.sh 中指定。位于 CATALINA_HOME/bin 下。
部署项目时,把war包放在tomcat的webapp下,意味着一个tomcat可运行多个web应用程序;此时如果有两个web应用程序,都有一个类,叫user,并且类全限定名一样,但是它两的具体实现不一样
Tomcat给每个 Web 应⽤创建⼀个类加载器实例(WebAppClassLoader),该加载器重写了loadClass⽅法,优先加载当前应⽤⽬录下的类,如果当前找不到了,才⼀层⼀层往上找,就达到web应用层级的依赖隔离
并不是Web应⽤程序下的所有依赖都需要隔离的,⽐如Redis就可以Web应⽤程序之间共享(如果有需要的话),因为如果版本相同,没必要每个Web应⽤程序都独⾃加载⼀份啊。做法也很简单,Tomcat就WebAppClassLoader上加了个⽗类加载器(SharedClassLoader),如果WebAppClassLoader⾃身没有加载到某个类,那就委托SharedClassLoader去加载。
二. 简单小结:
- 前置知识:JDK中默认类加载器有三个:AppClassLoader、Ext ClassLoader、BootStrap ClassLoader。
AppClassLoader的⽗加载器为Ext ClassLoader、Ext ClassLoader的⽗加载器为BootStrap ClassLoader。这⾥的⽗⼦关系并不是通过继承实现的,⽽是组合。
-
什么是双亲委派机制:加载器在加载过程中,先把类交由⽗类加载器进⾏加载,⽗类加载器没找到才由 ⾃身加载。
-
双亲委派机制⽬的:为了防⽌内存中存在多份同样的字节码(安全)
-
类加载规则:如果⼀个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。
-
如何打破双亲委派机制:⾃定义ClassLoader,重写loadClass⽅法(只要不依次往上交给⽗加载器进⾏加载,就算是打破双亲委派机制)
-
打破双亲委派机制案例:Tomcat
- 为了Web应⽤程序类之间隔离,为每个应⽤程序创建WebAppClassLoader类加载器
- 为了Web应⽤程序类之间共享,把ShareClassLoader作为WebAppClassLoader的⽗类加载器,如果WebAppClassLoader加载器找不到,则尝试⽤ShareClassLoader进⾏加载
- 为了Tomcat本身与Web应⽤程序类隔离,⽤CatalinaClassLoader类加载器进⾏隔离,CatalinaClassLoader加载Tomcat本身的类
- 为了Tomcat与Web应⽤程序类共享,⽤CommonClassLoader作为CatalinaClassLoader和ShareClassLoader的⽗类加载器
- ShareClassLoader、CatalinaClassLoader、CommonClassLoader的⽬录可以在Tomcat的catalina.properties进⾏配置
- 线程上下⽂加载器:由于类加载的规则,很可能导致⽗加载器加载时依赖⼦加载器的类,导致⽆法加载成功
(BootStrap ClassLoader⽆法加载第三⽅库的类),所以存在「线程上下⽂加载器」来进⾏加载。
8. JAVA 四种引用类型
- 强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
- 软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。有用但不是必须的对象,在发生内存溢出之前会被回收。
- 弱引用需要用 WeakReference 类来实现,它比软引用生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。有用但不是必须的对象,在下一次GC时会被回收。
- 虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。无法通过虚引用获得对象,虚引用的用途是在 gc 时返回一个通知
9. 分区收集算法
分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次 GC 所产生的停顿。
10. GC垃圾收集器
JDK1.6 中 Sun HotSpot 虚拟机的垃圾收集器如下:
- Serial(英文连续 单线程、 复制算法)是最基本垃圾收集器,使用复制算法,是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。
- ParNew 垃圾收集器 (Serial+多线程)其实是 Serial 收集器的多线程版本,用复制算法,【Parallel:平行的】很多java虚拟机运行在 Server 模式下新生代的默认垃圾收集器。
- Parallel Scavenge (多线程复制算法、高效)收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
- Serial Old 收集器 (单线程标记整理算法)是 Serial 垃圾收集器年老代版本,运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。作为年老代中使用 CMS 收集器的后备垃圾收集方案。
- CMS 收集器 收集器(多线程标记清除算法)Concurrent mark sweep(CMS) [并发标记清除] 收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,初始标记,并发标记, [并发预处理] 重新标记,并发清除 (标记的两个阶段就会stop the world)
- 问题1 内存碎片 -->碎片整理
- 问题2 需预留空间 -->空间担保
- 问题3 停顿时间不可预知
- G1 收集器 Garbage first 垃圾收集器
- 基于标记-整理算法,不产生内存碎片。
2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,优先回收垃圾最多的区域
三.简单总结(G1垃圾收集器特点):
- 从原来的「物理」分代,变成现在的「逻辑」分代,将堆内存「逻辑」划分为多个Region
- 使⽤CSet来存储可回收Region的集合
- 使⽤RSet来处理跨代引⽤的问题(注意:RSet不保留 年轻代相关的引⽤关系)
- G1可简单分为:Minor GC 和Mixed GC以及Full GC
- 【Eden区满则触发】Minor GC 回收过程可简单分为:(STW) 扫描 GC Roots、更新&&处理Rset、复制清除
- 【整堆空间占⼀定⽐例则触发】Mixed GC 依赖「全局并发标记」,得到CSet(可回收Region),就进⾏「复制清除」
- R⼤描述G1原理的时候,从宏观的⻆度看G1其实就是「全局并发标记」和「拷⻉存活对象」
- 使⽤SATB算法来处理「并发标记」阶段对象引⽤可能会修改的问题
- 提供可停顿时间参数供⽤户设置(G1会尽量满⾜该停顿时间来调整 GC时回收Region的数量)
11. JAVA IO/NIO
- 阻塞 IO 模型 在读写数据过程中会发生阻塞现象。
当用户线程发出** IO 请求之后,内核会去查看数据是否就绪**,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除 block 状态。
典型的阻塞 IO 模型例子为:data = socket.read();如果数据没有就绪,就会一直阻塞在 read 方法。
- 非阻塞 IO 模型 当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果.
如果结果是一个error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。所以事实上,在非阻塞 IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 IO不会交出 CPU,而会一直占用 CPU。典型的非阻塞 IO 模型一般如下:
while(true){
data = socket.read();
if(data != error){
//这里处理数据
break;
}
}
问题:在 while 循环中需要不断地去询问内核数据是否就绪,这样会导致 CPU 占用率非常高,因此一般情况下很少使用 while 循环这种方式来读取数据
- 多路复用 IO 模型 有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作
因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。在 Java NIO 中,是通过** selector.select()**去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。多路复用 IO 模式,通过一个线程就可以管理多个 socket,只有当socket 真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用 IO 比较适合连接数比较多的情况。
另外多路复用 IO 为何比非阻塞 IO 模型的效率高是因为在非阻塞 IO 中,不断地询问 socket 状态
时通过用户线程去进行的,而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效
率要比用户线程要高的多。
不过要注意的是,多路复用 IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件
逐一进行响应。因此对于多路复用 IO 模型来说,一旦事件响应体很大,那么就会导致后续的事件
迟迟得不到处理,并且会影响新的事件轮询。
- 信号驱动 IO 模型
在信号驱动 IO 模型中,当用户线程发起一个 IO 请求操作,会给对应的 socket 注册一个信号函
数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到
信号之后,便在信号函数中调用 IO 读写操作来进行实际的 IO 请求操作。
- 异步 IO 模型
异步 IO 模型才是最理想的 IO 模型,在异步 IO 模型中,当用户线程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个 asynchronous read 之后,它会立刻返回,说明 read 请求已经成功发起了,因此不会对用户线程产生任何 block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它 read 操作完成了。也就说用户线程完全不需要实际的整个 IO 操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接去使用数据了。
也就说在异步 IO 模型中,IO 操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用 IO 函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用 IO 函数进行实际的读写操作;而在异步 IO 模型中,收到信号表示 IO 操作已经完成,不需要再在用户线程中调用 IO 函数进行实际的读写操作。
:::info
注意,异步 IO 是需要操作系统的底层支持,在 Java 7 中,提供了 Asynchronous IO。
:::
NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统 IO 基于字节流和字符流进行操作,而** NIO 基于 Channel 和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区****中,或者从缓冲区写入到通道中**。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
NIO 和传统 IO 之间第一个最大的区别是,IO 是面向流的,NIO 是面向缓冲区的
**Channel **Channel 和 IO 中的 Stream(流)是差不多一个等级的。只不过 Stream 是单向的,譬如:InputStream, OutputStream,而 Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。
1. FileChannel --> IO
2. DatagramChannel --> udp
3. SocketChannel --> tcp(server)
4. ServerSocketChannel --> tcp(client)
Buffer 缓冲区,实际上是一个容器,是一个连续数组.
Selector 类是 NIO 的核心类,Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理
12. OSGI( ( 动态模型系统 )
OSGi(Open Service Gateway Initiative),是面向 Java 的动态模型系统,是 Java 动态化模块化系
统的一系列规范。
动态改变构造 OSGi 服务平台提供在多种网络设备上无需重启的动态改变构造的功能
模块化编程与热插拔
13. 深拷贝和浅拷贝
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
浅复制:仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出的对象也会相应的改变。
深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。
14. 创建对象的几种方式:
- new 调用了构造函数
- Class的 newInstance方法 调用了构造函数
- Constructor类的newInstance方法 调用了构造函数
- clone方法 没有调用构造函数
- 反序列化 没有调用构造函数
对象创建主要流程
虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,
必须先执行相应的类加载。类加载通过后,接下来分配内存。若Java堆中内存是绝对规整的,使用“指针碰撞“方式分配内存;如果不是规整的,就从空闲列表中分配,叫做”空闲列表“方式。划分内存时还需要考虑一个问题-并发,也有两种方式: CAS同步处理,或者本地线程分配缓冲(Thread Local AllocationBuffer,** TLAB**)。然后内存空间初始化操作,接着是做一些必要的对象设置(元信息、哈希码…),最后执行方法通过-XX:+/-UserTLAB参数来设定虚拟机是否使用TLAB。
对象访问定位:
- 指针: 指向对象,代表一个对象在内存中的起始地址。
- 句柄: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是
指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址
内存泄漏:
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,
Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。
但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,
尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景
Minor GC和MajorGC
- Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;
- Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。
:::info
**
什么是card table【卡表】**:空间换时间(类似bitmap),能够避免扫描⽼年代的所有对应进⽽顺利进⾏
Minor GC
(案例:⽼年代对象持有年轻代对象引⽤)
堆内存占⽐:年轻代占堆内存1/3,⽼年代占堆内存2/3。Eden区占年轻代8/10,Survivor区占年轻代
2/10(其中From 和To 各站1/10)
:::
内存结构和内存模型
- Java**内存模型(JMM Java Memory Model)是跟「并发」**相关的,它是为了屏蔽底层细节⽽提出的规范,希望在上层(Java层⾯上)在操作内存时在不同的平台上也有相同的效果
- Java内存结构(⼜称为运⾏时数据区域),它描述着当我们的class⽂件加载⾄虚拟机后,**各个分区的「逻辑结构」**是如何的,每个分区承担着什么作⽤。
本文存在部分引用借鉴,仅用于个人笔记,谢谢!
🤗 您的点赞和转发是对我最大的支持。写在最后!