day1_内存区域


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(类加载器)、常量池
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值