JVM是运行java代码的假想的计算机,是运行在操作系统之上的,不与硬件直接交互。
包含了以下
- 字节码指令集
- 寄存器
- 栈
- 垃圾回收
- 堆
- 存储方法域
线程私有
1.程序计数器
当前线程所执行的字节码的行号指示器。
字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
程序计数器的作用:由于jvm的多线程是通过线程轮流切换并分配处理器执行时间方式实现,所以一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。
执行什么方法 | 存放的是什么 |
---|---|
Java方法 | 当前执行的方法时的JVM指令地址 |
Native方法 | (Undefined) |
2.虚拟机栈
描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame),随着方法结束而销毁。
何时确定局部变量空间大小?编译期
局部变量空间除了(64位长度的long和double类型占用2个Slot)其余占用1个Slot
栈帧
存放 | |
---|---|
局部变量表 | 1)编译期可知的基本数据类型 |
2)对象引用 | |
3)returnAddress类型(指向了一条字节码指令的地址) | |
操作栈 | - |
动态链接 | - |
方法出口 | - |
3.本地方法栈
方法 | 不同 |
---|---|
虚拟机栈 | 为JVM执行Java方法(字节码)服务 |
本地方法栈 | 为JVM使用Native方法服务 |
线程共享
4.堆
jvm启动时创建,唯一目的是存放对象实例。
创建的对象和数组都保存在这里,也是垃圾收集器进行垃圾收集的最重要的区域。
由于现在收集器基本都用分代收集算法,所以还分为新生代和老年代。
5.方法区
存放
- 类结构信息
- 常量
- 静态变量
- 方法代码
6.运行时常量池
方法区的一部分。用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
堆中给对象分配内存策略
- 指针碰撞:假设堆中内存时绝对规整的,左边放着用过的内存,中间放着指针作为分界点的指示器,右边是空闲的内存,每次分配堆存就是把指针向空闲那边挪动一段与对象大小相等的距离。
- 空闲列表:堆中内存不规整时,jvm必须维护一个列表,记录那些内存时可用的,分配对象时从列表中找到一块足够大的空间划分给对象实例,并更新列表记录。
选择分配方式是由垃圾收集器是否带有压缩整理功能决定
1.对象的内存布局
布局 | 存储内容 | 用途 |
---|---|---|
对象头 | 哈希码、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间等 | 用于存储对象自身的运行时数据 |
类型指针 | jvm通过这个指针来确定这个对象时那个类的实例 | |
实例数据 | 对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容 | |
对齐填充 | 非必然存在 | 仅仅起着占位符的作用 |
2.对象的访问定位
句柄:
实现:堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址
优点:reference中存储的是稳定的句柄地址,gc发生时很多对象会被移动,但只会改变句柄中的实例数据指针,不会改变reference本身。直接指针:(Sun HotSpot使用这方式)
实现:reference中存储的直接就是对象地址
优点:速度快,节省了一次指针定位的时间开销
内存溢出
OutOfMemoryError
没有空闲内容,并且垃圾收集器也无法提供更多内存。
内存区域出现OOM的地方(程序计数器不出现OOM)
区域 | 原因 |
---|---|
虚拟机栈 | 如果扩展时无法申请到足够的内存 |
本地方法栈 | 和虚拟机栈一样 |
堆 | 可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定 JVM 堆大小或者出现 JVM 处理引用不及时,导致堆积起来,内存无法释放等。 |
方法区老版本 | 因为永久代大小有限,并且JVM对永久代(常量池回收、卸载不再需要的类型)GC不积极,所以不断新添加类型的时候就会出现OOM,类似Intern字符串缓存占用太多空间也会OOM |
直接内存 | NIO技术直接通过Native函数库分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。 |
虚拟机栈和方法栈还会出现StackOverFlowError:不断递归调用,而且没有退出条件时,就会导致不断压栈。
读后感笔记来自周志明的《深入理解Java虚拟机(第2版)》