1. 选择唯一性索引
唯一性索引的值是唯一的, 可以更快速的通过该索引来确定某条记录. 例如, 学生表中学号是具有唯一性的字段. 为该字段建立唯一性索引可以很快的确定某个学生的信息. 如果使用姓名的话, 可能存在同名现象, 从而降低查询速度.
2. 为经常需要排序, 分组和联合操作的字段建立索引
经常需要 ORDER BY,GROUP BY,DISTINCT 和 UNION 等操作的字段, 排序操作会浪费很多时间. 如果为其建立索引, 可以有效地避免排序操作.
3. 为常作为查询条件的字段建立索引
如果某个字段经常用来做查询条件, 那么该字段的查询速度会影响整个表的查询速度. 因此, 为这样的字段建立索引, 可以提高整个表的查询速度.
4. 限制索引的数目
索引的数目不是越多越好. 每个索引都需要占用磁盘空间, 索引越多, 需要的磁盘空间就越大. 修改表时, 对索引的重构和更新很麻烦. 越多的索引, 会使更新表变得很浪费时间.
5. 尽量使用数据量少的索引
如果索引的值很长, 那么查询的速度会受到影响. 例如, 对一个 CHAR(100)类型的字段进行全文检索需要的时间肯定要比对 CHAR(10)类型的字段需要的时间要多.
6. 尽量使用前缀来索引
如果索引字段的值很长, 最好使用值的前缀来索引. 例如, TEXT 和 BLOG 类型的字段, 进行全文检索会很浪费时间. 如果只检索字段的前面的若干个字符, 这样可以提高检索速度.
7. 删除不再使用或者很少使用的索引
表中的数据被大量更新, 或者数据的使用方式被改变后, 原有的一些索引可能不再需要. 数据库管理员应当定期找出这些索引, 将它们删除, 从而减少索引对更新操作的影响.
8. 最左前缀匹配原则, 非常重要的原则.
MySQL 会一直向右匹配直到遇到范围查询 (>,<,between,like) 就停止匹配, 比如 a 1=""and="" b="2" c=""> 3 and d = 4 如果建立 (a,b,c,d) 顺序的索引, d 是用不到索引的, 如果建立 (a,b,d,c) 的索引则都可以用到, a,b,d 的顺序可以任意调整.
9.= 和 in 可以乱序.
比如 a = 1 and b = 2 and c = 3 建立 (a,b,c) 索引可以任意顺序, MySQL 的查询优化器会帮你优化成索引可以识别的形式
10. 尽量选择区分度高的列作为索引.
区分度的公式是 count(distinct col)/count(*), 表示字段不重复的比例, 比例越大我们扫描的记录数越少, 唯一键的区分度是 1, 而一些状态, 性别字段可能在大数据面前区分度就 是 0, 那可能有人会问, 这个比例有什么经验值吗? 使用场景不同, 这个值也很难确定, 一般需要 join 的字段我们都要求是 0.1 以上, 即平均 1 条扫描 10 条 记录
11. 索引列不能参与计算, 保持列 "干净".
比如 from_unixtime(create_time) = '2014-05-29'就不能使用到索引, 原因很简单, b + 树中存的都是数据表中的字段值, 但进行检索时, 需要把所有元素都应用函数才能比较, 显然成本 太大. 所以语句应该写成 create_time = unix_timestamp('2014-05-29');
12. 尽量的扩展索引, 不要新建索引.
比如表中已经有 a 的索引, 现在要加 (a,b) 的索引, 那么只需要修改原来的索引即可
注意: 选择索引的最终目的是为了使查询的速度变快. 上面给出的原则是最基本的准则, 但不能拘泥于上面的准则. 读者要在以后的学习和工作中进行不断的实践. 根据应用的实际情况进行分析和判断, 选择最合适的索引方式.
Java 中一共有 4 种引用类型(其实还有一些其他的引用类型比如 FinalReference): 强引用, 软引用, 弱引用, 虚引用.
其中强引用就是我们经常使用的 Object a = new Object(); 这样的形式, 在 Java 中并没有对应的 Reference 类.
本篇文章主要是分析软引用, 弱引用, 虚引用的实现, 这三种引用类型都是继承于 Reference 这个类, 主要逻辑也在 Reference 中.
问题
在分析前, 先抛几个问题?
网上大多数文章对于软引用的介绍是: 在内存不足的时候才会被回收, 那内存不足是怎么定义的? 什么才叫内存不足?
网上大多数文章对于虚引用的介绍是: 形同虚设, 虚引用并不会决定对象的生命周期. 主要用来跟踪对象被垃圾回收器回收的活动. 真的是这样吗?
虚引用在 Jdk 中有哪些场景下用到了呢?
Reference
我们先看下 Reference.java 中的几个字段
- public abstract class Reference {
- // 引用的对象
- private T referent;
- // 回收队列, 由使用者在 Reference 的构造函数中指定
- volatile ReferenceQueue queue;
- // 当该引用被加入到 queue 中的时候, 该字段被设置为 queue 中的下一个元素, 以形成链表结构
- volatile Reference next;
- // 在 GC 时, JVM 底层会维护一个叫 DiscoveredList 的链表, 存放的是 Reference 对象, discovered 字段指向的就是链表中的下一个元素, 由 JVM 设置
- transient private Reference discovered;
- // 进行线程同步的锁对象
- static private class Lock { }
- private static Lock lock = new Lock();
- // 等待加入 queue 的 Reference 对象, 在 GC 时由 JVM 设置, 会有一个 java 层的线程 (ReferenceHandler) 源源不断的从 pending 中提取元素加入到 queue
- private static Reference
- }
一个 Reference 对象的生命周期如下:
主要分为 Native 层和 Java 层两个部分.
Native 层在 GC 时将需要被回收的 Reference 对象加入到 DiscoveredList 中(代码在 referenceProcessor.cpp 中
process_discovered_references 方法), 然后将 DiscoveredList 的元素移动到 PendingList 中(代码在 referenceProcessor.cpp 中 enqueue_discovered_ref_helper 方法),PendingList 的队首就是 Reference 类中的 pending 对象.
看看 Java 层的代码
- private static class ReferenceHandler extends Thread {
- ...
- public void run() {
- while (true) {
- tryHandlePending(true);
- }
- }
- }
- static boolean tryHandlePending(boolean waitForNotify) {
- Reference
- Cleaner c;
- try {
- synchronized (lock) {
- if (pending != null) {
- r = pending;
- // 如果是 Cleaner 对象, 则记录下来, 下面做特殊处理
- c = r instanceof Cleaner ? (Cleaner) r : null;
- // 指向 PendingList 的下一个对象
- pending = r.discovered;
- r.discovered = null;
- } else {
- // 如果 pending 为 null 就先等待, 当有对象加入到 PendingList 中时, jvm 会执行 notify
- if (waitForNotify) {
- lock.wait();
- }
- // retry if waited
- return waitForNotify;
- }
- }
- }
- ...
- // 如果时 CLeaner 对象, 则调用 clean 方法进行资源回收
- if (c != null) {
- c.clean();
- return true;
- }
- // 将 Reference 加入到 ReferenceQueue, 开发者可以通过从 ReferenceQueue 中 poll 元素感知到对象被回收的事件.
- ReferenceQueue q = r.queue;
- if (q != ReferenceQueue.NULL) q.enqueue(r);
- return true;
- }
流程比较简单: 就是源源不断的从 PendingList 中提取出元素, 然后将其加入到 ReferenceQueue 中去, 开发者可以通过从 ReferenceQueue 中 poll 元素感知到对象被回收的事件.
另外需要注意的是, 对于 Cleaner 类型 (继承自虚引用) 的对象会有额外的处理: 在其指向的对象被回收时, 会调用 clean 方法, 该方法主要是用来做对应的资源回收, 在堆外内存 DirectByteBuffer 中就是用 Cleaner 进行堆外内存的回收, 这也是虚引用在 java 中的典型应用.
看完了 Reference 的实现, 再看看几个实现类里, 各自有什么不同.
- SoftReference
- public class SoftReference extends Reference {
- static private long clock;
- private long timestamp;
- public SoftReference(T referent) {
- super(referent);
- this.timestamp = clock;
- }
- public SoftReference(T referent, ReferenceQueue q) {
- super(referent, q);
- this.timestamp = clock;
- }
- public T get() {
- T o = super.get();
- if (o != null && this.timestamp != clock)
- this.timestamp = clock;
- return o;
- }
- }
软引用的实现很简单, 就多了两个字段: clock 和 timestamp.clock 是个静态变量, 每次 GC 时都会将该字段设置成当前时间. timestamp 字段则会在每次调用 get 方法时将其赋值为 clock(如果不相等且对象没被回收).
那这两个字段的作用是什么呢? 这和软引用在内存不够的时候才被回收, 又有什么关系呢?
这些还得看 JVM 的源码才行, 因为决定对象是否需要被回收都是在 GC 中实现的.
- size_t
- ReferenceProcessor::process_discovered_reflist(
- DiscoveredList refs_lists[],
- ReferencePolicy* policy,
- bool clear_referent,
- BoolObjectClosure* is_alive,
- OopClosure* keep_alive,
- VoidClosure* complete_gc,
- AbstractRefProcTaskExecutor* task_executor)
- {
- ...
- // 还记得上文提到过的 DiscoveredList 吗? refs_lists 就是 DiscoveredList.
- // 对于 DiscoveredList 的处理分为几个阶段, SoftReference 的处理就在第一阶段
- ...
- for (uint i = 0; i < _max_num_q; i++) {
- process_phase1(refs_lists[i], policy,
- is_alive, keep_alive, complete_gc);
- }
- ...
- }
- // 该阶段的主要目的就是当内存足够时, 将对应的 SoftReference 从 refs_list 中移除.
- void
- ReferenceProcessor::process_phase1(DiscoveredList& refs_list,
- ReferencePolicy* policy,
- BoolObjectClosure* is_alive,
- OopClosure* keep_alive,
- VoidClosure* complete_gc) {
- DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
- // Decide which softly reachable refs should be kept alive.
- while (iter.has_next()) {
- iter.load_ptrs(DEBUG_ONLY(!discovery_is_atomic() /* allow_null_referent */));
- // 判断引用的对象是否存活
- bool referent_is_dead = (iter.referent() != NULL) && !iter.is_referent_alive();
- // 如果引用的对象已经不存活了, 则会去调用对应的 ReferencePolicy 判断该对象是不时要被回收
- if (referent_is_dead &&
- !policy->should_clear_reference(iter.obj(), _soft_ref_timestamp_clock)) {
- if (TraceReferenceGC) {
- gclog_or_tty->print_cr("Dropping reference (" INTPTR_FORMAT ": %s" ") by policy",
- (void *)iter.obj(), iter.obj()->klass()->internal_name());
- }
- // Remove Reference object from list
- iter.remove();
- // Make the Reference object active again
- iter.make_active();
- // keep the referent around
- iter.make_referent_alive();
- iter.move_to_next();
- } else {
- iter.next();
- }
- }
- ...
- }
refs_lists 中存放了本次 GC 发现的某种引用类型(虚引用, 软引用, 弱引用等), 而
process_discovered_reflist 方法的作用就是将不需要被回收的对象从 refs_lists 移除掉, refs_lists 最后剩下的元素全是需要被回收的元素, 最后会将其第一个元素赋值给上文提到过的 Reference.java#pending 字段.
ReferencePolicy 一共有 4 种实现: NeverClearPolicy,AlwaysClearPolicy,LRUCurrentHeapPolicy,LRUMaxHeapPolicy.
其中 NeverClearPolicy 永远返回 false, 代表永远不回收 SoftReference, 在 JVM 中该类没有被使用, AlwaysClearPolicy 则永远返回 true, 在 referenceProcessor.hpp#setup 方法中中可以设置 policy 为 AlwaysClearPolicy, 至于什么时候会用到 AlwaysClearPolicy, 大家有兴趣可以自行研究.
LRUCurrentHeapPolicy 和 LRUMaxHeapPolicy 的 should_clear_reference 方法则是完全相同:
- bool LRUMaxHeapPolicy::should_clear_reference(oop p,
- jlong timestamp_clock) {
- jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
- assert(interval >= 0, "Sanity check");
- // The interval will be zero if the ref was accessed since the last scavenge/gc.
- if(interval <= _max_interval) {
- return false;
- }
- return true;
- }
timestamp_clock 就是 SoftReference 的静态字段 clock,
java_lang_ref_SoftReference::timestamp(p)对应是字段 timestamp. 如果上次 GC 后有调用 SoftReference#get,interval 值为 0, 否则为若干次 GC 之间的时间差.
_max_interval 则代表了一个临界值, 它的值在 LRUCurrentHeapPolicy 和 LRUMaxHeapPolicy 两种策略中有差异.
- void LRUCurrentHeapPolicy::setup() {
- _max_interval = (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB;
- assert(_max_interval >= 0,"Sanity check");
- }
- void LRUMaxHeapPolicy::setup() {
- size_t max_heap = MaxHeapSize;
- max_heap -= Universe::get_heap_used_at_last_gc();
- max_heap /= M;
- _max_interval = max_heap * SoftRefLRUPolicyMSPerMB;
- assert(_max_interval >= 0,"Sanity check");
- }
看到这里你就知道 SoftReference 到底什么时候被被回收了, 它和使用的策略(默认应该是 LRUCurrentHeapPolicy), 堆可用大小, 该 SoftReference 上一次调用 get 方法的时间都有关系.
- WeakReference
- public class WeakReference extends Reference {
- public WeakReference(T referent) {
- super(referent);
- }
- public WeakReference(T referent, ReferenceQueue q) {
- super(referent, q);
- }
- }
可以看到, 对于 Soft references 和 Weak references clear_referent 字段传入的都是 true, 这也符合我们的预期: 对象不可达后, 引用字段就会被置为 null, 然后对象就会被回收(对于软引用来说, 如果内存足够的话, 在 Phase 1, 相关的引用就会从 refs_list 中被移除, 到 Phase 3 时 refs_list 为空集合).
但对于 Final references 和 Phantom references,clear_referent 字段传入的是 false, 也就意味着被这两种引用类型引用的对象, 如果没有其他额外处理, 只要 Reference 对象还存活, 那引用的对象是不会被回收的. Final references 和对象是否重写了 finalize 方法有关, 不在本文分析范围之内, 我们接下来看看 Phantom references.
可以看到 WeakReference 在 Java 层只是继承了 Reference, 没有做任何的改动. 那 referent 字段是什么时候被置为 null 的呢? 要搞清楚这个问题我们再看下上文提到过的
process_discovered_reflist 方法:
- size_t
- ReferenceProcessor::process_discovered_reflist(
- DiscoveredList refs_lists[],
- ReferencePolicy* policy,
- bool clear_referent,
- BoolObjectClosure* is_alive,
- OopClosure* keep_alive,
- VoidClosure* complete_gc,
- AbstractRefProcTaskExecutor* task_executor)
- {
- ...
- //Phase 1: 将所有不存活但是还不能被回收的软引用从 refs_lists 中移除(只有 refs_lists 为软引用的时候, 这里 policy 才不为 null)
- if (policy != NULL) {
- if (mt_processing) {
- RefProcPhase1Task phase1(*this, refs_lists, policy, true /*marks_oops_alive*/);
- task_executor->execute(phase1);
- } else {
- for (uint i = 0; i < _max_num_q; i++) {
- process_phase1(refs_lists[i], policy,
- is_alive, keep_alive, complete_gc);
- }
- }
- } else { // policy == NULL
- assert(refs_lists != _discoveredSoftRefs,
- "Policy must be specified for soft references.");
- }
- // Phase 2:
- // 移除所有指向对象还存活的引用
- if (mt_processing) {
- RefProcPhase2Task phase2(*this, refs_lists, !discovery_is_atomic() /*marks_oops_alive*/);
- task_executor->execute(phase2);
- } else {
- for (uint i = 0; i < _max_num_q; i++) {
- process_phase2(refs_lists[i], is_alive, keep_alive, complete_gc);
- }
- }
- // Phase 3:
- // 根据 clear_referent 的值决定是否将不存活对象回收
- if (mt_processing) {
- RefProcPhase3Task phase3(*this, refs_lists, clear_referent, true /*marks_oops_alive*/);
- task_executor->execute(phase3);
- } else {
- for (uint i = 0; i < _max_num_q; i++) {
- process_phase3(refs_lists[i], clear_referent,
- is_alive, keep_alive, complete_gc);
- }
- }
- return total_list_count;
- }
- void
- ReferenceProcessor::process_phase3(DiscoveredList& refs_list,
- bool clear_referent,
- BoolObjectClosure* is_alive,
- OopClosure* keep_alive,
- VoidClosure* complete_gc) {
- ResourceMark rm;
- DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
- while (iter.has_next()) {
- iter.update_discovered();
- iter.load_ptrs(DEBUG_ONLY(false /* allow_null_referent */));
- if (clear_referent) {
- // NULL out referent pointer
- // 将 Reference 的 referent 字段置为 null, 之后会被 GC 回收
- iter.clear_referent();
- } else {
- // keep the referent around
- // 标记引用的对象为存活, 该对象在这次 GC 将不会被回收
- iter.make_referent_alive();
- }
- ...
- }
- ...
- }
不管是弱引用还是其他引用类型, 将字段 referent 置 null 的操作都发生在 process_phase3 中, 而具体行为是由 clear_referent 的值决定的. 而 clear_referent 的值则和引用类型相关.
- ReferenceProcessorStats ReferenceProcessor::process_discovered_references(
- BoolObjectClosure* is_alive,
- OopClosure* keep_alive,
- VoidClosure* complete_gc,
- AbstractRefProcTaskExecutor* task_executor,
- GCTimer* gc_timer) {
- NOT_PRODUCT(verify_ok_to_handle_reflists());
- ...
- //process_discovered_reflist 方法的第 3 个字段就是 clear_referent
- // Soft references
- size_t soft_count = 0;
- {
- GCTraceTime tt("SoftReference", trace_time, false, gc_timer);
- soft_count =
- process_discovered_reflist(_discoveredSoftRefs, _current_soft_ref_policy, true,
- is_alive, keep_alive, complete_gc, task_executor);
- }
- update_soft_ref_master_clock();
- // Weak references
- size_t weak_count = 0;
- {
- GCTraceTime tt("WeakReference", trace_time, false, gc_timer);
- weak_count =
- process_discovered_reflist(_discoveredWeakRefs, NULL, true,
- is_alive, keep_alive, complete_gc, task_executor);
- }
- // Final references
- size_t final_count = 0;
- {
- GCTraceTime tt("FinalReference", trace_time, false, gc_timer);
- final_count =
- process_discovered_reflist(_discoveredFinalRefs, NULL, false,
- is_alive, keep_alive, complete_gc, task_executor);
- }
- // Phantom references
- size_t phantom_count = 0;
- {
- GCTraceTime tt("PhantomReference", trace_time, false, gc_timer);
- phantom_count =
- process_discovered_reflist(_discoveredPhantomRefs, NULL, false,
- is_alive, keep_alive, complete_gc, task_executor);
- }
- ...
- }
可以看到, 对于 Soft references 和 Weak references clear_referent 字段传入的都是 true, 这也符合我们的预期: 对象不可达后, 引用字段就会被置为 null, 然后对象就会被回收(对于软引用来说, 如果内存足够的话, 在 Phase 1, 相关的引用就会从 refs_list 中被移除, 到 Phase 3 时 refs_list 为空集合).
但对于 Final references 和 Phantom references,clear_referent 字段传入的是 false, 也就意味着被这两种引用类型引用的对象, 如果没有其他额外处理, 只要 Reference 对象还存活, 那引用的对象是不会被回收的. Final references 和对象是否重写了 finalize 方法有关, 不在本文分析范围之内, 我们接下来看看 Phantom references.
- PhantomReference
- public class PhantomReference extends Reference {
- public T get() {
- return null;
- }
- public PhantomReference(T referent, ReferenceQueue q) {
- super(referent, q);
- }
- }
可以看到虚引用的 get 方法永远返回 null, 我们看个 demo.
- public static void demo() throws InterruptedException {
- Object obj = new Object();
- ReferenceQueue
- PhantomReference
- Object objg = phanRef.get();
- // 这里拿到的是 null
- System.out.println(objg);
- // 让 obj 变成垃圾
- obj=null;
- System.gc();
- Thread.sleep(3000);
- //gc 后会将 phanRef 加入到 refQueue 中
- Reference phanRefP = refQueue.remove();
- // 这里输出 true
- System.out.println(phanRefP==phanRef);
- }
从以上代码中可以看到, 虚引用能够在指向对象不可达时得到一个'通知'(其实所有继承 References 的类都有这个功能), 需要注意的是 GC 完成后, phanRef.referent 依然指向之前创建 Object, 也就是说 Object 对象一直没被回收!
而造成这一现象的原因在上一小节末尾已经说了: 对于 Final references 和 Phantom references,clear_referent 字段传入的时 false, 也就意味着被这两种引用类型引用的对象, 如果没有其他额外处理, 在 GC 中是不会被回收的.
对于虚引用来说, 从 refQueue.remove(); 得到引用对象后, 可以调用 clear 方法强行解除引用和对象之间的关系, 使得对象下次可以 GC 时可以被回收掉.
End
针对文章开头提出的几个问题, 看完分析, 我们已经能给出回答:
1. 我们经常在网上看到软引用的介绍是: 在内存不足的时候才会回收, 那内存不足是怎么定义的? 为什么才叫内存不足?
软引用会在内存不足时被回收, 内存不足的定义和该引用对象 get 的时间以及当前堆可用内存大小都有关系, 计算公式在上文中也已经给出.
2. 网上对于虚引用的介绍是: 形同虚设, 与其他几种引用都不同, 虚引用并不会决定对象的生命周期. 主要用来跟踪对象被垃圾回收器回收的活动. 真的是这样吗?
严格的说, 虚引用是会影响对象生命周期的, 如果不做任何处理, 只要虚引用不被回收, 那其引用的对象永远不会被回收. 所以一般来说, 从 ReferenceQueue 中获得 PhantomReference 对象后, 如果 PhantomReference 对象不会被回收的话(比如被其他 GC ROOT 可达的对象引用), 需要调用 clear 方法解除 PhantomReference 和其引用对象的引用关系.
3. 虚引用在 Jdk 中有哪些场景下用到了呢?
DirectByteBuffer 中是用虚引用的子类 Cleaner.java 来实现堆外内存回收的, 后续会写篇文章来说说堆外内存的里里外外.
来源: http://developer.51cto.com/art/202201/698803.htm