灵魂一问:一个Java文件的执行全部过程你确定都清楚吗?

平时我们都使用 idea、eclipse 等软件来编写代码,在编写完之后直接点击运行就可以启动程序了,那么这个过程是怎么样的?

我们编写的 java 文件在由编译器编译后会生成对应的 class 字节码文件, 然后再将 class 字节码文件转给 JVM 。 JVM 会处理解析 class 文件,将其内部设置的类、方法、常量等信息全部提取出来,然后找到 main 方法开始一步一步编译成机器码并执行,中间会根据需要调用前面提取的数据。

首先要知道 java 之所以强大,原因之一就是 JVM 的强大。

强大之一是 JVM 是 \” 跨平台 \” 的。无论在哪种操作系统上执行,都可以转成对应的机器语言,不需要担心适配问题。

第二点就是 JVM 是 \” 跨语言 \” 的,因为 JVM 只认 class 文件,所以其他语言只需要一个编译器编译成 class 文件就可以使用 JVM 来编译执行了。

根据上面的说明可以知道 java 程序执行的核心是通过 JVM 来实现的,那么就需要知道 JVM 内部是如何执行的。

JVM 内部可以分为四大部分,运行时数据区域、类加载系统、执行引擎、本地接口和本地方法库。

类加载系统:主要就是指类加载器,用于把 class 数据文件加载到运行时数据区域,然后由数据区域来编译执行。

运行数据区域:搭配执行引擎来编译传来的文件中的代码,然后执行,并且根据需要通过本地方法接口调用本地方法。

执行引擎:主要用于代码的编译和 运行时对象的回收。

本地库接口和本地方法库:提供一些 java 无法实现,需要底层执行调用的方法,是 jvm 访问底层的重要途径。

用于进行类的加载。

一般分为启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器。图中的从自定义类加载器到启动类加载器一层一层使用箭头连接, 这种箭头并不是继承关系,而是上下级关系。上下级的联系是通过 ClassLoader 抽象类继承过来的 parent 属性设置的。

1、启动类加载器(Bootstrap ClassLoader)(引导类加载器),加载java 核心类库( <JAVA_HOME>/jre/lib/rt.jar), 无法被java程序直接引用,是用C++编写的 ,用来加载其他的类加载器(类加载器本质就是类),是所有加载器的父类。

2、拓展类加载器(Extension ClassLoader),用来加载java 的拓展库( <JAVA_HOME>/jre/lib/ext)。

3、系统类加载器(System ClassLoader )(应用程序类加载器),用来加载类路径下的 Java类

4、用户自定义类加载器,继承java.lang.ClassLoader类的方式实现。

官方文档中将类加载器分为引导类加载器和自定义类加载器,这是因为引导类加载器是使用其他语言实现的,而拓展类、系统类、自定义类加载器全部都是通过继承 ClassLoader 抽象类实现的,所以都统一被划分为自定义类加载器。

1、隐式装载:由加载器加载。

2、显式装载: 自定义加载,比如使用反射Class.forName(类路径), 类加载器 ClassLoader.getSystemClassLoader().loadClass(\”test.A\”);使用当前进程上下文的使用的类装载Thread.currentThread().getContextClassLoader().loadClass(\”test.A\”)。

类加载是动态的,它不会一次性加载所有类然后运行,而是保证程序运行的基础类(核心类库一部分的类)完全加载到JVM中就运行,这是为了节省内存开销。

主要包括全盘负责、双亲委托机制、缓存机制、可见性。

1、全盘负责:当一个 Class 类被某个类加载器所加载时,该 Class 所依赖引用的所有 Class 都会由这个加载器负责载入,除非显式的使用另一个 ClassLoader。( 当然只是这个加载器负责,并不一定就是由这个加载器加载,这是由于双亲委托机制的作用 )

2、缓存机制:当一个 Class 类加载完毕后,会放入缓存,在其他类需要引用这个类时就会从缓存中直接使用,这也是为什么我们在修改了文件后需要重启服务器才能使修改生效。

3、双亲委托机制: 当一个类加载器收到了类加载的请求时,它首先会将这个请求委派给父类,父类不能执行再自己尝试执行,父类如果存在父类,也会委派给父类,这样传到了启动类加载器加载,当启动类加载器不能读取到类时才会传给子类加载器,然后子类加载器再尝试加载。

好处:1、 防止自定义的类篡改核心类库中的代码。自定义的和类路径 . 类名与核心类库一样的类会委托给启动类加载,启动类加载器会根据包名 . 类名在内存查看是否已经加载,那么面对自定义的类启动类加载器会认为已经加载过了。如果是给系统类加载器或者自定义类加载器加载的话可能就会产生多个类名相同的类,那么其他类在调用对应基类的话就会报错。

2、防止同一个类被重复加载。

4、可见性:子类加载器可以访问父类加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没办法利用类加载器去实现容器的逻辑。

主要用于将 Class 数据文件编译成对应的本地机器码执行。

传统的编译工具,主要分为 字节码解释器 和 模版解释器。

字节码解释器 是在执行时通过纯软件代码模拟字节码的执行,效率低下;模版解释器则是主流使用的解释器, 原理是将每一条字节码 和一个模版函数关联,在 Class 字节码转成机器码的过程中会通过对应的模版函数生成对应的机器码,这样短期来看效率还不错,但是一旦 同一个的字节码被多次执行,那么每次都需要通过模版函数生成机器码,效率十分低下。

