欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JVM1-内存结构

程序员文章站 2022-06-06 22:39:54
...

1、JVM内存结构

JVM1-内存结构

分析JVM的内存结构主要就是分析JVM运行时数据存储区域。JVM运行时数据区主要包括堆、栈、方法区、程序计数器。而JVM的优化问题主要是在线程共享的数据区中如堆、方法区。

1.1 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。

为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。

1.2栈

JVM的栈包括Java虚拟机栈和本地方法栈。
Java虚拟机栈为JVM执行Java方法服务,本地方法栈为JVM使用的native方法服务。以下主要介绍Java虚拟机栈,以下简称栈。

栈帧是栈的元素,每个方法在执行时都会创建一个栈帧。栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程对于着一个栈帧在栈中压栈到出栈的过程。
JVM1-内存结构

1.2.1局部变量表

栈帧中,由一个局部变量表存储数据。局部变量表中存储了基本数据类型(boolean、byte、char、short、int、float、long、double)的局部变量(包括参数)、和对象的引用(String、数组、对象等),但是不存储对象的内容。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。

局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(long、double),JVM 会为其分配两个连续的变量槽来存储。以下简称 Slot 。

JVM 通过索引定位的方式使用局部变量表,索引的范围从0开始至局部变量表中最大的 Slot 数量。普通方法与 static 方法在第 0 个槽位的存储有所不同。非 static 方法的第 0 个槽位存储方法所属对象实例的引用。JVM1-内存结构

1.2.3操作数栈

通常进行算数运算的时候是通过操作数栈来进行的,又或者是在调用其他方法的时候通过操作数栈进行参数传递。操作数栈可以理解为栈帧中用于计算的临时数据存储区。
栈中会出现的异常:

  • *Error:栈溢出

  • OutOfMemoryError:内存不足

    public class OperandStack{
    public static int add(int a, int b){
    int c = a + b;
    return c;
    }
    public static void main(String[] args){
    add(100, 98);
    }
    }

JVM1-内存结构

1.3 Java堆

堆是Java虚拟机所管理的内存中最大的一块存储区域。堆内存被所有线程共享,主要存放new关键字创建的对象,所有对象实例以及数组都要在堆上分配。垃圾回收器就是收集堆上对象所占用的内存空间(堆上对象所占用的内存空间而不是对象本身)。

1.4 方法区

方法区同Java堆一样是被所有线程共享的,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。
JVM1-内存结构

常量池是方法区的一部分。常量池中存储编译器生成的各种字面量和符号引用。字面量就是Java中常量的意思。比如文本字符串,final修饰的常量等。方法引用则包括类和接口的全限定名,方法名和描述符,字段名和描述符等。

1.4.1 常量池

举例:Integer常量池(缓存池)、字符串常量池。
基本数据类型比较的是数值,而引用数据类型比较的是内存地址。

public void TestIntegerCache(){
    public static void main(String[] args){
		Integer i1 = new Integer(66);
        Integer i2 = new integer(66);
        Integer i3 = 66;
        Integer i4 = 66;
        Integer i5 = 150;
        Integer i6 = 150;
        System.out.println(i1 == i2);//false
        System.out.println(i3 == i4);//true
        System.out.println(i5 == i6);//false
    }
}
  1. i1 和 i2 使用 new 关键字,每 new 一次都会在堆上创建一个对象,所以 i1 == i2 为 false。
  2. i3 == i4 为什么是 true 呢?Integer i3 = 66 实际上有一步装箱的操作,即将 int 型的 66 装箱成 Integer,通过 Integer 的 valueOf 方法。

