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 对它进行标记清理, 对象序列化后也可以使它复活.