~~番薯丸子~~~~~~~  

一、Java 运行原理 
1、高级语言运行过程 
在程序真正运行在CPU上之前,必须要让OS的kernel理解我们在编辑器或者IDE里根据每种语言的语法规则敲入的源代码,kernel才能做出相关的调度,所以需要先将源代码转化成可执行的二进制文件,这个过程通常由编译器完成。有些编译器直接将源代码编译成机器码,载入内存后CPU可以直接运行。而机器码的格式与跟具体的CPU架构相关连,例如ARM CPU无法理解Intel CPU机器码。因此,同样的源代码需要根据不同的硬件进行特定的编译。高级语言转换到低级语言的桥梁就是编译器。程序员写好源代码,编译器将源码编译成可执行的机码,然后CPU读取机器码,执行程序。 
2、Java语言的执行过程 
这里写图片描述 
宽泛地讲,Java源代码(.java)经过java编译器(javac.exe)编译之后,并没有直接转化为机器码,而是转化成一种中间格式——字节码(.class),字节码再经过Java虚拟机解释,转化成机器码,然后经由操作系统到达CPU运行。整个执行过程如下图所示: 
这里写图片描述 
Java的跨平台是基于JVM虚拟机这一中间物来实现的,Java源程序经过编译器编译后生成虚拟机能够理解的字节码(ByteCode——class文件的内容),虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定系统上的机器码,然后在特定的机器上运行。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。 
这里写图片描述 
3、JVM——Java Virtual Machine 
JVM是一个虚构出来的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM 的主要工作是解释自己的指令集(即字节码)并映射到本地的 CPU 的指令集或 OS 的系统调用。 
三、 JVM的体系结构 
Class Loader:类装载器,从入口处开始按需加载.class文件,填充这些数据到运行时数据区 
Execution Engine:执行引擎,JVM的CPU,不断地取指令,JIT编译翻译执行字节码,或者执行本地方法 
Runtime Data Areas:运行时数据区,核心区,运行的时候操作所分配的内存区,包括方法区、堆、java栈、PC寄存器、本地方法栈 
这里写图片描述 
1、类加载器 
类加载器加载其实就是根据编译后的Class文件,将Java字节码载入JVM内存,并完成对运行数据处于的初始化工作,供执行引擎执行。 
类加载过程: 
装载——链接(验证,准备,解析)—— 初始化 
这里写图片描述 
1.Loading:,找到二进制字节码(Class文件)并加载至JVM内存中,标识一个被加载的类:类名+类所在的包名+Class Loader instance ID 
2.Linking:

  • Verifying:验证元数据,文件格式,字节码等,确保class文件包含的字节码信息符合JVM的规范,以免危及JVM安全;

  • Preparing:准备分配给类所需要内存的数据结构,指示在类中定义的字段、方法和接口;

  • Resolving:对类中的所有属性、方法进行验证,以确保其需要调用的属性、方法存在,以及具备应的权限;符号引用的转换等

