JVM

Posted baoziy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM相关的知识,希望对你有一定的参考价值。

1、  JVM的内存模型

a)         Java虚拟机的内存空间分为五个部分:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区

b)         程序计数器:程序计数器中存放的是当前线程正在执行的字节码指令的地址。如果当前线程执行的一个本地方法,那么当前的程序计数器为空。

                         i.              字节码解释器通过程序计数器来依次读取指令,从而实现程序的流程控制

                       ii.              程序计数器用来记录当前线程正在执行的位置,以便于在线程切换之后还能找到原来的程序执行到了什么地方

                      iii.              程序计数器是一块较小的内存空间,线程私有,随着线程的创而创建,随着线程的结束而销毁,也是唯一一个不会出现OutOfMerrary的区域。

c)         Java虚拟机栈:用来描述Java方法运行过程的内存模型,Java虚拟机会为每一个即将运行的Java方法创建一个叫做栈帧的区域,该区域用来存放该Java方法运行过程中需要的一些信息,这些信息主要包括:局部变量表、操作数栈、动态链接、方法的出口信息等。

                         i.              栈帧中的局部变量表的创建是在Java方法即将运行的时候创建的,但是局部变量表的大小是在编译阶段就已经确定的,创建的时候只是根据编译阶段确定好的大小分配内存空间。

                       ii.              Java虚拟机栈是线程私有的,随着线程的创建而创建,随着线程的结束而死亡,Java虚拟机栈一般会出现两种错误:Stackoverflow和OutOfMemoryError。

                      iii.              如果单个线程请求的栈的深度超多了虚拟机允许的最大深度,那么程序就会出现Stackoverflow,Java虚拟机会为每一个线程的虚拟机栈分配一定大小的内存空间,因此Java虚拟机栈能存放的栈帧的数量是有限的,如果栈帧不断进栈而不出栈,那么就会出现Java虚拟机栈的内存空间就会被消耗完毕,

                      iv.              不同于StackOverflowError,OutOfMemoryError指的是当整个虚拟机栈内存耗尽,并且无法再申请到新的内存时抛出的异常。JVM没有提供整个虚拟机栈内存空间的分配参数,虚拟机栈的最大内存大致上等于“JVM进程能占用的最大内存(依赖于具体操作系统) - 最大堆内存 - 最大方法区内存 - 程序计数器内存(可以忽略不计) - JVM进程本身消耗内存。

d)         本地方法栈的功能和Java虚拟机栈的功能是类似的,也会出现这两种异常错误,只是本地方法栈是用来描述本地方法运行过程的内存模型,同样也是线程私有的。

e)         堆:堆是用来存放对象的内存空间,几乎所有的对象都是存放在堆空间中。

                         i.              堆空间是整个虚拟机共享的内存空间

                       ii.              在虚拟机启动的时候创建堆内存空间,在虚拟机关闭的时候销毁对内存空间

                      iii.              堆是进行垃圾收集的主要场所

                      iv.              堆空间可以进一步划分为新生代和老年代,新生代又可以进一步划分为Eden,from survior和to survior区域,不同的区域存放不同周期的对象,针对不同的区域采用不同的垃圾收集方法,可以是垃圾收集效率更高,更具有针对性

                       v.              堆内存空间的大小可以是固定的,也可以是变化的,但是现在一般主流的虚拟机的对内存空间大小都是可变的。

f)          方法区:在Java虚拟机规范中定义方法区是堆的一个逻辑区域,方法区中主要存放的是类信息、常量、静态变量、以及即时编译器编译的代码等

                         i.              方法区也是整个虚拟机共享的。

                       ii.              方法区中存放的都是一些需要长期保存的对象,因此垃圾收集器在方法区中的垃圾收集效率比较低。

                      iii.              Java虚拟机规范中没有定义方法区的大小,所以方法区的大小可以是固定的,也可以是变化的。

                      iv.              运行时常量池:一个类经过编译器的编译会生成一个.class文件,这个文件中存放的是整个类的全部信息,当这个文件被虚拟机加载之后,常量就会被放在方法区的运行时常量池中,在程序运行期间也可以往里边添加常量,但是如果一个常量长时间没有被别的对象或者变量引用的时候就会被垃圾收集器清除掉。

g)         直接内存:直接内存指的是除了Java虚拟机内存之外的内存空间,他虽然不属于Java虚拟机的内存,但是也能够被Java虚拟机所使用,在NIO中引入了一种基于通道和缓存的IO方式,它通过调用本地方法直接使用Java虚拟机之外的内存,从而提升了数据的操作效率。直接内存的大小不受Java虚拟机的影响,但是既然是内存,那么当内存不足的时候也会出现OOM的一场错误。

2、  JVM的垃圾收集策略

