Java面试题
Java 集合源码分析
https://www.cnblogs.com/joemsu/p/7667509.html
ArrayList
内部数组:Object[] elementData;
默认大小10,最大为整型最大值Integer.MAX_VALUE
.
private void grow(int minCapacity) {
// 记录旧的length
int oldCapacity = elementData.length;
// 扩容1.5倍, 位运算符效率更高
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 判断是否小于需求容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 判断有没有超过最大的数组大小
if (newCapacity - MAX_ARRAY_SIZE > 0)
//计算最大的容量
newCapacity = hugeCapacity(minCapacity);
// 旧数组拷贝到新的大小数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
每次扩容默认增长1.5倍,若还小于需求容量,则增长为需求容量minCapacity
.
public E remove(int index) {
// 首先检查下标是否合法
rangeCheck(index);
// 修改计数器
modCount++;
// 记录旧值,返回
E oldValue = elementData(index);
// 计算要往前移动的元素个数
int numMoved = size - index - 1;
//个数大于0,进行拷贝,从index+1开始,拷贝numMoved个,拷贝起始位置是index
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 设置为null,以便GC
elementData[--size] = null;
return oldValue;
}
删除元素时要将后面的元素全部往前复制,非常耗时。这里用的是C/C++实现的Native函数System.arraycopy
来复制数组。
LinkedList
实现了Deque & List
接口,双向链表。
transient int size = 0;
transient Node first;
transient Node last;
// 内部节点类
private static class Node {
E item;
Node next;
Node prev;
Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
AbstractList
抽象类中有个modCount
变量,用来记录List内容的修改次数,add、remove操作均会使modCount++
;在遍历List过程中,每访问一个元素之前都会先检查modCount
是否有变化,若变了则立即抛出ConcurrentModificationException
.
使用迭代器遍历可以在遍历过程中删除元素。
如果对列表有频繁的增删操作,选择LinkedList
。
HashMap
不同于之前的jdk的实现,1.8采用的是数组+链表+红黑树,在链表过长的时候可以通过转换成红黑树提升访问性能。
//默认初始化map的容量:16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//map的最大容量:2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的填充因子:0.75,能较好的平衡时间与空间的消耗
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//将链表(桶)转化成红黑树的临界值
static final int TREEIFY_THRESHOLD = 8;
//将红黑树转成链表(桶)的临界值
static final int UNTREEIFY_THRESHOLD = 6;
//转变成树的table的最小容量,小于该值则不会进行树化
static final int MIN_TREEIFY_CAPACITY = 64;
//上图所示的数组,长度总是2的幂次
transient Node[] table;
//map中的键值对集合
transient Set> entrySet;
//map中键值对的数量
transient int size;
//用于统计map修改次数的计数器,用于fail-fast抛出ConcurrentModificationException
transient int modCount;
//大于该阈值,则重新进行扩容,
// threshold = capacity(table.length) * load factor
int threshold;
//填充因子
final float loadFactor;
阈值 = 容量 * 填充因子
扩容策略:容量和阈值都乘二。
用hashcode查找位置,用equals()比较。
最核心的构造函数:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//tableSizeFor()是用来将初始化容量转化大于输入参数且最近的2的整数次幂的数,比如initialCapacity = 7,那么转化后就是8。
this.threshold = tableSizeFor(initialCapacity);
}
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
tableSizeFor()方法的目的:返回大于或等于最接近输入参数的2的整数次幂的数。
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
// 这里,(n - 1) & hash 就是取模;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//总是先检查数组下标第一个节点是否满足key,满足则返回
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
这里的取模操作用的是
(n - 1) & hash
,要保证取模的结果能够均匀分布在0~n - 1上,n必须为2的整数次幂!否则有些值是永远取不到的!所以HashMap的容量大小取的是2的整数次幂。
resize()
重新调整的时候要遍历整个数组和桶中元素(重新哈希分布),非常耗时!
下面是一些关于HashMap的特征:
- 允许key和value为null
- 基本上和Hashtable(已弃用)相似,除了非同步以及键值可以为null
- 不能保证顺序
- 访问集合的时间与map的容量和键值对的大小成比例
- 影响HashMap性能的两个变量:填充因子和初始化容量
- 通常来说,默认的填充因为0.75是一个时间和空间消耗的良好平衡。较高的填充因为减少了空间的消耗,但是增加了查找的时间
- 最好能够在创建HashMap的时候指定其容量,这样能存储效率比使其存储空间不够后自动增长更高。毕竟重新调整耗费性能
- 使用大量具有相同hashcode值的key,将降低hash表的表现,最好能实现key的comparable
- 注意hashmap是不同步的。如果要同步请使用Map m = Collections.synchronizedMap(new HashMap(...));
- 除了使用迭代器的remove方法外其的其他方式删除,都会抛出ConcurrentModificationException.
- map通常情况下都是hash桶结构,但是当桶太大的时候,会转换成红黑树,可以增加在桶太大情况下访问效率,但是大多数情况下,结构都以桶的形式存在,所以检查是否存在树节点会增加访问方法的时间
ConcurrentHashMap & 1.7/1.8 HashMap/ConcurrentHashMap 实现区别
1.7 ConcurrentHashMap结构
1.8
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。
static final class Segment extends ReentrantLock implements Serializable
Segment继承了ReentrantLock,表明每个Segment都可以当做一个锁。
get不需要加锁,put和delete需要加锁。
static class Node implements Map.Entry {
final int hash;
final K key;
volatile V val;
volatile Node next;
...
get不需要加锁是通过volatile实现的。其他线程修改value或增删节点时,对当前线程立即可见。
Doug Lea 对这个问题的回复中提到:
We leave the tradeoff of consistency-strength versus scalability
as a user decision, so offer both synchronized and concurrent versions
of most collections, as discussed in the j.u.c package docs大意是我们将“一致性强度”和“扩展性”之间的对比交给用户来权衡,所以大多数集合都提供了synchronized和concurrent两个版本。
通过他的说法,我必须纠正我一开始以为ConcurrentHashMap完全可以代替HashTable的说法,如果你的环境要求“强一致性”的话,就不能用ConcurrentHashMap了,它的get,clear方法和迭代器都是“弱一致性”的。不过真正需要“强一致性”的场景可能非常少,我们大多应用中ConcurrentHashMap是满足的。—— http://ifeve.com/java-concurrent-hashmap-2/
1.7、1.8版本ConcurrentHashMap比较
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,相对而言,总结如下思考:
- JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
- JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
- JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,我觉得有以下几点:
- 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
- JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
- 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据。
—— http://www.importnew.com/28263.html
1.7 vs 1.8
Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。ConcurrentHashMap同样。
总结
JDK6,7中的ConcurrentHashmap主要使用Segment来实现减小锁粒度,把HashMap分割成若干个Segment,在put的时候需要锁住Segment,get时候不加锁,使用volatile来保证可见性,当要统计全局时(比如size),首先会尝试多次计算modcount来确定,这几次尝试中,是否有其他线程进行了修改操作,如果没有,则直接返回size。如果有,则需要依次锁住所有的Segment来计算。
jdk7中ConcurrentHashmap中,当长度过长碰撞会很频繁,链表的增改删查操作都会消耗很长的时间,影响性能,所以jdk8 中完全重写了concurrentHashmap,代码量从原来的1000多行变成了 6000多 行,实现上也和原来的分段式存储有很大的区别。
主要设计上的变化有以下几点:
- 不采用segment而采用node,锁住node来实现减小锁粒度。
- 设计了MOVED状态 当resize的中过程中 线程2还在put数据,线程2会帮助resize。
- 使用3个CAS操作来确保node的一些操作的原子性,这种方式代替了锁。
- sizeCtl的不同值来代表不同含义,起到了控制的作用。
至于为什么JDK8中使用synchronized而不是ReentrantLock,我猜是因为JDK8中对synchronized有了足够的优化吧。
—— http://www.importnew.com/22007.html
LinkedHashMap
在HashMap的基础之上加入了双向链表,将所有元素链接起来。
// 用于指向双向链表的头部
transient LinkedHashMap.Entry head;
//用于指向双向链表的尾部
transient LinkedHashMap.Entry tail;
/**
* 用来指定LinkedHashMap的迭代顺序,
* true则表示按照基于访问的顺序来排列,意思就是最近使用的entry,放在链表的最末尾
* false则表示按照插入顺序来
*/
final boolean accessOrder;
链表的顺序可以是插入顺序或访问顺序。
注意,这里按访问顺序排序的话,最近访问的元素会被放在链表的末尾,迭代时最后访问到!
所以利用该特性,可以实现一个最近最少使用的简单缓存。
迭代时按链表顺序访问,免去了遍历数组的过程,所以会比HashMap更快。
TreeMap
红黑树实现。作为键的类必须实现Comparable
接口。
HashSet、LinkedHashSet、TreeSet
都是内部持有一个HashMap、LinkedHashMap、TreeMap
实现。其中要注意的是,value不是null,而是一个Object对象。
杂
Cloneable接口
实现Cloneable
接口,必须实现clone
方法。
但如果只是简单的调用super.clone()
的话,实现的仅仅是浅拷贝,即只会复制引用。若想实现真正的“克隆”,或者说“深拷贝”,则必须自己写复制逻辑,手动的将各个域拷贝过去。
Java反射机制
https://blog.csdn.net/sinat_38259539/article/details/71799078
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意属性和方法;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。
一般而言,开发者社群说到动态语言,大致认同的一个定义是:“程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言”。从这个观点看,Perl,Python,Ruby是动态语言,C++,Java,C#不是动态语言。
尽管在这样的定义与分类下Java不是动态语言,它却有着一个非常突出的动态相关机制:Reflection。这个字的意思是“反射、映象、倒影”,用在Java身上指的是我们可以于运行时加载、探知、使用编译期间完全未知的classes。换句话说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体、或对其fields设值、或唤起其methods。这种“看透class”的能力(the ability of the program to examine itself)被称为introspection(内省、内观、反省)。Reflection和introspection是常被并提的两个术语。
——百度百科
ThreadLocal
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap
是Thread类持有的属性:
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的 Object。也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
——百度百科内存泄露是指你的应用使用资源之后没有及时释放,导致应用内存中持有了不需要的资源,这是一种状态描述;
而内存溢出是指你的应用的内存已经不能满足正常使用了,堆栈已经达到系统设置的最大值,进而导致崩溃,这事一种结果描述;
而且通常都是由于内存泄露导致堆栈内存不断增大,从而引发内存溢出。已经无用的对象却被某个地方持有引用,导致虚拟机无法回收它们。
链接:ThreadLocal如何导致内存泄露
为什么volatile能保证可见性
如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。这样对该字段的读将直接从主内存读,对该字段的写将立即刷新至主内存。
CopyOnWrite容器
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
Java中有两个COW容器:CopyOnWriteArrayList和CopyOnWriteArraySet.
public boolean add(T e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 复制出新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 把新元素添加到新数组里
newElements[len] = e;
// 把原数组引用指向新数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}
public E get(int index) {
return get(getArray(), index);
}
写(或者说复制)的时候需要加锁,避免复制出多个版本。
JVM类加载过程
类的生命周期:加载、(验证、准备、解析)链接、初始化、使用和卸载七个阶段
- 加载阶段:通过类的全限定名取得类的二进制流,转为方法区数据结构,在Java堆中生成对应的Class对象,作为方法区这些数据的访问入口
- 验证阶段:文件格式是以0xCAFEBABE开头,版本号是否合理,元数据,字节码符号引用的验证。
- 准备阶段:类变量(static变量)赋初值(0,NULL……),常量被赋正确的值。
- 解析阶段:符号引用替换为直接引用,类或接口的解析(需要判断是否为数组),字段解析(从本类找到接口->父接口->父类->祖父类 依次查找),类方法解析(与字段差不多,但是先父类后接口 ),接口方法解析(只搜父接口)
- 初始化:执行类构造器(static{}),static变量赋值语句,子类的
调用前保证父类的 被调用
HashMap如何实现的?
在JDK1.7及以前,HashMap中维护着Entry,Entry中维护着key,value以及hash和next指针,而整个HashMap实际就是一个Entry数组
当向 HashMap 中 put 一对键值时,它会根据 key的 hashCode 值计算出一个位置, 该位置就是此对象准备往数组中存放的位置。
如果该位置没有对象存在,就将此对象直接放进数组当中;如果该位置已经有对象存在了,则顺着此存在的对象的链开始寻找(为了判断是否是否值相同,map不允许
get方法类似,通过key取hash找到数组的某个位置,然后遍历这个数组上的每个Entry,直到key值equals则返回。
如果Hash碰撞严重,那么JDK1.7中的实现性能就很差,因为每次插入都要遍历完整条链去查看key值是否重复,每次get也要遍历整个链,在JDK1.8中,由于链表的查找复杂度为O(n),而红黑树的查找复杂度为O(logn),JDK1.8中采用链表/红黑树的方式实现HashMap,达到某个阀值时,链表转成了红黑树。
ConcurrentHashMap如何保证线程安全?
ConcurrentHashMap 定义了三个原子操作,用于对指定位置的节点进行操作。正是这些原子操作保证了 ConcurrentHashMap 的线程安全。
// 获得在i位置上的Node节点
static final Node tabAt(Node[] tab, int i) {
return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
//利用CAS算法设置i位置上的Node节点
static final boolean casTabAt(Node[] tab, int i,
Node c, Node v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
//利用volatile方法设置节点位置的值
static final void setTabAt(Node[] tab, int i, Node v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
put的时候会锁住当前Node,而锁住Node之前的操作是无锁的并且也是线程安全的,建立在之前提到的3个原子操作上。
HashMap和HashTable 区别
HashMap允许key和value为null,HashTable不允许。
HashMap是非线程安全的,HashTable是线程安全的
进程间通信有哪几种方式?
- 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 命名管道 (named pipe) : 命名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
- 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
- 套接字( socket ) : 套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
JVM分为哪些区,每一个区干吗的?
虚拟机将所管理的内存分为以下几个部分:
- 方法区:用于存储已经被虚拟机加载过的类信息,常量(JDK7中String常量池被移到堆中),静态变量(JDK7中被移到Java堆),及时编译期编译后的代码(类方法)等数据。
- 堆:用来存放对象实例的
- 程序计数器:指向正在执行的字节码地址
- 虚拟机栈:存放局部变量表,操作数栈,动态链接,方法出口
- 本地方法区:Native方法服务
JVM如何GC,新生代,老年代,永久代,都存储哪些东西?
JVM通过可达性分析算法标记出哪些对象是垃圾对象,然后将垃圾对象进行回收。一般在新生代中采用复制算法,在老年代中采用标记整理算法。新生代存储了新new出的对象,老年代存储了大的对象和多次GC后仍然存在的老年对象,永久代存储了类信息,常量,静态变量,类方法。
哪些对象可作为GC Roots对象?
在 Java 语言中,可作为 GC Roots 的对象包括下面几种:
- 虚拟机栈(栈帧的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI (即一般说的 Native 方法)引用的对象
什么时候会用到String对象的intern()方法?
intern()有两个作用,第一个是将字符串字面值放入常量池(如果没有的话);第二个是返回这个常量的引用。
如果有一个字符串直到运行时才知道它的值,但该字符串又被大量使用,那么就可以通过intern()方法将其放入常量池中,来大量减少对象的创建时间。
未
TCP如何保证可靠传输?三次握手过程?
TCP和UDP区别?
滑动窗口算法?
https://my.oschina.net/hosee/blog/652410
分布式锁
- 数据库实现:利用唯一索引,加锁时就往表中插入条记录,其他线程要加锁则会唯一性约束无法成功;
缺点:- 无法阻塞(加锁失败后,需要再发一次请求再次尝试)。
- 如果服务器宕机,则无法解锁,造成死锁(可以从应用层上加定时任务,超过时间则强制解锁)
- redis作为分布式锁:
- 第一种方式是缓存锁,就是使用setnx,即只有在某个key不存在情况才能set成功该key,这样就达到了多个进程并发去set同一个key,只有一个进程能set成功。(缺点和数据库锁一样,但是redis自带过期时间EX,则不需要从应用层加定时任务,虽然redis有主从复制,由于主从复制是异步的,仍然无法保证宕机后锁丢失)
- 第二种方式是Redlock,为了解决锁丢失,提出了Redlock算法。Redlock算法假设有N个redis节点,这些节点互相独立,一般设置为N=5,这N个节点运行在不同的机器上以保持物理层面的独立。只有客户端获得了超过3个节点的锁,而且获取锁的时间小于锁的超时时间,客户端才获得了分布式锁。
- zookeeper的分布式锁:zookeeper实现锁的方式是客户端一起竞争写某条数据,比如/path/lock(路径),只有第一个客户端能写入成功,其他的客户端都会写入失败。写入成功的客户端就获得了锁,写入失败的客户端,注册watch事件,等待锁的释放,从而继续竞争该锁。
什么时候不建议建索引?
数据量较小的时候,比如几千条;索引选择性较低的时候。
JVM 空间分配担保
在
发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。
Java GC 是在什么时候?对哪些对象?干了什么?
1,Java GC的时机
Minor GC:
- Eden区满了(Survivor满不会触发GC)
Full GC:
- System.gc()方法可能会触发Full GC
- 发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。
- 新生代对象晋升到老年代,或大对象直接进入老年代时,老年代空间不足
当执行Full GC后空间仍不足,就会抛出OOM异常。
降低GC的调优策略
- 通过NewRatio调整新生代老年代的比例
- 通过MaxTenuringThreshold控制对象进入老年前生存次数
2,对哪些对象
从GC Roots出发不可达,且经过第一次标记后依然没有复活的对象。
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。1,第一次标记并进行一次筛选。
筛选的条件是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。2,第二次标记
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。
Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
3,做什么
新生代采用复制算法,将Eden区和From Survivor区存活的对象复制到To Survivor区,需要停止用户线程(Stop The World);
老年代采用标记整理(除了CMS和G1)或标记清除算法(CMS)。标记清除算法先标记出需要回收的对象,然后回收掉(标记和清除两个过程的效率都不高,并且会产生大量内存碎片);标记整理算法第一步仍然是标记需要回收的对象,但随后是将所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。