程序计数器
Program Counter Register——程序计数器,在字节码执行过程中,记录下一条jvm指令执行的地址
特点
- 程序计数器是线程私有的:每个线程都独立执行自己的程序,所以这些线程需要自己的程序计数器来存储自己的程序的下一条指令地址
- 不会存在内存溢出:JVM规范指定了程序计数器是没有内存溢出的,程序计数器存储的东西大小基本固定,不会出现内存溢出
虚拟机栈
Java Vitual Machine Stacks——Java虚拟机栈
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈有多个栈帧组成,一个栈帧对应一个方法执行占用的内存
- 每个线程只能有一个活动栈帧,对应当前正在执行的方法
案例
public class Main {
public static void main(String[] args) {
A();
B();
C();
}
private static void A(){
}
private static void B(){
A();
}
private static void C(){
A();
}
}
问题辨析
1. 垃圾回收是否设计栈内存?
垃圾回收不涉及栈内存,因为栈内存对应者方法的执行,当一个方法执行结束后,会从栈中pop,不会出现多余的方法在栈中,所以不需要回收
2. 栈内存分配越大越好吗?
栈内存不是越大越好,栈内存越大,对应一个线程的内存占用就越大,线程数量就会减少,栈内存划分过大会导致并发场景下的性能问题
3. 方法内的局部变量是否是线程安全的?
只存在于方法内部的局部变量是线程安全的,栈内存是线程私有的,不同线程都有自己的方法栈,对局部变量的修改不会影响其他线程
/**
* 线程安全
*/
public static void a(){
StringBuilder sb = new StringBuilder();
sb.append(1).append(2).append(3);
System.out.println(sb);
}
/**
* 线程不安全,sb对象是方法外部的对象,其他线程可能对这个外部对象存在访问
* @param sb
*/
public static void b(StringBuilder sb){
sb.append(1).append(2).append(3);
System.out.println(sb);
}
/**
* 线程不安全,sb对象作为返回值返回了,其他线程可能对这个对象进行访问
* @return
*/
public static StringBuilder c(){
StringBuilder sb = new StringBuilder();
sb.append(1).append(2).append(3);
System.out.println(sb);
return sb;
}
判断局部变量是否线程安全,需要保证这个局部变量没有逃离这个方法,对于逃离方法的局部变量存在被其他线程访问的可能性
栈内存溢出
- 栈帧过多导致内存溢出
- 栈真内存过大导致内存溢出(开发过程中不常见)
栈帧过多导致栈内存溢出
由于方法的递归调用,不断有方法入栈而没有方法出栈,所以最终会出现栈内存溢出
开发中可能出现的stackOverflowError
public class Main {
public static void main(String[] args) throws JsonProcessingException {
Emp e1 = new Emp();
e1.setName("张三");
Emp e2 = new Emp();
e2.setName("李四");
Dept dept = new Dept();
e1.setDept(dept);
e2.setDept(dept);
List<Emp> list = new ArrayList<>();
list.add(e1);
list.add(e2);
dept.setEmpList(list);
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(dept));
}
}
class Emp{
private String name;
private Dept dept;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Dept getDept() {
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
}
class Dept{
private String name;
private List<Emp> empList;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Emp> getEmpList() {
return empList;
}
public void setEmpList(List<Emp> empList) {
this.empList = empList;
}
}
由于循环依赖,导致在json转换时不断递归转换,最终栈溢出
线程运行诊断
cpu占用过多
- 用top定位哪个进程对cpu占用过高
- ps H -eo pid,tid,%cpu | grep 进程id(使用ps命令进一步定位哪个线程引起cpu占用过高)
- jstack 进程id,来找到对应的问题代码
程序运行长时间没有结果(死锁检测)
使用jstack工具,在最后可以找到Found one Java-level deadlock
,查到出现死锁位置
本地方法栈
本地方法栈和虚拟机栈基本一致,区别在于,JVM栈用于为Java方法提供内存,而本地方法栈,是通过本地接口调用的方法(如C、C++方法)占有的内存
例如:Object.wait等方法,就有native修饰,该方法会调用本地的方法接口
堆
通过new关键字创建的对象都会使用堆内存,它是线程共享的,需要垃圾回收
内存结构
- 新生代:存放新生的对象,分为三个区域:Eden、from_survivor、to_survivor;Eden中存放最新创建的对象,survivor作为新生代和老年代间的缓冲区,经过垃圾回收剩余的对象会存留在survivor中
- 老年代:存放生命周期长的对象,对于新生代中经过多次垃圾回收的对象,会晋升到老年代;新生代中由于内存不足也会将一部分对象直接晋升到老年代
堆内存溢出
通过-Xmx8m虚拟机参数,将堆内存改为8m,然后不断通过字符串拼接创建新的字符串对象,导致堆内存溢出
堆内存诊断
- jps工具:查看当前系统中有哪些Java进程
- jmap工具:查看堆内存占用情况,通过jmap -heap 进程id
- jconsole工具:拥有图形界面的多功能的检测工具,可以连续监测
jmap诊断堆内存(堆内存-Xmx15m)
public class Main {
public static void main(String[] args){
Scanner in = new Scanner(System.in);
while(1!=in.nextInt()){
}
//占用10m的堆空间
byte[] arr = new byte[1024*1024*10];
while(2!=in.nextInt()){
}
arr = null;
while(3!= in.nextInt()){
}
System.gc();
while(4!=in.nextInt()){
}
}
}
通过jps查看运行的Java进程,jps
查看最初的堆内存情况,jmap -heap 12608
控制台输入1后,创建一个占用10m内存空间的数组
控制台输入2后,将对象的引用置空
控制台输入3后,触发垃圾回收
jconsole诊断堆内存(堆内存-Xmx20m)
通过jps获取进程id,然后通过jconsole 进程id
打开jconsole图形界面
输入1,占用10m堆内存
输入2,将引用置空
输入3,触发垃圾回收
jconsole除了检测堆内存,还可以查看线程、cpu等各项指标
jvisualvm
通过jvisualvm可以进一步定位到具体占用大的对象
方法区
JVM规范定义了方法区存储和类相关的一些数据,如类本身、类加载器、常量池等
- 在jdk1.6,方法区的实现是永久代;永久代的常量池中包括了字符串常量池StringTable
- 在jdk1.8,方法区的实现是元空间,元空间中,没有了StringTable,字符串常量池分到了堆中
方法区内存溢出
- 1.8以前会导致永久代内存溢出:java.lang.OutOfMemoryError:PermGen space;通过-XX:MaxPermSize来指定永久代大小
- 1.8以后会导致元空间内存溢出:java.lang.OutOfMemoryError:Metaspace;通过-XX:MaxMetaspaceSize来指定元空间大小
元空间内存溢出实例
通过-XX:MaxMetaspaceSize=10m
来指定元空间大小
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/***
* @author shaofan
* @Description
*/
public class Main extends ClassLoader{
public static void main(String[] args){
int j = 0;
try{
Main main = new Main();
for (int i = 0; i < 10000; i++,j++) {
//cw用来生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
//版本号、访问控制、类名、包名、父类、接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
byte[] code = cw.toByteArray();
//加载类
main.defineClass("Class"+i,code,0,code.length);
}
}finally {
System.out.println(j);
}
}
}
在3331个类的创建后,出现了元空间的内存溢出
运行过程中的方法区内存溢出
- 平时开发过程中,基本不会自己手动加载类;但是在spring、myabtis等框架中都用到了cglib库
- cglib通过字节码技术,在运行时创建被代理对象的子类,会导致方法区内存溢出
常量池
反编译查看二进制字节码
二进制字节码包含:类基本信息、常量池、类方法定义、虚拟机指令
通过javap -v class文件
来反编译字节码文件并显示详细信息
类基本信息
常量池
类方法和方法内部的虚拟机指令
工作流程
运行时常量池
- 常量池,就是一张表,jvm指令根据这些常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是class文件中的,当该类被加载,他的常量池就会放入运行时常量池,并将里面的符号地址(如#1、#2等)变为真是的物理地址