赞
踩
JVM,Java Virtual Machine(Java虚拟机),是Java语言的运行环境。
JVM的内部体系结构分为三部分:类加载器子系统、运行时数据区和执行引擎。
执行引擎包括:解释器、即时编译器、垃圾回收器。
大部分的程序代码,在转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过下图中的步骤:
众所周知,java代码可通过javac编译器生成字节码(这部分操作是在虚拟机之外进行的),如下图所示(对应上图中的橙色部分):
虚拟机中的执行引擎通过字节码解释器,将上述编译生成的字节码翻译成对应的机器指令,逐条读入,逐条解释翻译;
由于解释执行的速度比较慢,所以Java又引入了JIT技术,将源代码直接编译成和本地机器平台相关的机器语言,从而提高了执行速度。
这也是后来Java被称为“半编译半解释型语言”的原因。
JIT的工作原理如下图所示:
当一段代码在将来只会被执行一次,那么编译就是在浪费精力,因为将代码翻译成字节码,相对于编译这段代码并执行代码来说,要快得多。
但是,当一段代码频繁地调用方法,或是一个循环,也就是这段代码被多次执行时(“热区代码”),那么编译一次再多次执行,就比多次解释加执行快得多。
垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。
垃圾对象标记算法主要有以下两种:
1)引用计数算法:每个对象保存一个整型引用计数器,用来记录对象被引用的次数,当该对象被另一个对象引用时,计数器加1,当失去一个引用时,计数器减1;引用计数算法就是通过判断对象的引用数量来决定对象是否可以被当做垃圾对象回收掉。虽然引用计数法效率高,但是当两个对象互相引用时会导致这两个对象一直不会被回收,这是一个致命缺陷。所以JVM并没有采用该标记算法。
2)可达性分析算法:运行程序把所有的引用关系链看作一张图,通过GC-Roots根对象集合作为起始点,从每个根节点向下不断搜索被根对象集合所连接的对象是否可达,搜索路径称为引用链(Reference-Chain),如果对象到GC-Roots没有任何引用链存在,则说明此对象是不可用的,如图所示:
相对于引用计数法算法,可达性分析算法则避免了循环引用导致的问题,同样具备执行高效的特点,也是JVM采用的标记算法。
垃圾回收算法主要有四种:标记清除算法、标记整理算法、复制算法、分代收集算法。
1)标记清除法:分为标记和清除两个阶段;标记阶段:从根对象集合进行扫描,对存活的对象对象标记;清除阶段:再次扫描发现未被标记的对象并进行回收;
该算法效率不高,进行垃圾回收需要暂停应用程序,同时会产生大量内存碎片,后续程序运行过程中分配内存占用较大的对象时,会有连续内存不够情况,容易触发再一次垃圾收集动作。
2)标记整理算法:分为三个阶段,可以理解为比标记清除算法多了一个整理阶段;第一阶段标记出垃圾对象;第二阶段让所有存活的对象都向内存区一端移动;第三阶段直接清理掉边界端以外的内存,类似于磁盘整理的过程。
该垃圾回收算法效率不高,对象移动过程需要暂停应用程序,适用于对象存活率高的场景,比如老年代。
3)复制算法:复制算法将内存按容量划分为大小相等的两块,每次只使用其中的一块,当使用的这块的内存用完,就将还存活着的对象复制到另外一块空闲内存上,然后使用过的内存空间一次清理。
该算法实现简单,运行效率高,但是内存空间严重浪费,适用于对象存活率低的场景,比如新生代。
4)分代收集算法:分代收集算法根据年轻代和老年代的各自特点采用不同的算法机制,不同内存区域中对象生命周期也不同,因此对堆内存不同区域采用不同的回收策略可以提高垃圾回收执行效率。通常情况新生代对象存活率低,回收频繁,就采用复制算法;老年代存对象生命周期长,存活率高,就用标记清除算法或者标记整理算法。老年代的垃圾回收称之为Major GC或Full GC,新生代的垃圾回收称之为MinorGC或Young GC。
新生代对象满足一定条件后会转移到老年代中:
顾名思义,类加载就是把Java类加载到JVM中。类的整个生命周期分为五个阶段:加载->连接->初始化->使用->卸载,其中连接又分为三步:验证->准备->解析,如下图所示。
1)加载
1.通过全限定类名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
2)连接
验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
1.文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。
此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求。
2.元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。
第二阶段,保证不存在不符合 Java 语言规范的元数据信息。
3.字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。
4.符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用。
可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。
虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
3)初始化
到初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行 () 方法的过程。
4)使用
Object类是所有类的父类,常用的类有String类、Math类、Date类、System类等等。每个类都有构造方法。如果没有显式地为类定义构造方法,Java 编译器将会为该类提供一个默认构造方法。在创建一个对象的时候,至少要调用一个构造方法。构造方法的名称必须与类同名,一个类可以有多个构造方法。
5)卸载
Java的类加载器可分为四种:
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
主要分为两个步骤:
1)首先自底向上的检查类是否已经加载过,如果加载过就直接返回该类;
2)如果都没有加载过的话,那么就自顶向下的尝试加载该类。
只有当对类的主动使用时才会导致类的初始化,类的主动使用包括以下5种:
1)遇到new、get static、put static或invoke static这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类( 包含main()方法的那个类),虚拟机会先初始化这个主类。
5)当使用JDK 1.7的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例正好是对 REF_getStatic, REF_putStatic, REF_invokeStatic 进行方法句柄解析的结果时,并且这个方法句柄所对应的类没有进行过初始化, 则需要先触发其初始化。
当new一个对象时:
更多精彩:
参考文章与扩展阅读:
下面有关JVM内存,说法错误的是?
A 程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,是线程隔离的
B 虚拟机栈描述的是Java方法执行的内存模型,用于存储局部变量,操作数栈,动态链接,方法出口等信息,是线程隔离的
C 方法区用于存储JVM加载的类信息、常量、静态变量、以及编译器编译后的代码等数据,是线程隔离的
D 原则上讲,所有的对象都在堆区上分配内存,是线程之间共享的
答案:C
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。