在Java程序设计中,内存的申请、分配、释放都由JVM负责完成,因此开发人员就省去了这部分工作,不过这也意味着所开发的应用不是最优的。开发人员有必要对Java应用开发中与内存管理相关的技术做一定的了解。
垃圾回收(Garbage Collection)是Java程序设计中内存管理的核心概念,那么在JVM运行环境中什么对象是垃圾呢?
定义如下:一个对象创建后被放置在JVM的对内存(heap)中,当永远不再引用这个对象时,它将被JVM在对内存中回收。被创建的对象不能再生,同时也没有办法通过程序语句释放它们。
我们也可以这样定义:当对象在JVM运行空间中无法通过根集合(Root set)到达时,这个对象就被称为垃圾对象。根集合是由类中的静态引用域与本地引用域组成的。
JVM管理的两种类型的内存:对内存(heap)和栈内存(stack)。堆内存主要用来存储程序在运行时创建或实例化的对象与变量,而栈内存则是用来存储程序代码中声明为静态(static)或(非静态)的方法。
堆内存(heap)通常被分成两个区域:新对象(new object)与老对象(old object)区域。
新对象区域又可以细分为三个小区域:伊甸园(Eden)区域、From区域与To区域。伊甸园区域用来保存新创建的对象,就像堆栈一样,当区域中的对象满了之后,JVM系统将做可达性测试,检测哪些对象由根集合出发是不可达的,这些对象就可被JVM回收,并且将其从伊甸园区域拷贝到To区域,此时一些对象将发生状态交换,有的对象就从To区域被转移到From区域,此时From区域就有了对象。
在老对象区域中的对象仍然会有一个较长的生命周期,但经过一段时间后,这些对象就会变成短命对象,也就是垃圾对象,从而被JVM回收,建议不要频繁地强制系统做垃圾回收,这样会影响系统的整体性能。
JVM中对象的生命周期大致可分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)、释放阶段(Free)。
※ 创建阶段
系统通过下面的步骤,完成创建对象的过程:
(1) 为对象分配存储空间。
(2) 开始构造对象。
(3) 递归调用其超类的构造方法。
(4) 进行对象实例初始化与变量初始化。
(5) 执行构造方法体。
创建对象时几个关键应用规则:
(1) 避免在循环体中创建对象,即使对象占用的内存空间不大。
(2) 尽量及时使对象符合垃圾回收标准。
(3) 不要采用过深的继承层次。
(4) 访问本地变量优于访问类中的变量。
※ 应用阶段
在应用阶段,对象具备下列特征:
(1) 系统至少维护对象的一个强引用(Strong Reference);
(2) 所有对该对象的引用全部是强引用,除非我们显示使用了软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference)。
强引用是指JVM内存管理器从根引用集合(Root set)出发遍寻堆中所有到达对象的路径。当到达对象的任意路径都不含有引用对象时,这个对象的引用就被称为强引用。
软引用具有较强的引用功能,只有当内存不够的时候,才回收这类内存。另外这些引用对象还能保证在Java抛出OutOfMemory异常之前,被设置为NULL。它可以用于实现一些常用资源的缓存,实现Cache的功能,保证最大限度的试用内存而不引起OutOfMemory。但在某些时候对软引用的试用会降低应用程序的运行效率与性能。
弱引用对象与Soft引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收Soft引用对象,而对于Weak引用对象,GC总是进行回收。Weak引用对象常用于Map结构中,引用占用内存空间比较大的对象,一旦对象的强引用为null时,这个对象的引用就不存在了,GC能够快速回收该对象。
虚引用的用途较少,主要用于辅助finalize函数的使用。
※ 不可视阶段
当一个对象处在不可视阶段,说明我们不能在其它区域的代码中引用它,此时应该主动将其设置为空,帮助JVM及时地发现这个垃圾对象,并且回收系统资源。
※ 可收集阶段、终结阶段与释放阶段
对象可能处于三种情况:
(1) 回收器发现该对象已经不可到达。
(2) Finalize方法已经被执行。
(3) 对象空间已被重用。
Java程序设计中有关内存管理的其它经验:
(1) 最基本的建议就是尽早释放无用对象的引用。
(2) 尽量少用finalize函数。
(3) 如果需要使用经常用大的图片,可以使用soft应用类型。
(4) 注意集合数据类型,包括数组、树、图、链表等数据结构,这些数据结构对GC来说,回收更为复杂。另外注意一些全局变量,静态变量,这些变量往往容易造成不必要的内存资源浪费。
(5) 尽量避免在类的默认构造器中创建、初始化大量对象,防止在调用其自类的构造器时造成不必要的内存浪费。
(6) 尽量避免强制系统做垃圾回收,增长系统做垃圾回收的最终时间,降低系统性能。
(7) 尽量避免显示申请数组空间,当不得不显示地申请数组空间时尽量准确的估计出其合理值。
(8) 尽量在远程方法调用(RMI)类应用开发时使用瞬间值(transient)变量,除非远程调用端需要获取该瞬间值变量的值。
(9) 尽量在合适的场景下使用对象池技术以提高系统性能,缩减系统开销,但是要注意对象池的尺寸不宜过大,及时清除无效对象释放内存资源,综合考虑应用运行环境的内存资源限制,避免过高估计运行环境所提供内存资源的数量。
《Java优化编程》之表达式、语句与保留字
※静态代码块
有时候为了能在引用一个类的时候作一些必要的初始化工作,经常利用static关键字加入一些静态的代码块,完成这个超前功能。
静态代码块对静态变量的应用在声明上存在先后顺序的限制,这与方法中引用变量的情况是不同的,在方法体中引用的变量在类文件前后声明没有限制,但是如果你试图在静态代码块中引用静态变量,就要考虑其声明的先后顺序的限定,在静态代码块中引用的静态变量必须在静态代码块之前声明。
对静态变量的引用有下面的规则:
(1) 可以在非静态方法体中引用静态变量。
(2) 在静态方法体中不可以引用非静态变量。
(3) 可以在静态方法体中创建非静态变量。
※final关键字
保留字final的应用范围:
(1) 用来声明类的变量。
(2) 用来声明方法的常量参数,对变量起到相应的保护作用,防止变量被无意赋值。
(3) 用来声明不可覆盖的方法,不但可以防止父类中的方法不被子类覆盖,还可以加快应用的运行速度,提高系统性能。
(4) 用来声明不可继承的类。
※使用循环语句的几个建议
(1)当做数组拷贝操作时,采用System.arraycopy()方法完成拷贝操作要比采用循环的办法完成数组拷贝操作效率高。
(2)尽量避免在循环体中调用方法,因为方法调用是比较昂贵的。
(3)最好避免在循环体内存取数组元素,比较好的办法是在循环体内采用临时变量,在循环体外更改数组的值。这是因为在循环体内使用变量比存取数组元素要快。
(4)当没有使用JIT或HotSpot虚拟机时,尽量使用0值作为终结条件的比较元素,以提高循环的性能。
(5)避免在最终条件比较时采用方法返回值的方式进行判断,这样做将增大系统开销,降低系统性能。
(6)尽量避免在循环体中使用try-catch块,最好在循环体外使用try-catch块以提高系统性能。
(7)在多重循环中,如果可能,尽量将最长的循环放在最内层,最短的循环放在最外层,以减少循环层间的切换次数。
(8)如果循环体内有if-else类逻辑判断,并且循环次数很大,最好将if-else类逻辑判断移到循环体之外,以提高应用性能。
《Java优化编程》之Java核心类与性能优化
一些散列表的核心类是线程安全的,如Vector类;一些核心类不是线程安全的,如ArrayList类。在多线程环境下,可以通过Collections.syschronizedList()方法来使用ArrayList等非线程安全的散列核心类。另在处理已知容量的较大数组时,应提前调用ensureCapacity()方法初始化ArrayList对象,以提高软件应用性能。
通过StringBuffer中的append()方法将字符串累加可以提高系统的性能,或者先创建一个字符数组,然后向其中添加字符,达到累加的目的,返回最终生成的字符串,前提是应该大体知道这个字符串的最大长度。使用charAt()方法获取特定位置的字符是非常耗时的,更好的办法是将字符串通过调用toCharArray()方法转化为字符数组,然后通过数组索引值获取指定位置的字符。
众所周知I/O操作非常耗时,无疑将在很大程度上降低了系统性能,但是Java语言提高了专门用来提高系统I/O效率的缓冲类,这好比在数据读取与写入时提供了一个临时的缓冲区,每次可以读取一个缓冲区大小的数据块,然后将这个数据块一次性写入到目标设备中。另可以定制缓冲区,以提高系统的性能。
《Java优化编程》之类与接口
※类构造器的编写规则
(1)避免在类的构造器中初始化其它类;
(2)不要给构造器误添加返回值;
(3)尽量避免在构造器中对静态变量做赋值操作;
(4)不要在类的构造器中创建类的实例;
※类的继承规则
(1)单线继承规则。C++中类是可以多重继承的,而在Java语言中,一个类只能有一个父类,但接口却是可以多重继承德,这是Java语言中类与接口的重要区别。
(2)包内部继承规则。当一个类是另一个类的内部类时,或者一个类不想被所在包名称空间以外的成员访问时,不在类的声明中添加public关键字。内部类是Java语言特有的类型,其只能被主类以外的其它内部类继承,主类不能继承内部类。
※抽象类与接口的区别
共同点:
(1)抽象类和接口都不能被实例化;
(2)接口与抽象类都不可以独立运行,也就是说他们都不能作为应用的主类;
(3)接口可以有属性,抽象类也可以有属性;
不同点:
(1)接口中的所有属性与方法的声明都必须是public类型,除abstract方法外,抽象类的属性与方法既可以是public也可以是private或protected,abstract方法只能是public类型或者protected类型。
(2)接口中的方法不能有方法体,而抽象类中的方法是可以有方法体的,甚至是抽象方法。
(3)接口只能被类实现,而抽象类只能被继承;
(4)接口可以多重继承,而抽象类只能单线继承。
※内部类
内部类(Inner Class)是一种较为特殊的Java类形式,它嵌入一个主类中的类,属于类一部分。因此内部类可以直接访问主类中的私有成员,包括属性和方法。但是主类不能直接访问内部类中的任何成员,包括public、protected与private成员,但是主类可以通过创建内部类的实例对象,引用内部类中的任何成员。在内部类中不可以声明任何静态成员。内部类的一个典型应用是:在类中创建一个独立的线程类,然后调用主类的返回周期较长的方法经常用到。
※与性能相关的建议与经验
(1)避免复杂、昂贵的构造过程,例如一维数组比多维数组要快一些;
(2)内部类对象的创建相对一般类的实例化要复杂、耗时一些;
(3)在程序设计中尽量使用接口(Interface),因为接口的结构更容易被交换,以提高系统的性能;
(4)使用原数据类型的包装类比使用原数据要低效;
(5)最好把所有只进行一次初始化的变量,放在类构造器中一次性初始化;
(6)大多数的JVM在搜索接口列表时都是从后向前搜索的,因此经常用到的接口最好位于implements关键字列表的最后面。
(7)最好通过Class.forname()动态的装载类。
垃圾回收(Garbage Collection)是Java程序设计中内存管理的核心概念,那么在JVM运行环境中什么对象是垃圾呢?
定义如下:一个对象创建后被放置在JVM的对内存(heap)中,当永远不再引用这个对象时,它将被JVM在对内存中回收。被创建的对象不能再生,同时也没有办法通过程序语句释放它们。
我们也可以这样定义:当对象在JVM运行空间中无法通过根集合(Root set)到达时,这个对象就被称为垃圾对象。根集合是由类中的静态引用域与本地引用域组成的。
JVM管理的两种类型的内存:对内存(heap)和栈内存(stack)。堆内存主要用来存储程序在运行时创建或实例化的对象与变量,而栈内存则是用来存储程序代码中声明为静态(static)或(非静态)的方法。
堆内存(heap)通常被分成两个区域:新对象(new object)与老对象(old object)区域。
新对象区域又可以细分为三个小区域:伊甸园(Eden)区域、From区域与To区域。伊甸园区域用来保存新创建的对象,就像堆栈一样,当区域中的对象满了之后,JVM系统将做可达性测试,检测哪些对象由根集合出发是不可达的,这些对象就可被JVM回收,并且将其从伊甸园区域拷贝到To区域,此时一些对象将发生状态交换,有的对象就从To区域被转移到From区域,此时From区域就有了对象。
在老对象区域中的对象仍然会有一个较长的生命周期,但经过一段时间后,这些对象就会变成短命对象,也就是垃圾对象,从而被JVM回收,建议不要频繁地强制系统做垃圾回收,这样会影响系统的整体性能。
JVM中对象的生命周期大致可分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)、释放阶段(Free)。
※ 创建阶段
系统通过下面的步骤,完成创建对象的过程:
(1) 为对象分配存储空间。
(2) 开始构造对象。
(3) 递归调用其超类的构造方法。
(4) 进行对象实例初始化与变量初始化。
(5) 执行构造方法体。
创建对象时几个关键应用规则:
(1) 避免在循环体中创建对象,即使对象占用的内存空间不大。
(2) 尽量及时使对象符合垃圾回收标准。
(3) 不要采用过深的继承层次。
(4) 访问本地变量优于访问类中的变量。
※ 应用阶段
在应用阶段,对象具备下列特征:
(1) 系统至少维护对象的一个强引用(Strong Reference);
(2) 所有对该对象的引用全部是强引用,除非我们显示使用了软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference)。
强引用是指JVM内存管理器从根引用集合(Root set)出发遍寻堆中所有到达对象的路径。当到达对象的任意路径都不含有引用对象时,这个对象的引用就被称为强引用。
软引用具有较强的引用功能,只有当内存不够的时候,才回收这类内存。另外这些引用对象还能保证在Java抛出OutOfMemory异常之前,被设置为NULL。它可以用于实现一些常用资源的缓存,实现Cache的功能,保证最大限度的试用内存而不引起OutOfMemory。但在某些时候对软引用的试用会降低应用程序的运行效率与性能。
弱引用对象与Soft引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收Soft引用对象,而对于Weak引用对象,GC总是进行回收。Weak引用对象常用于Map结构中,引用占用内存空间比较大的对象,一旦对象的强引用为null时,这个对象的引用就不存在了,GC能够快速回收该对象。
虚引用的用途较少,主要用于辅助finalize函数的使用。
※ 不可视阶段
当一个对象处在不可视阶段,说明我们不能在其它区域的代码中引用它,此时应该主动将其设置为空,帮助JVM及时地发现这个垃圾对象,并且回收系统资源。
※ 可收集阶段、终结阶段与释放阶段
对象可能处于三种情况:
(1) 回收器发现该对象已经不可到达。
(2) Finalize方法已经被执行。
(3) 对象空间已被重用。
Java程序设计中有关内存管理的其它经验:
(1) 最基本的建议就是尽早释放无用对象的引用。
(2) 尽量少用finalize函数。
(3) 如果需要使用经常用大的图片,可以使用soft应用类型。
(4) 注意集合数据类型,包括数组、树、图、链表等数据结构,这些数据结构对GC来说,回收更为复杂。另外注意一些全局变量,静态变量,这些变量往往容易造成不必要的内存资源浪费。
(5) 尽量避免在类的默认构造器中创建、初始化大量对象,防止在调用其自类的构造器时造成不必要的内存浪费。
(6) 尽量避免强制系统做垃圾回收,增长系统做垃圾回收的最终时间,降低系统性能。
(7) 尽量避免显示申请数组空间,当不得不显示地申请数组空间时尽量准确的估计出其合理值。
(8) 尽量在远程方法调用(RMI)类应用开发时使用瞬间值(transient)变量,除非远程调用端需要获取该瞬间值变量的值。
(9) 尽量在合适的场景下使用对象池技术以提高系统性能,缩减系统开销,但是要注意对象池的尺寸不宜过大,及时清除无效对象释放内存资源,综合考虑应用运行环境的内存资源限制,避免过高估计运行环境所提供内存资源的数量。
《Java优化编程》之表达式、语句与保留字
※静态代码块
有时候为了能在引用一个类的时候作一些必要的初始化工作,经常利用static关键字加入一些静态的代码块,完成这个超前功能。
静态代码块对静态变量的应用在声明上存在先后顺序的限制,这与方法中引用变量的情况是不同的,在方法体中引用的变量在类文件前后声明没有限制,但是如果你试图在静态代码块中引用静态变量,就要考虑其声明的先后顺序的限定,在静态代码块中引用的静态变量必须在静态代码块之前声明。
对静态变量的引用有下面的规则:
(1) 可以在非静态方法体中引用静态变量。
(2) 在静态方法体中不可以引用非静态变量。
(3) 可以在静态方法体中创建非静态变量。
※final关键字
保留字final的应用范围:
(1) 用来声明类的变量。
(2) 用来声明方法的常量参数,对变量起到相应的保护作用,防止变量被无意赋值。
(3) 用来声明不可覆盖的方法,不但可以防止父类中的方法不被子类覆盖,还可以加快应用的运行速度,提高系统性能。
(4) 用来声明不可继承的类。
※使用循环语句的几个建议
(1)当做数组拷贝操作时,采用System.arraycopy()方法完成拷贝操作要比采用循环的办法完成数组拷贝操作效率高。
(2)尽量避免在循环体中调用方法,因为方法调用是比较昂贵的。
(3)最好避免在循环体内存取数组元素,比较好的办法是在循环体内采用临时变量,在循环体外更改数组的值。这是因为在循环体内使用变量比存取数组元素要快。
(4)当没有使用JIT或HotSpot虚拟机时,尽量使用0值作为终结条件的比较元素,以提高循环的性能。
(5)避免在最终条件比较时采用方法返回值的方式进行判断,这样做将增大系统开销,降低系统性能。
(6)尽量避免在循环体中使用try-catch块,最好在循环体外使用try-catch块以提高系统性能。
(7)在多重循环中,如果可能,尽量将最长的循环放在最内层,最短的循环放在最外层,以减少循环层间的切换次数。
(8)如果循环体内有if-else类逻辑判断,并且循环次数很大,最好将if-else类逻辑判断移到循环体之外,以提高应用性能。
《Java优化编程》之Java核心类与性能优化
一些散列表的核心类是线程安全的,如Vector类;一些核心类不是线程安全的,如ArrayList类。在多线程环境下,可以通过Collections.syschronizedList()方法来使用ArrayList等非线程安全的散列核心类。另在处理已知容量的较大数组时,应提前调用ensureCapacity()方法初始化ArrayList对象,以提高软件应用性能。
通过StringBuffer中的append()方法将字符串累加可以提高系统的性能,或者先创建一个字符数组,然后向其中添加字符,达到累加的目的,返回最终生成的字符串,前提是应该大体知道这个字符串的最大长度。使用charAt()方法获取特定位置的字符是非常耗时的,更好的办法是将字符串通过调用toCharArray()方法转化为字符数组,然后通过数组索引值获取指定位置的字符。
众所周知I/O操作非常耗时,无疑将在很大程度上降低了系统性能,但是Java语言提高了专门用来提高系统I/O效率的缓冲类,这好比在数据读取与写入时提供了一个临时的缓冲区,每次可以读取一个缓冲区大小的数据块,然后将这个数据块一次性写入到目标设备中。另可以定制缓冲区,以提高系统的性能。
《Java优化编程》之类与接口
※类构造器的编写规则
(1)避免在类的构造器中初始化其它类;
(2)不要给构造器误添加返回值;
(3)尽量避免在构造器中对静态变量做赋值操作;
(4)不要在类的构造器中创建类的实例;
※类的继承规则
(1)单线继承规则。C++中类是可以多重继承的,而在Java语言中,一个类只能有一个父类,但接口却是可以多重继承德,这是Java语言中类与接口的重要区别。
(2)包内部继承规则。当一个类是另一个类的内部类时,或者一个类不想被所在包名称空间以外的成员访问时,不在类的声明中添加public关键字。内部类是Java语言特有的类型,其只能被主类以外的其它内部类继承,主类不能继承内部类。
※抽象类与接口的区别
共同点:
(1)抽象类和接口都不能被实例化;
(2)接口与抽象类都不可以独立运行,也就是说他们都不能作为应用的主类;
(3)接口可以有属性,抽象类也可以有属性;
不同点:
(1)接口中的所有属性与方法的声明都必须是public类型,除abstract方法外,抽象类的属性与方法既可以是public也可以是private或protected,abstract方法只能是public类型或者protected类型。
(2)接口中的方法不能有方法体,而抽象类中的方法是可以有方法体的,甚至是抽象方法。
(3)接口只能被类实现,而抽象类只能被继承;
(4)接口可以多重继承,而抽象类只能单线继承。
※内部类
内部类(Inner Class)是一种较为特殊的Java类形式,它嵌入一个主类中的类,属于类一部分。因此内部类可以直接访问主类中的私有成员,包括属性和方法。但是主类不能直接访问内部类中的任何成员,包括public、protected与private成员,但是主类可以通过创建内部类的实例对象,引用内部类中的任何成员。在内部类中不可以声明任何静态成员。内部类的一个典型应用是:在类中创建一个独立的线程类,然后调用主类的返回周期较长的方法经常用到。
※与性能相关的建议与经验
(1)避免复杂、昂贵的构造过程,例如一维数组比多维数组要快一些;
(2)内部类对象的创建相对一般类的实例化要复杂、耗时一些;
(3)在程序设计中尽量使用接口(Interface),因为接口的结构更容易被交换,以提高系统的性能;
(4)使用原数据类型的包装类比使用原数据要低效;
(5)最好把所有只进行一次初始化的变量,放在类构造器中一次性初始化;
(6)大多数的JVM在搜索接口列表时都是从后向前搜索的,因此经常用到的接口最好位于implements关键字列表的最后面。
(7)最好通过Class.forname()动态的装载类。