JVM基础总结
JVM基础总结
1、JVM系统架构图
1.1 JVM的主要组成部分
JVM包含两个子系统:
-
Class loader(类装载)
-
Execution engine(执行引擎)
两个组件:
-
**Runtime data area(运行时数据区):**JVM的内存,包含方法区、堆、虚拟机栈、本地方法栈、程序计数器。
-
**Native Interface(本地接口):**与native libraries交互,是其他编程语言交互的接口。
java程序的运行机制
首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,如果有调用其他语言的部分,可以通过本地库接口(Native Interface)来实现整个程序的功能。
2、类加载器
通过类的权限定名获取该类的二进制字节流的代码块。
类加载器分类
- 启动类加载器(Bootstrap ClassLoader):加载java核心类库。
- 扩展类加载器(Extension ClassLoader):加载java的扩展库。
- 系统内类加载器(System ClassLoader):根据java应用的类路径(CLASSPATH)来加载java类。
双亲委派机制
当一个类加载器收到类加载请求时,它首先不会自己去加载这个类,而是将其委派给父类加载器去完成,每一层都如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,如果此时父类无法完成加载请求,子加载器才会尝试去加载类。
沙箱安全机制
沙箱机制将java代码限定在jvm特定的运行范围内,严格限制代码对本地资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
3、JVM内存(运行时数据区)
java虚拟机内存区域划分
方法区、堆、虚拟机栈、本地方法栈、程序计数器
-
方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据,是被所有线程共享的。
-
堆:java虚拟机中内存最大的一块,是被所有线程共享的,存储对象实例的区域(包括静态对象)。
-
虚拟机栈:用于存储局部变量、操作数栈、动态链接、方法出口等。
-
本地方法栈:调用native服务的。
-
**程序计数器:**当前线程所执行的字节码的行号指示器,记录方法之间的调用和执行情况。用来存储指向下一条指令的地址,也即为执行的代码
堆和栈区别
-
栈使用的数据结构里的栈,先进后出,物理地址分配是连续的。
-
堆在内存中物理地址分配是不连续的,是在运行期确认的,大小不固定。而栈在内存中是连续的,在编译期就确认,大小是固定的。
-
存放的内容不同,堆中存储的是对象实例,栈主要存储局部变量、操作数栈、返回结果。
队列和栈区别
- 队列:先进先出
- 栈:后进先出
内存分配方式
- 指针碰撞:如果内存规整,把指针往空闲空间移动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”
- 空闲列表:如果内存不规整,虚拟机必须维护一个列表记录哪些内存是可用的,在分配是从列表找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”
由java堆是否规整来决定,而java堆是否规整由所采用的垃圾回收器是否带有压缩整理功能决定。
解决对象创建的并发安全问题(保证线程安全方式)
- 同步处理:采用CAS+失败重试,保证更新操作的原子性
- CAS:CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。属于乐观锁的一种实现
- 本地线程分配缓冲:把内存分配的动作按照线程划分在不同的空间中进行,即在每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲
对象实例的访问定位方式
- 句柄:java堆划分一块内存作为句柄池,引用中存储对象的句柄地址。句柄中包含了对象实例数据和对象类型数据各自的具体地址信息。引用中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而引用本身不需要修改。
- 直接指针:引用中存储的直就是对象地址,优点是速度更快,可以节省一次指针定位的时间开销。HopSpot就是采用这种方式
内存泄露
- 内存泄露:不再被使用的对象或变量一直占据在内存中。(长生命周期的对象持有短生命周期对象的引用)
4、垃圾回收器(GC)
在JVM中,有一个垃圾回收线程,它是低优先级的,正常情况下不会执行,只有在虚拟机空闲或当前堆内存不足时,才会自动触发垃圾回收。
回收内存的目的。使java程序员不再需要考虑内存管理的问题,可以有效的防止内存泄露。
垃圾回收:
- 分代复制垃圾回收
- 标记垃圾回收
- 增量垃圾回收
JVM垃圾回收算法(root根搜索方法)
- 标记-清除算法:标记无用对象,然后进行清除回收。实现简单,不需要对象进行移动,但标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。(老年代垃圾回收)
- 复制算法:把内存空间划分为两个相等的区域,每次只使用其中一个区域,垃圾回收时,遍历当前使用区域,把正在使用的对象复制到另一个区域中,最后将当前使用的区域的可回收的对象进行回收。(年轻代垃圾回收)
- 标记-整理算法:结合标记-清除算法和复制算法的优点。第一阶段,从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记对象,并且把存活对象压缩到堆的其中一块,按顺序排放。(老年代垃圾回收)
- 分代收集算法:根据对象的存活周期将内存划分为几块,一般为年轻代、老年代和永久代
JVM中的垃圾回收器
-
新生代:Serial、ParNew、Parallel Scavenge
-
老年代:CMS、Serial Old、Parallel Old
-
整堆回收器:G1
- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效
- ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
- Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
- Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
- G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
分代垃圾回收器工作原理
分代回收器有两个分区:新生代和老年代,新生代默认的空间占1/3,老年代占2/3。新生代使用垃圾回收算法是复制算法,老年代使用标记整理算法,新生代有3个分区:Eden、From Survivor、To Survivor,默认占比8:1:1,执行流程如下:
-
触发Minor GC,把Eden+From Survivor存活的对象放入To Survivor区
-
清空Eden和From Survivor分区
-
From Survivor和To Survivor分区交换。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回收(Full GC),一般使用标记整理的执行算法。
判断对象回收的方法
- 引用计数器法:为每个对象创建一个引用计数,当计数器为0是就可以被回收,引用被释放时计数-1。缺点是不能解决循环引用的问题。
- 可达性分析算法:从GC Roots开始向下搜索,所走过的路径为引用链,如果一个对象实例没有任何引用链,则说明该对象可以被回收。
参考文献
-
Java虚拟机(JVM)面试题(2020最新版):https://thinkwon.blog.****.net/article/details/104390752
-
JAVA中的CAS:https://blog.****.net/weixin_37598682/article/details/81285176