引言
程序计数器
程序计数器(Program Counter Register,寄存器),Java源代码运行的第一步就是将源码编译成JVM指令,每一条指令都有对应的执行地址,JVM通过程序计数器来记住每条指令的下一个执行地址,这样就实现的程序代码的执行。
作用:是记住下一条jvm指令的执行地址
特点:
- 线程私有,每一个线程都有自己的一个程序计数器
- 不会存在线程溢出
Java虚拟机栈
定义
Java虚拟机栈(Java Virtual Machine Stacks)
- 每个虚拟机运行时需要的内存,成为虚拟机栈。
- 每个站有多个栈帧(Frame)组成,对应着诶此方法调用时所占用的内存。
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
- 指令-xss是可以调整栈的大小的
判断方法内的局部变量是否是线程安全的?
- 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。
栈内存溢出
- 栈帧过多引起栈内存溢出,比如递归过程中没有比较好的跳出递归条件,导致无限递归,此时必然会导致栈内存溢出。
- 栈帧过大导致栈内存溢出
线程运行诊断
1.CPU占用过高时
- 可以用top命令可以找到那个进程对cpu的占用过高,
- ps H -eo pid,tid,%cpu | grep 进程id 可以定位到该进程,查询出该进程中哪一个线程CPU占用过高
- jstack 进程号可以看到该进程的整个线程
2.程序运行很长时间没有结果
jstack命令可以看到死锁信息,代码中第一个线程给对象a加了锁,然后休眠,休眠之后在进行锁住b,但是第二个进程已经先把b锁住了,再打算锁住a,此时需要等待第一个线程解锁a。第一个线程现在又要来锁b,需要等待第二个进程解锁b,此时第一个第二个线程互相等待,进入死锁状态。
class A{}; class B{}; public class Demo1_3 { static A a = new A(); static B b = new B(); public static void main(String[] args) throws InterruptedException { new Thread(()->{ synchronized (a) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b) { System.out.println("我获得了 a 和 b"); } } }).start(); Thread.sleep(1000); new Thread(()->{ synchronized (b) { synchronized (a) { System.out.println("我获得了 a 和 b"); } } }).start(); } }
本地方法栈
先解释一下本地方法,简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。本地方法栈就是起到为本地方法提供内存空间的作用。
堆
定义
堆(Heap),通过new关键字来创建,创建对象时都会使用堆内存。
特点:
- 线程共享,堆中对象都需要考虑线程安全问题。
- 有垃圾回收机制
- -Xmx指令可以调整堆空间大小
堆内存溢出
public class Demo1_5 { public static void main(String[] args) { int i = 0; try { List<String> list = new ArrayList<>(); String a = "hello"; while (true) { list.add(a); // hello, hellohello, hellohellohellohello ... a = a + a; // hellohellohellohello i++; } } catch (Throwable e) { e.printStackTrace(); System.out.println(i); } } }
堆内存诊断
jps工具
- 查看当前系统中有哪些Java进程
jmap工具
- 查看堆内存占用情况 jmap -heap 进程id
jconsole工具
- 图形化界面,多功能的监测工具,可以连续监测
方法区
所有JVM线程共享一个方法区
存储和类结构相关的信息,包括成员变量、方法数据、方法函数、构造器和一些特殊方法。
方法区内存溢出
JDK1.8之前回导致永久代内存溢出
JDK1.8之后会导致元空间内存溢出
-XX:MaxMetaspaceSize=8m指令可以设置方法区内存空间,下面代码使用类加载器循环创建一万个类,导致方法区内存溢出。
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码 public static void main(String[] args) { int j = 0; try { Demo1_8 test = new Demo1_8(); for (int i = 0; i < 10000; i++, j++) { // ClassWriter 作用是生成类的二进制字节码 ClassWriter cw = new ClassWriter(0); // 版本号, public, 类名, 包名, 父类, 接口 cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); // 返回 byte[] byte[] code = cw.toByteArray(); // 执行了类的加载 test.defineClass("Class" + i, code, 0, code.length); // Class 对象 } } finally { System.out.println(j); } } }
运行时常量池
二进制字节码包含:类基本信息、常量池、类方法定义,类方法定义中包括了JVM指令
javap工具可以对.class文件进行反编译,-c参数可以显示出类的详细信息
常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
运行时常量池:常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变成真实地址。
直接内存
- 常见于NIO操作时,用于数据缓冲
- 分配回收成本较高,但读写性能高
- 不收JVM内存回收管理
分配和回收原理:
- 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
- ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存