JIT 的原理是将字节码关联的 模版数据直接转成机器码,然后将机器码缓存起来,后面如果再次执行这个字节码时就直接返回缓存中的机器码,省去了二次执行的时间,缺点是第一次的转换消耗比较长,所以以单次执行来看,JIT 的效率是不如 解释器的,但是一旦执行的字节码重复数多,JIT 的作用就体现出来了。HotSpot 中有两个 JIT 编译器, 分别是Client Compiler和Server Compiler,但大多数情况下我们简称为C1 编译器和 C2编译器。C1进行简单的优化,耗时短。C2进行耗时长的优化,代码执行效率更高。实际中C1和C2共同协作执行的。

当虚拟机启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。并且伴随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为有价值的本地机器指令,以换取更高的程序执行效率。

1、规定热点阀值。

每次方法调用时该方法的调用次数都会+1 ,当调用次数达到阀值,就会触发 JIT编译。热点阀值可通过 -XX:CompileThreshold= 来设定。

如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即 一段时间之内方法被调用的次数 。当超过 一定的时间限度 ,如果方法的调用次数仍不足以让它提交给 JIT编译,那这个方法的调用计数器就会 减少一半 ,这个过程称为方法调用计数器热度的 衰减 ,而这段时间就称为此方法统计的 半衰周期

可以使用-XX:-UseCounterDecay 来关闭热度衰减,也可以使用-XX:CounterHalfLifeTime设置半衰周期的时间。

2、回边计数器。

统计一个方法中循环体代码执行的次数。在字节码中遇到控制流向后跳转的指令称为 “回边”。显然,建立回边计数器统计的目的是为了触发OSR 编译( JIT编译)。

线程私有,是当前线程执行的字节码指示器,指示字节码的执行顺序。程序计数器的内存是单独的,不会受到其他变量、对象的影响。所以它不会发生内存溢出。也是 JVM唯一 一个没有规定任何 OOM 的区域。也不存在GC

线程私有。先进后出,是代码执行的核心位置, 一个方法在执行前会生成这个方法对应的栈帧,栈帧 包括局部变量表(保存局部变量)、操作数栈(进行局部变量的操作)、动态链接(其他对象、方法的引用)、方法返回值以及一些附加信息。然后进行压栈操作,开始方法的执行,如果此方法中调用了其他方法,那么会将调用的这个方法对应的栈帧压入栈,等到这个方法执行完之后,如果方法包含返回值,将这个返回值返回给上一个方法,然后这个被调用的栈帧出栈,随后继续执行上一个栈帧。

基本存储单元是 slot(变量槽),用于存储各种类型的数据,其中 long 和 double 会占用两个 slot,其他基本数据类型以及对象引用变量占用一个 slot。

这也说明了为什么类方法不能使用 this 而实例方法可以(实例方法会直接在索引为0的位置创建一个 this 参数保存,所以在实例方法中使用 this 就是直接使用这个参数的)

同时局部变量表的槽位是可以重用的,当前一个局部变量失效后,下一个变量使用空出来的位置。

上面这个方法是实例方法,包含 this, 应该有四个 index 槽位 ,但是因为 b 是在括号里作用的,出了括号就失效了,所以它的位置(index=3的位置)被新设置的 c 所占用。

先进后出结构,是当前方法执行的位置,在方法执行时,会根据编译生成的字节码按顺序将要操作的数据从局部变量表中进入入栈,栈中的数据只能从栈顶向下操作,不能跨数据。比如代码 x=x+1,在执行时会将 x 先压入栈,然后将 1 压入栈,然后读取到 + 的指令,将栈顶的两个数相加,再将加的结果存入局部变量表 x 的位置。如果调用了其他方法并获取了返回值,那么在调用方法执行完毕后,该方法的返回值会被压入栈顶,然后再进行后续的操作。

目前只是一种想法,还未实现。因为使用的是栈式架构,所以指令多,又由于操作数是存储在内存中的,所以频繁地读写必然会影响执行速度,所以提出将栈顶元素全部缓存在物理 CPU的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。

虚方法表

因为重写方法都是虚方法 , 这些方法在编译时期都需要网上寻找直到找到所执行对象的实际类型,然后进行权限验证。这个寻找的过程是比较耗时的,所以每个类会在方法区创建一个虚方法表来保存这些虚方法的实际入口。

1、正常返回:(boolean、byte、char、short、int)ireturn ; lreturn、freturn、dreturn、areturn(String);return(无返回值)。

2、异常返回:如果发生异常的方法没有捕获异常而是抛给上一级,那么该异常就会被返回给调用该方法的方法去处理。

线程共享。是 Java 虚拟机内存最大的一块,主要用于存储创建的对象。根据对象的寿命、大小等因素将对象存储区域划分分为新生代、老年代。在 1.7 开始引入了字符串常量池。因为对象的创建销毁是非常频繁的,所以对是 JVM 中的核心位置之一,也是 OOM 发生的主要位置之一。

本地方法栈(也就是最上面图中的本地接口)是 JVM 与底层交互的接口,用于调用 native 方法。作用与 Java 虚拟栈差不多,只不过是为 native 方法服务的,是由非 Java 语言编写的。

和堆一样是线程共享, 用于存储已被虚拟机加载的类型信息、常量、静态变量、即使编译器编译后的代码缓存等 。方法区的实现在 1.8 之前是永久代,使用的是 JVM 的内存,在1.8开始实现变成元空间,使用的是本地内存。之所以这样改变,是因为原来的方法区很容易发生 OOM,因为方法区的类信息被回收的条件非常苛刻,必须满足以下三点:

1、该类的所有对象都被回收;2、加载该类的类加载器被回收;3、该类对应的 Class 对象没有在任何地方被引用(无法在任何地方通过反射访问该类的方法)。

关于第三点的 Class 对象,在一个类被加载时,会在堆中创建一个用于用于访问这个类的类信息 Class 对象。而在成为元空间后,使用的是本地内存,所以方法区发生 OOM 的情况会极大改善。

当 Class 文件被类加载器加载到 JVM 中时,存储的位置就是在方法区,而在 Class 文件信息中包括着 class 文件的常量池,当 JVM 开始执行时,就会将文件常量池中的数据加载到 方法区内部的运行时常量池,变成运行时状态,并将符号引用转成直接引用。

符号引用和直接引用:当在调用中调用某个类的类方法、类属性、接口方法、接口属性时,因为在执行前,对应的类、接口都还在 Class 文件常量池中,没有加载到内存中,所以不能确定这些类、接口加载后的具体位置,这时就需要一种方式来确认位置,通常使用类的全名+属性名/方法名 来唯一标识要调用的方法/属性,这种标识就是符号引用,等到对应的类加载到内存后,再将这些唯一标识改成在内存中的位置,这种就是直接引用。

在 JDK 1.7 开始,字符串常量池就由方法区移入了堆中,字符串常量池是专门存放字符串常量的,至于为什么移入堆中,这是因为字符串的创建和对象一样频繁,销毁也就变得尤其频繁,而方法区的 GC 是伴随着 full gc 的, 因为 full gc 会造成 STW,在 full gc 期间其他程序都会停止,所以都会避免 full gc,而字符串常量池放在方法区中就减少了 字符串被回收的频率,提高了 OOM 的概率。

在 Class 数据文件被类加载器加载到 JVM 中到编译执行,中间经历 加载、链接、初始化、使用、卸载,其中链接又分为 验证、准备、解析。需要注意的是: 这些操作阶段不一定要等上个阶段完成后才能进行下一个阶段, 解析操作往往在初始化之后再执行。一部分验证和加载同时执行,一部分验证等到解析才会执行 。下面就一个个来说明每一步的操作。

通过类加载器将 Class 数据文件加载到方法区,并且在堆中创建一个 Class 对象用于访问方法区的类数据。

验证主要用于检验传来的二进制数据格式是否满足加载要求。虽然在 java 文件的编译阶段编译器已经进行了一次检查,但是 JVM 是与前面编译器编译的过程隔开的。

验证主要包括格式验证、语义验证、字节码验证、符号引用验证。

1、格式验证: 与加载过程同时进行的。 用于检验字节码模数是否正确、主版本和副版本是否在支持范围内、数据每一项是否有正确的长度等。

2、语义验证:校验不同类之间的关系是否正确,例如是否继承了抽象类但没有实现方法,是否继承了 final 类。

3、字节码验证:最复杂的一个验证。从方法层面验证各个操作是否正确,比如是否会跳转到不存在的指令,函数调用是否传递正确类型的值,变量赋值是否给了正确的类型

4、符号引用验证: 发生在解析操作 。将符号验证转化为直接引用时,验证符号引用是否能正确使用。

为类属性分配内存并设置零值( 这里不包括使用 static final 修饰的属性且赋值的值是一个字符串常量或一个基本数据类型常量或其他不触发方法的情况(也就是过程不会涉及构造器或者其他方法),因为字符串或者基本数据是常量,在编译时期就会分配地址,准备阶段直接就会显式初始化,而如果赋的值包括方法调用就需要在 <client> 方法里执行 )。如果属性值是常量,那么常量值就会在方法区中分配内存,而如果是对象,那么对象则会在堆中创建;并且实例属性参数也会跟随对象的创建在堆中,只有静态属性和对应的常量值在方法区中分配内存。而设置的零值是当前类型的默认值,比如 private int a = 2;那么设的零值就是 0, a = 2 是在后面的<client>方法中执行的。

将符号引用转成直接直接引用。符号引用主要包括类或接口、静态属性、类方法和接口方法这四种(都是类数据,在类加载后就能获取的)。

执行静态代码块方法以及静态属性的赋值。会将类中所有的关于类属性的赋值语句以及静态代码块中的语句收集起来集中进 <clinet> 方法中,然后执行。 执行的顺序就是按赋值以及静态代码块的排列顺序执行。

虚拟机在在执行 <client>方法时会加锁,使得此方法只会被一个线程加载,所以我们需要考虑类在加载时会不会发生异常死循环导致此类无法被加载

使用不必多说,就是调用类属性、方法。

上面说过一个类卸载所需要的条件:1、该类的所有对象都被回收;2、加载该类的类加载器被回收;3、该类对应的 Class 对象没有在任何地方被引用(无法在任何地方通过反射访问该类的方法)。那么具体原因是什么?

我们知道,对象被回收的条件是这个对象没有被引用,类也是如此,在类被加载到内存后,它会在堆中创建一个 Class 对象,并且和加载它的加载器互相关联,也就是图中的 MyClassLoader,而这个对象也和类对应的实例对象所关联,这种关联是无法切断的,而如果对应的三种变量都没有再引用,那么就相当于这个类信息没有被引用,那么也就可以被回收了。

Java 对类的使用方式分为主动使用和被动使用。主动使用会触发类的初始化,被动使用不会(但是还是会触发初始化之前的操作)。

1、创建某个类的对象

