1.Java垃圾回收机制
两个最基本的java回收算法:
- 复制算法
- 标记清理算法
标记清理:一块区域,标记可达对象(可达性分析),然后回收不可达对象,会出现碎片,那么引出
标记-整理算法:多了碎片整理,整理出更大的内存放更大的对象
两个概念:新生代和年老代
新生代:初始对象,生命周期短的
永久代:长时间存在的对象
整个java的垃圾回收是新生代和年老代的协作,这种叫做分代回收。
P.S:
- Serial New收集器是针对新生代的收集器,采用的是复制算法
- Parallel New(并行)收集器,新生代采用复制算法,老年代采用标记整理
- Parallel Scavenge(并行)收集器,针对新生代,采用复制收集算法
- Serial Old(串行)收集器,新生代采用复制,老年代采用标记整理
- Parallel Old(并行)收集器,针对老年代,标记整理
- CMS收集器,基于标记清理
- G1收集器:整体上是基于标记 整理 ,局部采用复制
首先要搞清垃圾回收的范围(栈需要GC去回收吗?),然后就是回收的前提条件如何判断一个对象已经可以被回收(这里只重点学习根搜索算法就行了),之后便是建立在根搜索基础上的三种回收策略,最后便是JVM中对这三种策略的具体实现。
1.范围:要回收哪些区域?
Java方法栈、本地方法栈以及PC计数器随方法或线程的结束而自然被回收,所以这些区域不需要考虑回收问题。Java堆和方法区是GC回收的重点区域,因为一个接口的多个实现类需要的内存不一样,一个方法的多个分支需要的内存可能也不一样,而这两个区域又对立于栈可能随时都会有对象不再被引用,因此这部分内存的分配和回收都是动态的。
2.前提:如何判断对象已死?
(1)引用计数法
引用计数法就是通过一个计数器记录该对象被引用的次数,方法简单高效,但是解决不了循环引用的问题。比如对象A包含指向对象B的引用,对象B也包含指向对象A的引用,但没有引用指向A和B,这时当前回收如果采用的是引用计数法,那么对象A和B的被引用次数都为1,都不会被回收。
下面是循环引用的例子,在Hotspot JVM下可以被正常回收,可以证实JVM 采用的不是简单的引用计数法。通过
-XX:+PrintGCDetails输出GC日志。
[Full GC (System) [Tenured: 2048K->366K(10944K), 0.0046272 secs] 4604K->366K(15872K),
[Perm : 154K->154K(12288K)], 0.0046751 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
(2)根搜索
通过选取一些根对象作为起始点,开始向下搜索,如果一个对象到根对象不可达时,则说明此对象已经没有被引用,是可以被回收的。可以作为根的对象有:栈中变量引用的对象,类静态属性引用的对象,常量引用的对象等。因为每个线程都有一个栈,所以我们需要选取多个根对象。附:对象复活
在根搜索中得到的不可达对象并不是立即就被标记成可回收的,而是先进行一次
标记放入F-Queue等待执行对象的finalize()方法,执行后GC将进行二次标记,复活
的对象之后将不会被回收。因此,使对象复活的唯一办法就是重写finalize()方法,
并使对象重新被引用。
[java] view plaincopy
package com.cdai.jvm.gc;
public class DeadToRebirth {
private static DeadToRebirth hook;
@Override
public void finalize() throws Throwable {
super.finalize();
DeadToRebirth.hook = this;
}
public static void main(String[] args) throws Exception {
DeadToRebirth.hook = new DeadToRebirth();
DeadToRebirth.hook = null;
System.gc();
Thread.sleep(500);
if (DeadToRebirth.hook != null)
System.out.println("Rebirth!");
else
System.out.println("Dead!");
DeadToRebirth.hook = null;
System.gc();
Thread.sleep(500);
if (DeadToRebirth.hook != null)
System.out.println("Rebirth!");
else
System.out.println("Dead!");
}
}
要注意的两点是:
第一,finalize()方法只会被执行一次,所以对象只有一次复活的机会。
第二,执行GC后,要停顿半秒等待优先级很低的finalize()执行完毕。
3.策略:垃圾回收的算法
(1)标记-清除
没错,这里的标记指的就是之前我们介绍过的两次标记过程。标记完成后就可以
对标记为垃圾的对象进行回收了。怎么样,简单吧。但是这种策略的缺点很明显,
回收后内存碎片很多,如果之后程序运行时申请大内存,可能会又导致一次GC。
虽然缺点明显,这种策略却是后两种策略的基础。正因为它的缺点,所以促成了
后两种策略的产生。
(2)标记-复制
将内存分为两块,标记完成开始回收时,将一块内存中保留的对象全部复制到另
一块空闲内存中。实现起来也很简单,当大部分对象都被回收时这种策略也很高效。
但这种策略也有缺点,可用内存变为一半了!
怎样解决呢?聪明的程序员们总是办法多过问题的。可以将堆不按1:1的比例分离,
而是按8:1:1分成一块Eden和两小块Survivor区,每次将Eden和Survivor中存活的对象
复制到另一块空闲的Survivor中。这三块区域并不是堆的全部,而是构成了新生代。
从下图可以看到这三块区域如何配合完成GC的,具体的对象空间分配以及晋升请
参加后面第6条补充。
为什么不是全部呢?如果回收时,空闲的那一小块Survivor不够用了怎么办?这就是
老年代的用处。当不够用时,这些对象将直接通过分配担保机制进入老年代。那么
老年代也使用标记-复制策略吧?当然不行!老年代中的对象可不像新生代中的,
每次回收都会清除掉大部分。如果贸然采用复制的策略,老年代的回收效率可想而知。
(3)标记-整理
根据老年代的特点,采用回收掉垃圾对象后对内存进行整理的策略再合适不过,将
所有存活下来的对象都向一端移动。
4.实现:虚
综上:新生代基本采用复制算法,老年代采用标记整理算法。cms采用标记清理。
----------
Java把内存分成两种:
一种叫做栈内存
一种叫做堆内存
在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。
堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。
引用变量是普通变量,定义时在栈中分配内存,引用变量在程序运行到作用域外释放。而数组&对象本身在堆中分配,即使程序运行到使用new产生数组和对象的语句所在地代码块之外,数组和对象本身占用的堆内存也不会被释放,数组和对象在没有引用变量指向它的时候(比如先前的引用变量x=null时),才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。这个也是java比较占内存的主要原因。
2.JSP的九大内置对象
- 1.request对象
- 2.response对象
- 3.session对象
- 4.out对象
- 5.page对象
- 6.application对象
- 7.exception对象
- 8.pageContext对象
- 9.config对象
3.类的加载过程
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
其中准备、验证、解析3个部分统称为连接(Linking),加载、验证、准备、初始化和卸载这5个阶段的顺序是
确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始
化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
类加载过程:
- 1, JVM会先去方法区中找有没有相应类的.class存在。如果有,就直接使用;如果没有,则把相关类的.class加载到方法区
- 2, 在.class加载到方法区时,会分为两部分加载:先加载非静态内容,再加载静态内容
- 3, 加载非静态内容:把.class中的所有非静态内容加载到方法区下的非静态区域内
- 4, 加载静态内容:
- 4.1、把.class中的所有静态内容加载到方法区下的静态区域内
- 4.2、静态内容加载完成之后,对所有的静态变量进行默认初始化
- 4.3、所有的静态变量默认初始化完成之后,再进行显式初始化
- 4.4、当静态区域下的所有静态变量显式初始化完后,执行静态代码块
- 5,当静态区域下的静态代码块,执行完之后,整个类的加载就完成了。
4.HashMap和Hashtable的区别
①继承不同
- public class Hashtable extends Dictionary implements Map
- public class HashMap extends AbstractMap implements Map
- HashTable直接使用对象的hashCode。
- HashMap重新计算hash值。
5.哈希冲突
解决冲突的方法:
- 1. 开放定址法:线性探测再散列、二次探测再散列、再随机探测再散列;
- 2. 再哈希法:换一种哈希函数;
- 3. 链地址法 :在数组中冲突元素后面拉一条链路,存储重复的元素(HashMap使用此方式);
- 4. 建立一个公共溢出区:其实就是建立一个表,存放那些冲突的元素。
什么时候会产生冲突:
当我们往HashMap中put元素的时候:当程序试图将一个key-value对放入HashMap中时,HashMap中调用 hashCode() 方法来计算hashCode。由于在Java中两个不同的对象可能有一样的hashCode,所以不同的键可能有一样hashCode,从而导致冲突的产升。HashMap底层是 数组和链表 的结合体。底层是一个线性数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。数组是 Entry[] 数组,静态内部类。 Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用 next ,这就构成了链表。所以很明显是链地址法。
- 1 . 程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置;
- 2 . 若 Entry 的存储位置上为 null ,直接存储该对象;若不为空,两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同,
- 3 . 循环遍历链表,如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖;如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部
6.DispatcherServlet
主要用作职责调度工作,本身主要用于控制流程,主要职责如下:
- 1、文件上传解析,如果请求类型是multipart将通过MultipartResolver进行文件上传解析;
- 2、通过HandlerMapping,将请求映射到处理器(返回一个HandlerExecutionChain,它包括一个处理器、多个HandlerInterceptor拦截器);
- 3、通过HandlerAdapter支持多种类型的处理器(HandlerExecutionChain中的处理器);
- 4、通过ViewResolver解析逻辑视图名到具体视图实现;
- 5、本地化解析;
- 6、渲染具体的视图等;
- 7、如果执行过程中遇到异常将交给HandlerExceptionResolver来解析。