深入理解Java虚拟机小结

本文详细介绍了Java虚拟机管理的内存区域划分及其特点,包括方法区、堆、虚拟机栈、本地方法栈和程序计数器。此外,还深入探讨了Java类的加载、验证、准备、解析和初始化过程。

1、Java内存区域划分:

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同的数据区域.

区域权限生命周期
1、方法区线程共享
2、堆线程共享虚拟机启动时创建,虚拟机关闭时销毁
3、虚拟机栈线程私有与线程同步
4、本地方法栈线程私有 
5、程序计数器线程私有
1、程序计数器:
  • 如果线程正在执行一个Java方法, 这个计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是native方法, 这个计数器值则为空;
2、java虚拟机栈:
  • 1、生命周期与线程同步;
  • 2、虚拟机栈描述的是Java方法执行的内存模型: 每个方法在执行的都会创建一个栈帧用于存放局部变量表, 操作数栈, 动态链接, 方法出口等信息;
  • 3、局部变量表存放了编译器可知的各种基本数据类型, 对象引用, 和指向了一条字节码指令的地址;
  • 4、局部变量表所需的内存空间在编译期间完成分配, 运行期间不会被改变;
  • 5、每个方法从调用直至执行完成的过程, 就对应着一个栈帧在虚拟机栈中入栈到出栈的过程;
3、本地方法栈:
  • native方法被调用时创建栈帧;
4、Java堆:
  • 1、此内存区域的唯一目的就是存放对象实例, 几乎所有的对象实例都要在这里创建;
  • 2、在虚拟机启动时创建;
5、方法区:
  • 1、方法区用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
  • 2、运行时常量池是方法区的一部分, 用于存放编译器生成的各种字面量和符号引用. 这部分内容将在类加载后进入方法区的运行时常量池中存放;
3、常量池:
  • 1、常量池位于方法区, 主要存放两大常量:
    1、字面量; 比较接近Java语言层面的常量概念, 如文本字符串, 声明为final的常量值等.
    2、符号引用;
    符号引用属于编译原理方面的概念, 包括下面是三类常量:
    1、类和接口的全限定名;
    2、字段的名称和描述符;
    3、方法的名称和描述符;

2、虚拟机类加载机制:

在Java语言里面, 类型的加载、连接和初始化过程都是在程序运行期间完成的;
这种策略虽然会令类加载时稍微增加一些性能开销, 但是会为Java应用程序提供高度的灵活性, Java里天生可以动态扩展的语言特性就是依赖运行时期动态加载和动态连接这个特点实现的;
例如:

1、如果编写一个面向接口的应用程序, 可以等到运行时再指定其实际的实现类;
2、用户可以通过Java预定义的和自定义类加载器, 让一个本地的应用程序可以在运行时从网络或其他地方加载一个二进制流作为代码的一部分. 可以联想到热修复和插件化;

类从被加载到虚拟机内存中开始, 到卸载出内存为止, 它的整个生命周期包括:

1、加载;
2、验证;
3、准备;
4、解析;
5、卸载;
6、使用;
7、初始化;

其中2,3,4称为连接;

虚拟机规范严格规定了有且只有5中情况必须立即对类进行"初始化"

  • 1、遇到new, getstatic, putstatic或invokestatic这4条字节码指令时, 如果类没有进行初始化, 则需要先触发其初始化. 生成这4条指令的最常见的Java代码场景是: 使用new关键字实例化对象的时候, 读取或设置一个类的静态字段(被final修饰, 已在编译器把结果放入常量池的静态字段除外)的时候, 以及调用一个类的静态方法的时候;
  • 2、使用Java.lang.reflect包的方法对类进行反射调用的时候, 如果类没有进行过初始化, 则需要先触发其初始化;
  • 3、当初始化一个类时, 如果发现其父类还没有进行过初始化, 则需要先触发其父类的初始化.
  • 4、当虚拟机启动时, 用户需要指定一个执行的主类, 虚拟机会先初始化这个类;

代码举例:

Example 01->:
public class SuperClass {
    static {
        System.out.println("SuperClass init");
    }
    public static int value = 123;
}
public class SubClass extends SuperClass {
    static {
        System.out.pirntln("SubClass init");
    }
}
public class Demo {
    public static void main(String[] args) {
        System.out.println(SubClass.init);
    }
}
  • 对于静态字段, 只有直接定义这个字段的类才会被初始化, 因此通过其子类来引用父类中定义的静态字段, 只会触发父类的初始化而不会触发子类的初始化.
常量传播优化:
public class ConstClass {
    static {
        System.out.println("ConstClass init");
    }
    public static final int ConstValue = 123;
}

public class Demo {
    public static void main(String[] args) {
        System.out.println(ConstClass.ConstValue);
    }
}
  • 编译阶段通过常量传播优化, 已经将此常量的值123存储到了Demo类的常量池中, 以后Demo类对自身常量池的引用了.