2、调用某个类的类属性、类方法

3、获取某个类的反射对象

4、初始化子类,如果父类没有初始化,会先触发父类的初始化(不适用接口)

5、如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。

6、虚拟机启动,调用主方法的类会被初始化

7、初次调用 MethodHanlder 实例时,初始化该 MethodHanlder 指向的方法所在的类。( 涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄所在的类)

1、访问的类属性不是当前类的属性,比如从父类继承而来的或者实现接口得到的,比如

这里只会触发 parent 的初始化,而不会触发 son 类的初始化,而如果 son 重写了属性 a 或者调用的是 son 的另一个属性 b ,那么就会触发 son 类的初始化,并且因为 son 继承了 parent 类,所以在 son 初始化前还会先初始化 parent。

2、通过数组定义类引用,不会触发此类的初始化(如果数组类型是基本数据类型,那么不需要加载;如果是引用数据类型,那么就进行类的加载,但不会进行初始化操作)

3、调用 static final 修饰的且是常量或者是字符串或是其他没有方法触发的情况,也不会触发初始化操作。

4、调用 ClassLoader 的 loadClass() 方法加载一个类,只会触发加载操作,而不会触发初始化操作。

每个类加载器都有其自己的命名空间,命名空间由该加载器及其所有的父加载器所加载的类组成。 在同一个命名空间中,不会出现类的完整名字(包名+类名)相同的两个类。但在不同的命名空间中,有可能出现完整名字相同的两个类。

所以, 在比较两个类是否是同一个类的前提是这两个类由同一个类加载器加载,如果这两个类是由两个类加载器加载的,那么这两个类必然不是同一个类。 一个类只能被一个类加载器加载一次,但是可以被多个类加载器加载。

返回该类加载器的父类加载器。

加载 name 类,如果找不到该类,就抛出异常。内部的实现是父类委托机制。

查找二进制的 name 类,返回该类的实例,这个类是 loadClass 内部调用的一个方法,JDK 维护了一个推荐的重写方法,鼓励我们去重写这个方法来实现对功能的拓展。JDK 1.2 之前还未引入父类委托机制,所以要拓展就需要去重写 loadClass 方法,1.2 引入父类委托机制后通过重写 findClass 方法来拓展,并且也没有破坏父类委托机制。

将字节数组 b 转换为 Class 的实例,off 和 len 参数表示实际 Class 信息在 byte 数组中的位置和长度。其中 b 是ClassLoader 从外部获取的。这是受保护的方法,只有在自定义的 ClassLoader 子类中使用。一般在 findClass 方法中被调用,在 findClass 方法中先类的字节码数组,然后调用 defineClass 获取类实例返回。

SecureClassLoader 扩展了 ClassLoader,增加一些方法,但是一般我们使用的是其子类 URLClassLoader,URLClassLoader 实现了 ClassLoader 很多抽象方法,如 findClass()、findResource() 。 我们在编写自定义类加载器时,如果没有特别复杂的实现,可以直接继承 URLClassLoader ,这样可以避免自己编写 findClass 以及获取字节流的方式,使自定义类加载更加简洁 。而拓展类加载器与系统类加载器也是继承 URLClassLoader 。

ClassLoader.loadClass 是一个实例方法,该方法将 Class 文件加载到内存中后,只会执行类加载过程的加载、验证、准备、 解析。初始化等到类的第一次使用时才会执行。Class.forName 是静态方法,该方法在将 Class 文件加载到内存的同时,还会执行类的初始化。

1、由于双亲委派机制是在 JDK1.2 之后才引入的,而在 Java 的第一个版本就有类加载器的概念以及抽象类 ClassLoader ,所以此时是没有双亲委派机制的,用户自定义类加载器就是直接重写 loadClass 方法,这也就是破坏了双亲委托机制。

2、第二次是为了弥补双亲委托机制的缺陷,因为双亲委托机制使得父类加载器无法使用子类加载器的类资源,这样对于父类需要调用子类加载器加载的类资源时就无法实现。为了解决这个问题,引入了线程上下文类加载器(默认为系统类加载器),当需要调用系统类加载器就可以使用这个属性进行加载。

3、IBM 公司设计的代码热部署,使得传统简单的树状继承关系,改成了更为复杂的网状结构,让每个模块都有自己自定义的类加载器。

1、隔离加载类,创建多个模块空间,确保相互间加载的类不会冲突。

2、修改类加载的方式。某些非必要导入的类可以自定义类加载器在某个事件按需导入。

3、扩展加载器,加载不同位置位置的资源。

4、防止源码外泄。在编译时加密。

1、因为同一个类被两个类加载器加载会生成不同的类对象,所以如果两个继承关系的类被两个类加载器加载,那么强制转换类型会报错。所以使用自定义类加载器需要结合场景,不能一味使用。

2、实现时推荐重写 findClass 方法,不破坏双亲委托机制。

Java 沙箱是将 Java 代码限定在 JVM 特定的运行范围中,并且严格限制代码对本地系统资源的访问。防止对本地系统造成破坏。

1、JDK 1.0 时期

将执行的 Java 代码分为本地和远程两种,本地代码默认视为可信赖的,而远程代码则看作不受信赖的。对于信赖的代码,可以访问一切本地资源。而不受信赖的代码,则会受到沙箱的限制,不能访问本地资源。

2、JDK 1.1 时期

由于1.0 中对远程代码限制太过激进,导致一些需要访问本地资源的远程代码无法访问,极大影响了程序的可用性,所以在 1.1 中进行了优化, 在前者基础上,增加了 安全策略。允许用户指定代码对本地资源的访问权限。

