第五章 初始化與清理(1)
1.用構造器確保初始化
在Java中,“初始化”和“創建”捆綁在一起,兩者不能分離。
2.構造器沒有返回值,這與返回值為空(void)明顯不同。
3.方法重載:方法名相同而形式參數不同。
注意:
1. 甚至形參順序的不同也足以區分兩個方法,如下示例:
public class OverloadingOrder {
static void f(String s, int i) {
print("String: " + s + ", int: " + i);
}
static void f(int i, String s) {
print("int: " + i + ", String: " + s);
}
public static void main(String[] args) {
f("String first", 11);
f(99, "Int first");
}
} /* Output:
String: String first, int: 11
int: 99, String: Int first
*///:~
一般不要這樣做,因為這會使代碼難以維護。
2. 根據方法返回值來區分重載方法是行不通的,因為當調用方法但是不關心返回值時,編譯器無法知曉該調用哪一種形式,如程序中僅僅寫出如下語句:
void f(){}
int f(){return 1;}
public static void main(String[] args){
f();
}
f();
行不需要利用返回值,僅僅是調用該函數,得到其他效果。
4.關于方法重載的傳參
- 整型常量被當做int值處理,即選擇接受int型參數的方法。
- 如果傳入的數據類型(實際參數類型)小于方法中聲明的形式參數類型,實際數據類型就會被提升。
- char型略有不同,如果無法找到恰好接受char型參數的方法,就會把char直接提升至int型。
- 方法接受較小的基本類型作為參數。如果傳入的實際參數較大,就得通過強制類型轉換來執行窄化轉換,否則,編譯器會報錯。
5.默認構造器
- 如果你寫的類中沒有構造器,則編譯器會自動幫你創建一個默認構造器。
- 但是,如果已經定義了一個構造器(無論是否有參數),編譯器就不會幫你自動創建默認構造器。
6.this關鍵字
可以在一條語句中定義類對象:
Banana a = new Banana(),
b = new Banana();
this 關鍵字只能在方法內部使用,表示對"調用方法的那個對象"的引用。注意,如果在方法內部調用同一個類的的另一個方法,就不必使用this,直接調用即可。人們期望只在必要處使用this。
7.在構造器中調用構造器
通常寫this表示對當前對象的引用。但是,在構造器中,如果為this添加參數列表,便產生對符合此參數列表的某個構造器的明確調用。
注意:
- 盡管可以用this調用一個構造器,但卻不能調用兩個。
- 必須將構造器置于最起始處,否則編譯器會報錯。
- 構造器參數的名稱和數據成員的名稱相同時,使用
this.數據成員名稱
解決沖突。 - 除構造器以外,編譯器禁止在其他任何方法中通過this調用構造器。
8.static含義
static方法就是沒有this的方法,在static方法的內部不能調用非靜態方法(不是完全不可能:若你傳遞一個對象的引用到靜態方法中(靜態方法可以創建其自身的對象),然后通過這個引用就可以調用非靜態方法和訪問非靜態成員了。但是通常要達到這樣的效果,只需要寫一個非靜態的方法即可。),反過來倒是可以。static方法可以在沒有創建任何對象的前提下,僅僅通過類本身來調用,與Smalltalk語言的"類方法"相對應。
9.清理:終結處理--finalize()
- finalize()不是C++中的析構函數,因為無論對象如何創建(即使是對象中含有其他對象的這種情況),垃圾回收器都會負責釋放對象占據的所有內存。之所以要有finalize(),是由于在分配內存時可能采用了類似C語言的做法,而非Java中的通常做法。這種情況逐一發生在使用"本地方法"的情況下,本地方法是一種在Java中調用非Java代碼的方式。本地方法目前只支持C和C++,但是它們可以調用其他語言寫的代碼,所以實際上可以調用任何代碼。在非Java代碼中,也許會調用C的malloc()函數系列來分配存儲空間,所以需要在finalize()中用本地方法調用free()。
- finalize()的工作原理:一旦垃圾回收器準備好釋放對象占用的存儲空間,將首先調用其finalize()方法,并且在下一次垃圾回收動作發生時,才會真正回收對象占用的內存。
原文:When the garbage
collector is ready to release the storage used for your object, it will first call finalize( ), and
only on the next garbage-collection pass will it reclaim the object’s memory.
- Java里的對象并非總是被垃圾回收,記住三點:
- 1.對象可能不被垃圾回收。
- 2.垃圾回收并不等于析構。
- 3.垃圾回收只與內存有關。
-
注意:
a.只要程序沒有瀕臨存儲空間用完的那一刻,對象占用的空間就總也得不到釋放。如果程序執行結束,并且垃圾回收器一直沒有釋放你創建的任何對象的存儲空間,則隨著程序的退出,那些資源也會全部交還給操作系統(針對1、2的說明)。
b.使用垃圾回收器的唯一原因就是為了回收程序不再使用的內存。所以對于與垃圾回收有關的任何行為來說(尤其是finalize()方法),它們也必須同內存及其回收有關。
- 最后,終結函數無法預料,常常是危險的,總之是多余的。
10.清理:垃圾回收
- Java不允許創建局部對象,必須使用new創建對象。在Java中,沒有用于釋放對象的delete,因為垃圾回收器會幫助你釋放存儲空間。然而,垃圾回收器的存在并不能完全代替析構函數(當然,絕對不能直接調用finalize(),所以,這也不是一種解決方案)。如果希望進行除釋放存儲空間之外的清理工作,還是得明確調用某個恰當的Java方法。這就等同于使用析構函數了,只是沒有它方便。通常,并不能指望finalize(),必須創建其他的"清理"方法,并明確的調用它們。
- 記住,無論是"垃圾回收"還是"終結",都不保證一定會發生。如果JVM并未面臨內存耗盡的情形,它是不會浪費時間去執行垃圾回收以恢復內存的。
- 只要對象存在沒有被適當清理的部分,程序就存在很隱晦的缺陷,finalize()可以用來最終發現這種情況——盡管它并不總是被調用。
- finalize()使用案例:
//: initialization/TerminationCondition.java
package io.github.wzzju; /* Added by Eclipse.py */
// Using finalize() to detect an object that
// hasn't been properly cleaned up.
class Book {
boolean checkedOut = false;
Book(boolean checkOut) {
checkedOut = checkOut;
}
void checkIn() {
checkedOut = false;
}
void checkOut() {
checkedOut = true;
}
protected void finalize() {
try {
super.finalize();
} catch (Throwable e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(checkedOut)
System.out.println("Error: checked out");
// Normally, you'll also do this:
// super.finalize(); // Call the base-class version
}
}
public class TerminationCondition {
public static void main(String[] args) {
Book novel = new Book(true);
// Proper cleanup:
novel.checkIn();
novel.checkOut();
// Drop the reference, forget to clean up:
new Book(true);
// Force garbage collection & finalization:
System.gc();
}
} /* Output:
Error: checked out
*///:~
注意:
1.System.gc()用于強制進行垃圾回收和終結動作(finalize())。
2.總應假設基類版的finalize()也要做某些重要的事,因此與使用super來調用它。
3.即使novel的checkedOut字段值也為true,但是卻未輸出,finalize()此時未調用(why?是因為還有引用指向對象實體,其為活的實體,故而不回收?)。
11.垃圾回收器如何工作?
- Java中所有對象(基本類型除外)都在堆上分配。
- 垃圾回收器對于提高對象的創建速度有明顯的效果。這意味著,Java從堆中分配空間的速度,可以和其他語言從棧上分配空間的速度相媲美。
It means that allocating storage for heap objects in Java can be nearly as fast as creating storage
on the stack in other languages.
- 垃圾回收器的工作原理:
For example, you can think of the C++ heap as a yard where each object stakes out its own
piece of turf. This real estate can become abandoned sometime later and must be reused. In
some JVMs, the Java heap is quite different; it’s more like a conveyor belt that moves
forward every time you allocate a new object. This means that object storage allocation is
remarkably rapid. The “heap pointer” is simply moved forward into virgin territory, so it’s
effectively the same as C++’s stack allocation. (Of course, there’s a little extra overhead for
bookkeeping, but it’s nothing like searching for storage.)
You might observe that the heap isn’t in fact a conveyor belt, and if you treat it that way,
you’ll start paging memory—moving it on and off disk, so that you can appear to have more
memory than you actually do. Paging significantly impacts performance. Eventually, after
you create enough objects, you’ll run out of memory. The trick is that the garbage collector
steps in, and while it collects the garbage it compacts all the objects in the heap so that you’ve
effectively moved the “heap pointer” closer to the beginning of the conveyor belt and farther
away from a page fault. The garbage collector rearranges things and makes it possible for the
high-speed, infinite-free-heap model to be used while allocating storage.
To understand garbage collection in Java, it’s helpful learn how garbage-collection schemes
work in other systems. A simple but slow garbage-collection technique is called reference
counting. This means that each object contains a reference counter, and every time a
reference is attached to that object, the reference count is increased. Every time a reference
goes out of scope or is set to null, the reference count is decreased. Thus, managing
reference counts is a small but constant overhead that happens throughout the lifetime of
your program. The garbage collector moves through the entire list of objects, and when it
finds one with a reference count of zero it releases that storage (however, reference counting
schemes often release an object as soon as the count goes to zero). The one drawback is that
if objects circularly refer to each other they can have nonzero reference counts while still
being garbage. Locating such self-referential groups requires significant extra work for the
garbage collector. Reference counting is commonly used to explain one kind of garbage
collection, but it doesn’t seem to be used in any JVM implementations.
In faster schemes, garbage collection is not based on reference counting. Instead, it is based
on the idea that any non-dead object must ultimately be traceable back to a reference that
lives either on the stack or in static storage. The chain might go through several layers of
objects. Thus, if you start in the stack and in the static storage area and walk through all the
references, you’ll find all the live objects. For each reference that you find, you must trace
into the object that it points to and then follow all the references in that object, tracing into
the objects they point to, etc., until you’ve moved through the entire Web that originated with
the reference on the stack or in static storage. Each object that you move through must still
be alive. Note that there is no problem with detached self-referential groups—these are
simply not found, and are therefore automatically garbage.
In the approach described here, the JVM uses an adaptive garbage-collection scheme, and
what it does with the live objects that it locates depends on the variant currently being used.
One of these variants is stop-and-copy. This means that—for reasons that will become
apparent—the program is first stopped (this is not a background collection scheme). Then,
each live object is copied from one heap to another, leaving behind all the garbage. In
addition, as the objects are copied into the new heap, they are packed end-to-end, thus
compacting the new heap (and allowing new storage to simply be reeled off the end as
previously described).
Of course, when an object is moved from one place to another, all references that point at the
object must be changed. The reference that goes from the heap or the static storage area to
the object can be changed right away, but there can be other references pointing to this object that will be encountered later during the “walk.” These are fixed up as they are found (you
could imagine a table that maps old addresses to new ones).
There are two issues that make these so-called “copy collectors” inefficient. The first is the
idea that you have two heaps and you slosh all the memory back and forth between these two
separate heaps, maintaining twice as much memory as you actually need. Some JVMs deal
with this by allocating the heap in chunks as needed and simply copying from one chunk to
another.
The second issue is the copying process itself. Once your program becomes stable, it might be
generating little or no garbage. Despite that, a copy collector will still copy all the memory
from one place to another, which is wasteful. To prevent this, some JVMs detect that no new
garbage is being generated and switch to a different scheme (this is the “adaptive” part). This
other scheme is called mark-and-sweep, and it’s what earlier versions of Sun’s JVM used all
the time. For general use, mark-and-sweep is fairly slow, but when you know you’re
generating little or no garbage, it’s fast.
Mark-and-sweep follows the same logic of starting from the stack and static storage, and
tracing through all the references to find live objects. However, each time it finds a live
object, that object is marked by setting a flag in it, but the object isn’t collected yet. Only
when the marking process is finished does the sweep occur. During the sweep, the dead
objects are released. However, no copying happens, so if the collector chooses to compact a
fragmented heap, it does so by shuffling objects around.
“Stop-and-copy” refers to the idea that this type of garbage collection is not done in the
background; instead, the program is stopped while the garbage collection occurs. In the Sun
literature you’ll find many references to garbage collection as a low-priority background
process, but it turns out that the garbage collection was not implemented that way in earlier
versions of the Sun JVM. Instead, the Sun garbage collector stopped the program when
memory got low. Mark-and-sweep also requires that the program be stopped.
As previously mentioned, in the JVM described here memory is allocated in big blocks. If you
allocate a large object, it gets its own block. Strict stop-and-copy requires copying every live
object from the source heap to a new heap before you can free the old one, which translates
to lots of memory. With blocks, the garbage collection can typically copy objects to dead
blocks as it collects. Each block has a generation count to keep track of whether it’s alive. In
the normal case, only the blocks created since the last garbage collection are compacted; all
other blocks get their generation count bumped if they have been referenced from
somewhere. This handles the normal case of lots of short-lived temporary objects.
Periodically, a full sweep is made—large objects are still not copied (they just get their
generation count bumped), and blocks containing small objects are copied and compacted.
The JVM monitors the efficiency of garbage collection and if it becomes a waste of time
because all objects are long-lived, then it switches to mark-andsweep. Similarly, the JVM
keeps track of how successful mark-and-sweep is, and if the heap starts to become
fragmented, it switches back to stop-and-copy. This is where the “adaptive” part comes in, so
you end up with a mouthful: “Adaptive generational stop-and-copy mark-andsweep.”
There are a number of additional speedups possible in a JVM. An especially important one
involves the operation of the loader and what is called a just-in-time (JIT) compiler. A JIT
compiler partially or fully converts a program into native machine code so that it doesn’t
need to be interpreted by the JVM and thus runs much faster. When a class must be loaded
(typically, the first time you want to create an object of that class), the .class file is located,
and the bytecodes for that class are brought into memory. At this point, one approach is to
simply JIT compile all the code, but this has two drawbacks: It takes a little more time,
which, compounded throughout the life of the program, can add up; and it increases the size
of the executable (bytecodes are significantly more compact than expanded JIT code), and
this might cause paging, which definitely slows down a program. An alternative approach is
lazy evaluation, which means that the code is not JIT compiled until necessary. Thus, code that never gets executed might never be JIT compiled. The Java HotSpot technologies in
recent JDKs take a similar approach by increasingly optimizing a piece of code each time it is
executed, so the more the code is executed, the faster it gets.
- 問題1.對象在堆中分配,對象的引用在哪?(stack or static storage area?)
- 問題2.對象中的引用在哪?(heap?)