程序计数器
程序计数器英文名叫:Program Counter Register
作用:用来记录下一条jvm指令的地址行号。
先来查看一段jvm指令,这些指令对应的java代码就是输出1-5
操作系统运行该Java程序时具体流程如下
语言解释:源文件通过编译转化为二进制字节码文件后,解释器会获取第一行jvm指令后解释为机器码供CPU执行,与此同时程序计数器记录下一条jvm指令的行号,随后解释器从程序计数器中获取应该解释哪条jvm指令供CPU执行,由于程序计数器需要频繁的更新jvm行号,为了保证响应速度,jvm在设计时采用寄存器实现程序计数器。
特点:
- 线程私有(即当CPU在执行线程1过程中去执行线程2,程序计数器会保留线程1的下一条指令的地址行号,等到线程1恢复执行时接着下一条指令接着执行。每个线程都有自己的程序计数器)
- 是JVM唯一一个不会存在内存溢出问题区域。
虚拟机栈
每个线程所需要的内存空间我们使用虚拟机栈来表示(早期也叫Java栈),每个栈由栈帧组成,栈帧对应的是方法调用时所需的内存,每个线程只存在一个活动栈帧,即正在执行的方法
接下来,我们结合代码理解虚拟机栈
在上述代码中,当我们执行到method2方法时,栈帧一共存在3个分别是main方法的栈帧,method1的栈帧与method2的栈帧,并且method2的栈帧在栈的顶部
栈问题
垃圾回收是否涉及栈内存
垃圾回收器针对的是堆内存中无用的对象,而栈内存对应的是调用方法所需要的内存,而方法执行结束后会自动释放内存,因此垃圾回收并不需要管理栈内存。
栈内存分配越大越好吗
并不是。电脑的内存大小是固定的,栈内存分配越多可能会导致线程数量减少。比如一个内存为500M的电脑,栈内存分配为1M,则理论说该电脑可以存在500个线程,如果栈内存分配为2M,则理论线程就为250个。
方法的局部变量是否线程安全
对于变量是否是线程安全,主要是看该变量是共享的还是私有的。
本地方法栈
与栈内存相似,不过提供的不是线程所需内存而是给本地方法提供内存(本地方法指的是通过java调用其他语言编写的方法)
堆
通过new创建出来的对象都存储在堆内存中
特点:堆内存中的变量是线程共享的。具有垃圾回收机制
堆问题
堆内存溢出
虽然堆存在垃圾回收机制,但是垃圾回收机制只能回收不再使用的对象,如果对象正在被使用是不会被回收的,由于堆内存默认大小为4G,当我们的程序在短时间内的执行可能不会暴露堆内存溢出的情况,这时我们可以设置堆内存小一些进行测试(命令为-Xmx[指定大小])。
堆内存诊断
- jps 工具
查看当前系统中有哪些 java 进程 - jmap 工具
查看堆内存占用情况 jmap - heap 进程id - jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
方法区
线程共享区域,用来存储类的结构
方法区可能会存在内存溢出的情况,在实际场景中,spring与mybatis经常会动态的加载一些类,因此当程序运行期间创建了足够多的类可能会导致方法区内存溢出(JDK1.8之前叫做永久代内存溢出,JDK1.8之后叫做源空间内存溢出)
常量池
首先查看一个示例代码,是一个最基本的入门案例
public class Demo1 {
public static void main(String[] args) {
System.out.println("hello world");
}
}
下面我们通过对该类的编译后的class文件进行反编译得到二进制字节码文件
javap -v Demo1.class
在二进制字节码文件中,存储了类的基本信息,常量池,类方法定义,包含了虚拟机指令
类的基本信息如下
常量池信息如下(只粘贴了一部分)
类方法定义信息如下
而虚拟机指令存储在类的方法定义中
在上面截图中,需要具体获取哪些静态变量加载哪些参数都需要从常量池中获取,而在常量池中的哪些位置需要从后面的#号中获取,就以该例子为例。首先是需要获取#2中的数据,那么我们去常量池中找该行的信息
运行时常量池
常量池是*.class文件中的,当该类被加载时,它的常量池信息就会被放入运行时常量池,并把里面的符号地址变为真实地址(简而言之就是在运行时会将常量池中的#1,#2转化为内存中的真实地址,而不是#1#2)