Java知识点汇总

427 阅读55分钟

Java基础

Java 中堆和栈有什么区别

堆与栈的区别很明显:

  1. 栈内存存储的是局部变量而堆内存存储的是实体;
  2. 栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;
  3. 栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。

堆、栈说明:

栈内存:栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。

堆内存:存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。

JAVA的四种引用,及应用场景

Java的对象引用,在jdk1.2之前只有强引用,在这之后加入了其他引用,即强弱软虚。 强引用(Strong Reference):最常用的引用类型,如Object obj=new Object()。只要强引用存在GC就不会这个对象,所以这也是引起OOM的原因。

软引用(Soft Reference):用于描述还有用但非必须的对象,当堆将发生OOM时则会回收软引用所指向的内存空间,若回收后依然空间不足才会抛出OOM。一般用于实现内存敏感的高速缓存。当真正对象被标记finalizable以及的finalize()方法调用之后并且内存已经清理, 那么如果SoftReference object还存在就被加入到它的 ReferenceQueue。只有前面几步完成后,软引用和弱引用的get方法才会返回null。

弱引用(Weak Reference):发生GC时必定回收弱引用指向的内存空间。 和软引用加入队列的时机相同。

虚引用(Phantom Reference) 又称为幽灵引用或幻影引用,虚引用既不会影响对象的生命周期,也无法通过虚引用来获取对象实例,仅用于在发生GC时接收一个系统通知。当一个对象的finalize方法已经被调用了之后,这个对象的虚引用会被加入到队列中。通过检查该队列里面的内容就知道一个对象是不是已经准备要被回收了。虚引用和软引用和弱引用都不同,它会在内存没有清理的时候被加入引用队列.虚引用的建立必须要传入引用队列,其他可以没有。

BelieveFrank

GC在收集一个对象的时候会判断是否有引用指向对象,在JAVA中的引用主要有四种:

强引用(Strong Reference)

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

软引用(Soft Reference)

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

下面举个例子,假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。

设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了内存溢出的问题。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

弱引用(Weak Reference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

虚引用(Phantom Reference)

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用于检测对象是否已经从内存中删除,跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

虚引用的唯一目的是当对象被回收时收到一个系统通知。

什么是线程安全?保障线程安全有哪些手段?

线程安全就是当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。保证线程安全可从多线程三特性出发:

  1. 原子性(Atomicity):单个或多个操作是要么全部执行,要么都不执行
    • Lock:保证同时只有一个线程能拿到锁,并执行申请锁和释放锁的代码
    • synchronized:对线程加独占锁,被它修饰的类/方法/变量只允许一个线程访问
  2. 可见性(Visibility):当一个线程修改了共享变量的值,其他线程能够立即得知这个修改
    • volatile:保证新值能立即同步到主内存,且每次使用前立即从主内存刷新;
    • synchronized:在释放锁之前会将工作内存新值更新到主存中
  3. 有序性(Ordering):程序代码按照指令顺序执行
    • volatile: 本身就包含了禁止指令重排序的语义
    • synchronized:保证一个变量在同一个时刻只允许一条线程对其进行lock操作,使得持有同一个锁的两个同步块只能串行地进入

简述一下类加载过程

加载

加载,是指Java虚拟机查找字节流(查找.class文件),并且根据字节流创建java.lang.Class对象的过程。这个过程,将类的.class文件中的二进制数据读入内存,放在运行时区域的方法区内。然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构。

类加载阶段: (1)Java虚拟机将.class文件读入内存,并为之创建一个Class对象。

(2)任何类被使用时系统都会为其创建一个且仅有一个Class对象。

(3)这个Class对象描述了这个类创建出来的对象的所有信息,比如有哪些构造方法,都有哪些成员方法,都有哪些成员变量等。

链接

链接包括验证、准备以及解析三个阶段。 (1)验证阶段。主要的目的是确保被加载的类(.class文件的字节流)满足Java虚拟机规范,不会造成安全错误。

(2)准备阶段。负责为类的静态成员分配内存,并设置默认初始值。

(3)解析阶段。将类的二进制数据中的符号引用替换为直接引用。

说明: 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。

直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量。

举个例子来说,现在调用方法hello(),这个方法的地址是0xaabbccdd,那么hello就是符号引用,0xaabbccdd就是直接引用。 在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

初始化

初始化,则是为标记为常量值的字段赋值的过程。换句话说,只对static修饰的变量或语句块进行初始化。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

谈谈你对Java中Hash码的理解

在Java中,哈希码代表了对象的一种特征,例如我们判断某两个字符串是否==,如果其哈希码相等,则这两个字符串是相等的,其次,哈希码是一种数据结构的算法,常见的哈希码的算法有:

Object类的HashCode,返回对象的内存地址经过处理后的结构,由于每个对象的内存地址都不一样,所以哈希码也不一样。

String类的HashCode,根据String类包含的字符串的内容,根据一种特殊的算法返回哈希码,只要字符串的内容相同,返回的哈希码也相同。

Integer类:返回的哈希码就是integer对象里所包含的那个整数的数值。 例如:Integer i1=new Integer(100) i1.hashCode的值就是100,由此可见两个一样大小的Integer对象返回的哈希码也一样。

break与continue的区别

break语句 (强行结束循环)作用:

  1. 可以用来从循环体内跳出循环体,即提前结束循环,接着执行循环下面的语句。
  2. 使流程跳出switch结构

注意:break语句不能用于循环语句和switch语句之外的任何其他语句中

continue语句作用:

  • 结束本次循环,即忽略循环体中continue语句下面尚未执行的语句,接着进行下一次是否执行循环的判定。

注意:continue语句不能用于循环语句之外的任何其他语句中

continue语句和break语句的区别:

  • continue语句只结束本次循环,而不是终止整个循环的执行。
  • break语句则是结束整个循环过程,不再判断执行循环的条件是否成立。break语句可以用在循环语句和switch语句中。在循环语句中用来结束内部循环;在switch语句中用来跳出switch语句。

注意:循环嵌套时,break和continue只影响包含它们的最内层循环,与外层循环无关。

final, finally, finalize的区别

final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。 内部类要访问局部变量,局部变量必须定义成final类型.

finally是异常处理语句结构的一部分,表示总是执行。

finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。 JVM不保证此方法总被调用

守护线程与阻塞线程的四种情况

守护线程 Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)

