JVM的东西太多了,我们刚开始学java的时候,就会接触堆、栈,还有方法区,因为我们要知道new出来的对象放在哪里,局部变量放在哪里,static修饰的变量放在哪里。 我从网上截一个图: 这里有三大部分: - classloader
- runtime data area
- execution engine
classloader就是类加载器。比如说我这里有一个Main.java文件: [Java] 纯文本查看 复制代码 package com.ocean;
public class Main {
public static final int intData = 100;
public static User user = new User();
public int compute(){
int a = 1;
int b = 2;
int c = (a + b) * 3;
return c;
}
public static void main(String[] args) {
Main main = new Main();
main.compute();
}
}
通过javac命令,将其编译成Main.class文件,然后classloader就会加载它。 但是,在这里有三个classloader。 首先是Bootstrap ClassLoader,它load的是java核心包,像java.lang,java.net,java.util,java.io,java.sql包中的class文件。 然后是Extension ClassLoader,它load的是$JAVA_HOME/jre/lib/ext中的class文件,它下的就不是核心库了,其实就是额外的库。 最后是Application ClassLoader。它加载的是类路径下的class。 我们感受一下这三个classloader的存在: [Java] 纯文本查看 复制代码 public void printClassLoaders() throws ClassNotFoundException {
System.out.println("Classloader of this class:"
+ Main.class.getClassLoader());
System.out.println("Classloader of Logging:"
+ Logging.class.getClassLoader());
System.out.println("Classloader of ArrayList:"
+ ArrayList.class.getClassLoader());
}
public static void main(String[] args) throws ClassNotFoundException {
Main main = new Main();
// main.compute();
main.printClassLoaders();
} Arraylist是由bootstrap classloader加载的,但它是由native code(C和C++)写的,所以展现不出来(null)。 类的加载是用代理模式实现的。 比如JVM委托classloader instance把Main.class加载到内存,那么application classloader会委托父类加载器即extension classloader去加载,extension classloader会再向上委托由父类加载器bootstrap classloader去加载,只有当bootstrap classloader和extension classloader无法加载时,application classloader才会自己加载。 注意,classloader加载的时候会verify和prepare。 verify包括标记为final类型的类是否有子类,类中的final方法是否被子类进行重写,重写是否符合规范等等。 prepare包括为类中的静态变量分配内存空间,被final修饰的static变量直接赋值等。 anyway,Main.class文件被加载到了method area。 我们很早就知道,method area里面会存常量、静态变量和类信息, 我们这里的initData就会存在方法区: public static final int intData = 100; 这个类信息,包括类的静态变量、初始化代码(静态变量的赋值和静态代码块)、实例变量、实例变量的初始化代码(构造方法)、实例方法、父类引用信息(super)。 今天主要讲的,就是runtime data area。 runtime data area里面的成分,像native method stacks,就没什么好讲的,每一条线程,都会有一个native method stacks,它也被叫做“C stacks”,就是用C语言写的方法。 好,我们进入正题。 我们进入main方法。 一个main方法,就是一条线程。 对于每条线程,都会有一个java virtual machine stack。 这里面,会有一个个stack frame,也就是说,每调用一个方法,就会有一个stack frame压进java virtual machine stack。当方法调用完毕,这个stack frame也就没了。所以这是一个first in last out的stack结构,main方法会最后一个结束,因为它是第一个被压进stack中的。 在每一个stack frame中,有local variable,就是局部变量,operand stack,操作数栈,还有dynamic linking,动态链接,以及method invocation completion,方法出口。 dynamic linking可以解释method override,还要结合 Run-Time Constant Pool来说,我们暂时不讲。 主要了解一下local variable与operand stack。 我们看一下用javap反编译后的jvm指令。 [Java] 纯文本查看 复制代码 Compiled from "Main.java"
public class com.ocean.Main {
public static final int intData;
public static com.ocean.User user;
public com.ocean.Main();
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: iconst_3
8: imul
9: istore_3
10: iload_3
11: ireturn
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/ocean/Main
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: return
static {};
Code:
0: new #5 // class com/ocean/User
3: dup
4: invokespecial #6 // Method com/ocean/User."<init>":()V
7: putstatic #7 // Field user:Lcom/ocean/User;
10: return
} 看一下compute方法的执行过程: iconst_1—>Push the int constant (-1, 0, 1, 2, 3, 4 or 5) onto the operand stack. 把int常量压入操作数栈,这里,值就是1。 istore_1—>Store int into local variable.The “n” must be an index into the local variable array of the current frame . The value on the top of the operand stack must be of type int. It is popped from the operand stack, and the value of the local variable at is set to value. 这个含义就是,把1这个值赋值给local variable a。 2: iconst_2 3: istore_2 这两句和上面一样。 iload_1—>Load int from local variable 加载int值。也就是说, local variable array 中下标为1的值(我们这里也正好是1,顺便说一下,这个array中,下标从0开始,0为this)被推到操作数栈栈顶。 iadd—>Both value1 and value2 must be of type int. The values are popped from the operand stack. The int result is value1 + value2. The result is pushed onto the operand stack. 1和2两个数从操作数栈栈顶弹出,然后相加的结果再压入栈顶。后面一样。最后 11: ireturn—>Return int from method。 返回int值。这里有几个东西。一个是程序计数器(pc register)。 每条线程都有一个pc register。 代码前面的01234就是程序计数器记录的值: 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 因为多线程运行的时候,一个方法可能运行到一半,cpu时间片就被别的线程抢走了,到时候这个方法再次抢到cpu时间片时,从哪里再开始运行呢?程序计数器就会做好记录。 修改程序计数器的是执行引擎。 最后就是方法出口,compute方法调用完毕之后该回到哪里去呢? [Java] 纯文本查看 复制代码 public static void main(String[] args) throws ClassNotFoundException {
1 Main main = new Main();
2 main.compute();
3 System.out.println("compute() is over!");
// main.printClassLoaders();
} 它肯定要回到main方法的第三行位置啊,这是由Normal Method Invocation Completion完成的。我们这里就不会抛什么异常了。 静态变量user存在方法区(存的是堆中User的地址),并且指向堆中的对象User。 堆里面,有young generation和old generation。 young generation包括eden space和survivor space。 survivor space有两个,一个是survivor1,另一个是survivor2。 old generation就是所谓的tenured space。 当我们创建了一个对象,JVM就把它放在eden space当中,伊甸园嘛,就是新生的对象。 eden space也有一个大小的,它满了之后,minor gc就会干活,它会把eden space当中的所有非垃圾对象挪到survivor space,那是survivor1还是survivor2,这不一定的。看名字也知道,这些就是还被引用指向的幸存对象。 一个对象从eden space挪到survivor space一次(比如说是s1),就可以认为年岁加一。 等到下一次eden space又满了,minor gc就会把eden space中的对象和s1中的对象挪到s2,这时最初在eden space中的对象年龄又加1,可以说是两岁了。 这些不死的对象就被minor gc这样在s1和s2之间挪来挪去,年龄不断增加,等到15岁时,就认为是老不死的对象了,并将其挪到老年代(tenured space)。 我们看一段程序: [Java] 纯文本查看 复制代码 package com.ocean;
import java.util.ArrayList;
public class TestHeap {
public static final String SUCCESS = "1";
public static void main(String[] args) throws InterruptedException {
ArrayList list = new ArrayList();
while(true){
list.add(new TestHeap());
Thread.sleep(10);
}
}
} 然后在visual gc下的分析:
|