3.Initialing:初始化执行类中的静态初始化代码、构造器代码以及静态属性 
类装载器类型: 
启动类装载器:JVM实现的一部分; 
用户自定义类装载器:是Java程序的一部分,必须是ClassLoader类的子类。 
类装载顺序: 
Jvm启动时,由Bootstrap向User-Defined方向加载类;应用进行Class Loader时,由User-Defined向Bootstrap方向查找并加载类; 
类加载采用父类委托制,子加载器能查询父加载器已缓存类,委托只能从下到上,反之不行。类加载器可以加载一个类,但是它不能卸载一个类。但是类加载器可以被删除或者被创建。一个类可以被不同的类加载器加载。 
这里写图片描述 
Bootstrap ClassLoader 
JVM的根ClassLoader,它是用C++实现的,在JVM启动的时候创建,负责装载$JAVA_HOME中jre/lib/rt.jar(Sun JDK的实现)中所有class文件,这个jar中包含了Java规范定义的所有接口以及实现。 
Extension ClassLoader 
装载除了基本的Java API以外的扩展类,它也负责装载其他的安全扩展功能。 
System ClassLoader 
负责加载应用程序类,加载启动参数中指定的Classpath中的jar包以及目录,在Sun JDK中ClassLoader对应的类名为AppClassLoader。 
User-Defined ClassLoader 
Java开发人员继承ClassLoader抽象类自行实现的ClassLoader,基于自定义的ClassLoader可用于加载非Classpath中的jar以及目录。 
2、执行引擎 
类加载器将.class文件载入内存之后,执行引擎以Java 字节码指令为单元,读取Java字节码;而后由解释器或者即时编译器(JIT Compiler)将字节码转化成平台相关的机器码。 
这里写图片描述
JVM实现技术: 
解释器:第一代JVM,一条一条地读取,解释并且执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。字节码这种“语言”基本来说是解释执行的。 
这里写图片描述 
即时编译器(just-in-time compiler):第二代JVM,狭义来说是当某段代码即将第一次被执行时进行编译,将class类文件解释成二进制文件后的结果缓存下来,当第二次执行时直接从缓存中取,因此JIT依赖更多内存缓存解释的结果。JIT编译是动态编译的一种特例。JIT编译一词后来被泛化,时常与动态编译等价;但要注意宽泛与狭义的JIT编译所指的区别。 
这里写图片描述 
自适应编译器(adaptive compiler):柔和第一代和第二代JVM,也是动态编译的一种,但它通常执行的时机比JIT编译迟,先让程序“以某种形式”先运行起来,收集一些信息之后再做动态编译,也就是说在所有执行过的代码里只寻找一部分来编译;而”收集信息”决定了编译哪部分代码,换个角度说“收集信息”就是在程序运行过程中监控代码执行的频率,自动缓存利用率高的代码,这样的编译可以更加优化。这个”某种形式”可以称为“baseline execution“,可以由解释器或简单的JIT编译器承担。 
这里写图片描述 
HotSpot是一个JVM的实现,得名于它得混合模式执行引擎(包括解释器和自适应编译器),这个JVM最初由Longview/Animorphic实现,随着公司被Sun/JavaSoft收购而成为Sun的JVM,并于JDK 1.3.0开始成为Sun的Java SE的主要JVM。在Sun被Oracle收购后,现在HotSpot VM是Oracle的Java SE的主要JVM。HotSpot是较新的JVM,用来代替JIT(Just in Time), Java原先是把源代码编译为字节码在虚拟机执行,这样执行速度较慢;而HotSpot将最需要编译的“热点”代码编译为本地(原生native)代码,如果已经被编译成本地代码的字节码不再被频繁调用了,那么Hotspot VM会把编译过的本地代码从cache里移除,并且重新按照解释的方式来执行它,这样显着提高了性能。 HotSpot VM 参数可以分为规则参数(standard options)和非规则参数(non-standard options)。Hotspot VM分为Server VM和Client VM两种,这两种VM使用不同的JIT编译器。 
3、运行时数据区 
当运行一个JVM Instance时,系统将分配给它一块内存区域(大小可设置),这一内存区域由JVM自行管理。从这一块内存中分出一块用来存储一些运行数据,例如创建的对象,传递给方法的参数,局部变量,返回值等等。这一块内存就称为运行数据区域。运行数据区域可以划分为6大块:Java栈、程序计数寄存器(PC寄存器)、本地方法栈(Native Method Stack)、Java堆、方法区域(包括运行常量池——Runtime Constant Pool)。其中每个线程私有程序计数器,JVM栈,本地方法栈,方法区和堆则由JVM实例中的所有线程共享,在同一个实例中可以启用多个线程。 
这里写图片描述 
程序计数器 
每个线程私有,线程启动时创建,用来存放当前正在被执行的字节码指令(JVM指令)的地址,如该方法为native的,则PC寄存器中不存储任何信息。 
JVM栈 
每个线程私有,线程启动时创建。存放着一系列的栈帧(Stack Frame),JVM只能进行压入(push)和弹出(pop)栈帧这两种操作。每当调用一个方法时,JVM就往栈里压入一个栈帧,方法结束返回时弹出栈帧。如果方法执行时出现异常,可用printStackTrace等方法来查看栈的情况。栈的示意图如下: 
这里写图片描述 
每个栈帧包含三个部分:本地变量数组,操作数栈,方法所属类的常量池引用

  • Local Variable Array:从0开始按顺序存放方法所属对象的引用、传递给方法的参数、局部变量。

  • Operand Stack:存放方法执行时的一些中间变量,JVM在执行方法时压入或者弹出这些变量。其实,操作数栈是方法真正工作的地方,执行方法时,局部变量数组与操作数栈根据方法定义进行数据交换。

  • Reference to Constant Pool:当JVM执行到需要常量池的数据时,就是通过这个引用来访问常量池的。栈帧中的数据还要负责处理方法的返回和异常。如果通过return返回,则将该方法的栈帧从Java栈中弹出。如果方法有返回值,则将返回值压入到调用该方法的方法的操作数栈中。另外,数据区中还保存中该方法可能的异常表的引用。

