一、内存结构

[TOC]

image-20200429092949049

1. 程序计数器

1. 定义: program counter register 程序计数器(寄存器)    
2. 作用: 保存下一条字节码指令的地址  

2. 虚拟机栈

2.1定义

  • 定义:每个线程运行需要的空间

  • 组成:由多个栈帧组成

  • 栈帧:是方法需要的内存.

    1. 存放方法参数,局部变量,返回地址
    2. 1个方法的调用对应1个栈帧的入栈和出栈
    3. 每个线程只能由1个活动栈帧,对应着当前正在执行的方法
    4. idea中的frames即为栈帧
  • 几个问题

    1. 垃圾回收是否涉及栈内存?答:不涉及
    2. 栈内存分配越大越好吗?
      • 线程栈的大小设置,一般用系统默认的就可以

      • 栈内存的size越大,能够支持更多次的递归调用,然而支持并发的线程数就越小
    3. 方法内的局部变量是否线程安全?看1个变量是否是多个线程共享的
      • 每个线程有自己的栈帧,局部变量存在栈帧中,如果不作为返回值,则是线程安全的。
      • 如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

        2.2. 栈内存溢出StackOverflowError

    4. 栈帧过多导致(递归,json转化时由于2个类的循环引用导致)
    5. 单个栈帧太大(jsp中)

      2.3. 线程运行诊断

  • nohup命令:nohup java cn.ustc.jvm.Demo1_16 & (让Java代码在后台运行)

  • top命令:监测进程对CPU,内存的占用情况,定位不到线程

  • ps命令:可以查看线程对cpu的占用情况

案例1: 定位cpu 占用过多-空循环

  • 用top定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • jstack(JDK提供) 进程id
    • 会列出所有线程,可以根据线程id 找到有问题的线程(nid),进一步定位到问题代码的源码行号
      public static public static void main(String[] args) {
              new Thread(null, () -> {
                  System.out.println("1...");
                  while(true) {
      
                  }
              }, "thread1").start();

案例2:Jstack排查死锁问题

  • 先用nohup运行,后直接用jstack得出下图

    3. 本地方法栈Native Method Stacks

  • 什么叫本地方法?
    • Java语言有限制,有些操作系统底层的功能得通过c/c++的方法去调用.
    • 本地方法很多,本地方法运行需要的内存叫本地方法栈

      4. 堆Heap

      4.1 定义

  • 通过 new 关键字,创建对象都会使用堆内存
  • 它是线程共享的,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制

    4.2 堆内存溢出 java.lang.OutOfMemoryError:Java heap space

    例:
    • 虚拟机参数:-Xmx8m 设置堆空间大小为8MB ,默认是4GB?
    • 排查堆内存溢出的问题可以将堆空间大小设置小些,尽早暴露出问题

      4.2 堆内存诊断

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

jmap例:演示堆内存-分别看Eden Space这个区域used情况

public class public class Demo1_4 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");
        Thread.sleep(30000);//用来在new 对象前敲jmap命令
        byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
        System.out.println("2...");
        Thread.sleep(20000);
        array = null;
        System.gc();//显示垃圾回收
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}


jconsole例:演示堆内存

  • 仍然用上面的main方法
  • 命令行输入jconsole->选择进程->可看到堆内存的动态变化情况

例:垃圾回收后,内存占用仍然很高,怎么用工具来排查?

  • jmap 查看进程的新生代和老年代内存使用情况

  • 用jconsole 对进程执行GC,观察出内存使用从250MB,只降到了200多MB

  • 再用jmap 查看发现新生代使用的减少到了6MB,但是老年代仍然使用了200多MB

  • jvisualvm命令-可视化虚拟机

    • 用堆Dump这个功能抓取堆的当前快照
    • 查找占用内存最大的前20个对象
    • 发现arraylist大小200,每个元素占1MB
    • 分析代码
      //演示查看对象个数 堆转储 dump
       public class Demo1_13 {
          public static void main(String[] args) throws InterruptedException {
              List students = new ArrayList<>();
              for (int i = 0; i < 200; i++) {
                  students.add(new Student());
      //            Student student = new Student();
              }
              Thread.sleep(1000000000L);
          }
      }
      class Student {
          private byte[] big = new byte[1024*1024];
      }

      5. 方法区

      5.1 定义

  • 逻辑上是堆的一个组成部分,具体实现不同厂商可能不一样,甚至上实现上不是

    • Hotspot虚拟机在1.8以前用的堆中的永久代区域实现的方法区
    • 1.8用的不是堆的内存,叫元空间,用的本地内存, 是操作系统的内存
  • 存储每个类的结构/信息,如field,(运行时)常量池,构造器和成员方法,类方法的代码

  • 是所有线程共享的,和堆一样

  • 在虚拟机启动时创建

  • 会发生OutOfMemoryError

5.2溢出java.lang.OutOfMemoryError

  • 元空间溢出

  • 永久代空间溢出

    /**
     * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
     * -XX:MaxMetaspaceSize=8m
     * 1.8用的元空间实现,默认没有大小限制,为了演示OutOfMemoryError,得修改上面的虚拟机参数
     */
    /**
     * 演示1.6 永久代内存溢出  java.lang.OutOfMemoryError: PermGen space
     * -XX:MaxPermSize=8m
     */
    public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
        public static void main(String[] args) {
            int j = 0;
            try {
                Demo1_8 test = new Demo1_8();
                for (int i = 0; i < 10000; i++, j++) {
                    // ClassWriter 作用是生成类的二进制字节码
                    ClassWriter cw = new ClassWriter(0);
                    //参数: 版本号, public, 类名, 包名, 父类, 接口
                    cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                    // 返回 byte[]
                    byte[] code = cw.toByteArray();
                    // 只执行了类的加载(没有初始化等其他过程)
                    test.defineClass("Class" + i, code, 0, code.length); // Class 对象
                }
            } finally {
                System.out.println(j);
            }
        }
    }
  • 实际场景中spring,mabatis大量使用了运行中加载、生成类-动态生成字节码技术,使用不当可能会导致方法区内存溢出

    5.3 运行时常量池

    什么是常量池?

  • class文件结构中的常量池,class文件还包括类的基本信息,类方法(虚拟机指令)

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量(字符串,基本类型常量) 等信息

什么是运行时常量池?

  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量 池,并把里面的符号地址变为真实地址

总结:

  • 方法区:在java的虚拟机中有一块专门用来存放已经加载的类信息、常量、静态变量以及方法代码的内存区域,叫做方法区。
  • 常量池:常量池是方法区的一部分,主要用来存放常量和类中的符号引用等信息。
  • 堆区:用于存放类的对象实例。
  • 栈区:也叫java虚拟机栈,是由一个一个的栈帧组成的后进先出的栈式结构,栈桢中存放方法运行时产生的局部变量、方法出口等信息。当调用一个方法时,虚拟机栈中就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法中调用了其他方法,则继续在栈顶创建新的栈桢。

jvm中的运行时内存区域还包括本地方法栈程序计数


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!