JVM方法调用与方法区

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/programmer_at/article/details/79764148

0. 方法区

0.1 内容加载的时间节点

0.1.1 类信息

==加载阶段==会将外部的class文件(二进制字节流)按照特定格式存储在方法区。
在HotSpot JVM还会在方法区实例化与这个class文件相对应的java.lang.Class类对象

0.1.2 静态变量

==准备阶段==为静态变量分配内存并设置初始值
==初始化阶段==为静态变量赋值

0.1.3 常量

三类常量:Enum,类中的static final修饰的字段,interface中的字段
在类加载完毕后,常量就会放到方法区了。

静态变量和常量都放在了方法区的==常量池==中
但是常量池的内容并非全部来自class文件中常量池的内容。运行时也有可能将新的常量放入池中,比如,String的intern()方法。
class文件中的常量池将在类加载后放入方法区的常量池。但是,方法区的常量池相对于class文件的常量池,具有动态性。

0.1.4 代码

代码存储在class文件的code属性
类加载完毕后,代码会放到方法区

对于JVM内存的其他区域的内容载入的时间节点就十分明确了
虚拟机栈:方法执行时会创建栈桢,存储局部变量,操作数,动态链接(常量池中该方法的引用),返回地址。方法执行完毕后,栈桢从栈中弹出。
Java堆:运行时实例化的对象会放到Java堆

0.1.5 虚方法表

使用虚方法表类提高动态分配是进行方法选择时搜索合适目标方法的效率。
方法表一般在类加载的连接阶段进行初始化,完成类变量初始化后,类的方法表也初始化完毕。

0.2 方法区会发生OOM吗?


在JDK1.7之前,方法区是放在永久代,而永久代的大小是固定的,频繁地调用String的intern方法(编译器会将字符串添加到常量池中),就可能导致OOM。此外,倘若一下子加载多个类,超过了永久代的大小,也会导致OOM。

在JDK1.7之后,方法区的常量池放在了堆,而类信息则放到了MetaSpace,大概长这样,
这里写图片描述
String的intern方法也有所改善了,不再一味地复制副本到常量池以致于OOM。而常量池扩容的原因在于,常量池不足以容纳运行时产生新的常量就会扩容。倘若一下子加载多个类, 以至于超过MeatSpace的容量,MetaSpace也会扩容。
那么JDK1.7后,方法区会不会发生OOM,还是会。毕竟,常量池和MetaSpace不能无限扩大。

只能说,==JDK1.7之后,方法区不再那么容易OOM了==。

JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。
引自:https://blog.csdn.net/soonfly/article/details/70147205

1. 解析调用

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析后的结果存储在==方法区的运行时常量池==。
在类加载阶段的解析阶段将符号引用转化为直接引用的前提:==该方法的调用在运行期不可变==, 即,静态办法和私有方法,这两种方法不可以通过继承或别的方式重写。

在==类装载的解析阶段==把符号引用全部转为可确定的直接引用,解析调用是一个静态的过程。

解析阶段方法调用的字节码是invokestatic和invokespecial。

2. 分派调用

2.1 静态分派

静态分派发生在编译阶段。==方法重载是静态分派的典型应用==,所以,重载时是通过参数的==静态类型==而不是实际类型作为判断依据的。

public class StaticDispatch {  

    static abstract class Human {  
    }  

    static class Man extends Human {  
    }  

    static class Woman extends Human {  
    }  

    public void sayHello(Human guy) {  
        System.out.println("hello,guy!");  
    }  

    public void sayHello(Man guy) {  
        System.out.println("hello,gentleman!");  
    }  

    public void sayHello(Woman guy) {  
        System.out.println("hello,lady!");  
    }  

    public static void main(String[] args) {  
        Human man = new Man();  
        Human woman = new Woman();  
        StaticDispatch sr = new StaticDispatch();  
        sr.sayHello(man);  // man的静态类型是Human,实际类型是Man
        sr.sayHello(woman);  // woman的静态类型是Human,实际类型是Woman
    }  
}  

输出:
hello, guy!
hello, guy!

通过静态分派确定的重载方法倘若存在多个,那么就选“更加合适”的版本。这种模糊选择的原因在于:字面量没有显式的静态类型,它的静态类型只能通过语言上的规则去理解和推断。

2.2 动态分派

动态分配发生在运行期。重写是动态分派的典型应用。所以,方法重写时是通过参数的==实际类型==而不是静态类型作为判断依据的。

public class DynamicDispatch {  

    static abstract class Human {  
        protected abstract void sayHello();  
    }  

    static class Man extends Human {  
        @Override  
        protected void sayHello() {  
            System.out.println("man say hello");  
        }  
    }  

    static class Woman extends Human {  
        @Override  
        protected void sayHello() {  
            System.out.println("woman say hello");  
        }  
    }  

    public static void main(String[] args) {  
        Human man = new Man();  
        Human woman = new Woman();  
        man.sayHello();  
        woman.sayHello();  
        man = new Woman();  
        man.sayHello();  
    }  
} 

输出:
man say hello
woman say hello
woman say hello

调用重写方法的字节码指令为invokevirtural指令。而invokevirtural指令第一步就是在运行期确定接受者的实际类型。

2.3 单分派与多分派

宗量:方法的接受者与方法的参数
单分派:根据多个宗量对目标方法进行选择
多分派:根据单个宗量对目标方法进行选择
Java是静态多分派(接受者+方法参量),动态单分派(接受者)

Reference

《深入理解Java虚拟机》,周志明著

猜你喜欢

转载自blog.csdn.net/programmer_at/article/details/79764148
今日推荐