`

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

Integer 的 valueOf 方法很简单,它判断变量是否在 IntegerCache 的最小值(-128)和最大值(127)之间,如果在,则返回常量池中的内容,否则 new 一个 Integer 对象。
3.i5==i6为false是因为150不在Integer常量池中的最大最小值之间从而new了一个新的对象。

再看一段拆箱的代码:

public static void main(String[] args){

   Integer i1 = new Integer(4);
   Integer i2 = new Integer(6);
   Integer i3 = new Integer(10);
   System.out.print(i3 == i1+i2);//true
}

由于i1和i2是Integer对象,不能使用+运算符,首先i1和i2先进行拆箱操作,即拆成int后再进行数值加法运算。i3也是拆箱后再与之比较数值是否相等的。因此i3==i1+i2比较的是int数值是否相等,所以为true。

1.4.2 String常量池

String是由final修饰的类,不可以被继承。通常两种方式来创建对象:

String str=new String("abcd");
String str="abcd";

第一种使用new创建的对象存放在堆中。每次调用都会创建一个新的对象。
第二种先在栈上创建一个String类的对象引用变量str,然后通过符号引用去字符串常量池中找"abcd",如果没有将"abcd"存放在常量池中,并将栈上的str变量引用指向常量池中的“abcd”。如果常量池中已经有“abcd”了,则不会再常量池中创建“abcd”,而是直接将 str 引用指向常量池中的“abcd”。

public static void main(String[] args){
   String str1 = "abcd";
   String str2 = "abcd";
   System.out.print(str1 == str2);//true
}

首先在栈上存放变量引用 str1,然后通过符号引用去常量池中找是否有 abcd,没有,则将 abcd 存储在常量池中,然后将 str1 指向常量池的 abcd。当创建 str2 对象,去常量池中发现已经有 abcd 了,就将 str2 引用直接指向 abcd 。所以str1 == str2,指向同一个内存地址。

public static void main(String[] args){
   String str1 = new String("abcd");
   String str2 = new String("abcd");
   System.out.print(str1 == str2);//false
}

str1 和 str2 使用 new 创建对象,分别在堆上创建了不同的对象。两个引用指向堆中两个不同的对象,所以为 false。

对于字符串常量的+号连接,在程序编译期JVM就会将其优化为+号连接后的值,所以在编译期其字符串常量的值就确定了。

String a = "a1";   
String b = "a" + 1;   
System.out.println((a == b)); //result = true  


​ String a = “atrue”;
​ String b = “a” + “true”;
​ System.out.println((a == b)); //result = true

String a = “a3.4”;
String b = “a” + 3.4;
System.out.println((a == b)); //result = true

对于字符串引用+号连接的问题:

对于字符串引用的 + 号连接问题,由于字符串引用在编译期是无法确定下来的,在程序的运行期动态分配并创建新的地址存储对象。

public static void main(String[] args){
   String str1 = "a";
   String str2 = "ab";
   String str3 = str1 + "b";
   System.out.print(str2 == str3);//false
}

对于上边代码,str3 等于 str1 引用 + 字符串常量“b”,在编译期无法确定,在运行期动态的分配并将连接后的新地址赋给 str3,所以 str2 和 str3 引用的内存地址不同,所以 str2 == str3 结果为 false。
反编译代码后:

public static void main(String args[]){        
	String s = "a";        
	String s1 = "ab";        
	String s2 = (new StringBuilder()).append(s).append("b").toString();        System.out.print(s1 = s2);    
}

发现new了一个StringBuilder对象,然后使用append方法优化了+操作符。new在堆上创建对象,而String s1="ab"在常量池中创建对象,两个引用指向的内存地址是不同的,所以false

我们已经知道了字符串引用的 + 号连接问题,其实是在运行期间创建一个 StringBuilder 对象,使用其 append 方法将字符串连接起来。这个也是我们开发中需要注意的一个问题,就是尽量不要在 for 循环中使用 + 号来操作字符串。看下面一段代码:

public static void main(String[] args){
    String s = null;
    for(int i = 0; i < 100; i++){
        s = s + "a";
    }
}

在 for 循环中使用 + 连接字符串,每循环一次,就会新建 StringBuilder 对象,append 后就“抛弃”了它。如果我们在循环外创建StringBuilder 对象,然后在循环中使用 append 方法追加字符串,就可以节省 n-1 次创建和销毁对象的时间。所以在循环中连接字符串,一般使用 StringBuilder 或者 StringBuffer,而不是使用 + 号操作。

public static void main(String[] args){
    StringBuilder s = new StringBuilder();
    for(int i = 0; i < 100; i++){
        s.append("a");
    }
}

使用final修饰的字符串:

public static void main(String[] args){
    final String str1 = "a";
    String str2 = "ab";
    String str3 = str1 + "b";
    System.out.print(str2 == str3);//true
}

final 修饰的变量是一个常量,编译期就能确定其值。所以 str1 + "b"就等同于 "a" + "b",所以结果是 true。

String str1 = “a”;
String str2 = “ab”;
String str3 = str1 + “b”;
System.out.print(str2 == str3);//true
}

final 修饰的变量是一个常量,编译期就能确定其值。所以 str1 + "b"就等同于 "a" + "b",所以结果是 true。
相关标签: JVM