Java 基础:知识点总结一
Java 基础:知识点总结一
dong4jserializable 的意义
- 比如说你的内存不够用了,那计算机就要将内存里面的一部分对象暂时的保存到硬盘中,等到要用的时候再读入到内存中,硬盘的那部分存储空间就是所谓的虚拟内存.
在比如过你要将某个特定的对象保存到文件中,我隔几天在把它拿出来用,那么这时候就要实现 Serializable 接口; - 在进行 java 的 Socket 编程的时候,你有时候可能要传输某一类的对象,那么也就要实现 Serializable 接口;最常见的你传输一个字符串,它是 JDK
里面的类,也实现了 Serializable 接口,所以可以在网络上传输. - 如果要通过远程的方法调用(RMI)去调用一个远程对象的方法,如在计算机 A 中调用另一台计算机 B 的对象的方法,那么你需要通过 JNDI 服务获取计算机
B 目标对象的引用,将对象从 B 传送到 A, 就需要实现序列化接口.
例如:
在 web 开发中,如果对象被保存在了 Session 中,tomcat 在重启时要把 Session 对象序列化到硬盘,这个对象就必须实现 Serializable 接口.
如果对象要经过分布式系统 进行网络传输或通过 rmi 等远程调用,这就需要在网络上传输对象,被传输的对象就必 须实现 Serializable 接口.
单例模式
- 懒汉模式
- 饿汉模式
- 同步锁
- 双锁机制
- 枚举实现
- 静态内部类实现
因为加载外部类时,是不会加载内部类的
1 | // 一个延迟实例化的内部类的单例模式 |
防止反射实例化对象
利用反射生成对象
1 | // 使用反射破坏单例模式 |
调用私有构造方法抛出异常
防止反序列化实例化对象
1 | import java.io.Serializable; |
1 | public class SerializableDemo1 { |
防止序列化反序列化破坏单例的方法:
添加 readResolve 方法
1 | private Object readResolve() { |
利用枚举创建单例
1 | /** |
使用反射破解枚举单例:
运行结果是抛出异常: 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 | protected final Object clone() throws CloneNotSupportedException { |
调用该方法将抛出异常,且 final 意味着子类不能重写 clone 方法,所以通过 clone 方法获取新的对象是不可取的.
使用序列化破解枚举单例
java.lang.Enum 类的 readObject 方法如下:
1 | private void readObject(ObjectInputStream in) throws IOException, |
同暴力反射一样,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
fork/join 类似 MapReduce 算法,两者区别是: Fork/Join 只有在必要时如任务非常大的情况下才分割成一个个小任务,而 MapReduce 总是在开始执行第一步进行分割.
看来,Fork/Join 更适合一个 JVM 内线程级别,而 MapReduce 适合分布式系统.
NIO 和 AIO
NIO :
- NIO 会将数据准备好后,再交由应用进行处理,数据的读取 / 写入过程依然在应用线程中完成,只是将等待的时间剥离到单独的线程中去.
- 节省数据准备时间(因为 Selector 可以复用)
AIO:
- 读完了再通知我
- 不会加快 IO, 只是在读完后进行通知
- 使用回调函数,进行业务处理
序列化和反序列化
- 只有实现了 Serializable 和 Externalizable 接口的类的对象才能被序列化. Externalizable 接口继承自 Serializable 接口,实现 Externalizable
接口的类完全由自身来控制序列化的行为,而仅实现 Serializable 接口的类可以采用默认的序列化方式 . - 默认实现 Serializable 接口的序列化是对于一个类的非 static, 非 transient 的实例变量进行序列化与反序列化。刚刚上面也说了,如果要对 static
实例变量进行序列化就要使用 Externalizable 接口,手动实现. - serialVersionUID 的作用
- 父类的序列化
- 要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。如果父类不实现的话的,就需要有默认的无参的构造函数。在父类没有实现
Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,
为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果你
kao 虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如 int 型的默认是 0, string
型的默认是 null.
- 要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。如果父类不实现的话的,就需要有默认的无参的构造函数。在父类没有实现
- 关键字 transient
- 当持久化对象时,可能有一个特殊的对象数据成员,我们不想用 serialization 机制来保存它。为了在一个特定对象的一个域上关闭 serialization,
可以在这个域前加上关键字 transient.
transient 是 Java 语言的关键字,用来表示一个域不是该对象序列化的一部分。当一个对象被序列化的时候,transient 型变量的值不包括在序列化的表示中,
然而非 transient 型的变量是被包括进去的
Integer
1 | int i = 0; |
在 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 | String i = "0"; |
false,true
强引用 软引用 弱引用 虚引用
强引用
- 如果一个对象具有强引用,GC 绝不会回收它;
- 当内存空间不足,JVM 宁愿抛出 OutOfMemoryError 错误.
- 一般 new 出来的对象都是强引用,如下
1 | // 强引用 |
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它.
当内存空 间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题.
软引用
如果一个对象具有软引用,当内存空间不足,GC 会回收这些对象的内存,使用软引用构建敏感数据的缓存.
在 JVM 中,软引用是如下定义的,可以通过一个时间戳来回收,下面引自 JVM:
1 | public class SoftReference<T> extends Reference<T> { |
软引用的声明的借助强引用或者匿名对象,使用泛型 SoftReference;可以通过 get 方法获得强引用。具体如下:
1 | // 软引用 |
如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存.
只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存.
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中.
弱引用
如果一个对象具有弱引用,在 GC 线程扫描内存区域的过程中,不管当前内存空间足够与否,都会回收内存,使用弱引用 构建非敏感数据的缓存.
在 JVM 中,弱引用是如下定义的,下面引自 JVM:
1 | public class WeakReference<T> extends Reference<T> { |
弱引用的声明的借助强引用或者匿名对象,使用泛型 WeakReference<T>
, 具体如下:
1 | // 弱引用 |
如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它
所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,
因此不一定会很快发现那些只具有弱引用的对象.
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中.
虚引用
如果一个对象仅持有虚引用,在任何时候都可能被垃圾回收,虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列联合使用,虚引用主要用来 跟踪对象被垃圾回收的活动.
在 JVM 中,虚引用是如下定义的,下面引自 JVM:
1 | public class PhantomReference<T> extends Reference<T> { |
虚引用 PhantomReference<T>
的声明的借助强引用或者匿名对象,结合泛型 ReferenceQueue<T>
初始化,具体如下:
1 | // 虚引用 |
“虚引用” 顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,
在任何时候都可能被垃圾回收.
虚引用主要用来 跟踪对象被垃圾回收的活动. 虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃
圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是
否已经加入了虚引用,来了解
被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动.
1 | import java.lang.ref.*; |
谈谈,Java GC 是在什么时候,对什么东西,做了什么事情
地球人都知道,Java 有个东西叫垃圾收集器,它让创建的对象不需要像 c/cpp 那样 delete、free 掉,你能不能谈谈,GC 是在什么时候,对什么东西,
做了什么事情?
一。回答:什么时候?
- 系统空闲的时候.
分析:这种回答大约占 30%, 遇到的话一般我就会准备转向别的话题,譬如算法、譬如 SSH 看看能否发掘一些他擅长的其他方面. - 系统自身决定,不可预测的时间 / 调用 System.gc () 的时候.
分析:这种回答大约占 55%, 大部分应届生都能回答到这个答案,起码不能算错误是吧,后续应当细分一下到底是语言表述导致答案太笼统,
还是本身就只有这样一个模糊的认识. - 能说出新生代、老年代结构,能提出 minor gc/full gc
分析:到了这个层次,基本上能说对 GC 运作有概念上的了解,譬如看过《深入 JVM 虚拟机》之类的。这部分不足 10%. - 能说明 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 的时间延迟,
以延长对象生存期.
二。回答:对什么东西
- 不使用的对象.
分析:相当于没有回答,问题就是在问什么对象才是 “不使用的对象”. 大约占 30%.
2 . 超出作用域的对象 / 引用计数为空的对象.
分析:这 2 个回答站了 60%, 相当高的比例,估计学校教 java 的时候老师就是这样教的。第一个回答没有解决我的疑问,gc
到底怎么判断哪些对象在不在作用域的?至于引用计数来判断对象是否可收集的,我可以会补充一个下面这个例子让面试者分析一下 - 从 gc root 开始搜索,搜索不到的对象.
分析:根对象查找、标记已经算是不错了,小于 5% 的人可以回答道这步,估计是引用计数的方式太 “深入民心” 了。基本可以得到这个问题全部分数.
PS: 有面试者在这个问补充强引用(类似 new Object (), 只要强引用还在就不会被回收)、弱引用(还有用但并非必须的对象,在系统将要发生 OOM 之前,
才会将这些对象回收)、软引用(只能生存到下一次垃圾收集之前)、幻影引用(无法通过幻影引用得到对象,和对象的生命周期无关,
唯一目的就是能在这个对象被回收时收到一个系统通知)区别等,不是我想问的答案,但可以加分. - 从 root 搜索不到,而且经过第一次标记、清理后,仍然没有复活的对象.
分析:我期待的答案。但是的确很少面试者会回答到这一点,所以在我心中回答道第 3 点我就给全部分数.
超出了作用域或引用计数为空的对象;从 gc root 开始搜索找不到的对象,而且经过一次标记、清理,仍然没有复活的对象.
三。回答:做什么
- 删除不使用的对象,腾出内存空间.
分析:同问题 2 第一点. 40%. - 补充一些诸如停止其他线程执行、运行 finalize 等的说明.
分析:起码把问题具体化了一些,如果像答案 1 那样我很难在回答中找到话题继续展开,大约占 40% 的人. - 能说出诸如新生代做的是复制清理、from survivor、to survivor 是干啥用的、老年代做的是标记清理、标记清理后碎片要不要整理、复制清理和标记清理有有什么优劣势等.
分析:也是看过《深入 JVM 虚拟机》的基本都能回答道这个程度,其实到这个程度我已经比较期待了。同样小于 10%. - 除了 3 外,还能讲清楚串行、并行(整理 / 不整理碎片)、CMS 等搜集器可作用的年代、特点、优劣势,并且能说明控制 / 调整收集器选择的方式.
总结:删除不使用的对象,回收内存空间;运行默认的 finalize, JVM 用 from survivor、to survivor 对它进行标记清理,对象序列化后也可以使它复活.