随便聊聊JVM

JVM

      • ClassLoader
      • runtime data area
        • stack frame
        • heap
      • static linking 和 dynamic linking

ClassLoader

JVM的东西太多了,我们刚开始学java的时候,就会接触堆、栈,还有方法区,因为我们要知道new出来的对象放在哪里,局部变量放在哪里,static修饰的变量放在哪里。

我从网上截一个图:

随便聊聊JVM_第1张图片
这里有三大部分:

  • classloader
  • runtime data area
  • execution engine

classloader就是类加载器。比如说我这里有一个Main.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的存在:

    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)。

其实还有一种类加载器,是自定义类加载器。自定义类加载器都会继承ClassLoader类。

我们可以自己写一个类加载器,来加载字节码文件:

package com.ocean.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;

public class CustomClassLoader extends ClassLoader {
    /**
     * 
     * @param name 全类名
     * @return 用字节码数据创建的Class对象
     */
    @Override
    protected Class<?> findClass(String name) {
        byte[] b = new byte[0];
        try {
            b = findClassFromFile(name);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return defineClass(name, b, 0, b.length);
    }

    /**
     * 
     * @param name 全类名
     * @return 字节码的字节数组
     * @throws IOException
     */
    private byte[] findClassFromFile(String name) throws IOException {

        InputStream in = getClass().getClassLoader().getResourceAsStream("com/ocean/classloader/" + name.substring(name.lastIndexOf(".") + 1) + ".class");

        byte[] buffer;

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

        int nextValue = 0;

        while ((nextValue = in.read()) != -1) {
            byteArrayOutputStream.write(nextValue);
        }

        buffer = byteArrayOutputStream.toByteArray();

        return buffer;
    }

    public void classpath() {

        System.out.println(getClass().getClassLoader().getResourceAsStream("com/ocean/classloader/SomeClass.class"));
    }
}

然后我们随便写一个SomeClass.java,编译好后进行测试:

package com.ocean.classloader;

public class TestCustomClassLoader {
    public static void main(String[] args) {
        CustomClassLoader customClassLoader= new CustomClassLoader();
        Class<?> aClass = customClassLoader.findClass("com.ocean.classloader.SomeClass");
        System.out.println(aClass.getSimpleName());


    }
}


类的加载是用代理模式实现的。

比如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。

runtime data area里面的成分,像native method stacks,就没什么好讲的,每一条线程,都会有一个native method stacks,它也被叫做“C stacks”,就是用C语言写的方法。

好,我们进入正题。

随便聊聊JVM_第2张图片
我们进入main方法。

一个main方法,就是一条线程。

对于每条线程,都会有一个java virtual machine stack。

stack frame

这里面,会有一个个stack frame,也就是说,每调用一个方法,就会有一个stack frame压进java virtual machine stack。当方法调用完毕,这个stack frame也就没了。所以这是一个first in last out的stack结构,main方法会最后一个结束,因为它是第一个被压进stack中的。

随便聊聊JVM_第3张图片
在每一个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指令。

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方法调用完毕之后该回到哪里去呢?

 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完成的。我们这里就不会抛什么异常了。


heap

好。现在要轮到堆了。

public static User user = new User();

静态变量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)。

随便聊聊JVM_第4张图片
我们看一段程序:

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下的分析:

随便聊聊JVM_第5张图片
这就很好地解释了我上面的话。

static linking 和 dynamic linking

因为之前因为篇幅原因没有讲静态链接和动态链接,现在补一手。

静态链接是在编译时期做的,动态链接是运行期做的。

首先看几个概念:

  • 直接引用和间接引用:

比如,我们要调用一个方法,肯定得找到那个方法在哪里吧,这样子就可以调用内存中的方法了。

直接引用就像一个指针,直接指向内存中的方法(或变量),即可以直接调用(或获取)了。

而间接引用,比如说有一个A类,它里面有个a()方法,有一个B类,它里面有一个b()方法,现在a()方法要调用B类的b()方法,它怎么找到这个b()方法呢?

package com.ocean.practice;

public class B {
    public void b(){

    }
}

package com.ocean.practice;

public class A {

    B b  = new B();

    public void a(){
        b.b();
    }


    public static void main(String[] args) {
        A a = new A();
        a.a();
    }
}

编译期间,jvm是不知道怎们去调用的:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #6                  // class com/ocean/practice/A
         3: dup
         4: invokespecial #7                  // Method "":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #8                  // Method a:()V
        12: return

第9行,它只知道要去调用a()方法,但是具体的情况不清楚。

关键在于,它把B类的信息全部存下来了。

