转载

Java基础-JVM内存划分

它保存的是 程序当前执行的指令的地址 (也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。

在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个 线程私有 的。

由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。

2.栈

Java基础-JVM内存划分

Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。

  • 局部变量表,顾名思义,想必不用解释大家应该明白它的作用了吧。就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。

  • 操作数栈,想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。

  • 指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。

  • 方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

由于每个线程正在执行的方法可能不同,因此 每个线程都会有一个自己的Java栈 ,互不干扰。

3.本地方法栈

本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method:一个Native Method就是一个java调用非java代码的接口)服务的。

4.堆

Java中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。在Java中,程序员基本不用去关心空间释放的问题,Java的垃圾回收机制会自动进行处理。因此这部分空间也是Java垃圾收集器管理的主要区域。另外,堆是被所有 线程共享 的,在JVM中只有一个堆。

5.方法区

method(方法区)又叫静态区,存放所有的①类(class),②静态变量(static变量),③静态方法,④常量和⑤成员方法。

1.又叫静态区,跟堆一样,被所有的线程共享。

2.方法区中存放的都是在整个程序中永远唯一的元素。这也是方法区被所有的线程共享的原因。

6.名流程

最后通过一个流程图,串起所有区域

// AppMain.java
public class AppMain {                         //运行时,JVM把AppMain的信息都放入方法区    

    public static void main(String[] args) { //main成员方法本身放入方法区。    
        Sample test1 = new  Sample( " 测试1 " );   //test1是引用,所以放到栈区里,Sample是自定义对象应该放到堆里面    
        Sample test2 = new  Sample( " 测试2 " );         
        test1.printName();    
        test2.printName();    
    }
    
} 
复制代码

image:JVM运行流程图

Java基础-JVM内存划分

总结:

  • 方法区:保存类的模板
  • 堆:保存类的实例
  • 栈:进行函数计算

7.扩展

  • 永久代

HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内容,能够省去专门为方法区编写内存管理代码的工作。对于其他虚拟机(如BEA JRockit/ IBM J9)来说是不存在永久代的概念的。

  • 运行时常量池

运行时常量池(Runtime Constant Pool)是 方法区的一部分 。Class文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期、运行期间生成的各种字面量和符号引用,这部分内容将类在加载后进入方法区的运行时常量池中存放。

String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1==str2);//false

String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
System.out.println("string" == "str" + "ing");// true
System.out.println(str3 == str4);//false

String str5 = "string";
System.out.println(str3 == str5);//true
复制代码

解释:

  • "abcd"是在常量池中拿对象,new String("abcd")是直接在堆内存空间创建一个新的对象。只要使用new方法,便需要创建新的对象。
  • 连接表达式 +,只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入常量池中。
  • 对于字符串变量的“+”连接表达式,它所产生的新对象都不会被加入字符串池中,其属于在运行时创建的字符串,具有独立的内存地址,所以不引用自同一String对象。

参考:

  • Java栈,PC寄存器,本地方法栈,堆,方法区和运行常量池
  • java虚拟机:运行时常量池
原文  https://juejin.im/post/5d9be1186fb9a04dd85918ba
正文到此结束
Loading...