1.jvm组成
1.1程序计数器
线程私有的,每个线程一份。用来记录正在执行的字节码指令的行号。
程序计数器 是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
1.2 虚拟机栈
线程私有,生命周期和线程一样(随着线程的创建而创建,随着线程的结束而死亡)。
除本地方法外,java方法调用都通过虚拟机栈(后文简称栈)实现。
栈由栈帧组成,方法调用时栈帧压入,结束后栈帧弹出
每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址
局部变量表
存放方法的所有 参数和在方法内部定义的局部变量
1.3 堆
介绍一下堆
堆是线程共享的区域,用来保存所有通过 new 关键字创建的 对象实例和数组

*什么东西存放在堆里
Java堆(Heap)中存放的是所有通过 new 关键字创建的 对象实例和数组**。
为了让这个概念更具体,我们来详细分解一下"对象实例"和"数组"里面到底是什么:
- 对象实例 (Object Instances)
这是堆最主要的存储内容。当你写下 new MyClass() 时,这个 MyClass 的一个实例就会在堆上被创建。这个实例对象内部包含了:
-
实例变量 (Instance Variables):
-
基本数据类型 :如果类成员变量是
int,double,boolean等基本类型,那么这些变量的值本身就直接存储在堆上的对象内部。 -
引用数据类型 :如果类成员变量是另一个对象(比如
String或另一个自定义类),那么存储在当前对象内部的是一个引用(或称指针),这个引用指向另一个在堆上存储的实际对象。
-
-
对象头信息 (Object Header):
-
Mark Word:存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁等。这部分数据在32位和64位虚拟机中大小不同。
-
类型指针 (Klass Pointer):一个指向方法区中该对象对应类信息的指针。通过这个指针,JVM可以知道这个对象是哪个类的实例。
-
数组长度 (Array Length) :这部分只有在对象是数组的情况下才会有。
-
- 数组 (Arrays)
数组在Java中也被当作对象来处理,所以它们同样存储在堆上。无论是基本数据类型数组(如 new int[10])还是引用类型数组(如 new String[10]),数组对象本身都在堆里。
-
new int[10]: 会在堆里创建一个数组对象。这个对象除了对象头信息,还包含了10个整数的连续空间,存放着具体的整数值。 -
new String[10]: 同样会在堆里创建一个数组对象。这个对象包含了10个空位,用来存放指向String对象的引用 。这些String对象本身也都在堆里(可能在也可能不在连续的空间)。
总结一下,堆里到底有什么?
您可以把Java堆想象成一个巨大的仓库,里面放着:
-
所有
new出来的东西本身(无论是普通对象还是数组对象)。 -
对象的所有成员变量(基本类型的值和引用类型的地址)。
-
每个对象和数组的"管理标签"(也就是对象头信息)。
与之相对,不会存放在堆里的主要是:
-
局部变量:存放在Java虚拟机栈(Stack)中。如果局部变量是基本类型,栈里存的就是值;如果是引用类型,栈里存的是指向堆中对象的地址。
-
类信息、静态变量、常量:存放在方法区(Method Area,在JDK 1.8后由元空间Metaspace实现)。
补充1:基本数据类型
-
整数类型
-
byte: 8位,用于存储小整数。 -
short: 16位。 -
int: 32位,最常用的整数类型。 -
long: 64位,用于存储大整数。
-
-
浮点类型 (用于小数)
-
float: 32位,单精度。 -
double: 64位,双精度,比float更精确,是小数的默认类型。
-
-
字符类型
char: 16位,用于存储单个Unicode字符 (例如'A','中')。
-
布尔类型
boolean: 只有两个值:true和false。
补充2:引用类型
除了8种基本类型之外,Java中所有其他的类型都是引用类型。变量中存储的不是对象本身,而是一个指向对象在堆内存 中位置的引用(或称为地址)。
主要包括:
-
类 (Class): 这是最常见的引用类型。
-
所有你自己创建的类,比如我们例子中的
Car。 -
Java自带的库类,比如
String,Object,System,ArrayList等。
-
-
接口 (Interface) : 如
List,Map,Runnable等。变量可以声明为接口类型,但它必须引用一个实现了该接口的类的对象。 -
数组 (Array) : 比如
int[],String[],Car[]等。在Java中,数组也是对象,所以数组变量也是引用类型。
关键特性:
-
变量在内存中(通常是栈)只存放一个地址。
-
真正的对象数据存放在堆上。
-
可以通过
new关键字来创建实例(对象)。 -
默认值为
null,表示不引用任何对象。 -
按照惯例,名称都是大写字母开头 (
String,Car)。
**补充内容
静态变量、实例变量和局部变量的存放位置
java
public class Car {
// 1. 静态变量 (Static)
// 存放在:【堆 (Heap)】(伴随 Class 对象)
// 意义:工厂墙上的记分牌,全厂只有一块
public static int totalCount = 0;
// 2. 非静态成员变量 / 实例变量 (Instance Variable)
// 存放在:【堆 (Heap)】(在 Car 对象内部)
// 意义:每辆车自己的颜色。车在堆里,颜色就在堆里。
private String color;
private int price;
// 方法
public void drive() {
// 3. 局部变量 (Local Variable)
// 存放在:【栈 (Stack)】
// 意义:工人开车时踩油门的力度。方法结束,力度也就没了。
int speed = 80;
// 特殊情况:引用类型的局部变量
// 'myDog' 这个引用(遥控器)在【栈】上
// 但它指向的 'Dog' 对象在【堆】上
Dog myDog = new Dog();
}
}
口诀:成员变量(静态+非静态)都在堆,局部变量都在栈
静态变量和非静态变量的区别
java
class Car {
// static 修饰:静态变量(属于类,大家共用一份)
// 就像那块"计分板"
static int count = 0;
// 没有 static 修饰:非静态变量(属于对象,每辆车各有一份)
// 就像车的"颜色"
String color;
public Car(String c) {
this.color = c; // 设置这辆特定车的颜色
count++; // 生产了一辆车,计分板总数 +1
}
}
public class Main {
public static void main(String[] args) {
// 1. 造第一辆车:红色
Car car1 = new Car("红色");
// 2. 造第二辆车:蓝色
Car car2 = new Car("蓝色");
// --- 见证区别 ---
// 区别 A:非静态变量是独立的
System.out.println(car1.color); // 输出:红色
System.out.println(car2.color); // 输出:蓝色
// (修改 car1 的颜色,不会影响 car2)
// 区别 B:静态变量是共享的
// 虽然我是通过 car1 去看 count,但看到的和 car2 是一样的
System.out.println(car1.count); // 输出:2
System.out.println(car2.count); // 输出:2
System.out.println(Car.count); // 输出:2 (推荐这种写法,直接问类)
}
}
详细对比表
| 维度 | 静态变量 (Static) | 非静态变量 (Instance) |
|---|---|---|
| 归属 | 属于 类 (Class) | 属于 对象 (Object) |
| 内存副本 | 只有 1 份 (无论创建多少对象) | 有 N 份 (创建多少对象就有多少份) |
| 存放位置 | 堆 (Heap) [JDK7+],跟随 Class 对象 | 堆 (Heap),在每个实例对象内部 |
| 生命周期 | 很长。类加载时诞生,程序结束才销毁。 | 较短。对象创建时诞生,对象被回收时销毁。 |
| 调用方式 | 推荐用 类名.变量名 (如 Car.count) |
必须用 对象名.变量名 (如 myCar.color) |
| 典型用途 | 计数器、全局配置、常量 (Math.PI) | 用户的名字、车的颜色、银行卡余额 |
String s = new String("abc"); 到底创建了几个对象?
答案是:通常是 2 个,最少是 1 个。
这行代码其实包含了两步操作:
-
"abc":处理字面量。 -
new String(...):处理new关键字。
情况 A:通常情况(创建了 2 个对象)
假设字符串常量池里还没有 "abc" 这个字符串。
-
第 1 个对象 :JVM 看到字面量
"abc",它会先在 字符串常量池(String Pool) 里创建一个 "abc" 对象。 -
第 2 个对象 :JVM 看到
new关键字,会在 堆(Heap) 中开辟一块新内存,创建一个新的 String 对象。这个对象会拷贝刚才那个 "abc" 的值。 -
引用 :最后,把堆里那个新对象的地址,赋值给栈上的变量
s。
结论:一个在池里,一个在堆里。共 2 个。
情况 B:特殊情况(创建了 1 个对象)
假设你的代码前面已经有一行 String t = "abc";,这意味着常量池里已经有 "abc" 了。
-
第 0 个对象 :JVM 看到字面量
"abc",去池里找,发现有了,就不创建了。 -
第 1 个对象 :JVM 看到
new关键字,依然必须在 堆(Heap) 里创建一个新对象(因为new意味着强制新建)。 -
引用 :把堆里新对象的地址赋给
s。
结论:池里的省了,只创建了堆里的那一个。共 1 个。