3、JDK 1.2 时期

1.1 中无法解决的是本地代码权限问题,因为本地都是可以访问本地资源的,所以在 1.2 中又引入了 代码签名。 无论是本地代码还是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。

4、JDK 1.6时期

也是当前最新的安全策略,相比于前代引入了域的概念。主要升级是将资源的访问进一步划分。虚拟机会把所有代码加载到系统域或应用域中。系统域是与关键资源交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。

1、扩展类加载器改名为平台类加载器(platform classloader)。可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取。

2、原来的 rt.jar(启动类加载器加载的核心源码)和 tool.jar(Java程序启动所需的 class 目录下的类) 被拆分成数十个 JMOD 文件,Java 类库也被改成可扩展的模式,所以拓展目录也就无需存在了。

3、平台类加载器和应用程序类加载器不再继承 URLClassLoader。现在 三大加载器全部继承于 jdk.internal.loader.BuiltinClassLoader

4、类加载器拥有了 name 属性,可以通过 getName() 获取,平台类加载器的 name 是 platform。应用类加载器的名称是 app。类加载器的名称在调试与类加载器相关的问题时会非常有用。

5、启动类加载器现在是 jvm 内部和 java 类库共同协作的实现的(c++和java,过去只是c++),但是为了与之前的代码兼容,在获取启动类加载器的场景中仍然为 null。

6、委派机制变化。在加载器收到加载请求后,会先判断该类是否属于某个系统模块,如果属于直接将这个请求发给这个模块的类加载器。

原文链接:http://www.cnblogs.com/mengxinJ/p/14251272.html

如果觉得本文对你有帮助,可以转发关注支持一下

java 性能优化:35 个小细节,让你提升 java 代码的运行效率

前言

代码优化 ,一个很重要的课题。可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了。

代码优化也是一样,如果项目着眼于尽快无BUG上线,那么此时可以抓大放小,代码的细节可以不精打细磨;但是如果有足够的时间开发、维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的。

代码优化的目标是

  • 减小代码的体积
  • 提高代码运行的效率

代码优化细节

1、尽量指定类、方法的final修饰符

带有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String,整个类都是final的。为类指定final修饰符可以让类不可以被继承,为方法指定final修饰符可以让方法不可以被重写。如果指定了一个类为final,则该类所有的方法都是final的。Java编译器会寻找机会内联所有的final方法,内联对于提升Java运行效率作用重大,具体参见Java运行期优化。 此举能够使性能平均提高50% 。

2、尽量重用对象

特别是String对象的使用,出现字符串连接时应该使用StringBuilder/StringBuffer代替。由于Java虚拟机不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理,因此,生成过多的对象将会给程序的性能带来很大的影响。

3、尽可能使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。

4、及时关闭流

Java编程过程中,进行数据库连接、I/O流操作时务必小心,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销,稍有不慎,将会导致严重的后果。

5、尽量减少对变量的重复计算

明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的,包括创建栈帧、调用方法时保护现场、调用方法完毕时恢复现场等。所以例如下面的操作:

for (int i = 0; i < list.size(); i++)

{…}

建议替换为:

for (int i = 0, int length = list.size(); i < length; i++)

{…}

这样,在list.size()很大的时候,就减少了很多的消耗

6、尽量采用懒加载的策略,即在需要的时候才创建

例如:

String str = \”aaa\”;if (i == 1)

{

list.add(str);

}

建议替换为:

if (i == 1)

{

String str = \”aaa\”;

list.add(str);

}

7、慎用异常

异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。

8、不要在循环中使用try…catch…,应该把其放在最外层

除非不得已。如果毫无理由地这么写了,只要你的领导资深一点、有强迫症一点,八成就要骂你为什么写出这种垃圾代码来了。

9、如果能估计到待添加的内容长度,为底层以数组方式实现的集合、工具类指定初始长度

比如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等等,以StringBuilder为例:

  1. StringBuilder() // 默认分配16个字符的空间
  2. StringBuilder(int size) // 默认分配size个字符的空间
  3. StringBuilder(String str) // 默认分配16个字符+str.length()个字符空间

可以通过类(这里指的不仅仅是上面的StringBuilder)的来设定它的初始化容量,这样可以明显地提升性能。比如StringBuilder吧,length表示当前的StringBuilder能保持的字符数量。因为当StringBuilder达到最大容量的时候,它会将自身容量增加到当前的2倍再加2,无论何时只要StringBuilder达到它的最大容量,它就不得不创建一个新的字符数组然后将旧的字符数组内容拷贝到新字符数组中—-这是十分耗费性能的一个操作。试想,如果能预估到字符数组中大概要存放5000个字符而不指定长度,最接近5000的2次幂是4096,每次扩容加的2不管,那么:

  1. 在4096 的基础上,再申请8194个大小的字符数组,加起来相当于一次申请了12290个大小的字符数组,如果一开始能指定5000个大小的字符数组,就节省了一倍以上的空间;
  2. 把原来的4096个字符拷贝到新的的字符数组中去。

这样,既浪费内存空间又降低代码运行效率。所以,给底层以数组实现的集合、工具类设置一个合理的初始化容量是错不了的,这会带来立竿见影的效果。但是,注意,像HashMap这种是以数组+链表实现的集合,别把初始大小和你估计的大小设置得一样,因为一个table上只连接一个对象的可能性几乎为0。初始大小建议设置为2的N次幂,如果能估计到有2000个元素,设置成new HashMap(128)、new HashMap(256)都可以。