用户线程即运行在前台的线程,而守护线程是运行在后台的线程。 守护线程作用是为其他前台线程的运行提供便利服务,而且仅在普通、非守护线程仍然运行时才需要,比如垃圾回收线程就是一个守护线程。当VM检测仅剩一个守护线程,而用户线程都已经退出运行时,VM就会退出,因为没有如果没有了被守护这,也就没有继续运行程序的必要了。如果有非守护线程仍然存活,VM就不会退出。

守护线程并非只有虚拟机内部提供,用户在编写程序时也可以自己设置守护线程。用户可以用Thread的setDaemon(true)方法设置当前线程为守护线程。

虽然守护线程可能非常有用,但必须小心确保其他所有非守护线程消亡时,不会由于它的终止而产生任何危害。因为你不可能知道在所有的用户线程退出运行前,守护线程是否已经完成了预期的服务任务。一旦所有的用户线程退出了,虚拟机也就退出运行了。 因此,不要在守护线程中执行业务逻辑操作(比如对数据的读写等)。

另外有几点需要注意:

1、setDaemon(true)必须在调用线程的start()方法之前设置,否则会跑出IllegalThreadStateException异常。

2、在守护线程中产生的新线程也是守护线程。 3、 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。

线程阻塞 线程可以阻塞于四种状态:

1、当线程执行Thread.sleep()时,它一直阻塞到指定的毫秒时间之后,或者阻塞被另一个线程打断;

2、当线程碰到一条wait()语句时,它会一直阻塞到接到通知(notify())、被中断或经过了指定毫秒时间为止(若制定了超时值的话)

3、线程阻塞与不同I/O的方式有多种。常见的一种方式是InputStream的read()方法,该方法一直阻塞到从流中读取一个字节的数据为止,它可以无限阻塞,因此不能指定超时时间;

4、线程也可以阻塞等待获取某个对象锁的排他性访问权限(即等待获得synchronized语句必须的锁时阻塞)。

注意,并非所有的阻塞状态都是可中断的,以上阻塞状态的前两种可以被中断,后两种不会对中断做出反应

谈谈你对重入锁的理解

重入锁

(1)重进入: 1.定义:重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。关联一个线程持有者+计数器,重入意味着锁操作的颗粒度为“线程”。

2.需要解决两个问题: 线程再次获取锁:锁需要识别获取锁的现场是否为当前占据锁的线程,如果是,则再次成功获取; 锁的最终释放:线程重复n次获取锁,随后在第n次释放该锁后,其他线程能够获取该锁。要求对锁对于获取进行次数的自增,计数器对当前锁被重复获取的次数进行统计,当锁被释放的时候,计数器自减,当计数器值为0时,表示锁成功释放。

3.重入锁实现重入性:每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁

(2)ReentrantLock是的非公平类中通过组合自定义同步器来实现锁的获取与释放。

 1 /**
 2 * Sync中的nonfairTryAcquire()方法实现
 3 * 这个跟公平类中的实现主要区别在于不会判断当前线程是否是等待时间最长的线程
 4 **/ 
 5 final boolean nonfairTryAcquire(int acquires) {
 6     final Thread current = Thread.currentThread();
 7     int c = getState();
 8     if (c == 0) {
 9         // 跟FairSync中的主要区别,不会判断hasQueuedPredecessors()
10         if (compareAndSetState(0, acquires)) {
11             setExclusiveOwnerThread(current);
12             return true;
13         }
14     }
15     else if (current == getExclusiveOwnerThread()) {
16         int nextc = c + acquires;
17         if (nextc < 0) // overflow
18             throw new Error("Maximum lock count exceeded");
19         setState(nextc);
20         return true;
21     }
22     return false;
23 }

nonfairTryAcquire()方法中,增加了再次获取同步状态的处理逻辑,通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。 成功获取锁的现场再次获取锁,只是增加了同步状态值,要求ReentrantLock在释放同步状态时减少同步状态值。

 1 /**
 2 * Sync中tryRelease()
 3 **/
 4 protected final boolean tryRelease(int releases) {
 5     // 修改当前锁的状态
 6     // 如果一个线程递归获取了该锁(也就是state != 1), 那么c可能不等0
 7     // 如果没有线程递归获取该锁,则c == 0
 8     int c = getState() - releases;
 9 
10 // 如果锁的占有线程不等于当前正在执行释放操作的线程,则抛出异常
11     if (Thread.currentThread() != getExclusiveOwnerThread())
12         throw new IllegalMonitorStateException();
13     boolean free = false;
14     // c == 0,表示当前线程释放锁成功,同时表示递归获取了该锁的线程已经执行完毕
15     // 则设置当前锁状态为free,同时设置锁的当前线程为null,可以让其他线程来获取
16     // 同时也说明,如果c != 0,则表示线程递归占用了锁资源,
17     // 所以锁的当前占用线程依然是当前释放锁的线程(实际没有释放)
18     if (c == 0) {
19         free = true;
20         setExclusiveOwnerThread(null);
21     }
22     // 重新设置锁的占有数
23     setState(c);
24     return free;
25 }

如果该锁被获取n次,则前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才返回true,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。 对于公平锁而言

 1 /**
 2 * FairSync中tryAcquire()的实现
 3 * 返回
 4 *   true: 获取锁成功
 5 *   false: 获取锁不成功
 6 **/
 7 protected final boolean tryAcquire(int acquires) {
 8     // 获取当前线程
 9     final Thread current = Thread.currentThread();
10     // 获取锁资源的状态
11     // 0: 说明当前锁可立即获取,在此种状态下(又是公平锁)
12     // >0并且当前线程与持有锁资源的线程是同一个线程则state + 1并返回true
13     // >0并且占有锁资源的不是当前线程,则返回false表示获取不成功
14     int c = getState();
15     if (c == 0) {
16         // 在锁可以立即获取的情况下
17         // 首先判断线程是否是刚刚释放锁资源的头节点的下一个节点(线程的等待先后顺序)
18         // 如果是等待时间最长的才会马上获取到锁资源,否则不会(这也是公平与不公平的主要区别所在)
19         if (!hasQueuedPredecessors() &&
20             compareAndSetState(0, acquires)) {
21             setExclusiveOwnerThread(current);
22             return true;
23         }
24     }
25     else if (current == getExclusiveOwnerThread()) {  //线程可以递归获取锁
26         int nextc = c + acquires;
27         // 超过int上限值抛出错误
28         if (nextc < 0)
29             throw new Error("Maximum lock count exceeded");
30         setState(nextc);
31         return true;
32     }
33     return false;
34 }

与非公平唯一的区别是判断条件中多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回了true,则表示有线程比当前线程更早地请求获取锁,所以需要等待前驱线程获取并释放锁后才能继续获取该锁。 但是非公平锁是默认实现:非公平性锁可能使线程“饥饿”,但是极少的线程切换,可以保证其更大的吞吐量。而公平性锁,保证了锁的获取按照FIFO原则,代价是进行大量的线程切换。

(3)synchronized可重入性 同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入

谈谈数组与链表的区别

  1. 数组是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素。但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样的道理,如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素。如果应用需要快速访问数据,很少或不插入和删除元素,就应该用数组。
  2. 链表恰好相反,链表中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起。比如:上一个元素有个指针指到下一个元素,以此类推,直到最后一个元素。如果要访问链表中一个元素,需要从第一个元素开始,一直找到需要的元素位置。但是增加和删除一个元素对于链表数据结构就非常简单了,只要修改元素中的指针就可以了。如果应用需要经常插入和删除元素你就需要用链表数据结构了。

