Java基础:知识点总结一

serializable 的意义

  1. 比如说你的内存不够用了, 那计算机就要将内存里面的一部分对象暂时的保存到硬盘中, 等到要用的时候再读入到内存中, 硬盘的那部分存储空间就是所谓的虚拟内存.
    在比如过你要将某个特定的对象保存到文件中, 我隔几天在把它拿出来用, 那么这时候就要实现 Serializable 接口;
  2. 在进行 java 的 Socket 编程的时候, 你有时候可能要传输某一类的对象, 那么也就要实现 Serializable 接口;最常见的你传输一个字符串, 它是 JDK
    里面的类, 也实现了 Serializable 接口, 所以可以在网络上传输.
  3. 如果要通过远程的方法调用(RMI)去调用一个远程对象的方法, 如在计算机 A 中调用另一台计算机 B 的对象的方法, 那么你需要通过 JNDI 服务获取计算机
    B 目标对象的引用, 将对象从 B 传送到 A, 就需要实现序列化接口.

例如:

在 web 开发中, 如果对象被保存在了 Session 中, tomcat 在重启时要把 Session 对象序列化到硬盘, 这个对象就必须实现 Serializable 接口.

如果对象要经过分布式系统 进行网络传输或通过 rmi 等远程调用, 这就需要在网络上传输对象, 被传输的对象就必 须实现 Serializable 接口.

单例模式

  1. 懒汉模式
  2. 饿汉模式
  3. 同步锁
  4. 双锁机制
  5. 枚举实现
  6. 静态内部类实现

因为加载外部类时, 是不会加载内部类的

1
2
3
4
5
6
7
8
9
10
11
// 一个延迟实例化的内部类的单例模式
public final class Singleton {
// 一个内部类的容器, 调用 getInstance 时, JVM 加载这个类
private static final class SingletonHolder {
static final Singleton singleton = new Singleton();
}
private Singleton(){}
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}

防止反射实例化对象
利用反射生成对象

1
2
3
4
5
// 使用反射破坏单例模式
Class c = Class.forName(Singleton.class.getName());
Constructor constructor = c.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton = (Singleton)ct.newInstance();

调用私有构造方法抛出异常
防止反序列化实例化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.Serializable;
/**
* Created by hollis on 16/2/5.
* 使用双重校验锁方式实现单例
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SerializableDemo1 {
// 为了便于理解, 忽略关闭流操作及删除文件操作. 真正编码时千万不要忘记
// Exception 直接抛出
public static void main(String[] args) throws IOException, ClassNotFoundException {
// Write Obj to file
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(Singleton.getSingleton());
// Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
// 判断是否是同一个对象
System.out.println(newInstance == Singleton.getSingleton());
}
}

防止序列化反序列化破坏单例的方法:
添加 readResolve 方法

1
2
3
private Object readResolve() {
return singleton;
}

利用枚举创建单例

1
2
3
4
5
6
/**
* Singleton pattern example using Java Enumj
*/
public enum EasySingleton{
INSTANCE;
}

使用反射破解枚举单例:
运行结果是抛出异常: Exception in thread "main" java.lang.NoSuchMethodException: cn.xing.test.Weekday.<init>()
明明 Weekday 有一个无参的构造函数, 为何不能通过暴力反射访问?
最新的 Java Language Specification (§8.9) 规定: Reflective instantiation of enum types is prohibited. 这是 java 语言的内置规范.

使用 clone 破解枚举单例
所有的枚举类都继承自 java.lang.Enum 类, 而不是 Object 类. 在 java.lang.Enum 类中 clone 方法如下:

1
2
3
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}

调用该方法将抛出异常, 且 final 意味着子类不能重写 clone 方法, 所以通过 clone 方法获取新的对象是不可取的.

使用序列化破解枚举单例
java.lang.Enum 类的 readObject 方法如下:

1
2
3
4
5
6
7
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}
private void readObjectNoData() throws ObjectStreamException {
throw new InvalidObjectException("can't deserialize enum");
}

同暴力反射一样, Java Language Specification (§8.9) 有着这样的规定: the special treatment by the serialization mechanism ensures that
duplicate instances are never created as a result of deserialization.

fork/join

20241229154732_FBhCkAUC.webp

