JVM 探究
JVM 的位置
- 在操作系统之上,可以看做是一个在操作系统上运行的软件,上面跑的都是 java 程序
JVM 的体系结构
- 栈用完系统会自动释放,所以不会存在垃圾
类加载器
-
虚拟机自带的加载器
-
启动类(根)加载器
-
BootClassLoader
-
C 语言编写的,无法直接获取
-
所在位置:rt.jar
-
-
扩展类加载器
- ExtClassLoader
- 所在位置:\jre\lib\ext
-
应用程序(系统类)加载器
- AppClassLoader
双亲委派机制
- 类加载器收到类加载的请求
- 将这个请求向上委托给类型加载器去完成,一直向上委托,直到启动类加载器
- 启动类加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通知子加载器进行加载
- 重复步骤3
沙箱安全机制
- 沙箱是一个限制程序运行的环境
基本组件
- 字节码校验器
- 确保 java 类文件遵循 java 语言规范,并不是所有的类文件都会经过字节码校验,比如核心类
- 类装载器
- 防止恶意代码区干涉善意代码 //双亲委派机制
- 守护了被信任的类库边界
- 将代码归入保护域,确定了代码可以进行哪些操作
native
-
凡是带了 native 关键字的,说明已经超出了 java 的作用范围,会去调用底层 C 语言的库
-
进入本地方法栈
-
调用本地方法接口(JNI:Java Native Interface)
-
JNI 的作用:扩展 Java 的使用,融合不同的编程语言为 Java 所用
- Java 在内存区域中专门开辟了一块标记区域:本地方法栈(Native Method Stack),登记 native 方法
- 在最终执行时,通过 JNI 加载本地库中的方法
PC 寄存器
-
程序计数器:Program Counter Register
-
每个线程都有一个程序计数器,是线程私有的,是一个指针,指向方法区中的方法字节码,是一个非常小的内存空间,几乎可以忽略
不计
-
它包含当前正在执行的指令的地址(位置)。当每个指令被获取,程序计数器的存储地址加一。在每个指令被获取之后,程序计数器
指向顺序中的下一个指令
方法区
-
方法区本质是一种规范,并不存在
-
所有定义的方法的信息都保存在该区域,此区域属于共享区域
-
静态变量,常量,类信息(构造方法,接口定义),运行时的常量池存在方法区中,但是实例变量存在推内存中,和方法区无关
栈
-
一种数据结构
-
程序=数据结构+算法
-
栈:先进后出,后进先出
队列:先进先出(FIFO:First Input First Output)
-
栈内存主管程序运行,生命周期和线程同步,线程结束,栈内存被释放
-
栈不存在垃圾回收问题
-
栈满(调用的深度比栈深度深的时候出现)的错误:StackOverflowError
栈内包含
- 8大基本类型
- 对象的引用
- 实例的方法
堆
- 一个 JVM 只有一个堆内存,堆内存的大小是可以调节的
分区
- 99% 的对象都是临时对象
新生代(占堆内存的1/3)
- 对象诞生,成长甚至死亡的地方
- 采用轻 GC 回收
Eden(占新生代8/10)
- 对象被 new 出来的地方
Survivor(占新生代2/10)
- 分为两块,每次只使用一块
老年代(占堆内存的2/3)
- 采用重 GC(Full GC)回收
对象在堆中的生命周期
-
new 的对象一般在新生代 Eden 区域中,Eden 区域满时,在 被使用的 Survivor 区域 new 对象
-
当 Eden 区域对象数量达到上限时,触发轻 GC,清除掉当前新生代中没有被引用的对象,被引用的对象复制进未被使用的 Survivor
区域
-
对象在 Survivor 区域的初始年龄为1,每经历一次轻 GC(伴随着一次复制算法的执行) 年龄+1,当年龄到达默认值15(可以通过-
XX:MaxTenuringThreshold修改参数)时,将此对象存入老年代
-
当老年代对象数量达到上限时,触发重 GC(Full GC),清除掉当前新生代和老年代中没有被引用的对象
-
当新生代和老年代都达到上限且没有可以清理的对象时,系统报错 OOM
永久区
永久区与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中
这个区域是方法区在 JVM 中的实现
这个区域常驻内存,用来存放 JDK 自身携带的 Class 对象,Interface 元数据,存储的是 Java 运行时的一些环境或类信息
JDK1.7 及以前这个区域存在垃圾回收
JDK1.8 及以后这个区域不存在垃圾回收
关闭 JVM 虚拟机就会释放这个区域的内存
- JDK1.6 之前:永久代,常量池在方法区
- JDK1.7:永久代,常量池在堆中
- JDK1.8:永久代变为元空间,常量池在元空间
内存调优
- 默认情况下,初始化的内存是电脑内存的 1/64 ,分配的最大内存是电脑内存的 1/4
OOM 问题解决
-
尝试修改参数扩大堆内存看结果
不报错,说明 OOM 是由内存不够引起的
报错,进行下一步
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
-XX:+PrintGCDetails 打印GC清理的垃圾信息
-
使用内存快照分析工具(MAT 或 Jprofiler)查看错误在哪一行
Jprofiler 的作用
-
分析 Dump 内存文件,快速定位内存泄漏
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
-Xms 设置初始化内存分配大小
-Xmx 设置最大分配内存
-XX:+HeapDumpOnOutOfMemoryError 获得 OOM 相关的 Dump 内存文件
-
获得堆中的数据
-
获得大的对象
-
…
-
GC:垃圾回收
- GC 的作用区域只有方法区和堆
GC 分类
-
轻 GC(Minor GC)
-
重 GC(Full GC)
-
JVM 在进行 GC 时,并不是对 Eden 区,Survivor 区域,老年代甚至 JDK1.7 之前 永久代的统一回收。大部分时候,回收的都是 Eden
区
判断是否可回收
引用计数法
-
每个对象中会添加一个计数器,当对象每新加一个引用时,计数器加1;当每有一个该对象的引用失效时,计数器减1,当计数器为0
时代表对象没有被引用可以被回收
-
计数器本身也会被消耗
-
缺点
- 需要额外的内存来存放计数器
- 运行期间需要维护计数器,带来额外开销
- 无法解决循环引用问题
可达性分析法(主流)
通过一系列被称为「GC Roots」的根对象作为起始节点集,从这些节点开始,通过引用关系向下搜寻,搜寻走过的路径称为「引用链」,如果某个对象到GC Roots没有任何引用链相连,就说明该对象不可达,即可以被回收。
- Object1,2,3 之间存在引用,且 GC ROOTS 可达,因此不会被回收
- Object4,5,6 之间存在引用,但由于 GC ROOTS 不可达,因此会被回收
- GC ROOTS 就是对象
- 可以作为 GC ROOTS 的对象
- 方法区静态属性引用的对象
- 方法区常量池引用的对象
- 方法栈中栈本地变量表引用的对象
- JNI 本地方法栈中引用的对象
- 获得同步锁的对象
GC 的算法
复制算法
复制算法最佳使用场景:对象存活率较低的时候,新生代
-
当 Eden 区域第一次发生轻 GC 时,存活的对象复制进未被使用的 Survivor 区域,清理 Eden 区域,该 Survivor 区域变为被使用的
Survivor 区域
-
当再次发生轻 GC 时,将 Eden 区域存活的对象复制进未被使用的 Survivor 区域,将被使用的 Survivor 区域中存活的对象复制到未
被使用的 Survivor 区域中,清理 Eden 区域和被使用的 Survivor 区域,该 Survivor 区域变为被使用的Survivor 区域
-
被使用的 Survivor 区域被称为 From Survivor 区域,未被使用的 Survivor 区域被称为 To Survivor 区域,To Survivor 区域始终为空
优点
- 没有内存碎片
缺点
- 浪费了内存空间(一半的 Survivor 区域)
标记清除法
标记:扫描内存中的对象,对所有被引用的对象进行标记
清除:扫描内存中的对象,对没有标记的对象进行清除
优点
- 不需要额外的空间
缺点
- 两次扫描,严重浪费时间
- 会产生大量内存碎片
标记整理(压缩)法
标记清除法的优化
标记:扫描内存中的对象,对所有被引用的对象进行标记
清除:扫描内存中的对象,对没有标记的对象进行清除
压缩:将所有存货的对象压缩到内存的一端,按顺序排放,之后清理边界外所有空间
优点
- 不需要额外的空间
- 没有内存碎片产生
缺点
- 效率低
三种算法的比较
复制算法 | 标记清除法 | 标记整理(压缩)法 | |
---|---|---|---|
算法效率(时间复杂度) | 高 | 中 | 低 |
内存整齐度 | 高 | 低 | 高 |
内存利用率 | 低 | 高 | 高 |
哪种算法最好
没有最好的算法,只有适合的算法
GC:分代收集算法
新生代
- 存活率低,复制算法
老年代
- 区域大,存活率高
- 标记清除法(内存碎片不是太多)+ 标记整理(压缩)法混合实现