Thinking in Java--第五章-初始化与清理

时间:2021-09-23 19:48:04

Java的一个对象创建的全部过程是怎样的?
Java的回收处理器到底是怎么运行的?
带着这两个问题开始这篇blog


Java中new一个新对象的全部过程是什么?

书中的原话是这样的:
总结一下对象的创建过程,假设有个名为Dog的类
1、即使没有显示地使用static字,构造器实际上也是静态方法。因此,当首次创建类型为Dog的对象时
(构造器可以看成静态方法),或者Dog类的静态方法/静态域被访问时,Java必须解释器必须查找类路径,
以定位Dog.class文件。

2、然后载入Dog.class,有关静态初始化的所有动作都会执行。因此,静态初始化只在Class对象首次加载时
进行一次。

3、当用new dog()创建对象时,首先将在堆上位Dog对象分配足够的存储空间。

4、这块存储空间会被清零,这就自动地将Dog对象中的所有基本类型数据设置成了默认值
(数字0,布尔和字符相同),而引用则被设置成了NULL

5、执行所有出现于字段定义处的初始化动作

6、执行构造器。


这里综合书中第五章和第七章的内容,加上我自己的理解 ,总结了一下
步骤如下(加载一个名为Battle的类):

  • 当你第一次访问该类的静态区域时(构造器也是静态方法,尽管它没有static关键字),加载器开始启动并找出Battle(假设)类的编译代码(在名为Battle.class的文件中。在对它加载的过程中,编译器注意到它有个基类(这是由.class文件中的extends的字节码得知),于是它会继续加载基类的.class文件(如果基类还有基类,那就一直加载下去),最终到根基类,这时根基类的static初始化就会被执行,再依次递归加载回来。
  • 在堆上位Battle对象分配足够的存储空间。如果内存资源不够,就牵扯到垃圾回收:
    1、 Java垃圾回收处理器回收时有些对象可能不被垃圾回收。
    2、 垃圾回收器只知道释放那些经由new分配的内存,它并不知道如何释放(并非使用new)的对象的“特殊内存”,所以Java允许我们在类内部定义一个名为finalize()的方法
    3、 finalize()这个方法的具体用途:1、释放(并非使用new)产生的对象。2、你必须实施清理。3、作为终结条件(下面会详细讲)
    4、Java虚拟机采用的是”自适应“的垃圾回收技术(下面会详细讲)
  • 这块存储空间会被清零,这就自动地将Dog对象中的所有基本类型数据设置成了默认值(数字0,布尔和字符相同),而引用则被设置成了NULL
  • 现在来加载这个类的非静态变量。
  • 执行构造器。(有基类的话也会一直递归下去,执行根基类的构造器…
    最终执行该类的构造器)。

自己写代码加深理解(类加载),下面的代码建议自己敲一下试试,感觉比较有价值

package com.sdkd.loadclass;

public class Test01 {
public Test01(int i){
System.out.println("Test01" + " " + i);
}
}
-----------------------------------------------
package com.sdkd.loadclass;

public class Test02 {
public Test02(int i){
System.out.println("Test02" + " " + i);
}
}
-----------------------------------------------
package com.sdkd.loadclass;

public class A {
private static Test01 common01 = new Test01(1);
private Test02 common02 = new Test02(2);
public A(){
System.out.println("A construtor");
}
private static Test01 common03 = new Test01(3);
private Test02 common04 = new Test02(4);
}
-----------------------------------------------

package com.sdkd.loadclass;

public class B extends A {

private static Test01 common06 = new Test01(6);
private Test02 common07 = new Test02(7);
public B(){
System.out.println("B construtor");
}
private static Test01 common08 = new Test01(3);
private Test02 common09 = new Test02(4);
/**
* @param args
*/

public static void main(String[] args) {
// TODO Auto-generated method stub
B b = new B();
}

}

“随着计算机革命的发展,”不安全“的编程方式已逐渐成为编程代价高昂的主因之一。”
初始化和清理正是涉及安全的两个问题。

第一(初始化)部分

一、用构造器确保初始化。我们可以将类的属性的初始化放在构造器内,这样就可以确保生成这个类时它得到初始化。
一个题:创建一个类,它包含一个在定义时被初始化的String域,以及另一个通过构造初始化的域,它们有何差异——我知道是它俩性能差别很大:
实例链接


二、方法重载。如果有了有参的构造器编译器就不会给你默认的构造器了。区分重载的方法;
独一无二的参数列表,可以是参数个数,顺序的变化。不能是返回值的不同:

void f(){
}
int f(int i){
}

你用的时候

f()

编译器无法识别。
涉及到基本类型的重载

  • 如果传入的实际参数小于方法中声明的形式参数类型,那么实际数据类型就会提升
  • 反之,需要用类型强转。
  • -

三、this关键字。this关键字是对当前对象的引用。
1、可以实现在一条语句里实现对同一对象实现多次操作。

public class Leaf{
int i = 0;
Leaf increment(){
++i;
return this;
}
void print(){
System.out.println("i = " + i);
}
public static void main(String[] args){
Leaf x = new Leaf();
x.print();
}
}/*Output
i = 3;
*/
//

2、将当前对象传递给其他方法也很有用:

class Person{
public void eat(Apple apple){
System.out.println("Yummy");
}
}
class Peeler{
static Apple peel(Apple apple){
return apple;
}
}
class Apple{
Apple getPeeled(){
return Peeler.peel(this);
}
}
public class Passingthis{
publci static void main(String[] args){
new Person().eat(new Apple());
}
}/*
Yummy
*/

3、在构造器中调用构造器

class A{
int a = 0;
String s = "abc";
A(int a){ this.a = a; }
A(String s){ this.s = s;}
A(int a, String s){
this(a);
//! this(s)不能调用两次this
this.s = s;
}
}

4、static就是没有this的方法,所以它的内部,不能不通过引用调用非静态方法。


四、成员初始化
这种代码是不允许的(编译时错误)

void f(){
int i;
++i;
}

尽管编译器也可以为i赋一个默认值,但未初始化的局部变量更可能是程序员的疏忽,所以采用默认值反而会掩盖这种失误。
然而如果类的数据成员是基本类型(注意不是局部变量),那么它就可以编译(char byte int 等)


五、构造器初始化

class Window{
Window(int marker) {
System.out.println("Window" + "(" + marker +")");
}
}
class House{
Window w1 = new Window();
House(){
print("House");
w3 = new Window(33);
}
Window w2 = new Window(2);
void f(){
print("f()");
}
Window w3 = new Window(3);
}
public class OrderOfInitialization{
public static void main(String[] args){
House h = new House();
h.f();
}
}/* Output
Window(1)
Window(2)
Window(3)
House()
Window(33)
f()*/
//

六、静态数据的初始化
1、无论创建多少个对象,静态数据都只占用一份存储区域,static不能应用于局部变量,因此它只能作用于域。如果一个域是静态的基本类型域,且没有对它初始化,那么它就会获得基本类型的标准初值。
2、初始化的顺序首先是静态对象,其次是非静态对象。
七、enum
讲一下它怎么用

public enum Spiciness{
NOT,MILD,MEDIUM
}
public class SimpleEnumUse{
public static void main(String[] args){
Spiness howHot = Spiciness.MEDIUM;
System.out.println(howHot + " " + howHot.ordinal());
}
}/*
MEDIUM 2
*/

第二(清理)部分

垃圾回收处理在第一个问题的答案里讲过了,这里就讲一下finlize()方法的用处之一—-“终结条件” 和 java虚拟机用的什么垃圾回收技术。
一、终结条件

class Book{
boolean checkedOut = false;
Book(boolean checkOut){
checkedOut = checkOut;
}
void checkIn(){ checkedOut = false ;}
protected void finalize(){
if(checkedOut){
System.out.println("Error: checked out");
}
}
}
public class TerminationCondition{
Book novel = new Book(true);
novel.checkIn();
new Book(true);
System.gc;
}
/* Output:
Error: checked out
*/
//

由于程序员失误,有一本书没有被签入,要是没有finalize(),很难发现这种缺陷。
二、垃圾回收器如何工作
Java虚拟机将采用一种“自适应”的垃圾回收技术。至于如何处理找到的存活对象,取决于不同的Java虚拟机实现。有一种作法名为“停止——复制”(stop-and-copy)。这意味着,先暂停程序的运行,(所以它不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部都是垃圾。当对象被复制到新堆时,它们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单、直接地分配新空间了。

“标记——清扫”所依据的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当它找到一个存活对象,就会给对象设一个标记,这个过程中不会回收任何对象。只有全部标记工作完成的时候,清除动作才会开始。在清处过程中,没有标记的对象将被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就得重新整理剩下的对象。

“停止——复制”的意思是这种垃圾回收方式不是在后台进行的;相反,垃圾回收动作发生的同时,程序将会被暂停。在Sun 公司的文档中你会发现,许多参考文献将垃圾回收视为低优先级的后台进程,但事实上垃圾回收器并非以这种方式实现——至少Sun公司早期版本的Java虚拟机中并非如此。当可用内存数量较低时,Sun版中的垃圾回收器才会被激活,同样,“标记——清扫”工作也必须在程序暂停的情况下才能进行。

如前文所述,这里讨论的Java虚拟机,内存分配单位是较大的“块”。如果对象较大,它会占用单独的块。严格来说,“停止——复制”要求你在释放旧有对象之前,必须先把所有存活对象从旧堆复制到新堆,这将导致大量内存复制行为。有了块之后,垃圾回收器在回收的时候就可以往废弃的块里拷贝对象了。每个块都用相应的“代数(generation count)”记录它是否还存活。通常,如果块在某处被引用,其代数会增加;垃圾回收器将对上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清除动作——大型对象仍然不会被复制(只是其代数会增加),内含小型对象的那些块则被复制并整理。Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到“标记——清扫”方式;同样, Java虚拟机会注意“标记——清扫”的效果,要是堆空间出现很多碎片,就会切换回“停止——复制”方式。这就是“自适应”技术。你可以给它个罗嗦的称呼:“自适应的、分代的、停止——复制、标记——清扫”式垃圾回收器。

Java虚拟机中有许多附加技术用以提升速度。尤其是与加载器操作有关的,被称为“即时”(Just-In-Time,JIT)编译的技术。这种技术可以把程序全部或部分翻译成本地机器码(这本来是Java虚拟机的工作),程序运行速度因此得以提升。当需要装载某个类(通常是在你为该类创建第一个对象)时,编译器会先找到其 .class 文件,然后将该类的字节码装入内存。此时,有两种方案可供选择。一种是就让即时编译器编译所有代码。但这种做法有两个缺陷:这种加载动作散落在整个程序生命周期内,累加起来要花更多时间;并且会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这将导致页面调度,从而降低程序速度。另一种做法称为“惰性编译(lazy uation)”,意思是即时编译器只在必要的时候才编译代码。这样,从不会被执行的代码也许就压根不会被JIT所编译。新版JDK中的Java HotSpot技术就采用了类似方法,代码每次被执行的时候都会做一些优化,所以执行的次数越多,它的速度就越快。

本人水平有限,若有发现错误,求指教。