From Taonce

  1. 数组是顺序的存储结构,链表是链式的存储结构。
  2. 数组的查找效率高,时间复杂度为O(1),插入和删除效率低,时间复杂度为O(n);链表插入和删除效率高,时间复杂度为O(1),查找效率低,时间复杂度为O(n)。
  3. 数组在内存中是一块连续的区域;而链表是分散的,它是通过指针指向下一个元素。
  4. 数组的空间大小是固定的,不能动态扩展;链表空间大小不固定,可动态扩展。

谈谈 static 关键字的用法

static 修饰的方法/变量等资源是静态资源 在内存中存放在方法区,所有允许访问的对象都可以访问(参考变量的修饰符 public protected等)

static修饰的变量只存在一份,所有可以访问的对象都允许进行修改

static 修饰的变量/方法在内存中被root引用,因此不会被GC回收,

static修饰的变量在类被加载的时候就会被加载

被static修饰的方法/代码块只能引用被static修饰的方法/变量

static的主要用法

  1. 用来修饰变量 可以不需要实例化对象就可以直接引用变量,引用方法ClassName.field;
  2. 修饰方法 可以不需要实例化对象就可以直接引用方法,引用方法 ClassName.method();
  3. 静态块 用来实现需要在类加载时就需要加载的逻辑

谈谈 ArrayList 和 LinkList 的区别

  1. ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。

  2. 对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。

  3. 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

两个都是多线程不安全。

From midNightHz

1、实现原理不一样 ArrayList是基于数据的实现,而LinkedList是基于链表的实现,两者的区别就是这两种数据结构的差别 2、初始化:ArrayList初始化时会初始化一个一定容量的数组,而linkedlist只是定义了链表的头元素和尾元素 3、添加元素 在list尾端添加元素区别并不是太大,但ArrayList是基于Array来实现的,会遇到一个很蛋疼大问题就是扩容问题 4、遍历 :遍历arraylist和linkedlist区别并不是很大 5、查询指定位置的元素 arraylist的查询速度为0(1),而linkedlist则双端的元素查询快,中间的元素查询慢 6、删除元素 数组删除元素是一个很蛋疼的问题,特别是移除数组头端的元素 ,需要一定当前下标元素后面的所有元素,而linkedlist删除双端的元素则非常快 删除中间的元素会比较慢(要遍历查到为指定位置的元素)

From 吉文杰 jiwenjie

  1. 因为 Array 是基于索引 (index) 的数据结构,它使用索引在数组中搜索和读取数据是很快的。Array 获取数据的时间复杂度是 O(1), 但是要删除数据却是开销很大的,因为这需要重排数组中的所有数据。
  2. 相对于 ArrayList , LinkedList 插入是更快的。因为LinkedList不像ArrayList 一样,不需要改变数组的大小,也不需要在数组装满的时候要将所有的数据重新装入一个新的数组,这是 ArrayList 最坏的一种情况,时间复杂度是 O(n) ,而 LinkedList 中插入或删除的时间复杂度仅为 O(1) 。ArrayList 在插入数据时还需要更新索引(除了插入数组的尾部)。
  3. 类似于插入数据,删除数据时,LinkedList 也优于 ArrayList 。
  4. LinkedList 需要更多的内存,因为 ArrayList 的每个索引的位置是实际的数据,而 LinkedList 中的每个节点中存储的是实际的数据和前后节点的位置( 一个LinkedList实例存储了两个值Node first 和 Node last 分别表示链表的其实节点和尾节点,每个 Node 实例存储了三个值:E item,Node next,Node pre)。

什么场景下更适宜使用LinkedList,而不用ArrayList

  1. 你的应用不会随机访问数据 。因为如果你需要LinkedList中的第n个元素的时候,你需要从第一个元素顺序数到第n个数据,然后读取数据。
  2. 你的应用更多的插入和删除元素,更少的读取数据。因为插入和删除元素不涉及重排数据,所以它要比ArrayList要快。
  3. 以上就是关于ArrayList和LinkedList的差别。你需要一个不同步的基于索引的数据访问时,请尽量使用ArrayList。ArrayList很快,也很容易使用。但是要记得要给定一个合适的初始大小,尽可能的减少更改数组的大小。

简述HashMap工作原理