#1 = Methodref          #9.#26         // java/lang/Object."":()V
   #2 = Class              #27            // com/ocean/practice/B
   #3 = Methodref          #2.#26         // com/ocean/practice/B."":()V
   #4 = Fieldref           #6.#28         // com/ocean/practice/A.b:Lcom/ocean/practice/B;
   #5 = Methodref          #2.#29         // com/ocean/practice/B.b:()V
   #6 = Class              #30            // com/ocean/practice/A
   #7 = Methodref          #6.#26         // com/ocean/practice/A."":()V
   #8 = Methodref          #6.#31         // com/ocean/practice/A.a:()V
   #9 = Class              #32            // java/lang/Object

以符号的形式(英文字母)存储B类的信息,就叫做对B类(或B类中方法和变量)的间接引用。

方法或变量的调用及获取,一定会经过间接引用向直接引用的一个转变(间接引用只是暂时记下来)。

  • 虚方法和非虚方法

类中的方法,可以分为虚方法和非虚方法。虚方法,就是真正调用的时候都可能不是调用自己本身的。什么意思呢?比如有一个Animal类,里面有个eat()方法,然后一个Monkey类继承了Animal类,并且重写了eat()方法,main方法里,我们这么写:

Animal animal = new Monkey();
animal.eat();

我们知道最后肯定是调用的monkey的eat()方法。

所以,对于Animal类来说,它的eat()方法就是虚的。

jvm指令集中,对应的调用虚方法的指令(助记符)是(invokevirtual)。

对于虚方法,是运行期才将间接引用转为直接引用。

另一方面,有4中非虚方法:
1.静态方法(invokestatic)
2.构造方法(invokespecial)
3.父类方法(invokespecial)
4.私有方法(invokespecial)

括号里面的是jvm指令。

静态方法直属于类,私有方法不能被重写,构造方法和父类方法也是能够唯一确定的,所以它是就是非虚的,这些方法,在类的编译期间,就将间接引用转为直接引用。

  • 静态分派和动态分派

我们很早就学过重写和重载,还背了一大堆它们之间的区别,知道它们都是多态的表现。

其实,更深层次的,它反映了java的语言特点,它们分别对应着动态分派和静态分派。

先举个静态分派的例子:

package com.ocean.practice;

public class Animal {
    public void eat(){
        System.out.println("animal eat...");
    }
}

package com.ocean.practice;

public class Monkey extends Animal {
    @Override
    public void eat() {
        System.out.println("monkey eat...");
    }
}

package com.ocean.practice;

public class TestStaticDispatch {

    public void test(Animal animal){
        System.out.println("test(animal)");
    }

    public void test(Monkey monkey){
        System.out.println("test(monkey)");
    }


    public static void main(String[] args) {
        TestStaticDispatch staticDispatch = new TestStaticDispatch();
        Animal animal = new Animal();
        Animal monkey = new Monkey();

        staticDispatch.test(animal);
        staticDispatch.test(monkey);
    }
}

测试结果:

test(animal)
test(animal)

结果都是调用了test(Animal animal)。为什么呢?对于像这样的语句:Animal monkey = new Monkey();,Animal叫做静态类型,而后面new出来的东西叫做动态类型。

静态分派就是重载的时候,参数用的是静态类型,而这些是在编译期就确定的。

看看jvm指令:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #6                  // class com/ocean/practice/TestStaticDispatch
         3: dup
         4: invokespecial #7                  // Method "":()V
         7: astore_1
         8: new           #8                  // class com/ocean/practice/Animal
        11: dup
        12: invokespecial #9                  // Method com/ocean/practice/Animal."":()V
        15: astore_2
        16: new           #10                 // class com/ocean/practice/Monkey
        19: dup
        20: invokespecial #11                 // Method com/ocean/practice/Monkey."":()V
        23: astore_3
        24: aload_1
        25: aload_2
        26: invokevirtual #12                 // Method test:(Lcom/ocean/practice/Animal;)V
        29: aload_1
        30: aload_3
        31: invokevirtual #12                 // Method test:(Lcom/ocean/practice/Animal;)V
        34: return

第26行和第31行,调用test方法时,都明确了参数是Animal。

动态分派:

package com.ocean.practice;

public class TestDynamicDispatch {

    public static void main(String[] args) {
        Animal animal = new Animal();
        Animal monkey = new Monkey();

        animal.eat();

        monkey.eat();
    }
}

测试结果:

animal eat...
monkey eat...

这和我们所学的完全一样。

jvm指令中:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class com/ocean/practice/Animal
         3: dup
         4: invokespecial #3                  // Method com/ocean/practice/Animal."":()V
         7: astore_1
         8: new           #4                  // class com/ocean/practice/Monkey
        11: dup
        12: invokespecial #5                  // Method com/ocean/practice/Monkey."":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method com/ocean/practice/Animal.eat:()V
        20: aload_2
        21: invokevirtual #6                  // Method com/ocean/practice/Animal.eat:()V
        24: return

第17行和第21行,它调用的都是Animal的eat(),所以,只有到运行期,才能将方法的间接引用转为直接引用,才能明确知道调用哪个方法,而这需要jvm自己去查找。

你可能感兴趣的:(#,jvm,java,jvm,java内存,面试)