JVM的内存结构(上)——程序计数器、虚拟机栈、本地方法栈

JVM的内存结构(上)——程序计数器、虚拟机栈、本地方法栈,第1张

JVM的内存结构(上)——程序计数器、虚拟机栈、本地方法

文章目录

JVM的内存结构

1、程序计数器(PC Register)2、虚拟机栈(JVM Stacks)

2.1、定义2.2、栈内存溢出2.3、线程运行诊断 3、本地方法栈(Native Method Stacks)

JVM的内存结构 1、程序计数器(PC Register)

作用:记住下一条jvm指令的执行地址

如下图:我们打的每一条java代码,其实都是在底层执行JVM的指令。每条指令都有其对应的地址,前面说过了,解释器的作用就是逐条执行我们的代码(JVM指令),那么,解释器怎么知道下一条要执行的指令是哪条呢?这就要看程序计数器的了。程序计数器一直保留的都是下一条指令的地址。

上图的流程大概就是:我们自己写java源码,然后java源码被虚拟机翻译成字节码,也就是JVM指令;这些JVM的指令都有各自的地址,他们依次将指令的地址传递给程序计数器,程序计数器就把拿到的地址依次给解释器,解释器寻着地址将我们的字节码(JVM指令)翻译成机器码后交给CPU执行。

特点:

1、线程私有(简单说就是,每个线程都有自己的程序计数器)

2、唯一不会出现内存溢出区域

2、虚拟机栈(JVM Stacks) 2.1、定义

虚拟机栈,是线程运行需要的内存空间。我们java的源码最后其实就是一个个封装起来的方法。每个方法,我们将他们当成一个栈帧(栈帧对应着每次方法调用时所占用的内存)。

每次调用某个方法,我们就把该方法作为一个栈帧(栈中的一个基本单位)放入栈中,如果该方法在执行过程中还调用了其他的方法,那么这些方法也会作为栈帧依次被放入我们的栈中。

如下图:栈帧1、2、3分别对应三个方法。栈帧1调用了栈帧2,栈帧2调用了栈帧3。此时,栈帧3肯定会先运行完,此时栈帧3先运行结束并d出,然后轮到栈帧2运行结束d出,最后是栈帧1运行结束后d出。

注意:每个线程只能有一个活动栈帧,对应的是当前正在执行的方法。

代码演示:

main方法调用method1()方法,method1()调用method2()方法。


他们的栈如下:(一共有三个栈帧,最下面是main、然后是method1、然后是method2)。其中method2()这个栈帧中放了三个变量(a、b、c)的值


method2()这个栈帧出栈后,我们可以看到,a、b、c 这三个变量已经被回收了。(这里特别说明一下,垃圾回收并不需要管理我们的栈内存,因为当栈帧被d出的时候,他就会自动释放内存,垃圾回收只需要对堆中无用的对象进行垃圾回收)

我们可以使用 -Xss size 来设置我们的栈内存,但是不建议设置而且栈内存越大并不是越好。栈内存大的话,能允许我们进行更多次的方法递归调用,但并不会增加运行的效率;由于内存的局限性,还会导致我们线程数目的变少。

默认情况下:

Linux和MacOS的栈内存是1024KB。

Windows则根据机身的虚拟内存决定。

虚拟机的命令写在下图这里

以下三条指令都可以修改我们的栈内存为1024KB。

扩展知识点:
1、方法内的局部变量是否是线程安全的?
这个具体要看该变量是否是线程私有的,如果是线程私有的,那么他是线程安全的,如果他是共有的,那么他就不是线程安全的。

2.2、栈内存溢出

导致栈内存溢出的情况有如下几种:

1、栈帧过多(多次的递归调用)

2、栈帧过大(不太容易出现)

栈帧过多的例子如下:

这里count是类的静态变量。

栈内存溢出的报错信息如下:


当然了,很多人可能会觉得,我才没那么傻写出上面这种代码呢?那如果,我们是在调用某个库的某个接口导致内存溢出的情况下呢?先看看下方的代码:(这是个一定会裂开的代码,可以自行先找找错)

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

public class StackOver {
    public static void main(String[] args) throws IOException {
        //创建一个部门对象
        Dept dept = new Dept();
        dept.setName("研发部门");

        //创建两个员工对象
        Emp emp1 = new Emp();
        emp1.setName("张三");
        emp1.setDept(dept);

        Emp emp2 = new Emp();
        emp2.setName("李四");
        emp2.setDept(dept);

        //将员工加入部门对象
        dept.setEmpList(Arrays.asList(emp1, emp2));

        //将部门对象转换为JSON对象
        ObjectMapper mapper = new ObjectMapper();
        System.out.println(mapper.writevalueAsString(dept));
    }

    //部门类
    static class Dept{
        private String name;    //部门名
        private List empList;  //员工列表

        public void setName(String name) {
            this.name = name;
        }

        public void setEmpList(List empList) {
            this.empList = empList;
        }

        public String getName() {
            return name;
        }

        public List getEmpList() {
            return empList;
        }
    }