1、加载:

加载阶段完成后, 虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中.
在加载阶段需要完成以下3件事情:

  • 1、通过一个类的全限定名来获取定义此类的二进制字节流;
  • 2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  • 3、在内存中生成一个代表这个类的java.lang.Class对象, 作为方法区这个类的各种数据的访问入口;
3、准备:
  • 1、准备阶段是正式为类变量分配内存并设置类变量初始值的阶段, 这些变量所使用的内存都将在方法区中进行分配;
  • 2、这时候进行内存分配的仅包括类变量(static), 而不包括实例变量, 实例变量将会在对象实例化时随着对象一起分配在Java堆中.

例如

public static int value = 123;

在准备阶段完成以后, value的值为0, 把value赋值为123的putstatic指令是程序被编译后, 存放于类构造器<clinit>()方法中. 所以把value赋值为123的动作将在初始化阶段才会执行;

4、解析:
  • 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程.
5、初始化:
  • 初始化阶段是执行类构造器<clinit>()方法的过程;
  • 1、<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的;
  • 2、<clinit>()方法与类的构造函数不同, 它不需要显示地调用父类构造器, 虚拟机会保证在子类的<clinit>()方法执行之前, 父类的<clinit>()方法已经执行完毕.
  • 3、由于父类的<clinit>()方法先执行, 也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作.

3、字节码指令:

1、描述符标识字符含义:
标识字符含义 标识字符含义
B基本数据byte J基本数据long
C基本数据char S基本数据short
D基本数据double Z基本数据boolean
F基本数据float V基本数据void
I基本数据int L基本数据对象类型
2、方法访问标志:
标志名称标志值含义
ACC_PUBLIC0x0001方法是否为public
ACC_PRIVATE0x0002方法是否为private
ACC_PROTECTED0x0004方法是否为protected
ACC_STATIC0x0008方法是否为static
ACC_FINAL0x0010方法是否为final
ACC_SYNCHRONIZED0x0020方法是否为synchronized
ACC_BRIDGE0x0040方法是否为由编译器产生的桥接方法
ACC_VARARGS0x0080方法是否接受不定参数
ACC_NATIVE0x0100方法是否为native
ACC_ABSTRACT0x0400方法是否为abstract
ACC_STRICTFP0x0800方法是否为strictfp
ACC_SYNTHETIC0x1000方法是否由编译器自动产生
  • 方法的定义可以通过访问标志, 名称索引, 描述符索引表达清楚, 但方法里面的代码经过编译器编译成字节码指令后, 存放在方法属性表集合中一个名为"Code"的属性里面.
3、Code属性:

Java程序方法体中的代码经过javac编译器处理后, 最终变为字节码指令存储在Code属性内, Code属性出现在方法表的属性集合之中.

4、解析与分派:

  • 1、方法调用并不等同于方法执行, 方法调用阶段唯一的任务是确定被调用方法的版本(即调用哪一个方法), 暂时还不涉及方法内部的具体运行过程. 在程序运行时, 进行方法调用是最普遍, 最频繁的操作, 一切方法调用在Class文件里面存储的都只是符号引用, 而不是方法在实际运行时内存布局中的入口地址(相当于之前说的直接引用). 这个特点使得Java方法调用需要在类加载期间, 甚至到运行期间才能确定目标方法的直接引用;

  • 2、解析:

1、所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用, 在类加载的解析阶段, 会将其中的一部分符号引用转化为直接引用, 这种解析能成立的前提是: 方法在程序真正运行之前就有一个可确定的调用版本, 并且这个方法的调用版本在运行期是不可改变的. 换句话说调用目标在程序代码写好, 编译器进行编译时就必须确定下来. 这类方法的调用称为解析;

2、在Java语言中符合"编译器可知, 运行期不可变"这个要求的方法, 主要包括静态方法和私有方法这两大类, 前者与类型直接关联, 后者在外部不可被访问, 这两种方法各自的特点决定了他们都不可能通过继承或别的方式重写其他版本, 因此他们都适合在类加载阶段进行解析;

3、与之相对应的是, 在Java虚拟机里面提供了5条方法调用字节码指令:

字节码指令含义
invokestatic调用静态方法
invokespecial调用实例构造器<init>方法, 私有方法和父类方法
invokevirtual调用所有的虚方法
invokeinterface调用接口方法, 会在运行时再确定一个实现此接口的对象
invokedynamic先在运行时动态解析出调用点限定符所引用的方法, 然后再执行该方法
  • 只要能被invokestatic和invokespecial指令调用的方法, 都可以在解析阶段中唯一确定的调用版本, 符合这个条件的有静态方法, 私有方法, 实例构造器和父类方法; 他们在类加载的时候就会把符号引用解析为该方法的直接引用;


作者:冉桓彬
链接:http://www.jianshu.com/p/a83cc1f61a04
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值