fork/join 类似 MapReduce 算法, 两者区别是: Fork/Join 只有在必要时如任务非常大的情况下才分割成一个个小任务, 而 MapReduce 总是在开始执行第一步进行分割.
看来, Fork/Join 更适合一个 JVM 内线程级别, 而 MapReduce 适合分布式系统.

NIO 和 AIO

NIO :

  1. NIO 会将数据准备好后, 再交由应用进行处理, 数据的读取 / 写入过程依然在应用线程中完成, 只是将等待的时间剥离到单独的线程中去.
  2. 节省数据准备时间(因为 Selector 可以复用)

AIO:

  1. 读完了再通知我
  2. 不会加快 IO, 只是在读完后进行通知
  3. 使用回调函数, 进行业务处理

序列化和反序列化

  • 只有实现了 Serializable 和 Externalizable 接口的类的对象才能被序列化. Externalizable 接口继承自 Serializable 接口, 实现 Externalizable
    接口的类完全由自身来控制序列化的行为, 而仅实现 Serializable 接口的类可以采用默认的序列化方式 .
  • 默认实现 Serializable 接口的序列化是对于一个类的非 static, 非 transient 的实例变量进行序列化与反序列化. 刚刚上面也说了, 如果要对 static
    实例变量进行序列化就要使用 Externalizable 接口, 手动实现.
  • serialVersionUID 的作用
  • 父类的序列化
    • 要想将父类对象也序列化, 就需要让父类也实现 Serializable 接口. 如果父类不实现的话的, 就需要有默认的无参的构造函数. 在父类没有实现
      Serializable 接口时, 虚拟机是不会序列化父对象的, 而一个 Java 对象的构造必须先有父对象, 才有子对象, 反序列化也不例外. 所以反序列化时,
      为了构造父对象, 只能调用父类的无参构造函数作为默认的父对象. 因此当我们取父对象的变量值时, 它的值是调用父类无参构造函数后的值. 如果你
      kao 虑到这种序列化的情况, 在父类无参构造函数中对变量进行初始化, 否则的话, 父类变量值都是默认声明的值, 如 int 型的默认是 0, string
      型的默认是 null.
  • 关键字 transient
  • 当持久化对象时, 可能有一个特殊的对象数据成员, 我们不想用 serialization 机制来保存它. 为了在一个特定对象的一个域上关闭 serialization,
    可以在这个域前加上关键字 transient.
    transient 是 Java 语言的关键字, 用来表示一个域不是该对象序列化的一部分. 当一个对象被序列化的时候, transient 型变量的值不包括在序列化的表示中,
    然而非 transient 型的变量是被包括进去的

Integer

1
2
3
4
int i = 0;
Integer j = new Integer(0);
System.out.println(i==j);
System.out.println(j.equals(i));

在 JDK1.5 以前, 会报错
在 JDK1.5 后, 由于引入了自动装箱和拆箱, 会输入 true,true

进制

如果下列的公式成立: 78+78=123. 则采用的是()进制表示的?
解析一:
设进制数为 x, 根据题设公式展开为 7x+8+7x+8=1x^2+2x+3, 由于进制数必须为正整数, 得到 x=13.
解析二:
等式左边个位数相加为 16, 等式右边个位数为 3, 即 16 mod x=3, x=13.

== 和 equals

1
2
3
4
String i = "0";
String j = new String("0");
System.out.println(i==j); // 比值
System.out.println(j.equals(i)); // 比内容

false,true

强引用 软引用 弱引用 虚引用

强引用

  • 如果一个对象具有强引用, GC 绝不会回收它;
  • 当内存空间不足, JVM 宁愿抛出 OutOfMemoryError 错误.
  • 一般 new 出来的对象都是强引用, 如下
1
2
// 强引用
User strangeReference=new User();

以前我们使用的大部分引用实际上都是强引用, 这是使用最普遍的引用. 如果一个对象具有强引用, 那就类似于必不可少的生活用品, 垃圾回收器绝不会回收它.
当内存空 间不足, Java 虚拟机宁愿抛出 OutOfMemoryError 错误, 使程序异常终止, 也不会靠随意回收具有强引用的对象来解决内存不足问题.

软引用

