JVM-内存结构-程序计数器、虚拟机栈、本地方法栈、堆

2022-09-03 13:35:42

一、前言

1.1 什么是 JVM ?

1)定义
Java Virtual Machine ,Java 程序的运行环境(Java 二进制字节码的运行环境)。

2)好处

  • 一次编译,处处执行
  • 自动的内存管理,垃圾回收机制
  • 数组下标越界检查

3)比较
JVM、JRE、JDK 的关系如下图所示
在这里插入图片描述

1.2 JVM种类和结构

种类:
在这里插入图片描述
jvm结构图:
在这里插入图片描述
ClassLoader:Java 代码编译成二进制后,会经过类加载器,这样才能加载到 JVM 中运行。
Method Area:类是放在方法区中。
Heap:类的实例对象。
当类调用方法时,会用到 JVM Stack、PC Register、本地方法栈。
方法执行时的每行代码是有执行引擎中的解释器逐行执行,方法中的热点代码频繁调用的方法,由 JIT 编译器优化后执行,GC 会对堆中不用的对象进行回收。需要和操作系统打交道就需要使用到本地方法接口。

1.3 认识内存结构概念

在开始学习jvm前,这里先介绍一下一些名称概念,以及jvm运行时所占用的内存块蓝图。
jvm数据区的内存分块有堆内存、方法区(元空间)、虚拟机栈、程序计数器、本地方法栈
后面还会介绍一个直接内存,它不等于本地内存,本地内存指的是JVM外的由操作系统控制的内存区域,其中包括元空间和直接内存,元空间都知道就不说了,直接内存主要被 Java NIO 使用的,java虚拟机运行时默认能触及的直接内存大小为电脑内存的四分之一,可以通过参数-XX:MaxDirectMemorySize来指定最大的直接内存大小。
在这里插入图片描述

二、内存结构

2.1 程序计数器

1)定义
Program Counter Register 程序计数器(其实就是寄存器)
作用:是记录下一条 jvm 指令的执行地址行号。
特点:

  • 是线程私有的
  • 不会存在内存溢出

2)作用

在这里插入图片描述

  • 解释器会解释指令为机器码交给 cpu
    执行,程序计数器会记录下一条指令的地址行号(即通过截图上的前面数字获取的),这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
  • 多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。

2.2 虚拟机栈

2.2.1 认识虚拟机栈

定义

  • 每个线程运行需要的内存空间,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

画图演示
在这里插入图片描述

代码演示

publicclassmain1{publicstaticvoidmain(String[] args){method1();}![在这里插入图片描述](https://img-blog.csdnimg.cn/0e3218e6a4284167881513ba6ceae8b9.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBATEwuTEVCUk9O,size_20,color_FFFFFF,t_70,g_se,x_16)publicstaticvoidmethod1(){method2(1,2);}publicstaticintmethod2(int a,int b){int c= a+ b;return c;}}

在这里插入图片描述
可见,虚拟机栈内共用三个栈帧。

2.2.2 常见问题

垃圾回收是否涉及栈内存?

不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。

栈内存分配越大越好吗?

不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。

方法内的局部变量是否线程安全?

如果方法内部局部变量没有逃离方法的作用访问,它是线程安全的
如果是局部变量引用了对象,并逃离方法的范围,需要考虑线程安全问题

举例:

publicstaticvoidmain(String[] args){}//下面各个方法会不会造成线程安全问题?//不会publicstaticvoidm1(){
        StringBuilder sb=newStringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());}//会,可能会有其他线程使用这个对象publicstaticvoidm2(StringBuilder sb){
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());}//会,其他线程可能会拿到这个线程的引用publicstatic StringBuilderm3(){
        StringBuilder sb=newStringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);return sb;}}

注:很多人对第三个方法抱有疑问,这里我也说一下自己的理解:第三个方法存在发生指令乱序并且为优先返回对象再做append操作,如此在append未执行完时返回的对象被其他线程使用,就会出现线程安全问题。(个人理解,可能有误,欢迎指正)

2.2.3栈内存溢出

1、递归调用可能造成栈内存溢出。 2、栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出
java.lang.stackOverflowError 。 注:可以通过使用 -Xss 指定栈内存大小!
在这里插入图片描述
演示递归导致的栈内存溢出:

privatestaticint count;publicstaticvoidmain(String[] args){try{method1();}catch(Throwable e){
            e.printStackTrace();
            System.out.println(count);}}privatestaticvoidmethod1(){
        count++;method1();}}

演示第三方库导致的栈内存溢出:

