JVM探究
这篇JVM笔记是看视频整理出来的,所以并不是特别详细透彻。想更深入了解的话推荐学习《深入理解Java虚拟机这本书》
1、 常见的jvm面试题
- 请你谈谈你对 JVM 的理解?java8 虚拟机和之前的变化更新?
- 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?
- JVM的常用调优参数有哪些?
- 内存快照如何抓取?怎么分析Dump文件?
- 谈谈 JVM 中,类加载器你的认识。
2、 知识点
JVM的位置 :操作系统之上
JVM体系结构
自己画的 jvm 简图:
详细的 jvm 内存图:
- 类加载器
类加载过程简图:
类加载器有四类:等级依次递减
- 虚拟机自带的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
双亲委派机制 当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
作用:
- 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
- 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
- Native 凡是带了 native 关键字的,索命 java 的作用范围达不到了,会去调用底层C语言的库,大致流程为:
- 执行到声明为 native 的方法时会进入 本地方法栈
- 调用本地方法接口,即 JNI (java native interface)
- 在最终执行的时候,通过 JNI 加载本地方法库中的方法
程序寄存器:Program Counter Register 每个线程都有一个pc,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,即将要执行的指令代码),让执行引擎读取下一条指令。 是一个非常小的空间,几乎可以忽略不计
方法区:Method Area 方法区是被所有线程共享。所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。 静态变量、常量、类信息(构造方法,接口定义)、运行时的常量池存在方法区中,但是 实例变量存在堆内存中,和方法区无关
栈: 主管程序的运行,生命周期和线程同步,即线程结束,对应的栈内存释放。对于栈来说,不存在垃圾回收问题 栈中存放8大基本类型 + 对象的引用 + 实例的方法
- 三种JVM:了解即可
- Sun公司:HotSpot
- BEA : JRockit
- IBM:J9vm
堆:Heap 一个JVM只有一个堆内存,堆内存的大小是可以调节的 类加载器读取了类文件后,会将我们所有引用类型的真实对象放入堆中。 堆还要细分为三个区:新生区、老年区、永久区。GC垃圾回收,主要在新生区和老年区 在JDK8以后,永久区改了个名字:元空间
- 新生区:对象诞生,成长的地方,也有可能是死亡的地方;研究表明:99%的对象都是临时对象。 新生区还可以分为三个区域:
- 伊甸园区:所有对象都是在伊甸园区 new 出来的
- 幸存区者(0,1),经历过轻GC在伊甸园区活下来的对象将进入幸存者区
老年区:在幸存者区经历过重GC活下来的将进入老年区
- 永久区:这个区域常驻内存。用来存放JDK自身携带的Class对象、Interface元数据。即存放的是java运行时的一些环境或类信息。这个区域不存在GC!关闭JVM就会释放这个区域的内存,该区域经历过几次变迁:
- JDK1.7以前:称为永久代,常量池在方法区
- JDK1.7:永久代,但是慢慢退化了,即
去永久代
,常量池在堆中 - JDK1.7之后:无永久代,改为元空间,并将常量池放入元空间中。
- 堆内存调优:默认情况下,分配的总内存是电脑内存的1/4,而初始化内存是分配总内存的1/16。 常用调优命令:
-Xms256m -Xmx1024m -XX:+PrintGCDetails
,改变分配总内存和初始化总内存的大小,并输出GC信息。 若碰见 OOM (OutOfMemoryError),解决思路为:- 尝试扩大堆内存
- 分析内存,看一下是哪个地方出现了问题(专业工具:eclipse自带MAT,还有很好用的 Jprofiler)
MAT,Jprofiler 作用:
- 分析 Dump 文件,快速定位内存泄漏 (-XX:+HeapDumpOnOutOfMemoryError)
- 获得堆中的数据
- 获得大的对象
- ….
- GC (常用算法)
- JMM:java内存模型JMM理解整理
3、 GC
JVM 在进行GC时,并不是对这三个区域(新生代,辛存区(from区,to区),老年区)统一回收,大部分时候,回收的都是新生代。
GC有两类:轻GC和重GC
GC常用算法: 标记清除法,标记压缩法,复制算法,引用计数法
3.1、 引用计数法(主流编程语言GC中,python使用的是此方法)
3.2、 复制算法(新生区常用算法)
- (复制过程)每次GC,都会将Eden区活的对象移到幸存区中:一旦Eden区被GC后,就是空的
- 幸存区中的to区和from区不是固定的:哪个区为空,那这个就为to区
- 当一个对象经历了15次GC(JVM默认值,可以修改)还没有死亡,那么对象进入老年区
好处: 没有内存碎片 坏处:浪费了一半的内存空间(to区永远是空的) 所以复制算法最佳使用场景为:对象存活度较低的时候,即新生区
3.3、标记清除算法
- 优点:不需要额外的空间
- 缺点:两次扫描(第一次标记,第二次清除),严重浪费时间,会产生内部碎片
3.4、标记清除压缩算法
- 即在标记清除算法的基础上,多了一次压缩操作,将存活下来的对象都放在一起,消除了内部碎片。
3.5、 总结
- 内存效率(时间复杂度):复制算法 > 标记清除算法 > 标记压缩算法
- 内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
- 内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
没有最优的算法,只有最合适的算法 –> GC:分代收集算法 即: 年轻代:存活率低 -> 复制算法 老年代:存活率高 -> 标记清除算法 + 标记压缩算法 混合实现
文档信息
- 本文作者:MikasaLee
- 本文链接:/2020/08/01/JVM/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)