java内存模型
线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享(公有):堆、方法区

堆内存:

堆内存分为新生代和老年代,年轻代和老年代默认的比例是1:3,其中年轻代又分为Eden和Survivor区,Survivor区包含s0和s1区,Eden、s0和s1区在年轻代的默认占比是8:1:1。
堆内存中存放的是对象信息。
程序在运行过程中会不断的创建对象,生成的对象会存放在eden区,eden区内存满了之后,会进行minor gc,然后将存活的对象移动到s0区,s0区满了之后,会重新进行minor gc,然后将存活的对象移动到s1,反复进行此操作,在minor gc过程中,会将存活大于15的对象移动到老年代。年轻代在gc过程中,由于内存不够,存放的大对象会直接移动到老年代。老年代内存满了之后会进行full gc,进行full gc的过程中,年轻代也会进行minor gc。
虚拟机栈:

每一个线程的启动都会在虚拟机栈中获取到一块独立的空间,虚拟机栈中又包含了栈帧和程序计数器,每一个方法对应一个栈帧。栈帧中包含了局部变量表、操作数栈、动态连接、方法出口,每一个栈帧中都存在这些信息。虚拟机栈会进行First In, Last Out(先进后出),代码在执行过程中会从栈顶开始执行,最先进入栈内的信息会最后执行。
- 程序计数器:每个线程独有的。存储的代码指定当前行的行号。
- 栈帧:一个方法对应一个栈帧。
–<栈帧内部>
- 局部变量表:方法中的变量。
- 操作数栈:线程执行过程中,在操作数栈中对变量赋值操作,赋值完成之后移动到局部变量表中。可以理解为临时存放运算的一块空间。
- 动态连接:每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。可以理解为方法中引用到别的方法。
- 方法出口:存放的返回主方法的信息,比如执行完栈顶的栈帧,返回下一个栈帧的信息。
本地方法栈:由c编写的的本地方法,给本地方法提供的一块空间。
方法区(元空间):存放类中的常量,静态变量,类信息等。
代码示例
代码区
public class Math {
public static int initData=666;
public static User user= new User();
public int compute(){
int a=1;
int b=2;
int c=(a+b)*10;
return c;
}
public void getUser(){
User user = new User();
user.setUuid("999");
user.setNum(999);
System.out.printf(user.toString());
}
public static void main(String[]args){
Math math=new Math();
math.getUser();
math.compute();
}
}
class User{
public User(int num, String uuid) {
this.num = num;
this.uuid = uuid;
}
int num;
String uuid;
public User() {
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
@Override
public String toString() {
return "User{" +
"num=" + num +
", uuid='" + uuid + '\'' +
'}';
}
}
字节码区
public class com.lcf.jvm.Math {
public static int initData;
public static com.lcf.jvm.User user;
public com.lcf.jvm.Math();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int compute();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
public void getUser();
Code:
0: new #2 // class com/lcf/jvm/User
3: dup
4: invokespecial #3 // Method com/lcf/jvm/User."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String 999
11: invokevirtual #5 // Method com/lcf/jvm/User.setUuid:(Ljava/lang/String;)V
14: aload_1
15: sipush 999
18: invokevirtual #6 // Method com/lcf/jvm/User.setNum:(I)V
21: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
24: aload_1
25: invokevirtual #8 // Method com/lcf/jvm/User.toString:()Ljava/lang/String;
28: iconst_0
29: anewarray #9 // class java/lang/Object
32: invokevirtual #10 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
35: pop
36: return
public static void main(java.lang.String[]);
Code:
0: new #11 // class com/lcf/jvm/Math
3: dup
4: invokespecial #12 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #13 // Method getUser:()V
12: aload_1
13: invokevirtual #14 // Method compute:()I
16: pop
17: return
static {};
Code:
0: sipush 666
3: putstatic #15 // Field initData:I
6: new #2 // class com/lcf/jvm/User
9: dup
10: invokespecial #3 // Method com/lcf/jvm/User."<init>":()V
13: putstatic #16 // Field user:Lcom/lcf/jvm/User;
16: return
}
类Math在加载过程中,类Math中静态变量 initData 和 user 会存放在方法区,方法区的user会指向堆内存的new User();方法getUser()里面创建的user对象不会放到堆内存中,因为jvm经过逃逸分析之后发现对象并没有逃出方法区,他会存放在栈内存(如果栈内存空间不够,对象还是会放在堆空间的)。
标量替换:对象经过逃逸分析分配到栈内存的时候,因为栈内存空间并不是一块完整的,jvm会将对象分解成对象的成员变量来替代,将对象属性存放在内存碎片,但是有一个地方会标识,分解的成员变量是属于这个对象的。

类Math的main方法会在虚拟机栈中分配到一块内存,内存中会存在三块栈帧内存:compute栈帧内存,getUser栈帧内存,main栈帧内存,因为main()方法代码块是最先执行的,compute()方法块代码是最后执行的,所以main在栈底,compute方法在栈顶,先进后出原则。
操作数栈:
比如compute()方法在运行过程中,会先将变量a存放到 局部变量表,然后执行字节码iconst_1(将 int 型 1 推送至栈顶),然后执行字节码istore_1(将栈顶 int 型数值存入第二个本地变量),进行出栈操作然后将1赋值给a变量。
java有一套字节码文件可以自行搜索
动态链接:代码在类加载过程中,不会直接加载compute()方法,而是指向代码compute()的引用,在运行过程中,调用到了compute()方法才会去加载,这种场景就是动态链接。通过字节码文件可以看到,main方法是指向compute()的引用。