Java程序从源文件创建到程序运行要经过两大步骤:

  1. 编译期:也叫前期,即源文件由编译器编译成字节码(ByteCode)
  2. 运行期:也叫后期,即字节码由java虚拟机解释运行。

因为java程序在后期运行时除了经过JVM的解释运行,还存在一种JIT运行,所以说Java被称为半解释语言( "semi-interpreted" language)。

一、编译期

编译期是指把源码交给编译器编译成计算机可以执行的文件的过程。

在Java中也就是把Java代码编成class文件的过程。编译期只是做了一些翻译功能,并没有把代码放在内存中运行起来,而只是把代码当成文本进行操作,比如:词法、语法分析,解语法糖、字节码生成等。

1、内存分配:

  • 编译期分配内存:并不是说在编译期就把程序所需要的空间在内存中分配好,而是说在编译期生成的代码中产生一些指令,在运行代码时通过这些指令把程序所需的内存分配好。只不过在编译期的时候就知道分配的大小,并且知道这些内存的位置。
  • 运行期分配内存:是指只有在运行期才确定内存的大小,存放的位置。

说明:在java中内存的分配是在运行期的,这也是为什么我们把jvm的内存模型称作运行时数据区(五大部分)。

2、对比静态编译:

在java中编译阶段只是生成了字节码,没有生成计算机认识的机器码。java在运行阶段才开始解释字节码,然后生成对应的机器码;或者通过JIT技术,在运行阶段将字节码编译成对应的机器码缓存起来使用。所以,java称为一种动态编译

C++使用的是静态编译,即在编译过程就生成了对应的机器码。

1)相比静态编译,java的动态编译有一些劣势:

  • JIT虽然可以极大提高性能,但是他实在运行时进行的优化,会占用用户程序运行资源;
  • java是动态的类型安全语言,意味着JVM需要频繁的进行类型检查;
  • java绝大部分方法是动态绑定的,意味着JIT在进行一些优化时(如方法内联)的难度要远大于C++的静态编译;
  • java是动态扩展语言,意味着运行时加载的类可能改变程序类型的继承关系,这使得编译期很多全局优化难于C++的静态编译;
  • java对象的内存分配绝大部分在堆上,而C++的对象既可以分配在栈上,也可以分配在堆上(需要手动回收),所以在内存回收时java性能要差一些

2)java动态编译优势:

  • java语言的一些性能劣势是为了换取开发效率上的优势,如:动态类型安全、动态扩展、垃圾回收。
  • 由于C++是静态编译,以运行期性能监控为基础的优化措施都不能做:频率预测、分支预测、裁剪未被选择的分支等;而这些java都是可以做的。

3、动态/静态语言、动态/静态类型语言、强/弱类型语言:

1)动态、静态语言:

动态语言是指运行期间可以改变其结构的语言:例如增加或者删除函数、对象、甚至代码。例如:JavaScript、Ruby、Python都是动态语言。java、C++属于静态语言。

动态语言举例:(javascript)

function Person(name, age, job)
{
  this.name = name;
  this.age = age;
  this.job = job
  this.hello = function(name){
    alert("Hello, " + name);
};

person = new Person("Eric", 28, 'worker');
alert(person.name + '' + person.age + '');
person.hello("Alice");
//为对象添加方法
person.work = function(){
  alert('I am working');
}
person.work();

//删除方法
delete person.work;
person.work();

2)动态、静态类型语言:

  • 动态类型语言是指类型的检查是在运行时做的。在写代码时不用给变量指定数据类型,第一次赋值给变量时,在内部将数据类型记录下来。例如:JavaScript、Ruby、Python是典型的动态类型语言。
  • 静态类型语言的数据类型检查发生在在编译阶段,也就是说在写程序时要声明变量的数据类型。C/C++、C#、Java都是静态类型语言的典型代表。

动态类型语言举例:(python)

def sum(a, b):

    return a + b;

print sum(1,2);
print sum("Hello ", "Word")