a)         为什么堆空间是进行垃圾收集的主要场所:程序计数器和Java虚拟机栈以及本地方法栈都是线程私有的,他们随着线程的创建而创建,随着线程的结束而销毁,那么垃圾收集器针对这三个区域的清除工作其实就会变的非常简单了。堆和方法区是整个虚拟机共享的内存区域,他们是在虚拟机启动的时候创建,但虚拟机关闭的时候销毁,所以他们的生存周期比较长,并且堆中存放的是运行过程中产生的所有对象,虽然每个对象的大小在程序编译阶段就已经确定了,但是只有在程序运行期间才能确定究竟创建多少个对象;方法区中存放的是类信息、常量、静态变量以及即时编译器编译的代码等,但是类的加载过程是在程序运行过程中执行的,当需要用到这个类对象时候才会去加载这个类,所以我们也不知道在程序运行过程中究竟需要加载多少个类。综上所述,堆和方法区才是进行垃圾收集的主要场所。

b)         如何判断哪些对象需要被回收?

                         i.              判断一个对象无效的依据就是这个对象不再被任何别的对象或者变量引用,判断的方法主要有引用计数法和可达性分析法。

                       ii.              引用计数法:每一个对象都有一个引用计数器,当这个对象被引用一次计数器加一,断开引用计数器减一,所以当引用计数器的值为0的时候表示该对象不再被任何别的对象或者变量引用。

                      iii.              可达性分析法:所有和GCRoots直接或者间接相关联的对象都是有效对象,其中GCRoots指的是:Java虚拟机栈中所引用的对象、本地方法栈中所引用的对象、方法区中静态属性或者常量所引用的对象。

                      iv.              判断一个对象是否失效使用可达性分析法,因为引用计数法无法解决循环引用问题。

c)         无效对象回收的过程:

                         i.              首先判断该对象是否重写了finalize()方法:如果没有则直接释放该对象的内存,如果重写了,那么就把该对象finalize()方法一个F-Queue的队列中

                       ii.              执行F-Queue队列中的finalize()方法,虚拟机以较低的优先级执行这些方法,并且不能保证这些方法都会被执行,如果执行finalize()的时候出现耗时情况则虚拟机直接停止并且释放掉内存

                      iii.              对象的重生或者死亡:如果在执行finalize()的时候对象被别的对象或者变量引用,那么该对象就获得重生,否则也就会被释放掉内存。

d)         垃圾收集算法:标记-清除算法、复制算法、标记-整理算法、分代收集算法

                         i.              标记-清除算法:利用可达性分析法标记出哪些对象是失效对象,并给这些失效对象做上标记,然后清除这些被标记的对象。该算法的标记和清除效率都比较低,清除之后还会产生大量的碎片空间,导致无法存储大对象,降低了内存空间利用率。

                       ii.              复制算法:将内存空间分为两部分,每次只在其中一部分上存储数据,当需要进行垃圾回收的时候,先标记出失效对象,然后再把未失效对象全部复制到另一部分区域上,然后再把当前区域全部清除掉。这种算法避免了碎片化空间的问题,但是这种方法导致内存空间减小了,因为每次都需要将存活对象复制一遍,所以效率也不是很高。这种算法一般用在新生代中。

                      iii.              标记-整理算法:在进行垃圾收集之前,将所有的失效对象做上标记,然后将所有未被标记的对象移到另一边,然后清空这一部分区域即可。这是一种老年代的垃圾收集算法,老年代中对象的生命周期比较长,因此每次垃圾收集之后就会有大量的对象存活,所以如果选用复制算法的话,每次垃圾收集都会复制大量的对象,效率就会比较低。并且老年代使用标记-整理算法还可以为新生代进行“分配担保“。

                      iv.              分代收集算法:分代收集算法是根据对象的存活周期的不同,将内存划分为几个区域。当前的商业虚拟机的垃圾收集都采用了该算法。一般把Java堆分成新生代(年轻代)和老年代(年老代)。这样就可以根据各年代中对象的存活周期来选择最合适的收集算法了。新生代,由于只有少量的对象能存活下来,所以选用“复制算法”,只需要付出少量存活对象的复制成本。老年代,由于对象的存活率高,没有额外的空间分担,就必须使用“标记-清除”或“标记-整理”算法。

e)         新生代中如何解决空间利用率问题:新生代中大量对象都是朝生夕死,进行一次垃圾收集只有少量的对象存活,新生代划分为Eden、Survior1、Survior2,内存大小是8:1:1。分配内存时用Eden和Survior1。当Eden+Survior1的内存即将满时JVM会发起一次MinorGC,将所有存活下来的对象复制到另一块Survior2中。接下来就使用Survior2+Eden内存分配。

f)          分配担保策略:

                         i.              当垃圾收集器准备要新生代进行垃圾收集的时候,首先会检查老年代中的连续内存区域是否可以装下整个新生代中对象的大小

                       ii.              如果能够完全装下,那么这一次垃圾收集就是没有风险的,可以直接进行

                      iii.              如果不能完全装下,此时如果进行垃圾收集工作就是有风险的,垃圾收集器会进行一次评估:根据以往的垃圾收集之后新生代中存活下来的对象来判断这一次垃圾收集能不能进行,如果以往 的情况能够装下,那么虽然有风险但是可以执行,如果不能装下,那么就需要对老年代进行垃圾收集。

