Java内存分配
在JDK7之前,JVM的堆空间和方法区是连在一起的,在物理空间上也是如此。
但是从JDK8开始,JVM取消了方法区,新增了元空间,把原来方法区的多种功能进行拆分,有的功能放到了堆中,有的功能放到了元空间中。
- 栈 方法运行时使用的内存,如main方法运行,进入方法栈中执行
- 堆 存储对象或数组,即new形式创建的,都存储在堆内存
- 方法区 存储可以运行的class文件
- 本地方法栈 JVM在使用操作系统功能的时候使用,与我们开发无关
- 寄存器 给CPU使用,与我们开发无关
数组的内存
1、只要是new出来的一定是在堆里面开辟了一个小空间
2、如果new了多个,那么在堆里面有多个小空间,每个小空间中都有各自的数据
下面给出了数组1直接赋值给数组2的情况(两个数组指向同一个空间)
int[] arr1 = {11, 22}; int[] arr2 = arr1;
方法的内存
以main方法为例,当main方法执行完毕后就会出栈,方法内定义的数据也随之销毁
基本数据类型与引用数据类型
- 基本数据类型:数据值是存储在自己的空间中。特点:赋值给其它变量,也是赋的真实的值。
- 引用数据类型:数据值是存储在其它的空间中,自己空间中存储的是地址值。
特点:赋值给其它变量,赋的是地址值。
方法传递基本数据类型的内存原理
传递基本数据类型时,传递的是真实的数据,形参的改变,不影响实际参数的值。
方法传递引用数据类型的内存原理
传递引用数据类型时,传递的是引用值,形参的改变,会影响实际参数的值。
对象的内存
实例化对象时:
当程序执行到如 Student s = new Student(); 时,JVM内存会依次有以下动作:
- 加载class文件。这里的Student是一个bean对象,对应有class类,程序在编译时生成的class文件会加载进JVM内存的方法区(JDK8之前,JDK8及8以后class文件加载进元空间)中。
- 声明局部变量
- 在堆内存中开辟一个空间
- 默认初始化。为对象的成员变量赋默认值,如String name = null; int age = 0;
- 显式初始化。如果在bean对象的定义中,定义成员变量时直接String name = "江总";这里就是显式初始化。
- 构造方法初始化。调用构造方法初始化。
- 将堆内存中的地址值赋值给左边的局部变量。其实就是把new Student()在堆空间中的地址给s。
一个对象的内存图
注意:由于这里的s是在main方法中定义的,当main方法执行完毕出栈后,JVM的垃圾回收机制会回收s指向堆空间。
两个对象的内存图
Student.class文件会在整个程序第一次尝试初始化Student时就加载进入方法区(JDK8以前),之后需要继续实例化Student时不需要重新加载方法区内的.class文件。
两个引用指向同一个对象
this关键字的内存原理
成员变量与局部变量的区别
字符串内存分析
字符串的多种实例化方式
串池StringTable(字符串常量池)
JDK7之前,StringTable在方法区中,从JDK7开始,它被移至堆内存中。
那既然字符串交由堆内存管理,为什么sout字符串的时候,打印出的不是地址值,而是真实值?
这是因为String类已经重写了toString方法。
直接赋值时的内存情况
重点是字符串常量池
手动使用构造方法的内存情况
如图,通过构造方法每次实例化新的字符串都会在堆内存中开辟全新的空间,这种方式创建就没有字符串常量池的事了。
“==”与equals方法
“==”号在比什么?要分为基本数据类型和引用数据类型讨论。
例1、当s1和s2都是直接创建的,首先是在字符串常量池中创建一个“abc”,并将引用值给s1,s2也是直接创建,串池监视到有“abc”,直接将该引用值给s2,所以这里s1 == s2 为true
例2、
StringBuilder对象
构造方法
成员方法
StringJoiner对象
字符串原理浅析
-
字符串存储的内存原理:
直接赋值,直接复用字符串常量池中的
new出来的不会复用,而是开辟一个全新的内存空间
-
“==”号比较的到底是什么?
基本数据类型比较数据值
引用数据类型比较地址值
-
字符串拼接的底层原理
情况一:等号的右边没有变量,都是字符串。
情况二:等号的右边有变量参与。
当程序执行到String s2 = s1 + "b";时,会先检查串池中有没有“b”,没有则添加,然后在堆内存中创建一个StringBuilder对象,利用其拼接好s1和“b”后,再创建一个String对象将值返回给s2,s3的创建同理。
所以在这个程序中,一次“+”就至少创建了两个对象,这也是为什么使用String对象拼接字符串效率很低的原因。
情况三:
这个情况不同于情况一,情况一是直接String s1 = "a" + "b" + "c";,由于触发字符串的优化机制,情况一就直接相当于String s1 = "abc";
下面这个情况在JDK不同版本有不同的处理机制。
当程序执行到String s4这里时:
JDK8之前,会做和情况二相近的处理,即这里会至少会有2个StringBuilder和2个String的创建。
JDK8,会对s1 + s2 + s3做一个预估,并用大概大小的数组去创建字符串。
但是依然不建议直接使用String去“+”来拼接字符串。
例题1:
这里也要分JDK版本进行分析:
JDK8之前:JVM会在堆内存中创建一个StringBuilder对象,然后调用其append方法完成拼接,拼接后,再调用其toString方法转换为String类型,而toString方法在底层也是new了一个String对象。
JDK8:系统会预估字符串拼接后的总大小,将要拼接的内容都放在数组中,此时也是产生了一个新的String对象。
所以无论是JDK8之前还是JDK8,这里打印的都是false。
例题2:
别忘了String s2 = "a" + "b" + "c";会触发字符串的优化机制,"a" + "b" + "c"直接就是串池中的“abc”了。
总结:
在字符串拼接过程中:
如果没有变量参与,都是字符串直接相加,编译之后就是拼接之后的结果,会复用串池中的字符串。
如果有变量参与,每一行拼接的代码,都会在内存中创建新的字符串,浪费内存。
-
StringBuilder为什么拼接效率远高于String?
StringBuilder是一个内容可变的容器。String拼接字符串虽然在底层也用到了StringBuilder,但是每次“+”都会创建一个全新的StringBuilder,所以拼接效率远低于StringBuilder。
-
StringBuilder源码分析
StringBuilder容器的默认容量是16,扩容规则是 老容量 * 2 + 2 = 新容量。
初始化StringBuilder时:
- 默认创建一个长度为16的字节数组
- 如果添加的内容长度小于16,直接存入
- 添加的内容大于16会按扩容规则扩容
- 如果扩容之后还不够,会以实际长度为准创建相应容量的字节数组
注意:不同JDK版本的StringBuilder初始化机制不同,建议自己翻源码。
JDK8源码阅读如下:
初始化时调用父类构造方法,capacity为16
父类构造方法为成员变量实例化大小
调用append方法
如果str为null,则返回拼接字符串"null",否则继续,minimumCapacity就是最小需求容量,成员变量count为0
如果最小需求容量大于初始容量(16),则走扩容机制
如果走扩容机制的话,进入newCapacity方法, 终于看到了*2+2的地方
如果扩容后的容量小于最小需求容量(扩容后还不够大),则直接将最小需求容量的值给这个newCapacity,最后的那个三目运算,其实就是在判断newCapacity的大小是否超级大:
newCapacity <= 0就表示int都溢出了!
MAX_ARRAY_SIZE就是int的最大值-8