[docs improve] Java基础常见知识&面试题总结(中)&Java 内存区域详解内容完善
@ -74,7 +74,7 @@ OSI 七层模型虽然失败了,但是却提供了很多不错的理论基础
|
||||
|
||||
**网络层负责为分组交换网上的不同主机提供通信服务。** 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报,简称数据报。
|
||||
|
||||
注意 ⚠️:**不要把运输层的“用户数据报 UDP”和网络层的“IP 数据报”弄混**。
|
||||
⚠️注意 :**不要把运输层的“用户数据报 UDP”和网络层的“IP 数据报”弄混**。
|
||||
|
||||
**网络层的还有一个任务就是选择合适的路由,使源主机运输层所传下来的分株,能通过网络层中的路由器找到目的主机。**
|
||||
|
||||
|
@ -190,7 +190,7 @@ Java 中的注释有三种:
|
||||
> - 在类,方法和变量修饰符中,从 JDK8 开始引入了默认方法,可以使用 `default` 关键字来定义一个方法的默认实现。
|
||||
> - 在访问控制中,如果一个方法前没有任何修饰符,则默认会有一个修饰符 `default`,但是这个修饰符加上了就会报错。
|
||||
|
||||
注意 ⚠️:虽然 `true`, `false`, 和 `null` 看起来像关键字但实际上他们是字面值,同时你也不可以作为标识符来使用。
|
||||
⚠️注意 :虽然 `true`, `false`, 和 `null` 看起来像关键字但实际上他们是字面值,同时你也不可以作为标识符来使用。
|
||||
|
||||
官方文档:[https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html)
|
||||
|
||||
@ -423,138 +423,6 @@ public class SuperSuperMan extends SuperMan {
|
||||
}
|
||||
```
|
||||
|
||||
### == 和 equals() 的区别
|
||||
|
||||
**`==`** 对于基本类型和引用类型的作用效果是不同的:
|
||||
|
||||
- 对于基本数据类型来说,`==` 比较的是值。
|
||||
- 对于引用数据类型来说,`==` 比较的是对象的内存地址。
|
||||
|
||||
> 因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
|
||||
|
||||
**`equals()`** 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。`equals()`方法存在于`Object`类中,而`Object`类是所有类的直接或间接父类,因此所有的类都有`equals()`方法。
|
||||
|
||||
`Object` 类 `equals()` 方法:
|
||||
|
||||
```java
|
||||
public boolean equals(Object obj) {
|
||||
return (this == obj);
|
||||
}
|
||||
```
|
||||
|
||||
`equals()` 方法存在两种使用情况:
|
||||
|
||||
- **类没有重写 `equals()`方法** :通过`equals()`比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 `Object`类`equals()`方法。
|
||||
- **类重写了 `equals()`方法** :一般我们都重写 `equals()`方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
|
||||
|
||||
举个例子(这里只是为了举例。实际上,你按照下面这种写法的话,像 IDEA 这种比较智能的 IDE 都会提示你将 `==` 换成 `equals()` ):
|
||||
|
||||
```java
|
||||
String a = new String("ab"); // a 为一个引用
|
||||
String b = new String("ab"); // b为另一个引用,对象的内容一样
|
||||
String aa = "ab"; // 放在常量池中
|
||||
String bb = "ab"; // 从常量池中查找
|
||||
System.out.println(aa == bb);// true
|
||||
System.out.println(a == b);// false
|
||||
System.out.println(a.equals(b));// true
|
||||
System.out.println(42 == 42.0);// true
|
||||
```
|
||||
|
||||
`String` 中的 `equals` 方法是被重写过的,因为 `Object` 的 `equals` 方法是比较的对象的内存地址,而 `String` 的 `equals` 方法比较的是对象的值。
|
||||
|
||||
当创建 `String` 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 `String` 对象。
|
||||
|
||||
`String`类`equals()`方法:
|
||||
|
||||
```java
|
||||
public boolean equals(Object anObject) {
|
||||
if (this == anObject) {
|
||||
return true;
|
||||
}
|
||||
if (anObject instanceof String) {
|
||||
String anotherString = (String)anObject;
|
||||
int n = value.length;
|
||||
if (n == anotherString.value.length) {
|
||||
char v1[] = value;
|
||||
char v2[] = anotherString.value;
|
||||
int i = 0;
|
||||
while (n-- != 0) {
|
||||
if (v1[i] != v2[i])
|
||||
return false;
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
### hashCode() 与 equals()
|
||||
|
||||
面试官可能会问你:“你重写过 `hashCode()` 和 `equals()`么?为什么重写 `equals()` 时必须重写 `hashCode()` 方法?”
|
||||
|
||||
一个非常基础的问题,面试中的重中之重,然而,很多求职者还是会回答不到点子上去。
|
||||
|
||||
#### hashCode() 有什么用?
|
||||
|
||||
`hashCode()` 的作用是获取哈希码(`int` 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
|
||||
|
||||
`hashCode()`定义在 JDK 的 `Object` 类中,这就意味着 Java 中的任何类都包含有 `hashCode()` 函数。另外需要注意的是: `Object` 的 `hashCode()` 方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。
|
||||
|
||||
```java
|
||||
public native int hashCode();
|
||||
```
|
||||
|
||||
散列表存储的是键值对(key-value),它的特点是:**能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)**
|
||||
|
||||
#### 为什么要有 hashCode?
|
||||
|
||||
我们以“`HashSet` 如何检查重复”为例子来说明为什么要有 `hashCode`?
|
||||
|
||||
下面这段内容摘自我的 Java 启蒙书《Head First Java》:
|
||||
|
||||
> 当你把对象加入 `HashSet` 时,`HashSet` 会先计算对象的 `hashCode` 值来判断对象加入的位置,同时也会与其他已经加入的对象的 `hashCode` 值作比较,如果没有相符的 `hashCode`,`HashSet` 会假设对象没有重复出现。但是如果发现有相同 `hashCode` 值的对象,这时会调用 `equals()` 方法来检查 `hashCode` 相等的对象是否真的相同。如果两者相同,`HashSet` 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 `equals` 的次数,相应就大大提高了执行速度。
|
||||
|
||||
其实, `hashCode()` 和 `equals()`都是用于比较两个对象是否相等。
|
||||
|
||||
**那为什么 JDK 还要同时提供这两个方法呢?**
|
||||
|
||||
这是因为在一些容器(比如 `HashMap`、`HashSet`)中,有了 `hashCode()` 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进`HastSet`的过程)!
|
||||
|
||||
我们在前面也提到了添加元素进`HastSet`的过程,如果 `HashSet` 在对比的时候,同样的 `hashCode` 有多个对象,它会继续使用 `equals()` 来判断是否真的相同。也就是说 `hashCode` 帮助我们大大缩小了查找成本。
|
||||
|
||||
**那为什么不只提供 `hashCode()` 方法呢?**
|
||||
|
||||
这是因为两个对象的`hashCode` 值相等并不代表两个对象就相等。
|
||||
|
||||
**那为什么两个对象有相同的 `hashCode` 值,它们也不一定是相等的?**
|
||||
|
||||
因为 `hashCode()` 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 `hashCode` )。
|
||||
|
||||
总结下来就是 :
|
||||
|
||||
- 如果两个对象的`hashCode` 值相等,那这两个对象不一定相等(哈希碰撞)。
|
||||
- 如果两个对象的`hashCode` 值相等并且`equals()`方法也返回 `true`,我们才认为这两个对象相等。
|
||||
- 如果两个对象的`hashCode` 值不相等,我们就可以直接认为这两个对象不相等。
|
||||
|
||||
相信大家看了我前面对 `hashCode()` 和 `equals()` 的介绍之后,下面这个问题已经难不倒你们了。
|
||||
|
||||
#### 为什么重写 equals() 时必须重写 hashCode() 方法?
|
||||
|
||||
因为两个相等的对象的 `hashCode` 值必须是相等。也就是说如果 `equals` 方法判断两个对象是相等的,那这两个对象的 `hashCode` 值也要相等。
|
||||
|
||||
如果重写 `equals()` 时没有重写 `hashCode()` 方法的话就可能会导致 `equals` 方法判断是相等的两个对象,`hashCode` 值却不相等。
|
||||
|
||||
**思考** :重写 `equals()` 时没有重写 `hashCode()` 方法的话,使用 `HashMap` 可能会出现什么问题。
|
||||
|
||||
**总结** :
|
||||
|
||||
- `equals` 方法判断两个对象是相等的,那这两个对象的 `hashCode` 值也要相等。
|
||||
- 两个对象有相同的 `hashCode` 值,他们也不一定是相等的(哈希碰撞)。
|
||||
|
||||
更多关于 `hashCode()` 和 `equals()` 的内容可以查看:[Java hashCode() 和 equals()的若干问题解答](https://www.cnblogs.com/skywang12345/p/3324958.html)
|
||||
|
||||
### 什么是可变长参数?
|
||||
|
||||
从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面的这个 `printVariable` 方法就可以接受 0 个或者多个参数。
|
||||
@ -670,13 +538,20 @@ Java 中有 8 种基本数据类型,分别为:
|
||||
|
||||
这八种基本类型都有对应的包装类分别为:`Byte`、`Short`、`Integer`、`Long`、`Float`、`Double`、`Character`、`Boolean` 。
|
||||
|
||||
包装类型不赋值就是 `Null` ,而基本类型有默认值且不是 `Null`。
|
||||
### 局部类型和包装类型的区别?
|
||||
|
||||
另外,这个问题建议还可以先从 JVM 层面来分析。
|
||||
- 包装类型不赋值就是 `null` ,而基本类型有默认值且不是 `null`。
|
||||
- 包装类型可用于泛型,而基本类型不可以。
|
||||
- 基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 `static` 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道对象实例都存在于堆中。
|
||||
- 相比于对象类型, 基本数据类型占用的空间非常小。
|
||||
|
||||
基本数据类型直接存放在 Java 虚拟机栈中的局部变量表中,而包装类型属于对象类型,我们知道对象实例都存在于堆中。相比于对象类型, 基本数据类型占用的空间非常小。
|
||||
⚠️注意 : **基本数据类型存放在栈中是一个常见的误区!** 基本数据类型的成员变量如果没有被 `static` 修饰的话,就存放在堆中。
|
||||
|
||||
> 《深入理解 Java 虚拟机》 :局部变量表主要存放了编译期可知的基本数据类型 **(boolean、byte、char、short、int、float、long、double)**、**对象引用**(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
|
||||
```java
|
||||
class BasicTypeVar{
|
||||
private int x;
|
||||
}
|
||||
```
|
||||
|
||||
### 包装类型的常量池技术了解么?
|
||||
|
||||
|
@ -220,35 +220,221 @@ System.out.println(person1.getAddress() == person1Copy.getAddress());
|
||||
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
|
||||
|
||||
```java
|
||||
public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
|
||||
|
||||
public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
|
||||
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。
|
||||
|
||||
protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
|
||||
|
||||
public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。
|
||||
|
||||
public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
|
||||
|
||||
public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
|
||||
|
||||
public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。
|
||||
|
||||
public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。
|
||||
|
||||
public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
|
||||
|
||||
protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作
|
||||
/**
|
||||
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
|
||||
*/
|
||||
public final native Class<?> getClass()
|
||||
/**
|
||||
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
|
||||
*/
|
||||
public native int hashCode()
|
||||
/**
|
||||
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
|
||||
*/
|
||||
public boolean equals(Object obj)
|
||||
/**
|
||||
* naitive 方法,用于创建并返回当前对象的一份拷贝。
|
||||
*/
|
||||
protected native Object clone() throws CloneNotSupportedException
|
||||
/**
|
||||
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
|
||||
*/
|
||||
public String toString()
|
||||
/**
|
||||
* native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
|
||||
*/
|
||||
public final native void notify()
|
||||
/**
|
||||
* native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
|
||||
*/
|
||||
public final native void notifyAll()
|
||||
/**
|
||||
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
|
||||
*/
|
||||
public final native void wait(long timeout) throws InterruptedException
|
||||
/**
|
||||
* 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。。
|
||||
*/
|
||||
public final void wait(long timeout, int nanos) throws InterruptedException
|
||||
/**
|
||||
* 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
|
||||
*/
|
||||
public final void wait() throws InterruptedException
|
||||
/**
|
||||
* 实例被垃圾回收器回收的时候触发的操作
|
||||
*/
|
||||
protected void finalize() throws Throwable { }
|
||||
```
|
||||
|
||||
#### == 和 equals() 的区别
|
||||
|
||||
**`==`** 对于基本类型和引用类型的作用效果是不同的:
|
||||
|
||||
- 对于基本数据类型来说,`==` 比较的是值。
|
||||
- 对于引用数据类型来说,`==` 比较的是对象的内存地址。
|
||||
|
||||
> 因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
|
||||
|
||||
**`equals()`** 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。`equals()`方法存在于`Object`类中,而`Object`类是所有类的直接或间接父类,因此所有的类都有`equals()`方法。
|
||||
|
||||
`Object` 类 `equals()` 方法:
|
||||
|
||||
```java
|
||||
public boolean equals(Object obj) {
|
||||
return (this == obj);
|
||||
}
|
||||
```
|
||||
|
||||
`equals()` 方法存在两种使用情况:
|
||||
|
||||
- **类没有重写 `equals()`方法** :通过`equals()`比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 `Object`类`equals()`方法。
|
||||
- **类重写了 `equals()`方法** :一般我们都重写 `equals()`方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
|
||||
|
||||
举个例子(这里只是为了举例。实际上,你按照下面这种写法的话,像 IDEA 这种比较智能的 IDE 都会提示你将 `==` 换成 `equals()` ):
|
||||
|
||||
```java
|
||||
String a = new String("ab"); // a 为一个引用
|
||||
String b = new String("ab"); // b为另一个引用,对象的内容一样
|
||||
String aa = "ab"; // 放在常量池中
|
||||
String bb = "ab"; // 从常量池中查找
|
||||
System.out.println(aa == bb);// true
|
||||
System.out.println(a == b);// false
|
||||
System.out.println(a.equals(b));// true
|
||||
System.out.println(42 == 42.0);// true
|
||||
```
|
||||
|
||||
`String` 中的 `equals` 方法是被重写过的,因为 `Object` 的 `equals` 方法是比较的对象的内存地址,而 `String` 的 `equals` 方法比较的是对象的值。
|
||||
|
||||
当创建 `String` 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 `String` 对象。
|
||||
|
||||
`String`类`equals()`方法:
|
||||
|
||||
```java
|
||||
public boolean equals(Object anObject) {
|
||||
if (this == anObject) {
|
||||
return true;
|
||||
}
|
||||
if (anObject instanceof String) {
|
||||
String anotherString = (String)anObject;
|
||||
int n = value.length;
|
||||
if (n == anotherString.value.length) {
|
||||
char v1[] = value;
|
||||
char v2[] = anotherString.value;
|
||||
int i = 0;
|
||||
while (n-- != 0) {
|
||||
if (v1[i] != v2[i])
|
||||
return false;
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
#### hashCode() 有什么用?
|
||||
|
||||
`hashCode()` 的作用是获取哈希码(`int` 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
|
||||
|
||||
`hashCode()`定义在 JDK 的 `Object` 类中,这就意味着 Java 中的任何类都包含有 `hashCode()` 函数。另外需要注意的是: `Object` 的 `hashCode()` 方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。
|
||||
|
||||
```java
|
||||
public native int hashCode();
|
||||
```
|
||||
|
||||
散列表存储的是键值对(key-value),它的特点是:**能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)**
|
||||
|
||||
#### 为什么要有 hashCode?
|
||||
|
||||
我们以“`HashSet` 如何检查重复”为例子来说明为什么要有 `hashCode`?
|
||||
|
||||
下面这段内容摘自我的 Java 启蒙书《Head First Java》:
|
||||
|
||||
> 当你把对象加入 `HashSet` 时,`HashSet` 会先计算对象的 `hashCode` 值来判断对象加入的位置,同时也会与其他已经加入的对象的 `hashCode` 值作比较,如果没有相符的 `hashCode`,`HashSet` 会假设对象没有重复出现。但是如果发现有相同 `hashCode` 值的对象,这时会调用 `equals()` 方法来检查 `hashCode` 相等的对象是否真的相同。如果两者相同,`HashSet` 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 `equals` 的次数,相应就大大提高了执行速度。
|
||||
|
||||
其实, `hashCode()` 和 `equals()`都是用于比较两个对象是否相等。
|
||||
|
||||
**那为什么 JDK 还要同时提供这两个方法呢?**
|
||||
|
||||
这是因为在一些容器(比如 `HashMap`、`HashSet`)中,有了 `hashCode()` 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进`HastSet`的过程)!
|
||||
|
||||
我们在前面也提到了添加元素进`HastSet`的过程,如果 `HashSet` 在对比的时候,同样的 `hashCode` 有多个对象,它会继续使用 `equals()` 来判断是否真的相同。也就是说 `hashCode` 帮助我们大大缩小了查找成本。
|
||||
|
||||
**那为什么不只提供 `hashCode()` 方法呢?**
|
||||
|
||||
这是因为两个对象的`hashCode` 值相等并不代表两个对象就相等。
|
||||
|
||||
**那为什么两个对象有相同的 `hashCode` 值,它们也不一定是相等的?**
|
||||
|
||||
因为 `hashCode()` 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 `hashCode` )。
|
||||
|
||||
总结下来就是 :
|
||||
|
||||
- 如果两个对象的`hashCode` 值相等,那这两个对象不一定相等(哈希碰撞)。
|
||||
- 如果两个对象的`hashCode` 值相等并且`equals()`方法也返回 `true`,我们才认为这两个对象相等。
|
||||
- 如果两个对象的`hashCode` 值不相等,我们就可以直接认为这两个对象不相等。
|
||||
|
||||
相信大家看了我前面对 `hashCode()` 和 `equals()` 的介绍之后,下面这个问题已经难不倒你们了。
|
||||
|
||||
#### 为什么重写 equals() 时必须重写 hashCode() 方法?
|
||||
|
||||
因为两个相等的对象的 `hashCode` 值必须是相等。也就是说如果 `equals` 方法判断两个对象是相等的,那这两个对象的 `hashCode` 值也要相等。
|
||||
|
||||
如果重写 `equals()` 时没有重写 `hashCode()` 方法的话就可能会导致 `equals` 方法判断是相等的两个对象,`hashCode` 值却不相等。
|
||||
|
||||
**思考** :重写 `equals()` 时没有重写 `hashCode()` 方法的话,使用 `HashMap` 可能会出现什么问题。
|
||||
|
||||
**总结** :
|
||||
|
||||
- `equals` 方法判断两个对象是相等的,那这两个对象的 `hashCode` 值也要相等。
|
||||
- 两个对象有相同的 `hashCode` 值,他们也不一定是相等的(哈希碰撞)。
|
||||
|
||||
更多关于 `hashCode()` 和 `equals()` 的内容可以查看:[Java hashCode() 和 equals()的若干问题解答](https://www.cnblogs.com/skywang12345/p/3324958.html)
|
||||
|
||||
### String
|
||||
|
||||
#### String、StringBuffer、StringBuilder 的区别?String 为什么是不可变的?
|
||||
#### String、StringBuffer、StringBuilder 的区别?
|
||||
|
||||
**可变性**
|
||||
|
||||
简单的来说:`String` 类中使用 `final` 关键字修饰字符数组来保存字符串,~~所以`String` 对象是不可变的。~~
|
||||
`String` 是不可变的(后面会详细分析原因)。
|
||||
|
||||
`StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中也是使用字符数组保存字符串,不过没有使用 `final` 和 `private` 关键字修饰,最关键的是这个 `AbstractStringBuilder` 类还提供了很多修改字符串的方法比如 `append` 方法。
|
||||
|
||||
```java
|
||||
abstract class AbstractStringBuilder implements Appendable, CharSequence {
|
||||
char[] value;
|
||||
public AbstractStringBuilder append(String str) {
|
||||
if (str == null)
|
||||
return appendNull();
|
||||
int len = str.length();
|
||||
ensureCapacityInternal(count + len);
|
||||
str.getChars(0, len, value, count);
|
||||
count += len;
|
||||
return this;
|
||||
}
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
**线程安全性**
|
||||
|
||||
`String` 中的对象是不可变的,也就可以理解为常量,线程安全。`AbstractStringBuilder` 是 `StringBuilder` 与 `StringBuffer` 的公共父类,定义了一些字符串的基本操作,如 `expandCapacity`、`append`、`insert`、`indexOf` 等公共方法。`StringBuffer` 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。`StringBuilder` 并没有对方法进行加同步锁,所以是非线程安全的。
|
||||
|
||||
**性能**
|
||||
|
||||
每次对 `String` 类型进行改变的时候,都会生成一个新的 `String` 对象,然后将指针指向新的 `String` 对象。`StringBuffer` 每次都会对 `StringBuffer` 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 `StringBuilder` 相比使用 `StringBuffer` 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
|
||||
|
||||
**对于三者使用的总结:**
|
||||
|
||||
1. 操作少量的数据: 适用 `String`
|
||||
2. 单线程操作字符串缓冲区下操作大量数据: 适用 `StringBuilder`
|
||||
3. 多线程操作字符串缓冲区下操作大量数据: 适用 `StringBuffer`
|
||||
|
||||
#### String 为什么是不可变的?
|
||||
|
||||
`String` 类中使用 `final` 关键字修饰字符数组来保存字符串,~~所以`String` 对象是不可变的。~~
|
||||
|
||||
```java
|
||||
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
|
||||
@ -283,7 +469,7 @@ public final class String implements java.io.Serializable, Comparable<String>, C
|
||||
>
|
||||
> **Java 9 为何要将 `String` 的底层实现由 `char[]` 改成了 `byte[]` ?**
|
||||
>
|
||||
> 新版的 String 其实支持两个编码方案: Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,`byte` 占一个字节(8位),`char` 占用2个字节(16),`byte` 相较 `char` 节省一半的内存空间。
|
||||
> 新版的 String 其实支持两个编码方案: Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,`byte` 占一个字节(8 位),`char` 占用 2 个字节(16),`byte` 相较 `char` 节省一半的内存空间。
|
||||
>
|
||||
> JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符。
|
||||
>
|
||||
@ -293,38 +479,6 @@ public final class String implements java.io.Serializable, Comparable<String>, C
|
||||
>
|
||||
> 这是官方的介绍:https://openjdk.java.net/jeps/254 。
|
||||
|
||||
`StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中也是使用字符数组保存字符串,不过没有使用 `final` 和 `private` 关键字修饰,最关键的是这个 `AbstractStringBuilder` 类还提供了很多修改字符串的方法比如 `append` 方法。
|
||||
|
||||
```java
|
||||
abstract class AbstractStringBuilder implements Appendable, CharSequence {
|
||||
char[] value;
|
||||
public AbstractStringBuilder append(String str) {
|
||||
if (str == null)
|
||||
return appendNull();
|
||||
int len = str.length();
|
||||
ensureCapacityInternal(count + len);
|
||||
str.getChars(0, len, value, count);
|
||||
count += len;
|
||||
return this;
|
||||
}
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
**线程安全性**
|
||||
|
||||
`String` 中的对象是不可变的,也就可以理解为常量,线程安全。`AbstractStringBuilder` 是 `StringBuilder` 与 `StringBuffer` 的公共父类,定义了一些字符串的基本操作,如 `expandCapacity`、`append`、`insert`、`indexOf` 等公共方法。`StringBuffer` 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。`StringBuilder` 并没有对方法进行加同步锁,所以是非线程安全的。
|
||||
|
||||
**性能**
|
||||
|
||||
每次对 `String` 类型进行改变的时候,都会生成一个新的 `String` 对象,然后将指针指向新的 `String` 对象。`StringBuffer` 每次都会对 `StringBuffer` 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 `StringBuilder` 相比使用 `StringBuffer` 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
|
||||
|
||||
**对于三者使用的总结:**
|
||||
|
||||
1. 操作少量的数据: 适用 `String`
|
||||
2. 单线程操作字符串缓冲区下操作大量数据: 适用 `StringBuilder`
|
||||
3. 多线程操作字符串缓冲区下操作大量数据: 适用 `StringBuffer`
|
||||
|
||||
#### 字符串拼接用“+” 还是 StringBuilder?
|
||||
|
||||
Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的元素符。
|
||||
@ -374,3 +528,205 @@ System.out.println(aa==bb);// true
|
||||
JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区。JDK1.7 的时候,字符串常量池被从方法区拿到了堆中。
|
||||
|
||||
你可以在 JVM 部分找到更多关于字符串常量池的介绍。
|
||||
|
||||
#### String s1 = new String("abc");这句话创建了几个字符串对象?
|
||||
|
||||
会创建 1 或 2 个字符串:
|
||||
|
||||
- 如果字符串常量池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。
|
||||
- 如果字符串常量池中没有字符串常量“abc”,那么它将首先在字符串常量池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。
|
||||
|
||||
**验证** :
|
||||
|
||||
```java
|
||||
String s1 = new String("abc");// 堆内存的地址值
|
||||
String s2 = "abc";
|
||||
System.out.println(s1 == s2);// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
|
||||
System.out.println(s1.equals(s2));// 输出 true
|
||||
```
|
||||
|
||||
**结果** :
|
||||
|
||||
```
|
||||
false
|
||||
true
|
||||
```
|
||||
|
||||
#### String 类型的变量和常量做“+”运算时发生了什么?
|
||||
|
||||
一个非常常见的面试题。
|
||||
|
||||
先来看字符串不加 `final` 关键字拼接的情况(JDK1.8):
|
||||
|
||||
```java
|
||||
String str1 = "str";
|
||||
String str2 = "ing";
|
||||
String str3 = "str" + "ing";//常量池中的对象
|
||||
String str4 = str1 + str2; //在堆上创建的新的对象
|
||||
String str5 = "string";//常量池中的对象
|
||||
System.out.println(str3 == str4);//false
|
||||
System.out.println(str3 == str5);//true
|
||||
System.out.println(str4 == str5);//false
|
||||
```
|
||||
|
||||
> **注意** :比较 String 字符串的值是否相等,可以使用 `equals()` 方法。 `String` 中的 `equals` 方法是被重写过的。 `Object` 的 `equals` 方法是比较的对象的内存地址,而 `String` 的 `equals` 方法比较的是字符串的值是否相等。如果你使用 `==` 比较两个字符串是否相等的话,IDEA 还是提示你使用 `equals()` 方法替换。
|
||||
|
||||

|
||||
|
||||
> 对于基本数据类型来说,== 比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。
|
||||
|
||||
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。
|
||||
|
||||
> **字符串常量池** 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
|
||||
>
|
||||
> ```java
|
||||
> String aa = "ab"; // 放在常量池中
|
||||
> String bb = "ab"; // 从常量池中查找
|
||||
> System.out.println(aa==bb);// true
|
||||
> ```
|
||||
>
|
||||
> JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区。JDK1.7 的时候,字符串常量池被从方法区拿到了堆中。
|
||||
|
||||
并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
|
||||
|
||||
> 在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 **常量折叠(Constant Folding)** 的代码优化。《深入理解 Java 虚拟机》中是也有介绍到:
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
|
||||
>
|
||||
> 对于 `String str3 = "str" + "ing";` 编译器会给你优化成 `String str3 = "string";` 。
|
||||
>
|
||||
> 并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
|
||||
>
|
||||
> - 基本数据类型( `byte`、`boolean`、`short`、`char`、`int`、`float`、`long`、`double`)以及字符串常量。
|
||||
> - `final` 修饰的基本数据类型和字符串变量
|
||||
> - 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、\>>、\>>> )
|
||||
|
||||
因此,`str1` 、 `str2` 、 `str3` 都属于字符串常量池中的对象。
|
||||
|
||||
引用的值在程序编译期是无法确定的,编译器无法对其进行优化。
|
||||
|
||||
对象引用和“+”的字符串拼接方式,实际上是通过 `StringBuilder` 调用 `append()` 方法实现的,拼接完成之后调用 `toString()` 得到一个 `String` 对象 。
|
||||
|
||||
```java
|
||||
String str4 = new StringBuilder().append(str1).append(str2).toString();
|
||||
```
|
||||
|
||||
因此,`str4` 并不是字符串常量池中存在的对象,属于堆上的新对象。
|
||||
|
||||
我画了一个图帮助理解:
|
||||
|
||||

|
||||
|
||||
我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 `StringBuilder` 或者 `StringBuffer`。
|
||||
|
||||
不过,字符串使用 `final` 关键字声明之后,可以让编译器当做常量来处理。
|
||||
|
||||
```java
|
||||
final String str1 = "str";
|
||||
final String str2 = "ing";
|
||||
// 下面两个表达式其实是等价的
|
||||
String c = "str" + "ing";// 常量池中的对象
|
||||
String d = str1 + str2; // 常量池中的对象
|
||||
System.out.println(c == d);// true
|
||||
```
|
||||
|
||||
被 `final` 关键字修改之后的 `String` 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。
|
||||
|
||||
如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。
|
||||
|
||||
示例代码如下(`str2` 在运行时才能确定其值):
|
||||
|
||||
```java
|
||||
final String str1 = "str";
|
||||
final String str2 = getStr();
|
||||
String c = "str" + "ing";// 常量池中的对象
|
||||
String d = str1 + str2; // 在堆上创建的新的对象
|
||||
System.out.println(c == d);// false
|
||||
public static String getStr() {
|
||||
return "ing";
|
||||
}
|
||||
```
|
||||
|
||||
**我们再来看一个类似的问题!**
|
||||
|
||||
```java
|
||||
String str1 = "abcd";
|
||||
String str2 = new String("abcd");
|
||||
String str3 = new String("abcd");
|
||||
System.out.println(str1==str2);
|
||||
System.out.println(str2==str3);
|
||||
```
|
||||
|
||||
上面的代码运行之后会输出什么呢?
|
||||
|
||||
答案是:
|
||||
|
||||
```
|
||||
false
|
||||
false
|
||||
```
|
||||
|
||||
**这是为什么呢?**
|
||||
|
||||
我们先来看下面这种创建字符串对象的方式:
|
||||
|
||||
```java
|
||||
// 从字符串常量池中拿对象
|
||||
String str1 = "abcd";
|
||||
```
|
||||
|
||||
这种情况下,jvm 会先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd";
|
||||
|
||||
因此,`str1` 指向的是字符串常量池的对象。
|
||||
|
||||
我们再来看下面这种创建字符串对象的方式:
|
||||
|
||||
```java
|
||||
// 直接在堆内存空间创建一个新的对象。
|
||||
String str2 = new String("abcd");
|
||||
String str3 = new String("abcd");
|
||||
```
|
||||
|
||||
**只要使用 new 的方式创建对象,便需要创建新的对象** 。
|
||||
|
||||
使用 new 的方式创建对象的方式如下,可以简单概括为 3 步:
|
||||
|
||||
1. 在堆中创建一个字符串对象
|
||||
2. 检查字符串常量池中是否有和 new 的字符串值相等的字符串常量
|
||||
3. 如果没有的话需要在字符串常量池中也创建一个值相等的字符串常量,如果有的话,就直接返回堆中的字符串实例对象地址。
|
||||
|
||||
因此,`str2` 和 `str3` 都是在堆中新创建的对象。
|
||||
|
||||
**字符串常量池比较特殊,它的主要使用方法有两种:**
|
||||
|
||||
1. 直接使用双引号声明出来的 `String` 对象会直接存储在常量池中。
|
||||
2. 如果不是用双引号声明的 `String` 对象,使用 `String` 提供的 `intern()` 方法也有同样的效果。`String.intern()` 是一个 Native 方法,它的作用是:如果字符串常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7 之前(不包含 1.7)的处理方式是在常量池中创建与此 `String` 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7 以及之后,字符串常量池被从方法区拿到了堆中,jvm 不会在常量池中创建该对象,而是将堆中这个对象的引用直接放到常量池中,减少不必要的内存开销。
|
||||
|
||||
示例代码如下(JDK 1.8) :
|
||||
|
||||
```java
|
||||
String s1 = "Javatpoint";
|
||||
String s2 = s1.intern();
|
||||
String s3 = new String("Javatpoint");
|
||||
String s4 = s3.intern();
|
||||
System.out.println(s1==s2); // True
|
||||
System.out.println(s1==s3); // False
|
||||
System.out.println(s1==s4); // True
|
||||
System.out.println(s2==s3); // False
|
||||
System.out.println(s2==s4); // True
|
||||
System.out.println(s3==s4); // False
|
||||
```
|
||||
|
||||
**总结** :
|
||||
|
||||
1. 对于基本数据类型来说,==比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。
|
||||
2. 在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 **常量折叠(Constant Folding)** 的代码优化。常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
|
||||
3. 一般来说,我们要尽量避免通过 new 的方式创建字符串。使用双引号声明的 `String` 对象( `String s1 = "java"` )更利于让编译器有机会优化我们的代码,同时也更易于阅读。
|
||||
4. 被 `final` 关键字修改之后的 `String` 会被编译器当做常量来处理,编译器程序编译期就可以确定它的值,其效果就相当于访问常量。
|
||||
|
||||
## 参考
|
||||
|
||||
- 深入解析 String#intern<https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html>
|
||||
- R 大(RednaxelaFX)关于常量折叠的回答:https://www.zhihu.com/question/55976094/answer/147302764
|
||||
|
@ -5,7 +5,9 @@ tag:
|
||||
- JVM
|
||||
---
|
||||
|
||||
如果没有特殊说明,都是针对的是 HotSpot 虚拟机。
|
||||
> 如果没有特殊说明,都是针对的是 HotSpot 虚拟机。
|
||||
>
|
||||
> 本文基于《深入理解Java虚拟机:JVM高级特性与最佳实践》进行总结补充。
|
||||
|
||||
## 写在前面 (常见面试题)
|
||||
|
||||
@ -20,19 +22,19 @@ tag:
|
||||
- **String 类和常量池**
|
||||
- **8 种基本类型的包装类和常量池**
|
||||
|
||||
## 一 概述
|
||||
## 前言
|
||||
|
||||
对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像 C/C++程序开发程序员这样为每一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。
|
||||
|
||||
## 二 运行时数据区域
|
||||
## 运行时数据区域
|
||||
|
||||
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK 1.8 和之前的版本略有不同,下面会介绍到。
|
||||
|
||||
**JDK 1.8 之前:**
|
||||
**JDK 1.8 之前** :
|
||||
|
||||

|
||||
|
||||
**JDK 1.8 :**
|
||||
**JDK 1.8 ** :
|
||||
|
||||

|
||||
|
||||
@ -48,28 +50,33 @@ Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成
|
||||
- 方法区
|
||||
- 直接内存 (非运行时数据区的一部分)
|
||||
|
||||
### 2.1 程序计数器
|
||||
### 程序计数器
|
||||
|
||||
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。**字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。**
|
||||
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
|
||||
|
||||
另外,**为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。**
|
||||
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
|
||||
|
||||
**从上面的介绍中我们知道程序计数器主要有两个作用:**
|
||||
从上面的介绍中我们知道了程序计数器主要有两个作用:
|
||||
|
||||
1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
|
||||
2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
|
||||
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
|
||||
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
|
||||
|
||||
**注意:程序计数器是唯一一个不会出现 `OutOfMemoryError` 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。**
|
||||
⚠️注意 :程序计数器是唯一一个不会出现 `OutOfMemoryError` 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
|
||||
|
||||
### 2.2 Java 虚拟机栈
|
||||
### Java 虚拟机栈
|
||||
|
||||
**与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。**
|
||||
与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
|
||||
|
||||
**Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。** (实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)
|
||||
Java 内存可以粗糙的区分为:
|
||||
|
||||
**局部变量表主要存放了编译期可知的各种数据类型**(boolean、byte、char、short、int、float、long、double)、**对象引用**(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
|
||||
- 堆内存(Heap)
|
||||
- 栈内存 (Stack)
|
||||
|
||||
**Java 虚拟机栈会出现两种错误:`StackOverFlowError` 和 `OutOfMemoryError`。**
|
||||
栈也就是 Java 虚拟机栈,由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。
|
||||
|
||||
局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
|
||||
|
||||
Java 虚拟机栈会出现两种错误:
|
||||
|
||||
- **`StackOverFlowError`:** 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
|
||||
- **`OutOfMemoryError`:** Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出`OutOfMemoryError`异常。
|
||||
@ -78,7 +85,7 @@ Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成
|
||||
|
||||
Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
|
||||
|
||||
**扩展:那么方法/函数如何调用?**
|
||||
🌈 拓展一下: **方法/函数如何调用?**
|
||||
|
||||
Java 栈可以类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。
|
||||
|
||||
@ -89,7 +96,7 @@ Java 方法有两种返回方式:
|
||||
|
||||
不管哪种返回方式都会导致栈帧被弹出。
|
||||
|
||||
### 2.3 本地方法栈
|
||||
### 本地方法栈
|
||||
|
||||
和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
|
||||
|
||||
@ -97,13 +104,13 @@ Java 方法有两种返回方式:
|
||||
|
||||
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 `StackOverFlowError` 和 `OutOfMemoryError` 两种错误。
|
||||
|
||||
### 2.4 堆
|
||||
### 堆
|
||||
|
||||
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。**
|
||||
|
||||
Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
|
||||
|
||||
Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(Garbage Collected Heap)**。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。**进一步划分的目的是更好地回收内存,或者更快地分配内存。**
|
||||
Java 堆是垃圾收集器管理的主要区域,因此也被称作 **GC 堆(Garbage Collected Heap)**。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。
|
||||
|
||||
在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:
|
||||
|
||||
@ -145,17 +152,17 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(G
|
||||
2. **`java.lang.OutOfMemoryError: Java heap space`** :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过`-Xmx`参数配置,若没有特别配置,将会使用默认值,详见:[Default Java 8 max heap size](https://stackoverflow.com/questions/28272923/default-xmxsize-in-java-8-max-heap-size))
|
||||
3. ......
|
||||
|
||||
### 2.5 方法区
|
||||
### 方法区
|
||||
|
||||
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 **Java 虚拟机规范把方法区描述为堆的一个逻辑部分**,但是它却有一个别名叫做 **Non-Heap(非堆)**,目的应该是与 Java 堆区分开来。
|
||||
|
||||
方法区也被称为永久代。很多人都会分不清方法区和永久代的关系,为此我也查阅了文献。
|
||||
|
||||
#### 2.5.1 方法区和永久代的关系
|
||||
#### 方法区和永久代的关系
|
||||
|
||||
> 《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 **方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。** 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。
|
||||
|
||||
#### 2.5.2 常用参数
|
||||
#### 方法区常用参数
|
||||
|
||||
JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小
|
||||
|
||||
@ -177,23 +184,23 @@ JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1
|
||||
|
||||
与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
|
||||
|
||||
#### 2.5.3 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
|
||||
#### 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
|
||||
|
||||
下图来自《深入理解 Java 虚拟机》第 3 版 2.2.5
|
||||
|
||||

|
||||
|
||||
1. 整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
|
||||
1、整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
|
||||
|
||||
> 当元空间溢出时会得到如下错误: `java.lang.OutOfMemoryError: MetaSpace`
|
||||
> 当元空间溢出时会得到如下错误: `java.lang.OutOfMemoryError: MetaSpace`
|
||||
|
||||
你可以使用 `-XX:MaxMetaspaceSize` 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。`-XX:MetaspaceSize` 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
|
||||
|
||||
2. 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 `MaxPermSize` 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
|
||||
2、元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 `MaxPermSize` 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
|
||||
|
||||
3. 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。
|
||||
3、在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。
|
||||
|
||||
### 2.6 运行时常量池
|
||||
### 运行时常量池
|
||||
|
||||
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)
|
||||
|
||||
@ -203,25 +210,25 @@ JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1
|
||||
|
||||
> **🐛 修正(参见:[issue747](https://github.com/Snailclimb/JavaGuide/issues/747),[reference](https://blog.csdn.net/q5706503/article/details/84640762))** :
|
||||
>
|
||||
> 1. **JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代**
|
||||
> 2. **JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代** 。
|
||||
> 3. **JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)**
|
||||
> 1. JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代
|
||||
> 2. **JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代 。**
|
||||
> 3. JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
|
||||
|
||||
相关问题:JVM 常量池中存储的是对象还是引用呢?: https://www.zhihu.com/question/57109429/answer/151717241 by RednaxelaFX
|
||||
相关问题:[JVM 常量池中存储的是对象还是引用呢? - RednaxelaFX - 知乎](https://www.zhihu.com/question/57109429/answer/151717241)
|
||||
|
||||
### 2.7 直接内存
|
||||
### 直接内存
|
||||
|
||||
**直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。**
|
||||
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
|
||||
|
||||
JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**通道(Channel)**与**缓存区(Buffer)**的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为**避免了在 Java 堆和 Native 堆之间来回复制数据**。
|
||||
|
||||
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
|
||||
|
||||
## 三 HotSpot 虚拟机对象探秘
|
||||
## HotSpot 虚拟机对象探秘
|
||||
|
||||
通过上面的介绍我们大概知道了虚拟机的内存情况,下面我们来详细的了解一下 HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程。
|
||||
|
||||
### 3.1 对象的创建
|
||||
### 对象的创建
|
||||
|
||||
下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。
|
||||

|
||||
@ -234,11 +241,18 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**
|
||||
|
||||
在**类加载检查**通过后,接下来虚拟机将为新生对象**分配内存**。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。**分配方式**有 **“指针碰撞”** 和 **“空闲列表”** 两种,**选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定**。
|
||||
|
||||
**内存分配的两种方式:(补充内容,需要掌握)**
|
||||
**内存分配的两种方式** (补充内容,需要掌握):
|
||||
|
||||
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的
|
||||
- 指针碰撞 :
|
||||
- 适用场合 :堆内存规整(即没有内存碎片)的情况下。
|
||||
- 原理 :过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
|
||||
- 使用该分配方式的 GC 收集器:Serial, ParNew
|
||||
- 空闲列表 :
|
||||
- 适用场合 : 堆内存不规整的情况下。
|
||||
- 原理 :虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
|
||||
- 使用该分配方式的 GC 收集器:CMS
|
||||
|
||||

|
||||
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。
|
||||
|
||||
**内存分配并发问题(补充内容,需要掌握)**
|
||||
|
||||
@ -259,7 +273,7 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**
|
||||
|
||||
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,`<init>` 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 `<init>` 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
|
||||
|
||||
### 3.2 对象的内存布局
|
||||
### 对象的内存布局
|
||||
|
||||
在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:**对象头**、**实例数据**和**对齐填充**。
|
||||
|
||||
@ -269,325 +283,23 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**
|
||||
|
||||
**对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。** 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
|
||||
|
||||
### 3.3 对象的访问定位
|
||||
### 对象的访问定位
|
||||
|
||||
建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有**① 使用句柄**和**② 直接指针**两种:
|
||||
建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有:**使用句柄**、**直接指针**。
|
||||
|
||||
1. **句柄:** 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
|
||||
#### 句柄
|
||||
|
||||

|
||||
如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
|
||||
|
||||
2. **直接指针:** 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
|
||||

|
||||
|
||||
#### 直接指针
|
||||
|
||||
如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
|
||||
|
||||

|
||||
|
||||
**这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。**
|
||||
|
||||
## 四 重点补充内容
|
||||
|
||||
### 4.1 字符串常量池常见问题
|
||||
|
||||
我们先来看一个非常常见的面试题:**String 类型的变量和常量做“+”运算时发生了什么?** 。
|
||||
|
||||
先来看字符串不加 `final` 关键字拼接的情况(JDK1.8):
|
||||
|
||||
```java
|
||||
String str1 = "str";
|
||||
String str2 = "ing";
|
||||
String str3 = "str" + "ing";//常量池中的对象
|
||||
String str4 = str1 + str2; //在堆上创建的新的对象
|
||||
String str5 = "string";//常量池中的对象
|
||||
System.out.println(str3 == str4);//false
|
||||
System.out.println(str3 == str5);//true
|
||||
System.out.println(str4 == str5);//false
|
||||
```
|
||||
|
||||
> **注意** :比较 String 字符串的值是否相等,可以使用 `equals()` 方法。 `String` 中的 `equals` 方法是被重写过的。 `Object` 的 `equals` 方法是比较的对象的内存地址,而 `String` 的 `equals` 方法比较的是字符串的值是否相等。如果你使用 `==` 比较两个字符串是否相等的话,IDEA 还是提示你使用 `equals()` 方法替换。
|
||||
|
||||

|
||||
|
||||
> 对于基本数据类型来说,== 比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。
|
||||
|
||||
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。
|
||||
|
||||
> **字符串常量池** 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
|
||||
>
|
||||
> ```java
|
||||
> String aa = "ab"; // 放在常量池中
|
||||
> String bb = "ab"; // 从常量池中查找
|
||||
> System.out.println(aa==bb);// true
|
||||
> ```
|
||||
>
|
||||
> JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区。JDK1.7 的时候,字符串常量池被从方法区拿到了堆中。
|
||||
|
||||
并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
|
||||
|
||||
> 在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 **常量折叠(Constant Folding)** 的代码优化。《深入理解 Java 虚拟机》中是也有介绍到:
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
|
||||
>
|
||||
> 对于 `String str3 = "str" + "ing";` 编译器会给你优化成 `String str3 = "string";` 。
|
||||
>
|
||||
> 并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
|
||||
>
|
||||
> 1. 基本数据类型(byte、boolean、short、char、int、float、long、double)以及字符串常量
|
||||
> 2. `final` 修饰的基本数据类型和字符串变量
|
||||
> 3. 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、\>>、\>>> )
|
||||
|
||||
因此,`str1` 、 `str2` 、 `str3` 都属于字符串常量池中的对象。
|
||||
|
||||
引用的值在程序编译期是无法确定的,编译器无法对其进行优化。
|
||||
|
||||
对象引用和“+”的字符串拼接方式,实际上是通过 `StringBuilder` 调用 `append()` 方法实现的,拼接完成之后调用 `toString()` 得到一个 `String` 对象 。
|
||||
|
||||
```java
|
||||
String str4 = new StringBuilder().append(str1).append(str2).toString();
|
||||
```
|
||||
|
||||
因此,`str4` 并不是字符串常量池中存在的对象,属于堆上的新对象。
|
||||
|
||||
我画了一个图帮助理解:
|
||||
|
||||

|
||||
|
||||
我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 `StringBuilder` 或者 `StringBuffer`。
|
||||
|
||||
不过,字符串使用 `final` 关键字声明之后,可以让编译器当做常量来处理。
|
||||
|
||||
```java
|
||||
final String str1 = "str";
|
||||
final String str2 = "ing";
|
||||
// 下面两个表达式其实是等价的
|
||||
String c = "str" + "ing";// 常量池中的对象
|
||||
String d = str1 + str2; // 常量池中的对象
|
||||
System.out.println(c == d);// true
|
||||
```
|
||||
|
||||
被 `final` 关键字修改之后的 `String` 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。
|
||||
|
||||
如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。
|
||||
|
||||
示例代码如下(`str2` 在运行时才能确定其值):
|
||||
|
||||
```java
|
||||
final String str1 = "str";
|
||||
final String str2 = getStr();
|
||||
String c = "str" + "ing";// 常量池中的对象
|
||||
String d = str1 + str2; // 在堆上创建的新的对象
|
||||
System.out.println(c == d);// false
|
||||
public static String getStr() {
|
||||
return "ing";
|
||||
}
|
||||
```
|
||||
|
||||
**我们再来看一个类似的问题!**
|
||||
|
||||
```java
|
||||
String str1 = "abcd";
|
||||
String str2 = new String("abcd");
|
||||
String str3 = new String("abcd");
|
||||
System.out.println(str1==str2);
|
||||
System.out.println(str2==str3);
|
||||
```
|
||||
|
||||
上面的代码运行之后会输出什么呢?
|
||||
|
||||
答案是:
|
||||
|
||||
```
|
||||
false
|
||||
false
|
||||
```
|
||||
|
||||
**这是为什么呢?**
|
||||
|
||||
我们先来看下面这种创建字符串对象的方式:
|
||||
|
||||
```java
|
||||
// 从字符串常量池中拿对象
|
||||
String str1 = "abcd";
|
||||
```
|
||||
|
||||
这种情况下,jvm 会先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd";
|
||||
|
||||
因此,`str1` 指向的是字符串常量池的对象。
|
||||
|
||||
我们再来看下面这种创建字符串对象的方式:
|
||||
|
||||
```java
|
||||
// 直接在堆内存空间创建一个新的对象。
|
||||
String str2 = new String("abcd");
|
||||
String str3 = new String("abcd");
|
||||
```
|
||||
|
||||
**只要使用 new 的方式创建对象,便需要创建新的对象** 。
|
||||
|
||||
使用 new 的方式创建对象的方式如下,可以简单概括为 3 步:
|
||||
|
||||
1. 在堆中创建一个字符串对象
|
||||
2. 检查字符串常量池中是否有和 new 的字符串值相等的字符串常量
|
||||
3. 如果没有的话需要在字符串常量池中也创建一个值相等的字符串常量,如果有的话,就直接返回堆中的字符串实例对象地址。
|
||||
|
||||
因此,`str2` 和 `str3` 都是在堆中新创建的对象。
|
||||
|
||||
**字符串常量池比较特殊,它的主要使用方法有两种:**
|
||||
|
||||
1. 直接使用双引号声明出来的 `String` 对象会直接存储在常量池中。
|
||||
2. 如果不是用双引号声明的 `String` 对象,使用 `String` 提供的 `intern()` 方法也有同样的效果。`String.intern()` 是一个 Native 方法,它的作用是:如果字符串常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7 之前(不包含 1.7)的处理方式是在常量池中创建与此 `String` 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7 以及之后,字符串常量池被从方法区拿到了堆中,jvm 不会在常量池中创建该对象,而是将堆中这个对象的引用直接放到常量池中,减少不必要的内存开销。
|
||||
|
||||
示例代码如下(JDK 1.8) :
|
||||
|
||||
```java
|
||||
String s1 = "Javatpoint";
|
||||
String s2 = s1.intern();
|
||||
String s3 = new String("Javatpoint");
|
||||
String s4 = s3.intern();
|
||||
System.out.println(s1==s2); // True
|
||||
System.out.println(s1==s3); // False
|
||||
System.out.println(s1==s4); // True
|
||||
System.out.println(s2==s3); // False
|
||||
System.out.println(s2==s4); // True
|
||||
System.out.println(s3==s4); // False
|
||||
```
|
||||
|
||||
**总结** :
|
||||
|
||||
1. 对于基本数据类型来说,==比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。
|
||||
2. 在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 **常量折叠(Constant Folding)** 的代码优化。常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
|
||||
3. 一般来说,我们要尽量避免通过 new 的方式创建字符串。使用双引号声明的 `String` 对象( `String s1 = "java"` )更利于让编译器有机会优化我们的代码,同时也更易于阅读。
|
||||
4. 被 `final` 关键字修改之后的 `String` 会被编译器当做常量来处理,编译器程序编译期就可以确定它的值,其效果就相当于访问常量。
|
||||
|
||||
### 4.2 String s1 = new String("abc");这句话创建了几个字符串对象?
|
||||
|
||||
会创建 1 或 2 个字符串:
|
||||
|
||||
- 如果字符串常量池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。
|
||||
- 如果字符串常量池中没有字符串常量“abc”,那么它将首先在字符串常量池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。
|
||||
|
||||
**验证:**
|
||||
|
||||
```java
|
||||
String s1 = new String("abc");// 堆内存的地址值
|
||||
String s2 = "abc";
|
||||
System.out.println(s1 == s2);// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
|
||||
System.out.println(s1.equals(s2));// 输出 true
|
||||
```
|
||||
|
||||
**结果:**
|
||||
|
||||
```
|
||||
false
|
||||
true
|
||||
```
|
||||
|
||||
### 4.3 8 种基本类型的包装类和常量池
|
||||
|
||||
Java 基本类型的包装类的大部分都实现了常量池技术。
|
||||
|
||||
`Byte`,`Short`,`Integer`,`Long` 这 4 种包装类默认创建了数值 **[-128,127]** 的相应类型的缓存数据,`Character` 创建了数值在 **[0,127]** 范围的缓存数据,`Boolean` 直接返回 `True` Or `False`。
|
||||
|
||||
两种浮点数类型的包装类 `Float`,`Double` 并没有实现常量池技术。
|
||||
|
||||
```java
|
||||
Integer i1 = 33;
|
||||
Integer i2 = 33;
|
||||
System.out.println(i1 == i2);// 输出 true
|
||||
Integer i11 = 333;
|
||||
Integer i22 = 333;
|
||||
System.out.println(i11 == i22);// 输出 false
|
||||
Double i3 = 1.2;
|
||||
Double i4 = 1.2;
|
||||
System.out.println(i3 == i4);// 输出 false
|
||||
```
|
||||
|
||||
**Integer 缓存源代码:**
|
||||
|
||||
```java
|
||||
/**
|
||||
*此方法将始终缓存-128 到 127(包括端点)范围内的值,并可以缓存此范围之外的其他值。
|
||||
*/
|
||||
public static Integer valueOf(int i) {
|
||||
if (i >= IntegerCache.low && i <= IntegerCache.high)
|
||||
return IntegerCache.cache[i + (-IntegerCache.low)];
|
||||
return new Integer(i);
|
||||
}
|
||||
private static class IntegerCache {
|
||||
static final int low = -128;
|
||||
static final int high;
|
||||
static final Integer cache[];
|
||||
}
|
||||
```
|
||||
|
||||
**`Character` 缓存源码:**
|
||||
|
||||
```java
|
||||
public static Character valueOf(char c) {
|
||||
if (c <= 127) { // must cache
|
||||
return CharacterCache.cache[(int)c];
|
||||
}
|
||||
return new Character(c);
|
||||
}
|
||||
|
||||
private static class CharacterCache {
|
||||
private CharacterCache(){}
|
||||
|
||||
static final Character cache[] = new Character[127 + 1];
|
||||
static {
|
||||
for (int i = 0; i < cache.length; i++)
|
||||
cache[i] = new Character((char)i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`Boolean` 缓存源码:**
|
||||
|
||||
```java
|
||||
public static Boolean valueOf(boolean b) {
|
||||
return (b ? TRUE : FALSE);
|
||||
}
|
||||
```
|
||||
|
||||
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
|
||||
|
||||
下面我们来看一下问题。下面的代码的输出结果是 `true` 还是 `false` 呢?
|
||||
|
||||
```java
|
||||
Integer i1 = 40;
|
||||
Integer i2 = new Integer(40);
|
||||
System.out.println(i1==i2);
|
||||
```
|
||||
|
||||
`Integer i1=40` 这一行代码会发生装箱,也就是说这行代码等价于 `Integer i1=Integer.valueOf(40)` 。因此,`i1` 直接使用的是常量池中的对象。而`Integer i2 = new Integer(40)` 会直接创建新的对象。
|
||||
|
||||
因此,答案是 `false` 。你答对了吗?
|
||||
|
||||
记住:**所有整型包装类对象之间值的比较,全部使用 equals 方法比较**。
|
||||
|
||||

|
||||
|
||||
**Integer 比较更丰富的一个例子:**
|
||||
|
||||
```java
|
||||
Integer i1 = 40;
|
||||
Integer i2 = 40;
|
||||
Integer i3 = 0;
|
||||
Integer i4 = new Integer(40);
|
||||
Integer i5 = new Integer(40);
|
||||
Integer i6 = new Integer(0);
|
||||
|
||||
System.out.println(i1 == i2);// true
|
||||
System.out.println(i1 == i2 + i3);//true
|
||||
System.out.println(i1 == i4);// false
|
||||
System.out.println(i4 == i5);// false
|
||||
System.out.println(i4 == i5 + i6);// true
|
||||
System.out.println(40 == i5 + i6);// true
|
||||
```
|
||||
|
||||
`i1` , `i2 ` , `i3` 都是常量池中的对象,`i4` , `i5` , `i6` 是堆中的对象。
|
||||
|
||||
`i4 == i5 + i6` 为什么是 true 呢?因为, `i5` 和 `i6` 会进行自动拆箱操作,进行数值相加,即 `i4 == 40` 。 `Integer` 对象无法与数值进行直接比较,所以 `i4` 自动拆箱转为 int 值 40,最终这条语句转为 `40 == 40` 进行数值比较。
|
||||
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
|
||||
|
||||
## 参考
|
||||
|
||||
@ -597,5 +309,3 @@ System.out.println(40 == i5 + i6);// true
|
||||
- <http://www.pointsoftware.ch/en/under-the-hood-runtime-data-areas-javas-memory-model/>
|
||||
- <https://dzone.com/articles/jvm-permgen-%E2%80%93-where-art-thou>
|
||||
- <https://stackoverflow.com/questions/9095748/method-area-and-permgen>
|
||||
- 深入解析 String#intern<https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html>
|
||||
- R 大(RednaxelaFX)关于常量折叠的回答:https://www.zhihu.com/question/55976094/answer/147302764
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 92 KiB |