HashMap是基于hashing算法的原理,通过put(key,value)和get(key)方法储存和获取值的。

  1. 存:我们将键值对K/V 传递给put()方法,它调用K对象的hashCode()方法来计算hashCode从而得到bucket位置,之后储存Entry对象。(HashMap是在bucket中储存 键对象 和 值对象,作为Map.Entry)
  2. 取:获取对象时,我们传递 键给get()方法,然后调用K的hashCode()方法从而得到hashCode进而获取到bucket位置,再调用K的equals()方法从而确定键值对,返回值对象。
  3. 碰撞:当两个对象的hashcode相同时,它们的bucket位置相同,‘碰撞’就会发生。如何解决,就是利用链表结构进行存储,即HashMap使用LinkedList存储对象。但是当链表长度大于8(默认)时,就会把链表转换为红黑树,在红黑树中执行插入获取操作。
  4. 扩容:如果HashMap的大小超过了负载因子定义的容量,就会进行扩容。默认负载因子为0.75。就是说,当一个map填满了75%的bucket时候,将会创建原来HashMap大小的两倍的bucket数组(jdk1.6,但不超过最大容量),来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

HashMap

补充答案
From midNightHz

从前有个叫妈个蛋村,村里有个地主,有钱又有颜,有能力可以娶很多妻妾,但是怎么样才能快速找到想要的妻妾呢,于是这个地主想了一个办法; 1、先建16个房子(标上0-15),为什么是16个呢,算命的说的 2、每娶到一个妻子(put),就根据妻子的生日,每年的第几天(hash值)算出要让这个妻子住在哪个房间,具体算法是这样的 hash%16 等于0就住0号房; 3、问题来了,这个有钱富豪娶了两个妻子,两个生日不同,但是要住同一个房间怎么办(hash碰撞)这个有钱的地主想了一个办法,让这些住同一个房间的妻子根据生日排个大小(二叉树) 4、过来一段时间以后,这位有钱的地主娶了12房姨太太,他想着房子快不够住了,怎么办,又建了16个房子(hashmap扩容),然后重新安排他们的住所

From 吉文杰 jiwenjie

HashMap的工作原理

  1. 什么时候会使用HashMap?他有什么特点? 是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象。
  2. 你知道HashMap的工作原理吗? 通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。
  3. 你知道get和put的原理吗?equals()和hashCode()的都有什么作用? 通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点
  4. 你知道hash的实现吗?为什么要这样实现? 在Java 1.8的实现中,是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。
  5. 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办? 如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并且重新调用hash方法。

关于Java集合的小抄中是这样描述的:

以Entry[]数组实现的哈希桶数组,用Key的哈希值取模桶数组的大小可得到数组下标。

插入元素时,如果两条Key落在同一个桶(比如哈希值1和17取模16后都属于第一个哈希桶),我们称之为哈希冲突。

JDK的做法是链表法,Entry用一个next属性实现多个Entry以单向链表存放。查找哈希值为17的key时,先定位到哈希桶,然后链表遍历桶里所有元素,逐个比较其Hash值然后key值。

在JDK8里,新增默认为8的阈值,当一个桶里的Entry超过閥值,就不以单向链表而以红黑树来存放以加快Key的查找速度。

当然,最好还是桶里只有一个元素,不用去比较。所以默认当Entry数量达到桶数量的75%时,哈希冲突已比较严重,就会成倍扩容桶数组,并重新分配所有原来的Entry。扩容成本不低,所以也最好有个预估值。

取模用与操作(hash & (arrayLength-1))会比较快,所以数组的大小永远是2的N次方, 你随便给一个初始值比如17会转为32。默认第一次放入元素时的初始值是16。

iterator()时顺着哈希桶数组来遍历,所以看起来是个乱序

接口和抽象类有什么区别

  1. 共同点

    是上层的抽象层。 都不能被实例化。 都能包含抽象的方法,这些抽象的方法用于描述类具备的功能,但是不比提供具体的实现。

  2. 区别

    在抽象类中可以写非抽象的方法,从而避免在子类中重复书写他们,这样可以提高代码的复用性,这是抽象类的优势,接口中只能有抽象的方法。 一个类只能继承一个直接父类,这个父类可以是具体的类也可是抽象类,但是一个类可以实现多个接口。

From jiwenjie

  1. 默认的方法实现 抽象类可以有默认的方法实现完全是抽象的。接口根本不存在方法的实现。

    抽象类中可以有已经实现了的方法,也可以有被abstract修饰的方法(抽象方法),因为存在抽象方法,所以该类必须是抽象类。但是接口要求只能包含抽象方法,抽象方法是指没有实现的方法。所以就不能像抽象类那么无赖了,接口就根本不能存在方法的实现。实现 抽象类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现。抽象类虽然不能实例化来使用,但是可以被继承,让子类来具体实现父类的所有抽象方法。

  2. 接口的实现,通过implements关键字。实现该接口的类,必须把接口中的所有方法给实现。不能再推给下一代。

  3. 抽象类可以有构造器,而接口不能有构造器

  4. 抽象方法可以有public、protected和default这些修饰符

  5. 接口方法默认修饰符是public。你不可以使用其它修饰符。

  6. 抽象类在java语言中所表示的是一种继承关系,一个子类只能存在一个父类,但是可以存在多个接口。

  7. 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 如果你往接口中添加方法,那么你必须改变实现该接口的类。