10、当复制大量数据时,使用System.arraycopy()命令

11、乘法和除法使用移位操作

例如:

for (val = 0; val < 100000; val += 5)

{

a = val * 8;

b = val / 2;

}

用移位操作可以极大地提高性能,因为在计算机底层,对位的操作是最方便、最快的,因此建议修改为:

for (val = 0; val < 100000; val += 5)

{

a = val << 3;

b = val >> 1;

}

移位操作虽然快,但是可能会使代码不太好理解,因此最好加上相应的注释。

12、循环内不要不断创建对象引用

例如:

for (int i = 1; i <= count; i++)

{Object obj = new Object();

}

这种做法会导致内存中有count份Object对象引用存在,count很大的话,就耗费内存了,建议为改为:

Object obj = null;for (int i = 0; i <= count; i++) { obj = new Object(); }

这样的话,内存中只有一份Object对象引用,每次new Object()的时候,Object对象引用指向不同的Object罢了,但是内存中只有一份,这样就大大节省了内存空间了。

13、基于效率和类型检查的考虑,应该尽可能使用array,无法确定数组大小时才使用ArrayList

14、尽量使用HashMap、ArrayList、StringBuilder,除非线程安全需要,否则不推荐使用Hashtable、Vector、StringBuffer,后三者由于使用同步机制而导致了性能开销

15、不要将数组声明为public static final

因为这毫无意义,这样只是定义了引用为static final,数组的内容还是可以随意改变的,将数组声明为public更是一个安全漏洞,这意味着这个数组可以被外部类所改变。

16、尽量在合适的场合使用单例

使用单例可以减轻加载的负担、缩短加载的时间、提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面:

  1. 控制资源的使用,通过线程同步来控制资源的并发访问
  2. 控制实例的产生,以达到节约资源的目的
  3. 控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信

17、尽量避免随意使用静态变量

要知道,当某个对象被定义为static的变量所引用,那么gc通常是不会回收这个对象所占有的堆内存的,如:

public class A

{

private static B b = new B();

}

此时静态变量b的生命周期与A类相同,如果A类不被卸载,那么引用B指向的B对象会常驻内存,直到程序终止

18、及时清除不再需要的会话

为了清除不再活动的会话,许多应用服务器都有默认的会话超时时间,一般为30分钟。当应用服务器需要保存更多的会话时,如果内存不足,那么操作系统会把部分数据转移到磁盘,应用服务器也可能根据MRU(最近最频繁使用)算法把部分不活跃的会话转储到磁盘,甚至可能抛出内存不足的异常。如果会话要被转储到磁盘,那么必须要先被序列化,在大规模集群中,对对象进行序列化的代价是很昂贵的。因此,当会话不再需要时,应当及时调用HttpSession的invalidate()方法清除会话。

19、实现RandomAccess接口的集合比如ArrayList,应当使用最普通的for循环而不是foreach循环来遍历

这是JDK推荐给用户的。JDK API对于RandomAccess接口的解释是:实现RandomAccess接口用来表明其支持快速随机访问,此接口的主要目的是允许一般的算法更改其行为,从而将其应用到随机或连续访问列表时能提供良好的性能。实际经验表明,实现RandomAccess接口的类实例,假如是随机访问的,使用普通for循环效率将高于使用foreach循环;反过来,如果是顺序访问的,则使用Iterator会效率更高。可以使用类似如下的代码作判断:

if (list instanceof RandomAccess)

{ for (int i = 0; i < list.size(); i++){}

}else{

Iterator<?> iterator = list.iterable(); while (iterator.hasNext()){iterator.next()}

}

foreach循环的底层实现原理就是迭代器Iterator,参见Java语法糖1:可变长度参数以及foreach循环原理。所以后半句”反过来,如果是顺序访问的,则使用Iterator会效率更高”的意思就是顺序访问的那些类实例,使用foreach循环去遍历。

20、使用同步代码块替代同步方法

这点在多线程模块中的synchronized锁方法块一文中已经讲得很清楚了,除非能确定一整个方法都是需要进行同步的,否则尽量使用同步代码块,避免对那些不需要进行同步的代码也进行了同步,影响了代码执行效率。

21、将常量声明为static final,并以大写命名

这样在编译期间就可以把这些内容放入常量池中,避免运行期间计算生成常量的值。另外,将常量的名字以大写命名也可以方便区分出常量与变量

22、不要创建一些不使用的对象,不要导入一些不使用的类

这毫无意义,如果代码中出现”The value of the local variable i is not used”、”The import java.util is never used”,那么请删除这些无用的内容

23、程序运行过程中避免使用反射

关于,请参见反射。反射是Java提供给用户一个很强大的功能,功能强大往往意味着效率不高。不建议在程序运行过程中使用尤其是频繁使用反射机制,特别是Method的invoke方法,如果确实有必要,一种建议性的做法是将那些需要通过反射加载的类在项目启动的时候通过反射实例化出一个对象并放入内存—-用户只关心和对端交互的时候获取最快的响应速度,并不关心对端的项目启动花多久时间。

24、使用数据库连接池和线程池

这两个池都是用于重用对象的,前者可以避免频繁地打开和关闭连接,后者可以避免频繁地创建和销毁线程

25、使用带缓冲的输入输出流进行IO操作

带缓冲的输入输出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,这可以极大地提升IO效率