如果一个对象具有软引用, 当内存空间不足, GC 会回收这些对象的内存, 使用软引用构建敏感数据的缓存.
在 JVM 中, 软引用是如下定义的, 可以通过一个时间戳来回收, 下面引自 JVM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class SoftReference<T> extends Reference<T> {

/**
* Timestamp clock, updated by the garbage collector
*/
static private long clock;

/**
* Timestamp updated by each invocation of the get method. The VM may use
* this field when selecting soft references to be cleared, but it is not
* required to do so.
*/
private long timestamp;

/**
* Creates a new soft reference that refers to the given object. The new
* reference is not registered with any queue.
*
* @param referent object the new soft reference will refer to
*/
public SoftReference(T referent) {
super(referent);
this.timestamp = clock;
}

/**
* Creates a new soft reference that refers to the given object and is
* registered with the given queue.
*
* @param referent object the new soft reference will refer to
* @param q the queue with which the reference is to be registered,
* or <tt>null</tt> if registration is not required
*
*/
public SoftReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
this.timestamp = clock;
}

/**
* Returns this reference object's referent. If this reference object has
* been cleared, either by the program or by the garbage collector, then
* this method returns <code>null</code>.
*
* @return The object to which this reference refers, or
* <code>null</code> if this reference object has been cleared
*/
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
}

软引用的声明的借助强引用或者匿名对象, 使用泛型 SoftReference;可以通过 get 方法获得强引用. 具体如下:

1
2
3
// 软引用
SoftReference<User>softReference=new SoftReference<User>(new User());
strangeReference=softReference.get();// 通过 get 方法获得强引用

如果一个对象只具有软引用, 那就类似于可有可物的生活用品. 如果内存空间足够, 垃圾回收器就不会回收它, 如果内存空间不足了, 就会回收这些对象的内存.
只要垃圾回收器没有回收它, 该对象就可以被程序使用. 软引用可用来实现内存敏感的高速缓存.
软引用可以和一个引用队列(ReferenceQueue)联合使用, 如果软引用所引用的对象被垃圾回收, JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中.

弱引用

如果一个对象具有弱引用, 在 GC 线程扫描内存区域的过程中, 不管当前内存空间足够与否, 都会回收内存, 使用弱引用 构建非敏感数据的缓存.
在 JVM 中, 弱引用是如下定义的, 下面引自 JVM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class WeakReference<T> extends Reference<T> {
/**
* Creates a new weak reference that refers to the given object. The new
* reference is not registered with any queue.
*
* @param referent object the new weak reference will refer to
*/
public WeakReference(T referent) {
super(referent);
}

/**
* Creates a new weak reference that refers to the given object and is
* registered with the given queue.
*
* @param referent object the new weak reference will refer to
* @param q the queue with which the reference is to be registered,
* or <tt>null</tt> if registration is not required
*/
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}

弱引用的声明的借助强引用或者匿名对象, 使用泛型 WeakReference<T>, 具体如下:

1
2
// 弱引用
WeakReference<User>weakReference=new WeakReference<User>(new User());

如果一个对象只具有弱引用, 那就类似于可有可物的生活用品. 弱引用与软引用的区别在于: 只具有弱引用的对象拥有更短暂的生命周期. 在垃圾回收器线程扫描它
所管辖的内存区域的过程中, 一旦发现了只具有弱引用的对象, 不管当前内存空间足够与否, 都会回收它的内存. 不过, 由于垃圾回收器是一个优先级很低的线程,
因此不一定会很快发现那些只具有弱引用的对象.
弱引用可以和一个引用队列(ReferenceQueue)联合使用, 如果弱引用所引用的对象被垃圾回收, Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中.

虚引用

如果一个对象仅持有虚引用, 在任何时候都可能被垃圾回收, 虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列联合使用, 虚引用主要用来 跟踪对象被垃圾回收的活动.
在 JVM 中, 虚引用是如下定义的, 下面引自 JVM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class PhantomReference<T> extends Reference<T> {

/**
* Returns this reference object's referent. Because the referent of a
* phantom reference is always inaccessible, this method always returns
* <code>null</code>.
*
* @return <code>null</code>
*/
public T get() {
return null;
}

/**
* Creates a new phantom reference that refers to the given object and
* is registered with the given queue.
*
* <p> It is possible to create a phantom reference with a <tt>null</tt>
* queue, but such a reference is completely useless: Its <tt>get</tt>
* method will always return null and, since it does not have a queue, it
* will never be enqueued.
*
* @param referent the object the new phantom reference will refer to
* @param q the queue with which the reference is to be registered,
* or <tt>null</tt> if registration is not required
*/
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}