From midNightHz

1、抽象类是个类,而接口就是一个接口,一个是三个字,一个是两个字 2、抽象类只能单继承,而接口可以多实现 3、抽象类可以有成员变量(私有 共有and so on);接口只可能有常量,而且都public 4、抽象类可以有所有的方法可以用所有的修饰符来修饰 public private protected static ,可以包含0-N个抽象方法;接口的方法都是public的,JDK1.8接口允许有一个static方法和多个default方法

From Moosphan

大体区别如下:

  • 抽象类可以提供成员方法的实现细节,而接口中只能存在 public 抽象方法;
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的;
  • 接口中不能含有构造器、静态代码块以及静态方法,而抽象类可以有构造器、静态代码块和静态方法;
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口;
  • 抽象类访问速度比接口速度要快,因为接口需要时间去寻找在类中具体实现的方法;
  • 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。如果你往接口中添加方法,那么你必须改变实现该接口的类;
  • 此外,Java 8 之后的接口可以有默认方法实现,通过 default 关键字表明即可。

From safier

抽象类是用来捕捉子类的通用特性的 。它不能被实例化,只能被用作子类的超类。抽象类是被用来创建继承层级里子类的模板。以JDK中的GenericServlet为例:

public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
    // abstract method
    abstract void service(ServletRequest req, ServletResponse res);
 
    void init() {
        // Its implementation
    }
    // other method related to Servlet
}

当HttpServlet类继承GenericServlet时,它提供了service方法的实现:

public class HttpServlet extends GenericServlet {
    void service(ServletRequest req, ServletResponse res) {
        // implementation
    }
 
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        // Implementation
    }
 
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
        // Implementation
    }
 
    // some other methods related to HttpServlet
}

接口

接口是抽象方法的集合。如果一个类实现了某个接口,那么它就继承了这个接口的抽象方法。这就像契约模式,如果实现了这个接口,那么就必须确保使用这些方法。接口只是一种形式,接口自身不能做任何事情。以Externalizable接口为例:

public interface Externalizable extends Serializable {
 
    void writeExternal(ObjectOutput out) throws IOException;
 
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

当你实现这个接口时,你就需要实现上面的两个方法:

public class Employee implements Externalizable {
 
    int employeeId;
    String employeeName;
 
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        employeeId = in.readInt();
        employeeName = (String) in.readObject();
 
    }
 
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
 
        out.writeInt(employeeId);
        out.writeObject(employeeName);
    }
}

抽象类和接口的对比

参数 抽象类 接口
默认的方法实现 它可以有默认的方法实现 接口完全是抽象的。它根本不存在方法的实现
实现 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现
构造器 抽象类可以有构造器 接口不能有构造器
与正常Java类的区别 除了你不能实例化抽象类之外,它和普通Java类没有任何区别 接口是完全不同的类型
访问修饰符 抽象方法可以有public、protected和default这些修饰符 接口方法默认修饰符是public。你不可以使用其它修饰符。
main方法 抽象方法可以有main方法并且我们可以运行它 接口没有main方法,因此我们不能运行它
多继承 抽象方法可以继承一个类和实现多个接口 接口只可以继承一个或多个其它接口
速度 它比接口速度要快 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。
添加新方法 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 如果你往接口中添加方法,那么你必须改变实现该接口的类。

什么时候使用抽象类和接口

  • 如果你拥有一些方法并且想让它们中的一些有默认实现,那么使用抽象类吧。
  • 如果你想实现多重继承,那么你必须使用接口。由于Java不支持多继承,子类不能够继承多个类,但可以实现多个接口。因此你就可以使用接口来解决它。
  • 如果基本功能在不断改变,那么就需要使用抽象类。如果不断改变基本功能并且使用接口,那么就需要改变所有实现了该接口的类。

Java8中的默认方法和静态方法

Oracle已经开始尝试向接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。现在,我们可以为接口提供默认实现的方法了并且不用强制子类来实现它。

谈谈 Java 中多线程实现的几种方式

Java中多线程实现的方式主要有三种:

  1. 继承Thread类
  2. 实现Runnable接口
  3. 使用ExecutorService、Callable、Future实现有返回结果的多线程

其中前两种方式线程执行完没有返回值,只有最后一种是带返回值的。