3)强、弱类型语言:

  • 强类型语言:强制数据类型定义的语言。没有强制类型转化前,不允许两种不同类型的变量相互操作,强类型定义语言是类型安全的语言。如Java属于强类型。例如Java中“int i = 0.0;”是无法通过编译的;
  • 弱类型语言:数据类型可以被忽略的语言。一个变量可以赋不同数据类型的值,允许将一块内存看做多种类型,比如直接将整型变量与字符变量相加。如C++、php属于弱类型。

注:强类型语言在性能上略逊色于弱类型语言,但可读性上要高于弱类型的。

4)为什么说java是一种准动态的语言?

根据上面的定义我们可以知道,java是一种静态语言,而且是静态类型的语言(强类型)。但是在java中引入很多动态的特性:

  • 反射机制
  • 动态编译
  • 动态执行javascript代码
  • 动态字节码操作
  • 动态转换类型

二、运行期

1、java类运行的过程大概可分为两个过程:

  • 类的加载:链接、初始化(分配内存、创建对象)等;
  • 类的执行:解释执行(将字节码翻译成机器码),或者通过JTI(编译成机器码);

需要说明的是:JVM主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。

说明:在运行期,会涉及到前面讲得所有知识点:

  • 类加载;
  • JVM内存模型;
  • 对象创建、访问、布局;
  • GC;

2、绑定:

1)定义:

绑定指的是把一个方法的调用与方法所在的类(方法主体)关联起来(这个方法被哪个类调用)。对Java来说,分为静态绑定和动态绑定。

  • 静态调用:也叫前期绑定。在程序执行前方法已经被绑定,也就是在编译期方法明确知道被哪个类调用。java当中的方法只有final、static、private和构造方法是前期绑定的。
  • 动态调用:也叫后期绑定。在运行时根据具体对象的类型进行绑定(只有运行时才知道方法被哪个类调用)。在java中,几乎所有的方法都是后期绑定的。

注:动态调用(后期绑定)是多态的一种方式。

2)对比C++:

在C++中默认的方法调用都是静态调用,只有方法被使用virutal关键字声明后(变成了虚函数),才具有动态调用(后期绑定)的特性。

3、方法内联:

1)定义:

简单通俗的讲就是把方法内部调用的其它方法的逻辑,嵌入到自身的方法中去,变成自身的一部分,之后不再调用该方法,从而节省调用函数带来的额外开支。

之所以出现方法内联是因为函数调用除了执行自身逻辑的开销外,还有一些不为人知的额外开销。这部分额外的开销主要来自方法栈帧的生成、参数字段的压入、栈帧的弹出、还有指令执行地址的跳转。

2)条件:

一个方法如果满足以下条件就很可能被jvm内联。

  1. 热点代码:如果一个方法的执行频率很高就表示优化的潜在价值就越大。那代码执行多少次才能确定为热点代码?这是根据编译器的编译模式来决定的。如果是客户端编译模式则次数是1500,服务端编译模式是10000。次数的大小可以通过-XX:CompileThreshold来调整。
  2. 方法体不能太大:jvm中被内联的方法会编译成机器码放在code cache中。如果方法体太大,则能缓存热点方法就少,反而会影响性能。
  3. 如果希望方法被内联,尽量用private、static、final修饰,这样jvm可以直接内联。如果是public、protected修饰方法jvm则需要进行类型判断,因为这些方法可以被子类继承和覆盖,jvm需要判断内联究竟内联是父类还是其中某个子类的方法。

所以了解jvm方法内联机制之后,会有助于我们工作中写出能让jvm更容易优化的代码,有助于提升程序的性能。

3)对比C++:

在java中,内联是靠运行时JVM自动进行判断、优化的。

在C++中需要开发人员主动告诉编译器哪些类成员函数需要内联,然后在编译的时候进行优化的。C++有两种方式定义内联函数:

  • 当在类的定义体外时,在该成员函数的定义前面加"inline"关键字;
  • 当在类的定义体内且声明该成员函数时,同时提供该成员函数的实现体(不需要关键字inline);

3、JIT以及一些优化手段(逃逸分析):

略。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