本地方法栈 
当程序通过JNI(Java Native Interface)调用本地方法(如C或者C++代码)时,就根据本地方法的语言类型建立相应的栈,此区域用于存储每个native方法调用的状态。 
堆(Heap) 
堆中存放的是程序创建的对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配。当堆中的空间无法满足新建对象所需的内存开销,会有溢出现象而导致程序崩溃,为了避免溢出,当对象执行结束时,其占据的内存空间需要等待GC(Garbage Collection)进行回收,因此这个区域对JVM的性能影响很大。 
注意: 堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,导致了new对象的开销是比较大的 
Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配。 
TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。 
方法区域 
每个线程共享的,启动一个JVM实例时被创建,它用于存运行放常量池、所加载的类的信息(域、方法、静态变量、final类型的常量)。开发人员在程序中通过Class对象中的getName、isInterface等方法获取的数据都来源于方法区域,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出Out Of Memory的错误信息。不同的JVM实现方式在实现方法区域的时候会有所区别。Oracle的HotSpot称之为永久区域(Permanent Area)或者永久代(Permanent Generation)。 
运行常量池 
其空间从方法区域中分配,用来存放类、方法、接口的常量和域的引用信息,当一个方法或者域被引用的时候,JVM就通过运行常量池中的引用信息来查找方法和域在内存中的的实际地址。 
四、JVM垃圾回收 
Garbage Collection的基本原理: 
将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。 
垃圾回收算法 
1、按照基本回收策略分为以下4种: 
Reference Counting:引用计数,比较古老的回收算法;原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,引用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。

Mark-Sweep:标记-清除,此算法执行分两阶段;第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。 
这里写图片描述 
Copying: 复制,把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题;但是此算法的缺点就是需要两倍内存空间。 
这里写图片描述 
Mark-Compact:标记-整理,结合了Mark-Sweep和Copying两个算法的优点;也分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。避免了Mark-Sweep算法的碎片问题,同时也避免了Copying算法的空间问题。 
这里写图片描述 
2、按分区对待的方式分为以下2种

  • Incremental Collecting:增量收集,实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。JDK5.0中的收集器没有使用这种算法的。

  • Generational Collecting:分代收集,基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。

3、按系统线程分为以下3种

  • 串行收集:串行收集使用单线程处理所有垃圾回收工作,因为无需多线程交互,实现容易,而且效率比较高。但是,其局限性是无法使用多处理器的优势,所以此收集适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。

  • 并行收集:并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势。

  • 并发收集:相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越长。

处理碎片 
由于不同Java对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行内存整理,就会出现零散的内存碎片。碎片最直接的问题就是会导致无法 分配大块的内存空间,以及程序运行效率降低。所以,在上面提到的基本垃圾回收算法中,“复制”方式和“标记-整理”方式,都可以解决碎片的问题。 
对象创建和对象回收 
垃圾回收线程是回收内存的,而程序运行线程则是消耗(或分配)内存的,一个回收内存,一个分配内存,从这点看,两者是矛盾的。因此,在现有的垃圾回收方式 中,要进行垃圾回收前,一般都需要暂停整个应用(即:暂停内存的分配),然后进行垃圾回收,回收完成后再继续应用。这种实现方式是最直接,而且最有效的解决二者矛盾的方式。 
但是这种方式有一个很明显的弊端,就是当堆空间持续增大时,垃圾回收的时间也将会相应的持续增大,对应应用暂停的时间也会相应的增大。一些对相应时间要求很高的应用,比如最大暂停时间要求是几百毫秒,那么当堆空间大于几个G时,就很有可能超过这个限制,在这种情况下,垃圾回收将会成为系统运行的一个瓶颈。 为解决这种矛盾,有了并发垃圾回收算法,使用这种算法,垃圾回收线程与程序运行线程同时运行。在这种方式下,解决了暂停的问题,但是因为需要在新生成对象的同时又要回收对象,算法复杂性会大大增加,系统的处理能力也会相应降低,同时碎片问题将会比较难解决。

五、JRE(Java Runtime Environment)和JDK(Java Development Kit) 
JRE是指运行Java程序所必须的环境集合,包含JVM标准实现及Java核心类库。JDK 是 Java 语言的软件开发工具包,针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。如果运行Java程序,只需安装JRE就可以了。如果编写Java程序,需要安装JDK。OpenJDK则是包含了开发与运行的开源实现。最主流的JDK是Sun公司发布的JDK,除了Sun之外,还有很多公司和组织都开发了属于自己的JDK,例如IBM,阿里等。 
根据应用领域的不同,JDK可分为三种版本:

SE(Standard Edition)标准版,通常用的一个版本,从JDK 5.0开始,改名为Java SE

EE(Enterprise Edition)企业版,使用这种JDK开发J2EE应用程序,从JDK 5.0开始,改名为Java EE

ME(Micro Edition)微型版,主要用于移动设备、嵌入式设备上的Java应用程序,从JDK 5.0开始,改名为Java ME

posted on 2019-05-17 17:05  番薯丸子  阅读(132)  评论(0编辑  收藏  举报