    //员工类
    static class Emp{
        private String name;    //员工名
        private Dept dept;      //所属部门

        public void setName(String name) {
            this.name = name;
        }

        public void setDept(Dept dept) {
            this.dept = dept;
        }

        public String getName() {
            return name;
        }

        public Dept getDept() {
            return dept;
        }
    }
}

这里,其实有个大bug,在我们将Dept对象转换为JSON对象的时候。他是这样的:

{
​ name: ‘研发部门’,
​ empList:[
​ {
​ name: ‘张三’,
​ //问题就出在这里,他的每个员工又包含了部门这个对象,然后部门又包含了员工,员工再包含部门,无穷尽也…
​ dept: [
​ name: ‘研发部门’,
​ empList: [ {name:‘张三’, dept:[…]}, {name:‘李四’, dept[…]} ]
​ ],…
​ }
​ ]
}

解决方法:(禁止套娃,我们已经知道员工是部门的empList属性中的一员了,那我们就没必要再在员工中输出他是哪个部门的了,使用注解@JsonIgnore将Emp类中的private Dept dept;属性进行忽略即可。)

    //员工类
    static class Emp{
        private String name;    //员工名
        @JsonIgnore
        private Dept dept;      //所属部门

        public void setName(String name) {
            this.name = name;
        }

        public void setDept(Dept dept) {
            this.dept = dept;
        }
        
        public String getName() {
            return name;
        }

        public Dept getDept() {
            return dept;
        }
    }
2.3、线程运行诊断

案例一:CPU占用过多

当某个程序出现CPU占用过多的情况的时候,大部分情况下,可能是我们的线程出现问题了。

我们可以写一个程序大致如下:

public class HighCpuUse {
    public static void main(String[] args) {
        //创建线程1
        new Thread(null, () -> {
            System.out.println("The Thread 1 is running......");
            while (true){
                //啥也不干,就这么耗着
            }
        }, "Thread1").start();

        //创建线程2
        new Thread(null, () -> {
            System.out.println("The Thread2 is running......");
            try {
                Thread.sleep(1000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Thread2").start();

        //创建线程3
        new Thread(null, () -> {
            System.out.println("The Thread3 is running......");
            try {
                Thread.sleep(1000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Thread3").start();
    }
}

在Linux系统运行后,使用top命令可以看到:

有个java程序,cpu占用106.7%,而且他的进程id是17656.

行,我们通过以下命令看一下进行进程号为17656的 进程号,线程号和CPU使用率来看一下:

ps H -eo pid,tid,%cpu | grep 17656


这里,我们可以看到,我们进程号为17656且线程号为17668,他的CPU占用率高达98.8%。然后我们再通过以下命令查看当前进程的所有线程的具体信息。

jstack 进程号
jstack 21345

运行结果如下:

通过17668(CPU占用最多的线程号)这个线程号转化成十六进制,可以得出,17668 = 0x4504(十六进制)。可以定位到导致CPU高占用的就是这个Thread1的进程,而这个进程他在源码中的第5行。


这个时候,我们就可以先把进程杀掉。然后去指定位置修改自己的源码了。杀死进程的命令:

kill -9 17668

案例2:程序运行很长时间没有结果(可能是出现了死锁)

假如我们现在有代码如下:

A.class

public class A {

}

B.class

public class B {
    
}

ThreadDeadLock.java

public class ThreadDeadLock {
    private static A a = new A();
    private static B b = new B();

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            //线程一开始就锁住a(同步阻塞)
            synchronized (a){
                try {
                    //睡两秒
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //获取b的资源
                synchronized (b){
                    System.out.println("我获得了a和b的资源");
                }
            }
        }, "Thread1").start();

        //睡眠1s
        Thread.sleep(1000);

        new Thread(()->{
            //线程一开始就锁住b(同步阻塞)
            synchronized (b){
                synchronized (a){
                    System.out.println("我获得了a和b的资源");
                }
            }
        }).start();
    }
}

使用javac编译后,使用以下命令:

nohup java ThreadDeadLock &

可以看到

我们的进程号是22065。

我们来看一下为啥子,这个程序没有输出内容 我获得了a和b的资源。

然后,我们就可以针对他所说的死锁,到源码第17和29行进行更改。

3、本地方法栈(Native Method Stacks)

本地方法栈的作用是给本地方法提供内存空间。那什么叫本地方法呢?其实就是指那些不是由Java编写的代码,由于我们的Java代码具有一定的限制,有的时候无法直接与 *** 作系统底层打交道,所以就需要一些由C或C++编写的本地方法来真正与 *** 作系统底层的API打交道,我们的Java代码则间接通过本地方法来调用底层的功能。而这些本地方法运行的时候使用的内存,我们将之称之为本地方法栈。

这种本地方法非常多,在我们的基础类库或执行引擎中,他们都会去调用这些本地方法。举个例子:我们所有类的祖先Object。

这些个方法中带有native的,我们可以看到,他们都是没有具体实现的。其实就是本地方法。这个类中还有很多,例如,clone()、notify()、notifyAll()等等。有兴趣的小伙伴可以自己去看看。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5708984.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存