publicclassDemo1_19{publicstaticvoidmain(String[] args)throws JsonProcessingException{
        Dept d=newDept();
        d.setName("Market");

        Emp e1=newEmp();
        e1.setName("zhang");
        e1.setDept(d);

        Emp e2=newEmp();
        e2.setName("li");
        e2.setDept(d);

        d.setEmps(Arrays.asList(e1, e2));// { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
        ObjectMapper mapper=newObjectMapper();
        System.out.println(mapper.writeValueAsString(d));}}classEmp{private String name;@JsonIgnoreprivate Dept dept;public StringgetName(){return name;}publicvoidsetName(String name){this.name= name;}public DeptgetDept(){return dept;}publicvoidsetDept(Dept dept){this.dept= dept;}}classDept{private String name;private List<Emp> emps;public StringgetName(){return name;}publicvoidsetName(String name){this.name= name;}public List<Emp>getEmps(){return emps;}publicvoidsetEmps(List<Emp> emps){this.emps= emps;}}

2.2.4线程运行诊断

案例一:cpu 占用过多 解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程

  • top 命令,查看是哪个进程占用 CPU 过高
  • ps H -eo pid, tid(线程id), %cpu | grep 进程id 通过 ps 命令进一步查看是哪个线程占用 CPU 过高
  • jstack 进程 id 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要转换。

2.3本地方法栈

一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法。

2.4 堆

2.4.1 定义

通过new关键字创建的对象都会使用堆内存

2.4.2 特点

它是线程共享的,堆中对象都需要考虑线程安全的问题
有垃圾回收机制

2.4.3 堆内存溢出

java.lang.OutofMemoryError :java heap space 堆内存溢出
案例:

/**
 * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m
 */publicclassDemo1_5{publicstaticvoidmain(String[] args){int i=0;try{
            List<String> list=newArrayList<>();
            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);}}}

结果:

java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.itcast.itheima.xpp.main1.main(main1.java:14)
22

注:可以通过 -Xmx 来指定堆内存大小
在这里插入图片描述

2.4.4 堆内存诊断(调试工具的使用)

jps相关命令参考:https://blog.csdn.net/weixin_41979002/article/details/123156872

  1. jps 工具 查看当前系统中有哪些 java 进程
  2. jmap 工具 查看堆内存占用情况 jmap - heap 进程id
  3. jconsole 工具 图形界面的,多功能的监测工具,可以连续监测
  4. jvisualvm 工具

以上命令都是jdk自带的调试工具,都存放在JAVA_HOME/bin/目录下,只需要将环境变量配置好,在idea内,都可以正常使用的。

案例演示:

publicclassDemo1_4{publicstaticvoidmain(String[] args)throws InterruptedException{
        System.out.println("1...");
        Thread.sleep(30000);byte[] array=newbyte[1024*1024*10];// 10 Mb
        System.out.println("2...");
        Thread.sleep(20000);
        array= null;
        System.gc();
        System.out.println("3...");
        Thread.sleep(1000000L);}}

在idea内执行上述代码命令查看堆内存占用情况。

2.4.4.1 jps和jmap

打印1后的30秒内,输入jps查看进程id
在这里插入图片描述
解读:Demo1_4的id便是我们的目标java进程id

在打印1后的30秒内,输入jmap -heap 8016查看进程的堆内存占用信息:
在这里插入图片描述在这里插入图片描述
解读:堆内存配置的最大值为3022.0MB,当前进程使用了堆内存4.8157196044921875MB

打印2后输入同样命令jmap -heap 8016查看堆内存:
在这里插入图片描述
解读:进程使用的堆内存增加了10MB。

打印3后再执行同样命令:
在这里插入图片描述
解读:堆内存释放了.

2.4.4.2 jconsole

同样的运行程序,在执行命令
在这里插入图片描述

在这里插入图片描述
根据折线图可以看到,堆内存先是增加,后下降。

2.4.4.3 jvisualvm

演示另一个案例,代码如下:

publicclassDemo1_13{publicstaticvoidmain(String[] args)throws InterruptedException{
        List<Student> students=newArrayList<>();for(int i=0; i<200; i++){
            students.add(newStudent());//            Student student = new Student();}
        Thread.sleep(1000000000L);}}classStudent{privatebyte[] big=newbyte[1024*1024];}

先用jps查看java进程id
在这里插入图片描述

再jmap查看一下堆内存占用情况
在这里插入图片描述
在这里插入图片描述
解读:
Eden Space新生代占用了19.730911254882812MB;
PS Old Generation老年代占用了184.6506576538086MB

执行jvisualvm命令打开工具:
在这里插入图片描述
在这里插入图片描述
解读:按上述步骤操作,可见垃圾回收,仅回收了20MB左右的堆内存而已,因此我们要进一步查看堆内存的占用情况。
在这里插入图片描述
点击后,可以将当前堆内存状况生成快照,便于我们分析。进入后可以看到如下的堆内存信息,再点击查找,可以看到该进程内占用堆内存最大的对象。
在这里插入图片描述
在这里插入图片描述
可以看到,ArrayList下有200多个student对象实例,每个实例都含一个占用了较大内存的byte[]数组。
我们再从头看代码,确实和我们用工具分析的一致。(由于这些student对象存在着强引用,故而GC无法回收)

  • 作者:沉泽·
  • 原文链接:https://blog.csdn.net/weixin_41979002/article/details/123153270
    更新时间:2022-09-03 13:35:42