java 虚拟机(一)

Java虚拟机其实是一种规范,不同的虚拟机对各个区域实现的具体方式是不一样的,今天主要讨论的是现在使用最广的hotspot。
虚拟机内存模型示意图
在这里插入图片描述程序计数器:
这个对我们开发者来说很少了解,但是在程序运行时其实使用频率是最高的,因为在任意确定时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令,为了线程切换又恢复后能回到原来正确的执行位置,每条线程都需要有一个独立的计数器,各条线程之间计数器互不影响,独立存储我们称这类的内存区为”线程私有”的内存。如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码执行的地址;如果正在执行的是一个本地(native)方法,这个计数器值为空(undefined)。此内存区域是唯一一个在《java 虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈:
与程序计数器一样,java 虚拟机栈也是线程私有的,他的生命周期与线程相同,虚拟机栈描述的是java方法执行的线程内存模型:每个方法执行的时候,java虚拟机都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法直至执行完毕的过程,就对应着虚拟机栈从入栈到出栈的过程,局部变量表存放了编译器可知的各种java 虚拟机数据类型(boolean,byte,char,short,int,float,long,double),引用对象类型,也可能是一个对象句柄,这些数据类型在局部变量表中以槽(solt)
表示,除了long和double 占用两个槽以外,其他类型都占一个槽,局部变量表所需的局部变量空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,方法运行期间不会改变局部变量表的大小,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError ,当栈无法申请到足够的内存的时候会抛出OutOfMemoryError异常。
本地方法栈:
本地方法栈与虚拟机栈所发挥的做哟个是非常相似的,其区别就是虚拟机栈为虚拟机执行java方法,本地方法栈是为本地native方法服务。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError ,当栈无法申请到足够的内存的时候会抛出OutOfMemoryError异常。
JAVA堆:
Java 堆是是java虚拟机所管理的内存中最大的一块。
在虚拟机启动时创建。此内存区域的唯一目的的就是存放对象实例,
在《java虚拟机规范》中对java堆的描述是“所有的对象实力以及数组都应该在堆上分配”
Java 堆是垃圾搜集其管理的内存区域,因此一些资料中称他为“GC堆”(garbage collect heap)。
如果在java堆中没有完成内存实力分配,并且堆也无法再扩展时,java虚拟机会报outOfMemory 异常。
方法区:
方法去与java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类型信息,常量,静态变量,编译后的代码缓存等数据,jdk1.8以及以后完全废弃了永久代的概念,改用了与jrockit一样的内地内存中的元空间来代替。如果方法去无法马努内存分配的需求是,将抛出OutOfMemoryError异常。
方法区中都保存什么类型信息
包括以下几点:
类的完整名称(比如,java.long.String)
类的直接父类的完整名称
类的直接实现接口的有序列表(因为一个类直接实现的接口可能不止一个,因此放到一个有序表中)
类的修饰符可以看做是,对一个类进行登记,这个类的名字叫啥,他粑粑是谁、有没有实现接口, 权限是啥;
类型的常量池 (即运行时常量池)
每一个Class文件中,都维护着一个常量池(这个保存在类文件里面,不要与方法区的运行时常量池搞混),里面存放着编译时期生成的各种字面值和符号引用;这个常量池的内容,在类加载的时候,被复制到方法区的运行时常量池 ;
字面值:就是像string, 基本数据类型,以及它们的包装类的值,以及final修饰的变量,简单说就是在编译期间,就可以确定下来的值;
符号引用:不同于我们常说的引用,它们是对类型,域和方法的引用,类似于面向过程语言使用的前期绑定,对方法调用产生的引用;
存在这里面的数据,类似于保存在数组中,外部根据索引来获得它们 ;
字段信息
· 声明的顺序
· 修饰符
· 类型
· 名字
方法信息
· 声明的顺序
· 修饰符
· 返回值类型
· 名字
· 参数列表(有序保存)
· 异常表(方法抛出的异常)
· 方法字节码(native、abstract方法除外,)
· 操作数栈和局部变量表大小
类变量(即static变量)
非final类变量
在java虚拟机使用一个类之前,它必须在方法区中为每个非final类变量分配空间。非final类变量存储在定义它的类中;
final类变量(不存储在这里)
由于final的不可改变性,因此,final类变量的值在编译期间,就被确定了,因此被保存在类的常量池里面,然后在加载类的时候,复制进方法区的运行时常量池里面 ;final类变量存储在运行时常量池里面,每一个使用它的类保存着一个对其的引用;
对类加载器的引用
jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。
对Class类的引用
jvm为每个加载的类都创建一个java.lang.Class的实例(存储在堆上)。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据(类的元数据)联系起来, 因此,类的元数据里面保存了一个Class对象的引用;
方法表
为了提高访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,jvm的实现者还可以添加一些其他的数据结构,如方法表。jvm对每个加载的非虚拟类的类型信息中都添加了一个方法表,方法表是一组对类实例方法的直接引用(包括从父类继承的方法。jvm可以通过方法表快速激活实例方法。(译者:这里的方法表与C++中的虚拟函数表一样,但java方法全都 是virtual的,自然也不用虚拟二字了。正像java宣称没有 指针了,其实java里全是指针。更安全只是加了更完备的检查机制,但这都是以牺牲效率为代价的,个人认为java的设计者 始终是把安全放在效率之上的,所有java才更适合于网络开发)
运行时常量池:
运行时常量池是方法去的一部分,class文件中除了有类的版本,字段,方法,接口描述等信息外,还有一项常量池表,用来存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

通过编译器将Test.java文件编译为Test.class,利用javap -verbose Test.class对编译后的字节码进行分析,
总结
一个类文件首先加载到方法区,一些符号引用被解析(静态解析)为直接引用或者等到运行时分派(动态绑定),经过一系列的加载过程(class文件的常量池被加载到方法区的运行时常量池,各种其它的静态存储结构被加载为方法区运行时数据解构等等)
然后程序通过Class对象来访问方法区里的各种类型数据,当加载完之后,程序发现了main方法,也就是程序入口,那么程序就在栈里创建了一个栈帧,逐行读取方法里的代码所转换为的指令,而这些指令大多已经被解析为直接引用了,那么程序通过持有这些直接引用使用指令去方法区中寻找变量对应的字面量来进行方法操作。
操作完成后方法返回给调用方,该栈帧出栈。内存空间被GC回收,堆里被new的那些也就被来及回收机制GC了。
全流程包括以下几步:源码编写–编译(javac编译和jit编译,java语法糖)—类文件被加载到虚拟机(类Class文件结构,虚拟机运行时内存分析,类加载机制)—-虚拟机执行二进制字节码(虚拟机字节码执行系统)—垃圾回收(JVM垃圾回收机制)

转自:肥肥的雪球

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值