虚引用 PhantomReference<T> 的声明的借助强引用或者匿名对象, 结合泛型 ReferenceQueue<T> 初始化, 具体如下:

1
2
// 虚引用
PhantomReference<User> phantomReference=new PhantomReference<User>(new User(),

“虚引用”顾名思义, 就是形同虚设, 与其他几种引用都不同, 虚引用并不会决定对象的生命周期. 如果一个对象仅持有虚引用, 那么它就和没有任何引用一样,
在任何时候都可能被垃圾回收.
虚引用主要用来 跟踪对象被垃圾回收的活动. 虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用. 当垃
圾回收器准备回收一个对象时, 如果发现它还有虚引用, 就会在回收对象的内存之前, 把这个虚引用加入到与之关联的引用队列中. 程序可以通过判断引用队列中是
否已经加入了虚引用, 来了解
被引用的对象是否将要被垃圾回收. 程序如果发现某个虚引用已经被加入到引用队列, 那么就可以在所引用的对象的内存被回收之前采取必要的行动.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import java.lang.ref.*;
import java.util.HashSet;
import java.util.Set;

class User {

private String name;

public User()
{}

public User(String name)
{
this.name=name;
}

@Override
public String toString() {
return name;
}

public void finalize(){
System.out.println("Finalizing ... "+name);
}
}

/**
* Created by jinxu on 15-4-25.
*/
public class ReferenceDemo {

private static ReferenceQueue<User> referenceQueue = new ReferenceQueue<User>();
private static final int size = 10;

public static void checkQueue(){
/* Reference<? extends User> reference = null;
while((reference = referenceQueue.poll())!=null){
System.out.println("In queue : "+reference.get());
}*/
Reference<? extends User> reference = referenceQueue.poll();
if(reference!=null){
System.out.println("In queue : "+reference.get());
}
}

public static void testSoftReference()
{
Set<SoftReference<User>> softReferenceSet = new HashSet<SoftReference<User>>();
for (int i = 0; i < size; i++) {
SoftReference<User> ref = new SoftReference<User>(new User("Soft " + i), referenceQueue);
System.out.println("Just created: " + ref.get());
softReferenceSet.add(ref);
}
System.gc();
checkQueue();
}

public static void testWeaKReference()
{
Set<WeakReference<User>> weakReferenceSet = new HashSet<WeakReference<User>>();
for (int i = 0; i < size; i++) {
WeakReference<User> ref = new WeakReference<User>(new User("Weak " + i), referenceQueue);
System.out.println("Just created: " + ref.get());
weakReferenceSet.add(ref);
}
System.gc();
checkQueue();
}

public static void testPhantomReference()
{
Set<PhantomReference<User>> phantomReferenceSet = new HashSet<PhantomReference<User>>();
for (int i = 0; i < size; i++) {
PhantomReference<User> ref =
new PhantomReference<User>(new User("Phantom " + i), referenceQueue);
System.out.println("Just created: " + ref.get());
phantomReferenceSet.add(ref);
}
System.gc();
checkQueue();
}

public static void main(String[] args) {
testSoftReference();
testWeaKReference();
testPhantomReference();
}
}

谈谈, Java GC 是在什么时候, 对什么东西, 做了什么事情

地球人都知道, Java 有个东西叫垃圾收集器, 它让创建的对象不需要像 c/cpp 那样 delete、free 掉, 你能不能谈谈, GC 是在什么时候, 对什么东西,
做了什么事情?

一. 回答: 什么时候?

  1. 系统空闲的时候.
    分析: 这种回答大约占 30%, 遇到的话一般我就会准备转向别的话题, 譬如算法、譬如 SSH 看看能否发掘一些他擅长的其他方面.
  2. 系统自身决定, 不可预测的时间 / 调用 System.gc() 的时候.
    分析: 这种回答大约占 55%, 大部分应届生都能回答到这个答案, 起码不能算错误是吧, 后续应当细分一下到底是语言表述导致答案太笼统,
    还是本身就只有这样一个模糊的认识.
  3. 能说出新生代、老年代结构, 能提出 minor gc/full gc
    分析: 到了这个层次, 基本上能说对 GC 运作有概念上的了解, 譬如看过《深入 JVM 虚拟机》之类的. 这部分不足 10%.
  4. 能说明 minor gc/full gc 的触发条件、OOM 的触发条件, 降低 GC 的调优的策略.

分析: 列举一些我期望的回答: eden 满了 minor gc, 升到老年代的对象大于老年代剩余空间 full gc, 或者小于时被 HandlePromotionFailure 参数强制
full gc;gc 与非 gc 时间耗时超过了 GCTimeRatio(GC 时间占总时间的比率, 默认值为 99, 即允许 1% 的 GC 时间, 仅在使用 Parallel Scavenge
收集器时生效)的限制引发 OOM, 调优诸如通过 NewRatio 控制新生代老年代比例, 通过 MaxTenuringThreshold 控制进入老年前生存次数等……能回答道这个阶段就会给我带来比较高的期望了,
当然面试的时候正常人都不会记得每个参数的拼写, 我自己写这段话的时候也是翻过手册的. 回答道这部分的小于 2%.
总结: 程序员不能具体控制时间, 系统在不可预测的时间调用 System.gc() 函数的时候;当然可以通过调优, 用 NewRatio 控制 newObject 和 oldObject
的比例,
用 MaxTenuringThreshold 控制进入 oldObject 的次数, 使得 oldObject 存储空间延迟达到 full gc, 从而使得计时器引发 gc 时间延迟 OOM 的时间延迟,
以延长对象生存期.

二. 回答: 对什么东西

  1. 不使用的对象.
    分析: 相当于没有回答, 问题就是在问什么对象才是“不使用的对象”. 大约占 30%.
    2 . 超出作用域的对象 / 引用计数为空的对象.
    分析: 这 2 个回答站了 60%, 相当高的比例, 估计学校教 java 的时候老师就是这样教的. 第一个回答没有解决我的疑问, gc
    到底怎么判断哪些对象在不在作用域的?至于引用计数来判断对象是否可收集的, 我可以会补充一个下面这个例子让面试者分析一下
  2. 从 gc root 开始搜索, 搜索不到的对象.
    分析: 根对象查找、标记已经算是不错了, 小于 5% 的人可以回答道这步, 估计是引用计数的方式太“深入民心”了. 基本可以得到这个问题全部分数.
    PS: 有面试者在这个问补充强引用(类似 new Object(), 只要强引用还在就不会被回收)、弱引用(还有用但并非必须的对象, 在系统将要发生 OOM 之前,
    才会将这些对象回收)、软引用(只能生存到下一次垃圾收集之前)、幻影引用(无法通过幻影引用得到对象, 和对象的生命周期无关,
    唯一目的就是能在这个对象被回收时收到一个系统通知)区别等, 不是我想问的答案, 但可以加分.
  3. 从 root 搜索不到, 而且经过第一次标记、清理后, 仍然没有复活的对象.
    分析: 我期待的答案. 但是的确很少面试者会回答到这一点, 所以在我心中回答道第 3 点我就给全部分数.

超出了作用域或引用计数为空的对象;从 gc root 开始搜索找不到的对象, 而且经过一次标记、清理, 仍然没有复活的对象.

三. 回答: 做什么

  1. 删除不使用的对象, 腾出内存空间.
    分析: 同问题 2 第一点. 40%.
  2. 补充一些诸如停止其他线程执行、运行 finalize 等的说明.
    分析: 起码把问题具体化了一些, 如果像答案 1 那样我很难在回答中找到话题继续展开, 大约占 40% 的人.
  3. 能说出诸如新生代做的是复制清理、from survivor、to survivor 是干啥用的、老年代做的是标记清理、标记清理后碎片要不要整理、复制清理和标记清理有有什么优劣势等.
    分析: 也是看过《深入 JVM 虚拟机》的基本都能回答道这个程度, 其实到这个程度我已经比较期待了. 同样小于 10%.
  4. 除了 3 外, 还能讲清楚串行、并行(整理 / 不整理碎片)、CMS 等搜集器可作用的年代、特点、优劣势, 并且能说明控制 / 调整收集器选择的方式.

总结: 删除不使用的对象, 回收内存空间;运行默认的 finalize, JVM 用 from survivor、to survivor 对它进行标记清理, 对象序列化后也可以使它复活.