1. 程序计数器
2. 虚拟机栈
3. 本地方法栈
4. 堆
5. 方法区
1. 程序计数器
1.1定义
Program Counter Register 程序计数器(寄存器) 物理上是通过寄存器实现的
作用
● 记住下一条jvm指令的执行地址
特点
● 是线程私有的,每个线程都有自己的程序计数器
● 不会存在内存溢出
1.2作用
例如下面代码:getstatic指令交给解释器,解释器将其变成机器码,再交给CPU运行。与此同时,把下一条指令的地址(这里是astore_1的地址3),放入程序计数器。第一条指令执行完以后,解释器从程序计数器里根据3取到下一条指令,再重复刚才的过程。这就是程序计数器 记住下一条jvm指令的执行地址。
jvm指令 #数字(表示引用常量池中的#数字项) java源代码
(即下面一行行的代码)
(前面的数字可以看作是指令的执行地址)
0: getstatic #20 // PrintStream out = System.out;
3: astore_1 // --
4: aload_1 // out.println(1);
5: iconst_1 // --
6: invokevirtual #26 // --
9: aload_1 // out.println(2);
10: iconst_2 // --
11: invokevirtual #26 // --
14: aload_1 // out.println(3);
15: iconst_3 // --
16: invokevirtual #26 // --
19: aload_1 // out.println(4);
20: iconst_4 // --
21: invokevirtual #26 // --
24: aload_1 // out.println(5);
25: iconst_5 // --
26: invokevirtual #26 // --
29: return
第一条jvm指令执行:
第二条jvm指令执行:
解释器把每条指令解释成机器码,机器码再到CPU上运行(CPU只认识机器码。)
2.虚拟机栈
2.1 定义
Java Virtual Machine Stacks (Java 虚拟机栈)
■ 每个线程运行时所需要的内存,称为虚拟机栈
■ 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
■ 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法(栈顶部的方法)
问题辨析
1. 垃圾回收是否涉及栈内存?
不涉及,栈帧内存在每一次方法调用完成后,都会弹出栈,被自动回收掉。
2. 栈内存分配越大越好吗?
不是,栈的内存越大,线程数越少。如物理内存大小一定,若一个线程用1MB内存,总共的物理内存有500MB,理论上有500个线程可以同时运行。如果为每个线程的栈内存设置为2MB内存,则理论上只能有250个线程同时运行。
栈内存大了,只是可以进行更多次的方法递归调用。
3. 方法内的局部变量是否线程安全?
■ 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的(线程私有)
■ 如果是局部变量引用了对象,并逃离方法的作用范围(作为返回值返回),需要考虑线程安全(多个线程共享)
/**
* 局部变量的线程安全问题
*/
public class Demo1_17 {
public static void main(String[] args) {
//主线程里创建了一个对象,并进行操作。
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
//创建新线程,将同一个对象传给了m2方法
//则两个线程使用的是同一个对象,对象不再是线程私有因此m2是非线程安全的
new Thread(()->{
m2(sb);
}).start();
}
//线程安全
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
//非线程安全
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
//非线程安全,该方法把局部对象返回了,其他线程可能拿到该对象进行并发修改
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
2.2 栈内存溢出
● 栈帧过多导致栈内存溢出
1.如递归调用没有出口,一直进行方法调用,产生栈帧。
/**
* 演示栈内存溢出 java.lang.StackOverflowError
* -Xss256k 指定占内存大小为256KB
*/
public class Demo1_2 {
private static int count;
public static void main(String[] args) {
try {
method1();
} catch (Throwable e) {
e.printStackTrace();
System.out.println(count);
}
}
private static void method1() {
count++;
method1();
}
}
2.第三方库导致的栈内存溢出
package cn.itcast.jvm.t1.stack;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.List;
/**
* json 数据转换
*/
//职员和部门两个类的循环引用,导致的栈内存溢出
public class Demo1_19 {
public static void main(String[] args) throws JsonProcessingException {
Dept d = new Dept();
d.setName("Market");
Emp e1 = new Emp();
e1.setName("zhang");
e1.setDept(d);
Emp e2 = new Emp();
e2.setName("li");
e2.setDept(d);
d.setEmps(Arrays.asList(e1, e2));
// { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(d));
}
}
class Emp {
private String name;
//转换员工的时候,遇到部门属性就不转了。转换的员工不包含该属性
@JsonIgnore
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> emps;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Emp> getEmps() {
return emps;
}
public void setEmps(List<Emp> emps) {
this.emps = emps;
}
}
● 栈帧过大导致栈内存溢出
2.3 线程运行诊断
案例1:cpu占用过多
package cn.itcast.jvm.t1.stack;
/**
* 演示 cpu 占用过高
*/
public class Demo1_16 {
public static void main(String[] args) {
new Thread(null, () -> {
System.out.println("1...");
while(true) {
}
}, "thread1").start();
new Thread(null, () -> {
System.out.println("2...");
try {
Thread.sleep(1000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2").start();
new Thread(null, () -> {
System.out.println("3...");
try {
Thread.sleep(1000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread3").start();
}
}
定位
■ 用top命令定位哪个进程对cpu的占用过高
■ ps H -eo pid,tid,%cpu | grep 进程id(用ps命令进一步定位是哪个线程引起的cpu占用过高)
■ jstack 进程id(有问题的进程)
○ 可以根据进程id找到有问题的线程,进一步定位到问题代码的源码行号
案例2:程序运行很长时间没有结果
可能产生死锁
package cn.itcast.jvm.t1.stack;
/**
* 演示线程死锁
*/
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();
}
}
3. 本地方法栈
作用:
给本地方法的执行提供内存空间。
本地方法:
就是不是由java代码编写的代码,使用C或C++编写的本地方法与操作系统底层打交道,本地方法使用的内存就是本地方法栈。本地方法如Object的clone方法等。