继承Thread类实现多线程:

继承Thread类本质上也是实现Tunnable接口的一个实例,他代表一个线程的实例,并且启动线程的唯一方法是通过Thread类的start()方法,start()方法是一个native方法,他将启动一个新线程,并执行run( )方法。

实现Runnable接口方式实现多线程:

实例化一个Thread对象,并传入实现的Runnable接口,当传入一个Runnable target参数给Thread后,Thraed的run()方法就会调用target.run( );

使用ExecutorService、Callable、Future实现有返回结果的多线程:

可返回值的任务必须实现Callable接口,类似的无返回值的任务必须实现Runnable接口,执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,在结合线程池接口ExecutorService就可以实现有返回结果的多线程。

继承 Thread 类本身

public class Test {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();
    }
}

class MyThread extends Thread {
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

#实现Runnable接口 *用法需要在外层包裹一层 Thread *

public class Test {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();
        new Thread(mr).start();
    }
}

class MyRunnable implements Runnable {
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

#实现 Callable 接口 比较少见,不常用 需要实现的是 call() 方法

代码拷过来的,确实没用过

public class Test {
    public static void main(String[] args) throws Exception {
        ExecutorService es = Executors.newSingleThreadExecutor();

        // 自动在一个新的线程上启动 MyCallable,执行 call 方法
        Future<Integer> f = es.submit(new MyCallable());

        // 当前 main 线程阻塞,直至 future 得到值
        System.out.println(f.get());

        es.shutdown();
    }
}

class MyCallable implements Callable<Integer> {
    public Integer call() {
        System.out.println(Thread.currentThread().getName());

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return 123;
    }
}

String,StringBuilder,StringBuffer的区别

  1. 可变不可变 String:字符串常量,在修改时不会改变自身;若修改,等于重新生成新的字符串对象。 StringBuffer:在修改时会改变对象自身,每次操作都是对 StringBuffer 对象本身进行修改,不是生成新的对 象;使用场景:对字符串经常改变情况下,主要方法:append(),insert()等。
  2. 线程是否安全 String:对象定义后不可变,线程安全。 StringBuffer:是线程安全的(对调用方法加入同步锁),执行效率较慢,适用于多线程下操作字符串缓冲区 大量数据。 StringBuilder:是线程不安全的,适用于单线程下操作字符串缓冲区大量数据。
  3. 共同点 StringBuilder 与 StringBuffer 有公共父类 AbstractStringBuilder(抽象类)。 StringBuilder、StringBuffer 的方法都会调用 AbstractStringBuilder 中的公共方法,如 super.append(...)。 只是 StringBuffer 会在方法上加 synchronized 关键字,进行同步。最后,如果程序不是多线程的,那么使用 StringBuilder 效率高于 StringBuffer。

HashMap和Hashtable的区别

参考答案
  1. HashMap是map接口的子类,是将键映射到值的对象,其中键和值都是对象,并且不能包含重复键,但可以包含重复值。HashMap允许null key和null value,而hashtable不允许。

  2. HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,由于非线程安全,效率上可能高于Hashtable。

  3. HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。

  4. Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。

  5. Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。但是如果使用Java 5或以上的话,可以用ConcurrentHashMap代替Hashtable。

  6. Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差。

谈谈你对java三大特性的理解

封装

封装最好理解了。封装是面向对象的特征之一,是对象和类概念的主要特性。

封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

继承

面向对象编程 (OOP) 语言的一个主要功能就是“继承”。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

通过继承创建的新类称为“子类”或“派生类”。

被继承的类称为“基类”、“父类”或“超类”。

继承的过程,就是从一般到特殊的过程。

多态

多态性(polymorphisn)是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。

实现多态,有二种方式,覆盖,重载。

覆盖,是指子类重新定义父类的虚函数的做法。

重载,是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。

JVM

谈谈JVM的内存结构和内存分配

Java内存模型 Java虚拟机将其管辖的内存大致分三个逻辑部分:方法区(Method Area)、Java栈和Java堆。

方法区是静态分配的,编译器将变量绑定在某个存储位置上,而且这些绑定不会在运行时改变。

Java Stack是一个逻辑概念,特点是后进先出。一个栈的空间可能是连续的,也可能是不连续的。

Java堆分配(heap allocation)意味着以随意的顺序,在运行时进行存储空间分配和收回的内存管理模型。

java内存分配 基础数据类型直接在栈空间分配; 方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收; 引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量;

方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完后从栈空间回收; 局部变量 new 出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收; 方法调用时传入的实际参数,先在栈空间分配,在方法调用完成后从栈空间释放; 字符串常量在 DATA 区域分配 ,this 在堆空间分配;

数组既在栈空间分配数组名称, 又在堆空间分配数组实际的大小!

From BelieveFrank

内存结构
程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

本方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

Java堆

对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。(栈上分配、标量替换会导致对象不分配在堆内存中) Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。 根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

内存分配策略
对象优先在Eden分配

大多数情况下,对象在新生代Eden去中分配,(注:java堆中的新生代可分为Eden区和两个Survivor区),当Eden区中没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

Minor GC 和 Full GC的区别