26、顺序插入和随机访问比较多的场景使用ArrayList,元素删除和中间插入比较多的场景使用LinkedList这个,理解ArrayList和LinkedList的原理就知道了

27、不要让public方法中有太多的形参

public方法即对外提供的方法,如果给这些方法太多形参的话主要有两点坏处:

  1. 违反了面向对象的编程思想,Java讲求一切都是对象,太多的形参,和面向对象的编程思想并不契合
  2. 参数太多势必导致方法调用的出错概率增加

至于这个”太多”指的是多少个,3、4个吧。比如我们用JDBC写一个insertStudentInfo方法,有10个学生信息字段要插如Student表中,可以把这10个参数封装在一个实体类中,作为insert方法的形参。

28、字符串变量和字符串常量equals的时候将字符串常量写在前面

这是一个比较常见的小技巧了,如果有以下代码:

String str = \”123\”;

if (str.equals(\”123\”)) {…}

建议修改为:

String str = \”123\”;

if (\”123\”.equals(str))

{

}

这么做主要是可以避免空指针异常

29、请知道,在java中if (i == 1)和if (1 == i)是没有区别的,但从阅读习惯上讲,建议使用前者

平时有人问,”if (i == 1)”和”if (1== i)”有没有区别,这就要从C/C++讲起。

在C/C++中,”if (i == 1)”判断条件成立,是以0与非0为基准的,0表示false,非0表示true,如果有这么一段代码:

int i = 2;

if (i == 1)

{

}else{

}

C/C++判断”i==1″不成立,所以以0表示,即false。但是如果:

int i = 2;if (i = 1) { … }else{ … }

万一程序员一个不小心,把”if (i == 1)”写成”if (i = 1)”,这样就有问题了。在if之内将i赋值为1,if判断里面的内容非0,返回的就是true了,但是明明i为2,比较的值是1,应该返回的false。这种情况在C/C++的开发中是很可能发生的并且会导致一些难以理解的错误产生,所以,为了避免开发者在if语句中不正确的赋值操作,建议将if语句写为:

int i = 2;if (1 == i) { … }else{ … }

这样,即使开发者不小心写成了”1 = i”,C/C++编译器也可以第一时间检查出来,因为我们可以对一个变量赋值i为1,但是不能对一个常量赋值1为i。

但是,在Java中,C/C++这种”if (i = 1)”的语法是不可能出现的,因为一旦写了这种语法,Java就会编译报错”Type mismatch: cannot convert from int to boolean”。但是,尽管Java的”if (i == 1)”和”if (1 == i)”在语义上没有任何区别,但是从阅读习惯上讲,建议使用前者会更好些。

30、不要对数组使用toString()方法

看一下对数组使用toString()打印出来的是什么:

public static void main(String[] args){ int[] is = new int[]{1, 2, 3};

System.out.println(is.toString());

}

结果是:

[I@18a992f

本意是想打印出数组内容,却有可能因为数组引用is为空而导致空指针异常。不过虽然对数组toString()没有意义,但是对集合toString()是可以打印出集合里面的内容的,因为集合的父类AbstractCollections重写了Object的toString()方法。

31、不要对超出范围的基本数据类型做向下强制转型

这绝不会得到想要的结果:

public static void main(String[] args){

long l = 12345678901234L;int i = (int)l;

System.out.println(i);

}

我们可能期望得到其中的某几位,但是结果却是:

1942892530

解释一下。Java中long是8个字节64位的,所以12345678901234在计算机中的表示应该是:

0000 0000 0000 0000 0000 1011 0011 1010 0111 0011 1100 1110 0010 1111 1111 0010

一个int型数据是4个字节32位的,从低位取出上面这串二进制数据的前32位是:

0111 0011 1100 1110 0010 1111 1111 0010

这串二进制表示为十进制1942892530,所以就是我们上面的控制台上输出的内容。从这个例子上还能顺便得到两个结论:

  1. 整型默认的数据类型是int,long l = 12345678901234L,这个数字已经超出了int的范围了,所以最后有一个L,表示这是一个long型数。顺便,浮点型的默认类型是double,所以定义float的时候要写成””float f = 3.5f”
  2. 接下来再写一句”int ii = l + i;”会报错,因为long + int是一个long,不能赋值给int

32、公用的集合类中不使用的数据一定要及时remove掉

如果一个集合类是公用的(也就是说不是方法里面的属性),那么这个集合里面的元素是不会自动释放的,因为始终有引用指向它们。所以,如果公用集合里面的某些数据不使用而不去remove掉它们,那么将会造成这个公用集合不断增大,使得系统有内存泄露的隐患。

33、把一个基本数据类型转为字符串,基本数据类型.toString()是最快的方式、String.valueOf(数据)次之、数据+””最慢

把一个基本数据类型转为一般有三种方式,我有一个Integer型数据i,可以使用i.toString()、String.valueOf(i)、i+””三种方式,三种方式的效率如何,看一个测试:

public static void main(String[] args){

int loopTime = 50000;

Integer i = 0; long startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++)

{

String str = String.valueOf(i);

}

System.out.println(\”String.valueOf():\” + (System.currentTimeMillis() – startTime) + \”ms\”);

startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++)

{

String str = i.toString();

}

System.out.println(\”Integer.toString():\” + (System.currentTimeMillis() – startTime) + \”ms\”);

startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++)

{

String str = i + \”\”;

}

System.out.println(\”i + \\\”\\\”:\” + (System.currentTimeMillis() – startTime) + \”ms\”);

}

运行结果为:

String.valueOf():11ms Integer.toString():5ms i + \”\”:25ms

所以以后遇到把一个基本数据类型转为String的时候,优先考虑使用toString()方法。至于为什么,很简单:

  1. String.valueOf()方法底层调用了Integer.toString()方法,但是会在调用前做空判断
  2. Integer.toString()方法就不说了,直接调用了
  3. i + “”底层使用了StringBuilder实现,先用append方法拼接,再用toString()方法获取字符串

三者对比下来,明显是2最快、1次之、3最慢

34、使用最有效率的方式去遍历Map

遍历Map的方式有很多,通常场景下我们需要的是遍历Map中的Key和Value,那么推荐使用的、效率最高的方式是:

public static void main(String[] args){

HashMap<String, String> hm = new HashMap<String, String>();

hm.put(\”111\”, \”222\”);Set<Map.Entry<String, String>> entrySet = hm.entrySet();

Iterator<Map.Entry<String, String>> iter = entrySet.iterator(); while (iter.hasNext())

{

Map.Entry<String, String> entry = iter.next();

System.out.println(entry.getKey() + \”\\t\” + entry.getValue());

}

}

如果你只是想遍历一下这个Map的key值,那用”Set keySet = hm.keySet();”会比较合适一些

35、对资源的close()建议分开操作

意思是,比如我有这么一段代码:

try{

XXX.close();

YYY.close();

}catch (Exception e)

{…}

建议修改为:

try{ XXX.close(); }catch (Exception e) { … }try{ YYY.close(); }catch (Exception e) { … }

虽然有些麻烦,却能避免资源泄露。我想,如果没有修改过的代码,万一XXX.close()抛异常了,那么就进入了cath块中了,YYY.close()不会执行,YYY这块资源就不会回收了,一直占用着,这样的代码一多,是可能引起资源句柄泄露的。而改为上面的写法之后,就保证了无论如何XXX和YYY都会被close掉。

由于篇幅限制,小编这里只将此实战文档的所含内容全部展现出来了,需要获取完整文档用于学习的朋友们可以关注一下小编,后台私信“java”或者【资料】获取免费领取方式!

Java 源代码动态编译、类加载和代码执行(Java 8)

Java 的一个重要特性是动态的类加载机制。通过在运行时动态地加载类,Java 程序可以实现很多强大的功能。下面通过一个具体的实例来说明 Java 程序中,如何动态地编译 Java 源代码、加载类和执行类中的代码。这里的代码示例适用的版本是 Java 8。

示例所实现的功能很简单,就是对表达式求值。输入的是类似 1 + 1 或 3 * (2 + 3) 这样的表达式,返回的是表达式的值。示例的做法是动态创建一个 Java 源文件,编译该文件生成 class 文件,加载 class 文件之后再执行。比如,需要求值的表达式是 1 + 1,那么所生成的 Java 源文件如下所示,其中 1 + 1 的部分是动态的。

我们只需要编译该源文件,加载编译之后的 class 文件,再通过反射 API 来调用其中的 calculate 方法就可以得到表达式求值的结果。

第一步是动态生成 Java 源代码并编译。生成 Java 源代码比较简单,直接用字符串连接就可以了。当然了,在生成逻辑比较复杂时,推荐的做法是使用字符串模板引擎,如 Handlebars。在下面的代码中,getJavaSource 方法生成 Java 源代码,compile 方法进行编译。

在进行编译的时候,使用的是 JDK 标准的 JavaCompiler 接口。从源代码字符串中创建了一个 JavaFileObject 对象作为编译时的源代码单元。编译时的选项 -d 指定了编译结果的输出路径,这里是一个临时文件夹。compile 方法的返回值是一个 Pair 对象,包含了 class 文件的路径,以及随机生成的 Java 包的名称。

上面的代码用到了一个帮助类 StringContentJavaFileObject,表示从字符串创建的 JavaFileObject 对象。

编译完成之后的第二步是动态加载类。这一步并没有实现自定义的类加载器,而且使用内置的系统类加载器。系统类加载器通过 ClassLoader.getSystemClassLoader() 方法来获取。系统类加载器在 classpath 上查找类。这里用了一个比较 hack 的技巧来动态修改系统类加载器的 classpath。

在下面的代码中,ClasspathUpdater 的 addPath 方法可以把一个 Path 对象表示的路径,添加到系统类加载器的查找路径中。这是因为系统类加载器自身是 URLClassLoader 类型的加载器,其中的 addURL 方法可以添加新的查找路径。只不过 addURL 方法是 protected,这里通过反射 API 来进行调用。

上面介绍的 ClasspathUpdater 类中的使用技巧,只对 Java 8 生效。在 Java 9 引入模块系统时,对系统类加载器进行了修改。系统类加载器被替换成了应用类加载器。应用类加载器不再是 URLClassLoader 类型了,就不能使用这个技巧了。

最后一步就是执行动态加载的 Java 类。这一步比较简单,只需要用 Class.forName 方法来查找 Java 类,再找到对应的 Method 对象,直接调用即可。下面的代码给出了示例。

最后把整个流程串起来。在下面的代码中,需要求值的表达式是 (1 + 1) * 3 / 5.0。首先调用 DynamicCompilation.compile 方法进行动态编译,得到 class 文件的路径和完整的类名。class 文件的路径通过 ClasspathUpdater.addPath 方法添加到 classpath 中。完整的类名则传递给 Invoker.invoke 方法来执行。最后输出的结果是表达式的值。

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。