文章目录
- 1 程序计数器
- 2 虚拟机栈(JVM 栈)
- 2.1 基本概念以及演示
- 2.2 栈内存溢出的情况
- 2. 3 线程排查
- 3 本地方法栈
- 4 堆
- 4.1 堆内存溢出以及诊断
- 5 方法区
JVM的内存区域,主要分为了5个部分: 方法区, 堆, 程序计数器, 虚拟机栈,本地方法栈。其中程序计数器,虚拟机栈和本地方法栈是线程私有的。
1 程序计数器
程序计数器中存放的是下一条执行指令的地址。程序计数器的特点是:
- 是线程私有的
- 不会出现内存溢出的情况(而栈,堆则会出现内存溢出)。
2 虚拟机栈(JVM 栈)
2.1 基本概念以及演示
- 虚拟机栈:每个线程所需要的内存。而这个栈内存,我们可以通过参数
-Xss
来设置的,在Linux中默认好像是1MB,这个值可以在IDEA中设置,如下所示:
但是并不是说分配到的栈内存越大约好的,因为物理内存是固定的,那么如果栈内存越大,那么导致线程数就变少了。 - 栈帧:每次方法被调用时所需要分配的内存(需要给这个方法的参数,局部变量,返回值分配内存)。当这个方法执行完毕之后,就会将这个栈帧从栈内存中跳出,自动释放这个栈帧占用的内存。
- 活动栈帧: 当前线程正在执行的方法(这个方法位于栈顶)
如下面的代码:
public class Demo1 {
public static void main(String[] args) {
method1();
}
private static void method1() {
method2(2, 3);
}
private static int method2(int a, int b) {
int c = a + b;
return c;
}
}
给method1方法的地方打一个端点,此时进入调试模式,就可以看到对应的栈的情况了:
当我们进入到了method1方法内部,在进入method2方法内部,此时栈中的情况就有了3个栈帧,如下所示:
此时就有了3个栈帧(method2, method1, main),并且当前线程正在执行method2方法,所以method2是一个活动栈帧(位于栈顶)。
当method2执行完毕之后,method2这个栈帧就会从栈中弹出,自动挥手这部分的内存,此时栈中就剩下了method1, main这2个栈帧了,如下所示:
同样的,当method1执行完毕返回之后,method1这个栈帧从栈中弹出,并被回收,而栈中只剩下了main栈帧,当main方法执行完毕之后,main从栈中弹出并被回收。
所以整个过程中栈的变化流程为:
根据上面提到的,当这个方法使用完之后,对应的栈帧就会从栈中弹出,并且自动回收这部分的内存,所以垃圾回收并没有涉及到栈内存,也即是说不会回收栈。
同时需要注意的是,每次方法被调用,都会产生一个新的栈帧,那么这时候方法内的局部变量是否存在一个线程安全的问题?
答案取决与这个局部变量是否逃离了这个方法,所谓的逃离,就是说这个局部变量(是一个引用类型)是否是从方法外部传入的变量,或者说这个局部变量作为方法的返回值返回。
如果局部变量既不是由方法外部传入的,并且没有作为返回值返回,那么是线程安全的,否则如果这个局部变量是由方法外部传入,或者作为返回值返回,并且这个局部变量是一个引用类型,那么此时可能会出现线程安全问题,如下所示:
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
StringBuilder stringBuilder = new StringBuilder();
new Thread(() -> {
method3(stringBuilder);
}).start();
new Thread(()->{
method3(stringBuilder);
}).start();
stringBuilder.append(1);
stringBuilder.append(2);
stringBuilder.append(3);
Thread.sleep(2000);
System.out.println(stringBuilder.toString());
}
private static StringBuilder method4(){
//局部变量作为返回值返回,并且是一个引用类型,所以这个局部变量需要考虑一下线程安全问题
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(4);
stringBuilder.append(5);
return stringBuilder;
}
private static void method3(StringBuilder stringBuilder) {
//这个方法中的参数是从方法外部传入的,并且是是一个引用类型,所以这个局部变量需要考虑线程安全问题
stringBuilder.append(4);
stringBuilder.append(5);
}
}
2.2 栈内存溢出的情况
如果出现了栈内存溢出,那么就会抛出错误StackOverFlowError
。那么导致栈内存溢出的原因主要有:
- 栈帧过多导致的栈内存溢出(这个是常见的)
栈帧过多,也即是方法被调用的多了,并且这些方法并没有执行完,所以不会从栈中跳出,从而导致栈溢出。
例如我们在进入递归的时候,如果这个递归结束的条件没有设置好,就会导致不断进入递归,而无法结束方法调用,此时就会出现了栈内存溢出。如下面的代码所示:
最后的结果如下所示:public class Demo1 { static int count = 0; public static void main(String[] args) { try{ method1(); }catch (Error e){ System.out.println(count); e.printStackTrace(); } } private static void method1() { ++count; method1(); method2(2, 3); } }
另一种可能就是我们将对象以字符串形式输出这个对象信息的时候,对象之间存在循环引用的关系,从而导致StackOverFlowError,如下所示:
public class Demo1 {
public static void main(String[] args) {
//method1();
Employee e1 = new Employee();
e1.setName("a");
Department department = new Department();
department.setName("d_01");
e1.setDepartment(department);
department.setEmployeeList(Arrays.asList(e1));
System.out.println(department);
}
}
class Employee{
private String name;
private Department department;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", department=" + department +
'}';
}
}
class Department{
private String name;
private List<Employee> employeeList;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Employee> getEmployeeList() {
return employeeList;
}
public void setEmployeeList(List<Employee> employeeList) {
this.employeeList = employeeList;
}
@Override
public String toString() {
return "Department{" +
"name='" + name + '\'' +
", employeeList=" + employeeList +
'}';
}
}
正如上面的代码所示: Employee -> Department,而Department中的List<Employee>又依赖于Employee,此时就出现了循环以来的问题,最后打印Department的时候,就会抛出StackOverFlowError
错误了。
- 栈帧过大导致的栈内存溢出.
2. 3 线程排查
如果要排查占用内存最高的线程,我们可以使用linux中top命令进行排查,对应的步骤如下所示:
- 切换成root身份,之后输入
top -c
命令,来查看各个进程占用内存的情况,此时就可以得知占用内存最高的进程id了 - 输入命令
top -pH PID(PID是上面查到的进程ID)
,这样就可以得知这个进程下面的各个线程占用内存的情况,此时可以得知这个进程下面哪个线程tid占用内存最高。 - 输入命令
printf "%x\n" tid
,将上一步中得到的占用内存最高的线程id以十六进制的形式打印出来 - 输入命令
jstack PID | grep xxx
,来查看PID这个进程下面的各个线程的运行情况,同时利用grep xxx,来查找tid为xxx的线程,其中xxx就是上面获得的线程id的十六进制形式(第3步可以得知)
由于死锁而导致程序迟迟无法输出,那么我们同样可以根据上面的步骤,来进行排查,但是由于程序无法迟迟输出,那么可能导致占用的内存并不是很多,所以使用top
命令不会很容易找到我们需要寻找的进程的进程id。
所以我们需要先利用命令ps -ef | grep "xxx.java"
,来寻找我们需要看的进程,从而得到他对应的进程ID。最后通过命令jstack PID
来查看这个进程下面的各个线程的运行状态。
如下面一段死锁的代码:
public class DeadLock {
static LockA lockA = new LockA();
static LockB lockB = new LockB();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lockA){
System.out.println("ThreadA got lockA");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println("ThreadA got lockB");
}
}
}).start();
new Thread(() ->{
synchronized (lockB){
System.out.println("ThreadB got lockB");
synchronized (lockA){
System.out.println("ThreadB got lockA");
}
}
}).start();
}
static class LockA{
}
static class LockB{
}
}
当我们使用linux进行排查的时候,首先利用ps -ef | grep "DeadLock"
,如下所示:
然后我们利用命令jstack 6536
查看这个进程下面的各个线程的运行状况:
3 本地方法栈
4 堆
堆是通过new创建出来的对象,存放到堆中的。所以堆拥有的特点是:
- 线程共享的。和前面3者不同,堆它是线程共享的,所以需要考虑到线程安全的问题
- 有垃圾回收机制。当一个对象不在被使用的时候,那么就会被回收。而虚拟机栈则没有垃圾回收机制,因为每当方法执行完毕之后,对应的栈帧就会从栈中弹出,并且会自动回收这个栈帧对应的内存。
4.1 堆内存溢出以及诊断
尽管堆中存在垃圾回收机制,但是如果线程中的某一个list一直在使用,并且list中的元素数量不断增加,那么当元素的数量达到某一个数字的时候,就会出现堆内存溢出,发生了OutOfMemoryError: Java heap space
,如下面的代码所示:
public class Demo3 {
public static void main(String[] args) {
int i = 0;
try{
List<String> list = new ArrayList<>();
String a = "hello";
while(true){
list.add(a);
a = a + a;
++i;
}
}catch(Error e){
System.out.println(i);
e.printStackTrace();
}
}
}
尽管我们可以通过-Xmx
参数来设置堆内存的大小,在短暂解决堆内存溢出的问题,但这是治标不治本的方法,所以我们需要进行堆内存诊断,常见的手段主要有:
- jps : 通过这个命令,可以查看当前系统的java进程,同时获得对应的进程ID
- jmap: 可以通过命令
jmap -head pid
来查看这个进程的堆内存情况。所以在使用这条命令之前,需要获取对应的进程id,所以需要使用jps来获取进程id. - jconsole: 是一个可视化工具,可以很清楚看到堆内存的运行情况
- jvisualvm: 同样是一个可视化工具,但是比jconsole更加完善,因为可以dump,存储某一个时刻下面的堆内存使用的情况,然后进行分析。
5 方法区
方法区的特点:
- 线程共享的
- 在虚拟机启动的时候就创建了的
- 存储的信息主要有: class(类的信息,例如属性,方法等)、classloader(类加载器)、常量池