JVM学习笔记(2)---Java内存区域

作者: 18587a1108f1 | 来源:发表于2018-08-23 10:52 被阅读0次

Java与C的内存管理区别

在C/C++中,需要使用 delete/free 等函数来手动释放内存;
而在Java中,因为Java采用了 垃圾回收机制,程序猿无需释放内存,由 虚拟机自动管理内存,会较少的出现内存泄漏和溢出的问题。
因此我们需要深入了解虚拟机是如何使用内存的,万一出现内存泄漏和溢出,也方便排查。

运行时的数据区域

Java虚拟机运行时数据区

程序计数器(Program Counter Register)

程序计数器用于记录当前线程 正在执行字节码 指令地址。
计数器通过改变记录的值,来选取下一条要执行的字节码指令,循环、跳转、异常处理、线程恢复等都是通过计数器完成的。每个线程有独立的计数器,互不影响。

虚拟机栈(VM Stack)

虚拟机栈用于记录 执行Java方法
栈只保存基本数据类型的对象(byte、char、int等)和自定义对象的引用(reference类型,不是对象)
当方法执行时,虚拟机栈会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 方法的执行过程就是一个栈帧入栈出栈的过程。

Java虚拟机规范中,对该区域规定了两种异常:

  • StackOverFlowError:线程请求的栈深度大于虚拟机允许的栈深度
  • OverOfMemoryError:动态扩展的线程无法申请到足够的内存时 (OOM常见情况

StackOverFlowError示例:通过递归不断增加栈深度,导致栈溢出

public class JVMStacksOverFlowError {
    public int stackLength = 1;
    public void JVMStackOverFlowError() {
        stackLength++;
        JVMStackOverFlowError();
    }
    public static void main(String[] args) throws Throwable {
        JVMStacksOverFlowError stack = new JVMStacksOverFlowError();
        try {
            stack.JVMStackOverFlowError();
        } catch (Throwable e) {
            System.out.println("stack length is :" + stack.stackLength);
            e.printStackTrace();
        }
    }
}

运行结果如图所示:


StackOverFlowError

OutOfMemoryError示例:通过不断创建线程来分配栈内存,导致内存不够

public class JVMOutOfMemoryError {
    public int threadCount = 0;
    public void addNewThread() {
        while (true) {
            threadCount++;
            new Thread() {
                @Override
                public void run() {
                    while (true) ;
                }
            }.start();
        }
    }

    public static void main(String[] args) throws Throwable {
        JVMOutOfMemoryError ofmeMain = new JVMOutOfMemoryError();
        try {
            ofmeMain.addNewThread();
        } catch (Throwable e) {
            System.out.println("thread count is :" + ofmeMain.threadCount);
            e.printStackTrace();
        }
    }
}

运行结果如图所示:


OutOfMemoryError

本地方法栈(Native Method Stack)

本地方法栈用于记录 执行Native方法
当虚拟机调用本地(native)方法时,虚拟机不会创建新的栈帧,虚拟机栈会保持不变,虚拟机只是简单的动态连接并直接调用相关的本地方法。
如果本地方法接口是c连接模型的话,它的本地方法栈就是c栈。当c程序调用一个c函数时,传递给该函数的参数以相应的顺序压入栈,它的返回值以确定的方式返回给调用方。这就是虚拟机实现中本地方法栈的行为。(本地方法栈说明)
本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

堆(Heap)

堆用于存储 对象实例,管理 垃圾回收
堆是JVM所管理的内存中最大的一块。

堆内存

根据分代收集算法,堆被分为新生代老生代
新生代:Young Generation,主要用来存放新生的对象实例。
老年代:Old Generation/Tenured Generation,主要存放应用程序声明周期长的内存对象实例。

方法区(Method Area)

方法区存储已经被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等等。
参见 方法区存储信息

方法区

栈、堆、方法区比较

堆区:

1.存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
2.jvm只有一个堆区(heap),被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身

栈区:

1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

方法区:

1.又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。

通过代码堆三者进行形象的解释:

//JVM先把ApplicationMain类放入方法区
public class ApplicationMain {

    //main方法放入方法区
    public static void main(String[] args) {
        //obj1是引用,指向SimpleObject实例的引用,放入虚拟机栈中
        //new的SimpleObject是自定义的对象,指向方法区中SimpleObject类的内存地址,放入堆中
        SimpleObject obj1 = new SimpleObject("obj1");
        //调用printName方法,找到堆中的对象,根据对象找到方法区中的类信息
        //根据类信息中方法的字节码,从而让线程执行方法
        obj1.printName();
    }

    //SimpleObject类信息放入方法区
    static class SimpleObject {
        //new Simple实例后,name引用放入栈区,name对象放入堆区
        private String name;

        /**
         * 构造方法
         *
         * @param name
         */
        public SimpleObject(String name) {
            this.name = name;
        }

        //printName方法放入方法区
        public void printName() {
            System.out.println(name);
        }
    }
}

代码执行过程,各内存块的存储过程如下:

  1. 启动后,系统收到用户指令,启动一个Java虚拟机进程,先找到ApplicationMain.class文件,读取二进制数据,把类信息放入方法区,这个过程为ApplicationMain的加载过程;
  2. 程序计数器定位到 main() 方法的字节码,并开始执行它的第一条指令
    SimpleObject obj1 = new SimpleObject("obj1");
  3. JVM要创建一个SimpleObject对象实例,并使局部变量obj1引用这个实例。
    JVM先去方法区寻找类SimpleObject的类型信息,未找到,则加载了SimpleObject类。把信息存在方法区;然后JVM为创建的新对象实例在堆中分配内存,这个实例持着指向方法区SimpleObject类的类型信息的引用,引用即SimpleObject类在方法区中的内存地址
    4.局部变量obj1指向了实例的引用,被存放在运行main方法的主线程的虚拟机栈中。
    5.之后obj1.printName();执行printName方法时,JVM根据obj1的引用,定位到堆区的实例,再根据实例的引用找到方法区中的类型信息,从而找到printName方法的字节码,从而让线程执行printName方法。

相关文章

网友评论

    本文标题:JVM学习笔记(2)---Java内存区域

    本文链接:https://www.haomeiwen.com/subject/vdjeiftx.html