  • 新生代GC(Minor GC):指的是发生在新生代中的垃圾收集动作,java对象的创建和回收非常频繁,所以Mnior GC非常频繁,一般回收速度也比较快。
  • 老年代GC(Major GC/Full FC):指发生在老年代中的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC慢10倍以上。
大对象直接进入老年代

大对象是指,需要大量连续内存空间的java对象(写程序的时候应该避免“短命大对象”),经常出现大对象,容易导致内存还有不少空间时,就提前触发垃圾收集以获取足够的连续空间来分给他们。 虚拟机提供-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接进入老年代,这么做为目的是为了避免在Eden以及两个Survivor区之间发生大量的内存复制(新生代的垃圾收集算法采用复制算法)。

长期存活的对象将进入老年代

虚拟机给每个对象定义一个对象年龄(Age)的计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1.其在Survivor中没经历一次Minior GC,Age就加1,当其Age增加到一定程度(默认15岁),就将其晋升到老年代。年龄阈值可以通过参数-XX:MxTenuringThreshold设置。

动态对象的年龄判定

为了能更好的适应不同程序的内存状况,虚拟机不是永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代。

空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代中最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试进行一次Minor GC。如果小于或者HandlePromotionFailure设置为不允许,那这时就改为一次Full GC。 分配担保解释:新生代使用复制算法完成垃圾收集,为了节约内存Survivor的设置的比较小,当Minor GC后如果还有大量对象存活,超过了一个Survivor的内存空间,这时就需要老年代进行分配担保,把Survivor中无法容纳的对象直接进入老年代。若虚拟机检查老年代中最大可用连续空间大于新生代所有对象总空间那么就能保证不需要发生Full GC,因为老年代的内存空间够用。反之,如果老年代中最大可用连续空间小于新生代所有对象总空间就需要在尝试Minor GC失败后进行Full Gc或者直接Full GC。

谈谈4种gc算法

1:标记—清除 Mark-Sweep 过程:标记可回收对象,进行清除 缺点:标记和清除效率低,清除后会产生内存碎片

2:复制算法 过程:将内存划分为相等的两块,将存活的对象复制到另一块内存,把已经使用的内存清理掉 缺点:使用的内存变为了原来的一半 进化:将一块内存按8:1的比例分为一块Eden区(80%)和两块Survivor区(10%) 每次使用Eden和一块Survivor,回收时,将存活的对象一次性复制到另一块Survivor上,如果另一块Survivor空间不足,则使用分配担保机制存入老年代

3:标记—整理 Mark—Compact 过程:所有存活的对象向一端移动,然后清除掉边界以外的内存

4:分代收集算法 过程:将堆分为新生代和老年代,根据区域特点选用不同的收集算法,如果新生代朝生夕死,则采用复制算法,老年代采用标记清除,或标记整理

谈谈Java的垃圾回收机制以及触发时机

内存回收机制:就是释放掉在内存中已经没有用的对象,要判断怎样的对象是没用的,有两种方法:(1)采用标记数的方法,在给内存中的对象打上标记,对象被引用一次,计数加一,引用被释放,计数就减一,当这个计数为零时,这个对象就可以被回收,但是,此种方法,对于循环引用的对象是无法识别出来并加以回收的,(2)采用根搜索的方法,从一个根出发,搜索所有的可达对象,则剩下的对象就是可被回收的,垃圾回收是在虚拟机空闲的时候或者内存紧张的时候执行的,什么时候回收并不是由程序员控制的,可达与不可达的概念:分配对象使用new关键字,释放对象时,只需将对象的引用赋值为null,让程序不能够在访问到这个对象,则称该对象不可达。

在以下情况中垃圾回收机制会被触发: (1)所有实例都没有活动线程访问 ;(2)没有其他任何实例访问的循环引用实例;(3)Java中有不同的引用类型。判断实例是否符合垃圾收集的条件都依赖于它的引用类型。

From safier

垃圾收集算法

1. Mark-Sweep(标记-清除)算法

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:

image

从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

2. Copying(复制)算法

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

image

这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。

3. Mark-Compact(标记-整理)算法

了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

image

4. Generational Collection(分代收集)算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

  目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

  而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

  注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。