jvm 内存空间划分与作用详解

355 阅读6分钟

虚拟机栈:

Stack Frame 栈帧(每个方法执行的时候都会形成一个栈帧,存储局部变量表(存放的 8 个基本数据类型(把值直接放在局部变量表里)、对象的引用类型(1.通过句柄的方式;2. 直接指向的方式)来操作真正想要使用的对象)、操作数栈、动态链接、方法出口相关信息);虚拟机栈归属于一个特定的线程,是线程独有的一块内存空间

一个方法的执行过程,就是这个方法对于栈帧的入栈、出栈过程。

程序计数器(Program Counter):

当前执行的字节码的行号指示器;线程私有

本地方法栈:

与虚拟机栈类似,结构也类似,主要用于执行 native 方法

堆(Heap):

java 虚拟机管理的最大的一块内存,由所有线程共享,虚拟机启动时就会创建,唯一目的就是存放对象实例。java 是通过引用(引用本身是一个变量,位于局部变量表)操作对象,对象位于 Heap 上,而引用位于 Stack 上,

与 堆 相关的一个重要概念是 GC,是 GC 的主要工作区域。现代几乎所有的 GC 都是采用分代收集算法,所以,堆空间也基于这一点进行了相应的划分:新生代和老年代。再细致,Eden 空间, From Survivor 空间和 To Survivor 空间

方法区(Method Area):

存储元信息,如存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。 永久代(Permanent Generation),从jdk 1.8 开始,已经彻底废弃了永久代,使用 元空间(meta space) 存放每个 Class 的结构信息,包括常量池、字段描述、方法描述;GC 的非主要工作区域。

运行时常量池

本身是 方法区 的一部分。存放编译期生成的各种字面量和符号引用

直接内存(Direct Memory):

并不是 jvm 关系的区域,由操作系统来管理。 与 java NIO 密切相关,jvm 是通过堆上的 DirectByteBuffer 来操作直接内存

关于 java 对象的创建过程

new 关键字创建对象的 3 个步骤:

  1. 在堆内存中创建对象的实例
  2. 为对象的实例成员变量赋值(静态变量在 初始化 阶段完成了)(调用<init>方法)
  3. 将对象的引用返回

指针碰撞

前提是堆中的空间通过一个指针进行分割,一侧是已经被占用的空间,另一侧是未被占用的空间。

空闲列表:

前提是堆内存空间中已被使用与未被使用的空间是交织在一起的,这时,虚拟机就需要通过一个列表来记录哪些空间是可以使用的,哪些空间是已被使用的,接下来找出可以容纳下新创建对象且未被使用的空间,在此空间存放该对象,同时还要修改列表上的对象。

对象在内存的布局:

  1. 对象头(对象要运行时的一些信息,如 hashcode)
  2. 实例数据(即我们在一个类中所声明的各项信息)
  3. 对齐填充(可选) !

引用访问对象的方式:

  1. 使用句柄的方式。
  2. 使用直接指针的方式。
  public static void main(String[] args) {
        //-Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError 设置jvm对空间最小和最大以及遇到错误时把堆存储文件打印出来
        //打开jvisualvm装在磁盘上的转存文件
        List<MyTest1> list = new ArrayList<>();
        while (true) {
            list.add(new MyTest1());
            System.gc();
        }

    }

虚拟机栈溢出

测试调整虚拟机栈内存大小为: -Xss160k,此处除了可以使用JVisuale监控程序运行状况外还可以使用jconsole

public class MyTest2 {

    private int length;

    public int getLength() {return length;}

    public void test () {
        this.length++;
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        test();
    }

    public static void main(String[] args) {
        MyTest2 myTest2 = new MyTest2();

        try {
            myTest2.test();
        } catch (Throwable e) {
            System.out.println(myTest2.getLength());
            e.printStackTrace();
        }
    }
}

死锁

public class MyTest3 {
    public static void main(String[] args) {
        new Thread(() -> A.method(), "Thread-A").start();

        new Thread(() -> B.method(), "Thread-B").start();

    }
}


class A {
    // 当一个线程进入静态方法,持有的不是这个类的对象的锁,而是这个类对应的 Class 的锁
    public static synchronized void method() {
        System.out.println("method from A");

        try {
            Thread.sleep(5000);
            B.method();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class B {
    public static synchronized void method() {
        System.out.println("method from B");

        try {
            Thread.sleep(5000);
            A.method();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

方法区产生内存溢出错误

如果不采取一些措施,方法区产生内存溢出错误是极难的。jdk 1.8 开始,方法区废除了永久代,采用了元空间,元空间采用的是操作系统本地的内存,初始内存是 21 M,并且随着对内存空间的不断占用,元空间虚拟机会进行垃圾回收,如果回收不够的话,还会进行内存的扩展,一直扩展到物理内存的最大限度。

显示设定元空间的大小

设置元空间大小:-XX:MaxMetaspaceSize=100m 关于元空间参考:www.infoq.cn/article/jav…

jmap -clstats PID 打印类加载器数据。(-clstats 是 -permstat 的替代方案,在 JDK8 之前,-permstat 用来打印类加载器的数据)。下面的例子输出就是 DaCapo’s Avrora benchmark 程序的类加载器数据

jstat -gc LVMID 用来打印元空间的信息,具体内容如下

jvm 命令使用

/**
 * Created BY poplar ON 2019/11/26
 * jmam命令的使用 -clstats<pid>进程id  to print class loader statistics
 * jmap -clstats 3740
 *
 * jstat -gc 3740
 *  S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
 * 512.0  512.0   0.0    0.0   24064.0   9626.0   86016.0     1004.1   4864.0 3758.2 512.0  409.1     144    0.064   0      0.000    0.064
 * MC元空间总大小,MU元空间已使用的大小
 */
public class MemoryTest4 {
    public static void main(String[] args) {
        while (true)
            System.out.println("hello world");
    }
    //查看java进程id jps -l
    // 使用jcmd查看当前进程的可用参数:jcmd 10368 help
    //查看jvm的启动参数 jcmd 10368 VM.flags
   // 10368:-XX:CICompilerCount=3 -XX:InitialHeapSize=132120576 -XX:MaxHeapSize=2111832064 -XX:MaxNewSize=703594496
    // -XX:MinHeapDeltaBytes=524288 -XX:NewSize=44040192 -XX:OldSize=88080384 -XX:+UseCompressedClassPointers
    // -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC

}

jvm 常用命令

jcmd (从JDK 1. 7开始增加的命令)

  1. jcmd pid VM.flags: 查看JVM的启动参数
  2. jcmd pid help: 列出当前运行的Java进程可以执行的操作
  3. jcmd pid help JFR.dump:查看具体命令的选项
  4. jcmd pid PerfCounter.print:看JVm性能相关的参数
  5. jcmd pid VM.uptime:查有JVM的启动时长
  6. jcmd pid GC.class_ histogram: 查看系统中类的统计信息
  7. jcmd pid Thread.print: 查看线程堆栈信息 == jstack 查看或者是导出 java 应用程序中线程的堆栈信息
  8. jcmd pid GC.heap dump filename 导出Heap dump文件, 导出的文件可以通过jvisualvm查看
  9. jcmd pid VM.system_ properties:查看JVM的属性信息
  10. jcmd pid VM.version: 查看目标 jvm 进程的版本信息
  11. jcmd pid VM.command_line: 查看 jvm 启动的命令行参数信息

jmc:java mission control jfr:java Filght Recorder