g)         堆内存空间的分配策略:

                         i.              对象优先在Eden区域中进行分配

                       ii.              大对象直接进入老年代

                      iii.              生命周期较长的对象直接进入老年代

                      iv.              在servior区域中,相同年龄的对象内存超过servior内存一半的对象以及比这些年龄大的对象都要进入到老年代

3、  类加载

a)         类加载器的作用:用于把Java类生成的.class文件加载进虚拟机内存中,并且存放在Java虚拟机的方法区,并且生成一个Class对象作为这个类被外界访问的一个接口。

b)         类加载器的种类:启动类加载器、扩展类加载器、应用程序类加载器

c)         双亲委派模型:如果一个类加载器收到一个类的加载请求,那么当前类加载器会把这个请求提交给父类加载器,如果父类加载器能够正常执行该请求,那么就会返回这个类对应的Class对象,如果父类加载器不能够正确执行,则由当前类加载器执行加载请求,但是不管那个类加载器加载该类,都会保证每一个类都只能由一个类加载器进行加载,并且保证同一个类只能返回一个Class对象。

d)         类的生命周期:加载、验证、准备、解析、初始化、使用、卸载。

                         i.              加载:通过类的全限定名找到.class文件,把这个文件加载进入Java虚拟机的方法区,然后创建一个Class对象作为外界访问该对象的接口

                       ii.              验证:用于检验存入方法区中的二进制字节流是否符合Java虚拟机规范。我们知道Java语言是安全语言。它的安全性是通过编译器实现的,因为验证环节会验证数组指针是否越界,代码是否会跳转到一个不存在的地方等等问题,如果出现这样的问题编译器是不允许通过的。编译器和虚拟机其实是相互独立的,虚拟机只接受二进制字节码,不关心如何生成的,并且二进制字节码确实可以通过其他的方法获取到,这样就必须要验证才能保证这些二进制字节码是符合Java虚拟机规范的。主要验证文件格式、元数据、字节码、以及符号引用

                      iii.              准备:为方法区中静态成员变量分配内存空间,并且为静态成员变量设置默认初始值。

                      iv.              解析:Java虚拟机将常量池中的符号引用变为直接引用的过程

                       v.              初始化:就是执行类构造器Clinit()方法的过程,该方法收集类中静态代码块中的成员属性赋值语句和静态成员变量的赋值语句进行显示初始化。(准备阶段的是默认初始化)

e)         在Java语言中,类的加载过程是在程序运行期间完成的,虽然会增加程序运行过程的开销,但是随之带来的好处就是能够提高程序的灵活性,Java语言的灵活性主要体现在能够实现动态扩展,所谓的动态扩展指的是动态加载和动态链接。

f)          类加载过程中执行初始化的时机(Java虚拟机规范中只规定了类初始化过程开始的时机):

                         i.              当类加载过程中遇到如下指令(new,putstatic,getStatic,invocStatic):遇到new指令用于创建一个对象、获取或者设置一个类的静态成员变量、调用一个类的静态成员函数

                       ii.              当执行java.lang.Reflect进行反射调用的时候。如果当前类没有被初始化,则会执行初始化操作

                      iii.              当初始化一个类时,如果当前类的父类还没有被初始化,那么就先初始化该类的父类,然后再初始化当前类

                      iv.              在Java虚拟机启动的时候,Java虚拟机首先初始化带有main函数的类。

g)         主动引用和被动引用:JVM规范中要求只有在满足上述四个条件其中之一的时候才会执行类的初始化过程,这种情况叫做主动引用,简介满足上诉四种条件叫做被动引用

h)         类的加载过程和接口的加载过程的比较:类和接口都需要进行初始化,他们的初始化过程大致是相同的,不同点在于当类在初始化过程发现父类还没被初始化的时候会先去初始化父类,但是接口在执行初始化时,不去考虑当前接口的父接口是否已经被初始化了,只有在需要使用父接口的方法属性的时候才执行初始化过程。

4、  Class文件结构

a)         Java语言的平台无关性:Java语言具有平台无关性,也就是说Java语言能够在任何的操作系统上运行,之所以这样是因为Java语言运行在Java虚拟机上,不同 的操作系统都有对应的Java虚拟机,从而实现了一次编译到处运行。

b)         Java虚拟机不但有平台无关性,而且还具有语言无关性的特点,Java虚拟机只认二进制字节码,不关心这些二进制字节码是如何产生的,所以只要能产生的二进制字节码符合Java虚拟机规范都能在Java虚拟机上运行。

c)         Class文件是一个二进制文件,里边的所有数据都是通过01二进制位来表示的。

以上是关于JVM的主要内容,如果未能解决你的问题,请参考以下文章

jvm基础--JVM参数配置

jvm基础--JVM内存模型

jvm基础--JVM内存模型

JVM基础:深入学习JVM堆与JVM栈(转)

JVM堆与JVM栈

JVM内存管理和JVM垃圾回收机制