Java虚拟机知识总结
java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,有些区域随着虚拟机进程的启动而存在,有的区域则是依赖用户线程的启动和结束而建立和销毁。 java虚拟机主要将内存划分为: 1.程序计数器: 是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复都需要依赖这个计数器来完成。 为了线程切换之后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间的计数器互不影响,独立存储。 如果线程正在执行一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址, 如果执行的Native方法,这个计数器的值则为空,这个区域是唯一一个在虚拟机规范中没有规定任何内存溢出的区域。0 2.虚拟机栈: 其也是线程私有的,生命周期和线程相同,每个方法执行的时候都会创建一个栈帧,用于存放局部变量表,方法返回地址等信息, 每一个方法被调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。 局部变量表中存放了编译期可以知道的各种基本数据类型、对象引用(可能是一个指向对象起始地址的引用指针,也可能是一个代表对象的句柄)。 long和double类型的数据会占用2个局部变量空间,其余为一个。 一般虚拟机栈是可以动态扩展的。这个区域可能会有栈溢出和内存溢出等错误。 3.本地方法栈: 相对虚拟机栈,本地方法栈是为本地方法提供服务的,这个区域可能会有栈溢出和内存溢出的在错误。 4.java堆: 这个java内存管理中最大的一块,在虚拟机启动的时候创建,其唯一的目的是存放对象实例,几乎所有的对象实例都在这里分配内存。 所有的对象实例和数组都在这个区域上分配。这是垃圾回收器的主要工作区域。 java堆可以是物理上不连续的内存空间,主要逻辑上联系就可以了,其可以是固定大小,也可以是动态扩展的。 5.方法区: 这是线程共享的内存区域,用于存放虚拟机加载的类信息、常量、静态变量。 这个区域也不需要是物理上连续的。其中还有一个很重要的区域就是运行时常量池。 6.运行时常量池: 方法区的一部分,用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后存放在运行时常量池中。 用的比较多的主要是String的intern()方法。 程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭。
********************************************************************************************************************************* 对象访问: 不同虚拟机实现的对象访问的方式有所不同,主流的访问方式有两种:使用句柄和直接指针。 1.如果使用句柄访问方式,java堆中会划分出一块内存来作为句柄池,对象引用中存放的是对象的句柄地址,而句柄资质包含了对象和类型数据各自的具体地址信息。 2.如果使用的直接指针的访问方式,java对对象的布局中就必须考虑如何防止访问类型数据的相关信息,对象的引用中存放的是对象的地址。 使用句柄的的好处在于对象引用中存放的是稳定的句柄地址,在对象被移动的时候只会改变句柄中的实例数据指针,而引用本身不用被修改。 使用直接地址的好处在于速度更快,节省了一次指针的定位开销。 如何确定对象是否还存活着? 1.引用计数算法: 给对象中添加一个引用计数器,每当有一个地方引用它的时候,计数器加1,当引用失效的时候,计数器减一,在任何时刻计数器为0的对象就不可能再被引用。 这种方法实现简单,效率高,但java语言并没有采用这种算法来管理内存,因为它不能解决对象之间相互循环引用的问题。 2.java和C#都使用的是一种根搜索算法: 思路是通过一系列的名为GC Root的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径成为引用链,当一个对象到GC Root没有任何引用链的时候,证明此对象是不可用的。 在根搜索算法中不可达的对象,也并非是非死不可的,要真正回收一个对象,至少要经过两次标记过程。 如果对象在进行根搜索后没有发现与gcroots相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要进行finalize()方法,当对象没有重写finalize()方法,或者finalize()已经被虚拟机调用过了,虚拟机将这两种情况视为没有必要执行,这样对象将立即被回收s。 如果这对对象被绑定为有必要执行finalize()方法,那么这个对象将会被放置在一个队列中,并在稍后由一条虚拟机自动建立,优先级较低的线程去执行,这里的执行是指虚拟机会触发这个方法,但不保证会等待他运行结束,这样做的原因是: 如果一个对象的finalize方法执行缓慢,或者发生了死循环,它将导致队列中的其他对象永久处于等待状态,甚至会导致整个内存回收系统崩溃,finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将会对队列中的对象进行第二次小规模的标记,如果对象要在finalize()方法中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可。 譬如把自己(this)赋值给某个变量或者对象的成员变量,那在第二次标记的时候它将被移除队列。 public class FinalizeEcapeGC{ public static FinalizeExcapeGC SAVE_HOOK = null; protected void fianlize() throws Throwable{ super.finalize(); System.out.println("fianlize method executed!"); FinalizeEcapeGC.SAVE_HOOK = this; //将this赋值给成员变量 } public static void main(String args[]){ SAVE_HOOK = new FinalizeExcapeGC(); //成功逃脱 SAVE_HOOK = null; System.gc(); //逃脱失败 SAVE_HOOK = null; System.gc(); } } 代码中两段完全一样的代码片段,执行结果是不一样的,这是因为任何一个对象的finalize()方法只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了(即只可以自救一次)。fianlize()方法的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,finalize()方法的作用可以用try-finaily代替,尽量少用。 方法区中的垃圾回收主要回收两部分的内容:废弃常量和无用的类。以常量池中的字符串常量回收为例,加入常量池中有一个字符串常量"abc",但是没有任何一个String对象引用常量池中的"abc",则这个时候会发生内存回收。 常量池中的其他类(接口)、方法、字段的符号引用也与此类似。 判断一个类是否为无用的类的条件相对苛刻的多,需要满足以下三个条件: 1.该类的所有实例都已经回收 2.加载该类的ClassLoader已经被回收 3.该类的相应的java.lang.Class对象没有在任何地方被引用,无法在地方通过反射访问该类的方法 虚拟机满足以上三个条件,才可以对类进行回收,也仅仅是“可以”。
*********************************************************************************************************************************
常见的垃圾回收算法有: 1.标记-清除算法 2.复制算法 3.标记-整理算法 4.分代-收集算法 java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,不同厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别,并且一般会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。 java技术体系中所提供的自动内存管理最终可以归结为自动化的解决了两个问题:给对象分配内存以及回收分配给对象的内存。 上面讲的时候回收内存,对于给对象分配内存,往大方向上讲,就是在堆上分配,分配的规则并不是百分之百的固定,其细节取决于当前使用的哪一种垃圾回收器的组合,还有虚拟机中与内存相关的参数的设置。 实现语言的无关性仍然是虚拟机和字节码存储格式,是用java编译器可以把java代码编译成存储字节码的class文件,同时使用其他某些语言也可以将程序代码编译成class文件,虚拟机并不关心class的来源是什么语言,只要符合他的class文件应有的结构就可以在java虚拟机中运行。 java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比java语言本身更强大。因此一些java语言本身无法有效支持的语言特性并不代表字节码本身无法有效支持,这也是虚拟机为其他语言提供实现的基础。
********************************************************************************************************************************
class文件的结构: class文件是一组以8位字节为基础的二进制流,各个数据项目严格按照顺序紧凑排列在class文件之中,中间没有添加任何分隔符,这使得整个class文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。 当遇到需要占用8位字节以上空间的数据项的时候,则会按照高位在前的方式分割成若干个8位字节进行存储。 class文件格式采用一种类似C语言结构体的微结构来存储,这种微结构中只有两种数据类型:无符号数和表。 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1,2,4,8个字节的无符号数。 无符号数可以用来描述数字、索引引用、数量值、或者按UTF-8编码构成字符串值。 表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性的以“_info”结尾,表用于描述有层次关系的复合结构的数据,整个class文件本质上就是一张表。 无论是无符号数还是表,当需要描述同一个类型但数量不定的多个数据的时候,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的集合。 每个class文件的头4个字节称为魔数,它的唯一作用是用于确定这个文件是否为一个能被虚拟机接受的class文件,很多文件存储标准中都使用魔数来进行身份识别,使用魔数而不是用扩展名识别主要是基于安全考虑,因为扩展名可以很随意的被改动。 紧接着魔数的4个字节存储的是class文件的版本号,第五和第六个字节是次版本号,第七和第八个字节是主版本号。 紧接着主次版本号之后的是常量池入口,常量池是class文件结构中与其他项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一,同时还class文件中第一个出现的表类型数据项目。 常量池中主要存放两大类常量:字面量和符号引用。 字面量有如:文本字符串、被声明为final的常量值。 符号引用则属于编译原理方面的概念:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。 java代码在进行javac编译的时候,并不像c那样有链接的这一步骤,而是在虚拟机加载class文件的时候进行动态链接,也就是说在class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法直接被虚拟机使用的,当虚拟机运行的时候,需要从常量池中获得对应的符号引用,再在类创建或运行的时候解析并翻译到具体的内存地址中。 常量池结束之后,紧接着的是2个字节的代表访问标识,这个标识用于识别一些类或接口层次的访问信息,包括:这个class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等等。
*********************************************************************************************************************************
虚拟机的类加载机制; class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能被运行和使用。 虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。 在java语言中,类型的加载和链接都是在程序运行期间完成的,这样会在类加载时稍微增加一些性能开销,但是去能为应用程序提供高度的灵活性。 类从被加载到虚拟机内存中开始,到写在出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中验证、准备、解析统称为连接。 虚拟机规范中并没有进行强制约束什么情况下需要开始加载一个类,但是对于初始化阶段,虚拟机规定了有且只有四种情况必须立即对类进行初始化(而加载、验证、准备自然需要在此之前开始)。 1.遇到new、getstatic、putstatic、invokestatic这四条字节码指令的时候,如果类没有进行过初始化,就需要先触发器初始化。 2.使用反射对类进行调用的时候,如果类没有进行过初始化,就需要先触发器初始化。 3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 4.当虚拟机启动的时候,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类。 类加载的详细过程: 1.加载,加载阶段虚拟机需要完成以下三件事情: 通过一个类的全限定名来获取定义此类的二进制字节流 将这个字节流所代表的静态存储结构转化为方法去的运行时数据结构 在java堆中生成代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。 加载阶段是开发期间可控性最强的阶段,因为加载阶段可以使用系统提供的类加载器来完成,也可以使用自定义的类加载器区控制字节流的获取方式。 加载阶段完成之后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义。 2.验证是链接阶段的第一步,这一阶段的目的是为了确保class文件的字节流中包含的信息符合当前虚拟就的要求,并且不会危害虚拟机自身的安全。 但是验证的具体实现并没有给出明确的规定。javay语言是相对安全的语言(对比c),使用纯粹的java代码无法做到诸如访问数组边界之外的的数据,将一个对象转型为他并未实现的类型,跳转到不存在的代码之类的事情。 验证大致可以分为四个阶段: 文件格式验证:验证字节流是否符合class文件格式的规范,并且能被当前版本的虚拟机处理。 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求。 字节码验证:进行数据流和控制流分析,保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在链接的第三阶段--解析阶段中发生。 3.准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。 4.解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 符号引用:以一组符号来描述所引用的目标,符号可以使任何形式的字面量,只要使用时能无歧义的定位到目标即可。 直接引用:直接引用可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。 5.初始化是类加载过程的最后一步,这个阶段是真正开始执行类中定义的java程序代码。 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,即比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则即使这两个来源于同一个class文件,只要加载他们的类加载器不同,那这两个类就必定不相等。 这里所指的“相等”包括Class对象的equals方法和isInstance方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定的情况。 站在java虚拟机的角度讲,只存在两种不同的类加载器,一种是启动类加载器(由c++实现),是虚拟机的一部分;另一种是所有其他的类加载器(由java语言实现),并且全部继承于java.lang.ClassLoader。 站在开发者的角度主要有三种系统提供的类加载器: 1.启动类加载器:主要负责加载<JAVA_HOME>\LIB目录中的类库,无法被java程序直接引用。 2.扩展类加载器:主要负责加载<JAVA_HOME>\LIB\EXT目录中的类库。 3.应用程序类加载器:负责加载classpath上所指定类库。
*********************************************************************************************************************************
执行引擎是java虚拟机最核心的组成部分之一,虚拟机的执行引擎可以自行制定指令集和执行引擎的结构体系,并且能够执行哪些不被硬件直接支持的指令集格式。 在不同的虚拟机实现里面,执行引擎在执行java代码的时候可能有解释执行(通过解释器执行)和编译执行两种选择,也可能两者兼备,甚至还可能包含几个不同级别的编译器执行引擎。 总体上可以看做:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的执行结果。 现在主流的虚拟机都包含了即时编译器,class文件中的代码到底会被解释执行还是编译执行,并不是一定的。 java发展处了可以直接生成本地代码的编译器,而c/c++语言也出现了通过解释器执行的版本。 java虚拟机规范中视图定义一种java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下能够带到一致的并发效果。 java内存模型的主要目标是定义程序中各个变量的访问股则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节,此处的变量指的是实例字段,静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然不存在竞争问题。 java内存模型规定所有的变量都储存在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行而不能直接读写主内存中的变量,不同的线程之间也无法直接访问对方工作内存中的变量,线程间的变量值的传递均需要通过主内存来完成。 当一个变量被定义成volatile之后,它具备两个特性: 保证此变量对所有的线程的可见性,即当一个线程修改了这个变量的值,新值对于其他的线程来说是可以立得知的,而普通的变量值做不到这点,因为变量值在线程间传递均需要通过主内存来完成。 但这不能保证volatile变量的运行在并发下是安全的,因为java里面的运算并非原子操作,可能在实际运算前又被其他其他线程改变了。 无论是普通变量还是volatile变量,在变量读取前从主内存刷新变量值都是依赖于主内存作为传递媒介的方式来实现可见性,volatile变量鞥保证心智立即同步到主内存,以及每次使用前立即从主内存刷新。 java内存模型要求lock,unlock,read,load,assign,use,store和write这八个操作都具有原子性,但是对于没有被vloatile修饰的64位的数据类型(long,double)的读写操作划分为两次32位的操作来进行。 除了volatile之外,synchronized和final关键字也能实现可见性: 同步块的可见性是由“对一个变量执行“unlock”操作之前,必须先把次变量同步到主内存中”这条规则实现。 final关键字修饰的字段在构造器中一旦被初始化完成,并且构造器没有“this”的引用传递出去,那么其他线程中就能看见final字段的值。
*********************************************************************************************************************************
java线程的实现: 线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源又可以独立调度。 每个java.lag.Thread类的实例就代表一个线程,不过Thread类和大部分的java API有着显著的差别,它的所有关键方法都被声明为Native,即方法可能没有使用或者无法使用与平台无关的手段实现。 实现线程主要有三种方式:使用内核线程的实现、使用用户线程的实现、使用用户线程加轻量级进程混合实现。 线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是: 协同式线程调度:线程的执行时间由线程本身控制,线程把自己的工作执行完成之后,要主动通知系统切换到另外的一个线程上去。 抢占式线程调度:每个线程由系统来分配执行时间,线程的切换不由线程本身来决定。 java中的线程调度是系统自动完成的,但是我们可以建议系统给某些线程多分配一点执行时间,另外一些线程则少分配一点--这需要设置线程的优先级。 在两个线程同时ready的时候,优先级越高的线程越容易被系统选择执行。 线程的优先级并不是很靠谱,因为java线程最终还是映射到系统的原生线程上来实现,所有线程调度最终还是操作系统说了算,而实际操作系统的优先级不为10级。 线程的状态切换: java语言中定义了5中线程状态: 1.新建(new):创建后尚未启动的线程处于这种状态。 2.准备(ready):处于此状态的线程正在等待着cpu为他分配执行时间。 3.运行(run):处于此状态的线程正在执行。 4.无限期等待(wait):处于这种状态的线程不会被分配cpu执行时间,要等待被其他线程显式唤醒。 Object.wait()和Thread.join()会让线程进入无限期等待状态。 5.限期等待(sleep):不会被分配cpu执行时间,不过不需要等待被其他的线程显式唤醒,在一定时间之后他们会由系统自动唤醒。 6.阻塞:在等待获得一个排他锁。 7.结束:已终止线程的线程状态,线程已经结束执行。 按照线程安全的”安全程度“由强至弱,我们可以将java语言中的各种操作共享的数据分为以下5类:不可变,绝对线程安全,相对线程安全,线程兼容和线程独立。 1.不可变对象一定是线程安全的 2.vector是一个线程安全的容器,因为它的方法被synchronized修饰,虽然效率低,但是确实安全。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。