JVM深入理解
一.JVM介绍
JVM应用百度百科的原话是:
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JAVA语言的一个非常重要的特点就是与平台的无关性。
而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了
与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够
"一次编译,到处运行"的原因。
这句话能够很清楚的明白一件事情:之所以JAVA能够流行起来,JVM是一个非常关键的因数.所以学习JVM的原理和运转流程是很有必要的,对优化JAVA性能也是非常有必要的一个基础条件.
二.运行时区域简单介绍
JAVA虚拟机所管理的内存将会包含了有以下几个运行时数据区域,下图可看:
1.程序计数器
是记录当前线程所执行器的行号指示器.字节码解释器就是改变计数器的值来选取吓一条字节码指令来执行的.因为程序计数器是在私有区域,对于线程来说,每个线程都将拥有一个程序计数器.各个线程计数器都是互不影响,独立存储的.
程序计数器主要记录的是虚拟机的字节码地址,如果当前执行本地方法,那么程序计数器的值为空.
2.JAVA虚拟机栈
虚拟机栈也是线程私有的,是为了描述java方法执行的内存模型:但方法执行时,虚拟机栈会创建一个栈帧,是用来存储局部变量表,操作数栈,动态链接,和方法的返回地址等信息的.方法在执行的过程,就是一个栈帧的入栈到出栈的过程.
1.局部变量表
是存储8大基本类型(boolean,int,byte,long,double,short,float,char),和一个叫做对象引用的reference(只是一个存储的对象指针,指向此对象的位置).
3.本地方法栈
类似于虚拟机栈的功能,但是本地方法栈是虚拟机是使用到了NATIVE方法.
4.JAVA堆
java堆是管理内存的最大区域,这一区域是线程共享的,几乎所有的对象创建后都会保存在堆里面,并且堆也是垃圾回收的重点关注对象,因为垃圾回收对java对象回收的收益是非常高的,在后面我会详细讲解垃圾回收机制有关的知识.
java堆按照年龄段分为:新生代和老年代.
5.方法区
方法区也是线程共享的,它主要存放关于类的信息,常量,静态变量和编译后的代码等.方法区也有人称其为'永久代',因为垃圾回收虽然也会回收方法区的数据,但是回收的效益很低,几乎不会被回收,所以称为'永久代'.
运行时常量池:用于存放通过编译期编译过后的的字面量和符号引用,相当于存放数据结构的的地方.对于class文件,java虚拟机都对其每一个部分有着非常严格的说明与限制.不管是什么语言,在java虚拟机编译后都会统一
编译成class文件,并且在虚拟机上运行.
6.对象的创建与定位
对于java语言,大家都知道是一门对象语言,所以对java来说,对象的创建是非常平常的,让我们来一探究竟.
对代码而言,可能创建对象仅仅就是new这样一个关键字而已,但是java底层一定是做了很多操作的:
1.当虚拟机检查到new关键字时,会检查常量池是否有这个类的符号引用.
2.如果有这个类的符号引用,会检查这个类是否被加载,解析和初始化了,就是常常人们说的'类加载'.
3.如果没有,就会去类加载,如果已经加载了,jvm就会为新对象分配内存空间.新对象的内存大小会在类加载的时候就确定.
4.分配完对象的内存大小,会对这个类进行一些初始化设置,比如:元数据信息,对象的hash码,GC信息等,这些都会被分配到对象的对象头里面.下图为对象的对象头信息图:
三.垃圾收集
1.判断对象是否已经死去
在垃圾收集器,在收集时,会对这个对象判断是否已经不用了,不用了就代表已经死了,需要进行垃圾收集了.所以垃圾收集器时怎么判定对象是否已经死了呢?
有两种方式:计数算法和可达性算法
计数算法:相当于给每一个对象的引用都加1,当引用失效的时候就减1,然后判断此对象的计数器是否为0,如果是就说明此对象已经没有引用了,需要垃圾收集了.
虽然这个方式简单,但是有一个问题,如果对象之间存在相互调用的情况,那么它们的计数器都不为0,导致无法收集.
可达性算法:
基本思路时一个链路式的判定方法,把'GC ROOTS'作为根节点,从根节点往下寻找,搜索这个对象的引用链路,当这个对象没法达到GC Roots的话,说明此
对象不可达了,需要垃圾收集.
2.垃圾收集算法
到目前为此,垃圾收集算法有非常多种,但是我也不能说哪一种时完美的,没有完美的垃圾收集器,只有合适的垃圾收集器.所以,在不同场合,需要选择不同的垃圾收集器
来完成运行中的垃圾收集任务.但是底层的算法大概能分为以下几种:
1.标记-清除算法
看名字就应该知道,此算法有2个阶段,'标记'和'清除'.当发现对象已经死了,就标记,到时候垃圾收集器就会统一'清除'对象.
2.复制算法
复制算法是为了解决效率问题产生的。大多数场景都可以定义为:Survivor和Eden,比例为2:8,而Survivor又可以分为:from Survivor和to Survivor,比例为:1:1.
对于新生代的对象来说,基本90%以上的对象都是‘朝生夕死’的,所以当回收时,就将from Survivor和Eden区还存活的对象复制到to Survivor,其余的空间全部
清除。这样就可以提升性能。
3.标记-整理算法
使用复制算法效率高,但是会浪费一部分的空间,如果不想浪费空间,就可以采用标记-整理算法。它和‘标记-清除’算法很类似,但是,当标记完需要回收的对象时,
‘标记-整理’算法会把存活下来的对象整体向一个方向移动,就可以直接清除掉需要回收的对象。
3.垃圾收集器
垃圾收集器,就是根据各种垃圾回收算法,运用到不同的场合的一种实现。大致的垃圾收集器可以用如下图表示:
每一种垃圾收集器都会运用在不同的场合,或者多个垃圾收集器共用,使得新生代和老年代都可以高效率的运行,目前最前沿的垃圾收集器是G1收集器。
号称最低延迟的'stop the world'。关于每一种垃圾收集器详细的意义,可以自行在网上查阅资料,在这里我就不过多说明了。
四.类文件结构
类文件就是通过编译器编译过后,生成的.class文件,之所以java语言是跨平台的,其实关键就是在类文件class身上。
1.类文件结构
class文件其实就是一组由8位字节为基本单元的二进制流,文件的排序必须严格按照规定顺序排列在class文件中,并且没有任何空隙。class文件的结构大概就是:
2种:无符号数和表。
无符号数:是一种基本数据结构,以:u1,u2,u4,u8分别代表1字节,2字节,4字节和8字节的无符号数。无符号数可以用来描述数字,数量值,字符串等。
表:就是由多个无符号数组合而成的一种复合结构。其实整个class文件就是一张表罢了。
1.魔数
class文件的前4个字节被称为“魔数”,它被当做唯一确定文件是否是class文件的标识。魔术的值为:0xCAFFEBABE.
2.版本号
第5,6和第7,8都是版本号,第5,6个字符是此版本号,第7,8个字符是主版本号。
3.常量池
它是整个class文件中和其他项目关联最多的结构了。也是最大的一块结构。它是存储class文件中的资源仓库。常量池的常量并不是固定的,所以在常量池的入口会放置一个u2的
数据类型,代表当前常量池的计量值。常量池主要存放2大类常量:字面量和符号引用。
字面量:文本符号,final的常量值等
符号引用:类,接口的全限定名,字段名,方法名
常量池的基本类型有如下几十种:
所以说常量池是最繁琐的结构,数据,每一种的结构数据都不一样。
2.字节码指令
字节码指令都是由一个字节长度以及后面跟随的多个操作参数组成的。字节码的操作是通过在虚拟机栈的操作数栈实现的。
由于一开始就已经限制了字节码指令长度为一个字节,所以所有的指令就不能超过256个。我这里就不罗列字节码的指令了。
五.类加载机制
上一章我们讲了class文件结构,但是jvm是怎么加载类文件的呢?下面我们来一起探讨下这个问题
1.类加载时机
类加载的整个生命周期大致分为下列7个阶段:
在什么时候会触发第一个阶段‘加载’呢?有如下5种情况:
1.遇到new,getstatic,putstatic,invokestatic字节码时,如果没有类加载,那么就需要进行类加载操作。
2.使用refect反射时,如果类没有初始化。
3.当初始化一个类,发现其父类没有初始化时。
4.当虚拟机启动,执行的主类没有初始化时。
5.当jdk为1.7以上,如果MethodHandle的解析结果为REF_getStatic,REF_putstatic,REF_invokestatic的类没有初始化时。
2.类加载过程
1.加载
这个阶段需要完成:
1.通过全限定名获取类的二进制流。
2.将这个流所代表的静态结构转换为方法区的运行时数据结构。
3.在内存中生成一个代表这个类的java.lang.class对象。作为这个类的访问入口。
2.验证
验证当前字节流中的信息是否符合虚拟机的要求,包含:
1.文件格式:
1.是否是以‘魔数’开头。
2.主,副版本号是否符合当前虚拟机。
3.常量池中的常量是否有不被支持的类型。
4.其他等等要求。
2.元数据验证:
1.验证当前类的父类,类的字段等信息是否符合规范
3.字节码验证:
对每一个字节码都进行验证,检查字节码是否符合规范,逻辑。
4.符号引用:
对常量池中的符号引用做验证。
3.准备
为类的变量分配初始值的阶段。但是这个类的变量限于:被static修饰的变量,并且当前赋值和java代码没哟关系,仅仅是赋变量的初始值,比如int类型的初始值为0.
4.解析
就是把常量池的符号引用解析为直接引用的过程。
5.初始化
这一阶段,才开始定义java代码。在准备阶段已经赋了一次初始值,这个阶段是第二次赋值。它会把java代码的初始值给赋上。
3.类加载器
类加载器顾名思义就是用来加载类的。比较两个类是否相等,其实最底层是比较他们是否为同一个加载器加载的。类加载器有一个很重要的词语:双亲委派。
类加载器分大类就2类:启动类加载器(C语言实现),其他类加载器(java语言实现)。下图为加载器的继承图:
这个模式就是双亲委派模型。意思是:当一个类加载器收到类加载的信息时,一般是先委派给父类完成,一般就会传递到顶层加载,当父类无法加载时,子加载器才会自己尝试加载。
双亲委派对于java的稳定来说至关重要。
这次基本就说到这,强烈建议大家去看<<深入理解Java虚拟机:JVM高级特性与最佳实践>>这本书。