契子:明年就要离开学校找工作了,时间过的真快,想一想这几年,做了一些事,也有一些事并没有做好,有很多收获,也有不少遗憾。感性的话在此不宜多说,既然选择了程序员这条道路,也要有把它到做事业的态度。在正式找工作前还有几个月的时间,做东西,尝试新的技术固然很爽,但是基础也很重要,在这短短的几个月的时间里,我将把以前学过的一些知识,Java,数据结构,算法,网络,OS&Linux,J2EE等等知识查缺补漏,好好梳理一遍,不光是为了找工作,也是一种必须要坚持的态度。
对于Java知识的整理,基于《Effetive Java》2nd和《Java编程思想》4th辅以JVM和设计模式的相关知识,结合书本上的知识和我的理解进行整理。好了,开始我的一篇——创建和销毁对象。
1. Java中的构造器:
构造器是一种特殊类型的方法,它和类同名,没有返回类型,和new关键字结合可以返回对象实例的引用。TIJ中说它是一种静态方法,但是通过字节码我们可以看到其实并没有static关键字,它的行为也和其他静态方法有异(可以访问非静态成员变量),因此这种说法并不完全准确,这里不再深究。
1.1 定义构造器:
一个类可以有多个构造器,如果你没有定义构造器,Java编译器会在语义分析的阶段,首先添加一个默认构造器。
多个构造器可以通过方法重载(overload)实现,注意只有同方法名和不同参数列表可以区别不同的重载版本,返回类型并不能区分。
尤其是使用基本类型参数重载时,要注意类型的自动转换如(char—>int,小转大)和窄化转换(强制类型转换,大转小),当然会使用最匹配的类型。
1.2 this关键字:
通过this指针我们可以访问类的实例变量和方法,但最好是在必要的时候(需要返回或使用该实例,内部类访问外部类同名实例变量方法,构造器设置属性等)使用它,否则你不必添加它,编译同样会帮你添加。
在存在多个重载版本的构造器时我们可以在构造器内使用this调用其他构造器,可以避免一些重复的代码:
public ConstructorTest(int a) {
this.a = a;
}
public ConstructorTest(int a, String s) {
this(a);
this.s = s;
}
PS:在构造器存在很多参数情况下,重叠构造器是一种选择,但是更好的做法是使用Builder模式,后面会讲到。
1.3 static关键字:
static(静态),static方法和static变量是类方法和类变量,它们不能使用this引用,都放在方法区中,供各个线程共享。static变量初始化和static初始化其,会在类加载(隐式加载或显示加载)后执行一次。
2. 清理,终结对象(finalize)/垃圾回收(CG):
这涉及到很多内容。Java提供了垃圾回收器,但内存泄漏可能以很隐秘的方式发生(使用引用数组时),同时对于对象中可能使用的一些资源必须在对象不再使用时进行释放(Connection,FileInputStream等)。
首先对象实例作为类的副本存放在Java堆中,在JVM中,一般使用可达性分析进行垃圾回收,也就是说,如果顺着引用追溯的话,“活”的对象应该可以到达CG Root(包括,静态变量,常量引用,栈中的本地变量表以及本地方法栈JNI native方法中的引用)。垃圾回收器会对不可达对象进行标记,在堆的不同区域使用不同的方法进行回收。
标记-清除:如果只有很少的垃圾的话,它很快,而且简单,但是如果垃圾很多的话,会产生大量的碎片;
复制:我们可以将需要进行垃圾回收的内存区域分为2个部分,比如A和B,需要CG时,将A的存活的对象直接复制到B(之前为空)中,清空A就可以了,不需要考虑碎片的问题,实际上在JVM(Hotspot)中分成了3个部分,一般比例可以为8:1:1,它们分别命名为eden,surivor1,surivor2,因为据统计Java程序中95%以上的对象很快就不再使用,因此eden很大,surivor可以较小(存活的对象少)。这实际上多用与新生代的垃圾回收。
标记-整理:有新生代当然也有老年代了,与新生代不同,老年代的对象相对稳定的多,垃圾回收很少,毕竟是经过了minor CG洗礼的不会那么容易挂掉,开个玩笑。标记-整理与清除的不同的地方在,它并不是直接在原位置清除掉,而是将存活的对象移向一端,之后直接就可以一起清除掉挂了的对象。因为我们也说了老年代的对象回收的少,因此移动的也相对较少。这样就不会有大多的碎片了。
因此我们可以看到,JVM多采用分代回收的方式,对于不同的情况分而治之。
释放资源,终结和垃圾回收有什么关系:
首先,
垃圾回收只和内存的使用状况有关,当内存不足(或满足我们设置的条件)时,才会进行CG。
finalize()是什么时候执行的呢,对于那些不可达的对象,到它们真正被回收
至少需要经过两次标记阶段:
(1)首先筛选那些不需要执行finalize方法的对象,没有override finalize方法的和已经执行过finalize方法的对象,那它们就可以“等死”了,对于finalize尚未执行的对象,它们进入F-Queue队列,相当于是“死缓”,还有一线生机;
(2)F-Queue队列中的,有一个终结线程专门去调用这些对象的finalize方法(所以finalize方法是一个回调方法),如果在finalize方法有和CG Roots有了关联,OK,它活了,否则“等死”去。
因此,我们看到
finalize方法依赖直接于垃圾回收和终结线程,终结线程的优先级很低,这代表它可能很长时间都得不到执行,而垃圾回收也是你无法直接控制的(System.gc和System.runFinalize也是要看JVM脸的),所以
finalize和C++中的析构函数并不是一回事;
而对于数据库连接、文件访问句柄等等占用数据库资源和系统资源的对象,
我们必须及时的释放/关闭它们。你可以定义一个close方法,在try-finally中保证必要的关闭得到执行,Java中甚至有Closable接口,FileInputStream,Connection等都实现了它们。
Finalize方法到底有什么用:
(1)你可以在finalize方法检查close方法是否已经执行,这时一种安全敏感的做法,FileInputStream,Connection,Timer都是这样做的。
(2)使用JNI时,如果本地对象中要释放敏感资源,需要显示override finalize方法,进行释放。
(3)可以在finalize方法中拯救自己。
如果你要使用它,注意在继承体系中,要我们手动维持“终止方法链”,这和构造器方法是一样的道理。
总的来说除此以外尽量不要使用finalize方法。
3. 初始化:
如果想真正弄清楚对象初始化,而不是仅仅记住一些像成员变量的初始值这样的规则,我觉得应该了解一个类在第一个创建对象时是如何从字节码编程的可用的对象的。
在第一次使用一个类的时候,无论是显示加载一个类(Class.forName等)还是隐式加载一个类(A.staticVariable,new A())时,首先要有ClassLoader进行加载:
(1)ClassLoader首先通过类名定位到类文件的位置(通过classpath等),将字节码加载到内存,通过准备、字节码验证和resolve等环节将等到一个个Class对象,放到方法区中;
(2)在此之后就是类初始化,这是类中的静态变量和静态初始化器将按照位置顺序进行初始化工作,静态变量同样放在方法区中;
(3)如果你进行是实例创建的化,接下来的工作首先是在堆上分配内存了,具体的方法可能有指针碰撞和空闲列表;
(4)获得了内存空间后,首先全部置零,这也就是为什么类的成员变量会还有初始值的原因,之后如果指定了初始化值,同样这里也是按顺序进行的;
(5)最后将执行<init>也就是我们定义使用的构造器来进行我们自定义的初始化过程了,这里就可以获得我们想要的对象实例的引用了。
所以在类中,各个部分的初始化顺序是:静态变量,静态初始化器(按位置顺序)——>非静态成员变量(按位置顺序)——>构造器;
说完了基本过程,我们来看看在Java中一些具体的类型是怎样进行初始化的。
3.1 数组初始化:
在Java中数组同样也是一种对象,但它并不是由某个类实例化而来,而是有JVM直接创建的,它的父类是Object,因此你可以在数组上使用Object的方法。
首先来复习下基本的语法:
通过数组初始化器:int[] a = {12,3};
通过new动态创建:int[] a = new int[5];
对于垃圾回收来说,数组同时也是一种特殊的类型,看下面的例子:
public class MStack {
private static final int DEFAULT_SIZE = 20;
private Object[] elements = new Object[DEFAULT_SIZE];
private int size = 0;
public MStack() {
elements = new Object[DEFAULT_SIZE];
}
public void push(Object element) {
ensureCapacity();
elements[size++] = element;
}
public Object pop() {
if(size == 0) {
throw new RuntimeException("empty stack cannot pop");
}
return elements[--size];
}
public void ensureCapacity() {
if(size == elements.length) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
这是《Effective Java》的一个例子,该例中的Stack在pop是并没有将已经出栈的引用置为null;这些引用是“过期引用”,这些引用虽然没有被使用,但是它将随着Arrays.copy一起被复制到更大的数组中,对于JVM来说它们同样是存活的对象,但是对我们的应用程序来说这些是无用的。在一个需要长期运行的服务中如果出现这样的问题很容易导致OOM。
3.2 可变参数列表:
JDK1.5的特性,它和数组息息相关。实际上,可变参数列表实际还是通过数组来传递一组参数的,我觉得可以看作是一种语法糖。
使用可变参数列表时,如果有多个重载版本,会根据所传递的参数类型执行最匹配的版本,但是需要注意一些会产生“冲突”的情况:
public class VarArgsInit {
//overload with var argument
public static void f(Long...longs) {
System.out.println("f_long_varArgs");
}
public static void f(Character...characters) {
System.out.println("f_character_varArgs");
}
public static void f(float f, Character...characters) {
System.out.println("f_float_character_varArgs");
}
public static void g(float f, Character...characters) {
System.out.println("g_float_character_varArgs");
}
public static void g(char c, Character...characters) {
System.out.println("g_char_character_varArgs");
}
public static void main(String[] args) {
// f(); //Error:(19, 9) java: reference to f is ambiguous
// f();
f(1, 'a'); //OK
// f('a', 'b'); //Error:(19, 9) java: reference to f is ambiguous
g('a', 'b'); //OK
}
这个例子中,f('a','b')会引起编译错误,因为它会同时匹配第3个和第2个f()版本(因为'a'可以转换成float),解决方法,很简单g方法的两个版本就不会有这种冲突。
3.3 枚举:
JDK1.5的添加特性。enum也是类,它派生自Enum抽象类,但是与普通的类不同的时,编译器会给它添加一些特性,我觉得可以认为enum是一种具有特殊功能的class:
我们来看看一个枚举类型的字节码:
final enum hr.test.Color {
// 所有的枚举值都是类静态常量
public static final enum hr.test.Color RED;
public static final enum hr.test.Color BLUE;
public static final enum hr.test.Color BLACK;
public static final enum hr.test.Color YELLOW;
public static final enum hr.test.Color GREEN;
private static final synthetic hr.test.Color[] ENUM$VALUES;
// 初始化过程,对枚举类的所有枚举值对象进行第一次初始化
static {
0 new hr.test.Color [1]
3 dup
4 ldc <String "RED"> [16] //把枚举值字符串"RED"压入操作数栈
6 iconst_0 // 把整型值0压入操作数栈
7 invokespecial hr.test.Color(java.lang.String, int) [17] //调用Color类的私有构造器创建Color对象RED
10 putstatic hr.test.Color.RED : hr.test.Color [21] //将枚举对象赋给Color的静态常量RED。
......... 枚举对象BLUE等与上同
102 return
};
// 私有构造器,外部不可能动态创建一个枚举类对象(也就是不可能动态创建一个枚举值)。
private Color(java.lang.String arg0, int arg1){
// 调用父类Enum的受保护构造器创建一个枚举对象
3 invokespecial java.lang.Enum(java.lang.String, int) [38]
};
public static hr.test.Color[] values();
public static hr.test.Color valueOf(java.lang.String arg0);
}
从字节码解析中,首先可以看到:
(1)它是final的,因此我们无法继承它;
(2)所有枚举值,都是Color的实例,它们都是public static final的;
我们还有看到,编译器为enum添加了3个方法:
(1)私有构造器,保证无法从动态创建一个该类型的枚举对象;同时我们也无法使用反射创建一个enum类型实例:
public enum MEnum {
E1;
static class A {
private A(){
}
}
public static void main(String[] args) throws Exception {
Class<A> a = A.class;
Constructor constructor = a.getDeclaredConstructor();
constructor.setAccessible(true);
constructor.newInstance();
Class<?> ec = MEnum.class;
Constructor constructor1 = ec.getDeclaredConstructor(String.class, int.class);
constructor1.setAccessible(true);
constructor1.newInstance("YJH", 2);
}
}
结果:A类可以正常创建,而enum类型,java.lang.IllegalArgumentException: Cannot reflectively create enum objects,因为在class.newInstance中有这样的检查:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
(2)values静态方法;
(3)valueOf(String)静态方法:它们都是编译器为具体的enum类型添加的,你在Enum抽象类中看不到它们;
enum可以说是严格全局不可修改的安全类型,它同样可以进行安全的序列化而不用担心不唯一的情况,正因为如此,使用单元素枚举创建单例对象是一种极佳的方法,同时可以不用担心反射攻击。
PS:enum可以和switch结合使用,十分方便;
4. 创建和销毁对象的实践:
《Effetive Java》中对于这一块给出了一些优秀的建议,以后在每一篇终结之后我都附上关于这篇的好的实践模式和要注意的反模式。学习这些思想和设计模式我觉得对我使用和理解Java中不同模块以及Spring等框架有很大的好处,因为它们都是基于这些的思想和模式建立的,能够帮助我更好的理解它们的结构和功能。
4.1 使用静态工厂方法代替构造器: 优点:
(1)在具有比较复杂参数的构造器的时候,使用这很难通过重载版本来区别它们之间的功能差别,而使用静态工厂方法可以根据功能命名,像Executors中创建不同功能的线程池实例一样,静态工厂方法掩盖了构造器的复杂性;
(2)不必在每次调用它们的时候都创建一个新对象,静态工厂方法可以用于单例,享元模式,不可变类(final class,final field),不可实例化类(private 构造器)这些不同的场景;
(3)返回原返回类型的任何子类型:这一点影响觉得深远,广大,首先,在collection包中有Collection,List,Set,Map,Iterator等接口,Collections工具类,提供了很多具有附加功能的集合类实现,而它们都是定义在Collections中的嵌套类,通过静态工厂方法返回,还有Iterator,也是基于内部类实现的,通过它来返回,
静态工厂方法可以隐藏具体的实现,支持面向接口的编程。
在开发J2EE项目时,经常用到Java Persistence API,websocket API,servlet API等等,它们是J2EE规范的一部分,我们仅仅引用了API,接口,而具体的实现我们可以会用到hibernate,spring的子项目,像servlet和websokcet API,它们的具体实现则多由J2EE 应用服务器实现它们,另外tomcat8.0也提供了websocket的实现。
这就是“
服务提供者框架(service provider framework)”,提供者提供
Service接口的具体实现,提供者可以使用
提供者注册接口注册自己,提供者也可以实现
provider接口或者通过类名直接注册,客户端使用者通过
服务访问接口(其实就是静态工厂方法);
(4)本来是用来简化有泛型参数时对象创建的,不过有了diomand表达式,java已经可以自己推导类型了;
4.2 多个构造器参数时使用Builder模式:
重叠构造器和Bean+setter创建的方式真的不好维护,写过都知道,builder模式不仅仅可以灵活的组配参数;还可以创建不可变的对象。
public final class HasBuilder {
private final int i1;
private final int i2;
private final String s1;
public HasBuilder(Builder builder) {
this.i1 = builder.i1;
this.i2 = builder.i2;
this.s1 = builder.s1;
}
public static class Builder {
private int i1;
private int i2;
private String s1;
public Builder i1(int i1) {
this.i1 = i1;
return this;
}
public Builder i2(int i2) {
this.i2 = i2;
return this;
}
public Builder s1(String s1) {
this.s1 = s1;
return this;
}
public HasBuilder build() {
return new HasBuilder(this);
}
}
}
你可以通过在build构建在具体的设值方法里进行约束检查。
4.3 建立合适的单例:
大致总结一下,有5种不同的单例模式:
(1)饿汉模式;
(2)懒汉模式:延迟加载,这就涉及到了线程安全的问题,用synchronized方法关键字效率太低;
(3)基于双检锁的单例:JDK1.5是安全的,
需要通过volitale来保证可见性,一定要有手写它的能力!;
(4)基于静态内部类的方式:让静态内部类持有一个static final的实例,因为是内部类,所以自然也就可以延迟加载;
public class SingletonWithInnerClass {
private SingletonWithInnerClass() {
System.out.println("initialized");
}
private static class SingletonHolder {
private static final SingletonWithInnerClass s = new SingletonWithInnerClass();
}
public static SingletonWithInnerClass getInstance() {
return SingletonHolder.s;
}
public static void main(String[] args) {
Class c = SingletonWithInnerClass.class; //这里并没有进行初始化
System.out.println("start initialization:");
SingletonWithInnerClass singletonWithInnerClass = SingletonWithInnerClass.getInstance();
}
}
这段代码的输出结果:
start initialization:
initialized
可见是延迟加载的;
(5)单元素枚举的方法,前面已经讨论过了,最佳,无偿序列化,防止反射攻击;
4.4 私有构造器防止实例化:
对于一些工具类或者存放全局变量来说,使用private构造器可以防止继承/实例化,如果使用接口和抽象类来实现,是一种反模式;
4.5 避免创建不必要的对象:
(1)注意String是有常量池的,它实际上是通过private final char[]来存放的,所以它是不可变的,只有第一次使用这个字符串组合的时候才进入常量池:
new String("abc");实际上是有两个字符串对象,"abc"是编译期存在,它已经进入常量池了;
(2)对于Calendar这样的实例化代价较大的对象考虑尽量复用;
(3)使用自动装箱类型一定要特别小心,以免在循环中因为自动装箱而创建大量对象,能用基本类型就不要用装箱类型;
(4)小对象的创建和销毁代价是很小的,因此,使用对象池的时候一定要考虑是不是值得,使用对象池管理不当也可能造成内存泄漏。
4.6 消除过期引用:
(1)自己管理内存的时候:之前提到的MyStack(自己管理内存之外),还有两个情形容易导致内存泄漏:
(2)缓存:不要让缓存的引用成为阻止垃圾回收的唯一原因,尽量使用weakHashMap,它不会影响引用,当然使用它需要注意,只有缓存项的生命周期依赖与它的外部引用时才可以使用它;常见的情况,使用一个后台线程Timer或者ScheduledTreadPoolExecutor或者添加新条目的时候检查(LinkedHashMap提供了这样的机制);
(3)回调:这种基于观察者模式的方式都需要监听器或回调来注册,因此如果不再合适的时候释放也会造成泄漏,用弱引用是一种好的做法;
其实看看内存泄漏原因直接起来就是管理不当的引用池,这时由JVM可达性分析机制决定的;
|