jdk5之前,switch能够作用在byte,short,char,int(实际上都是提升为int)等四个基本类型,jdk5引入了enum(也是int),jdk7引入了字符串(实际上是调用了string的hashcode,因此本质上还是int),但是不能使用long,因为switch 对应的 JVM 字节码 lookupswitch、tableswitch 指令只支持 int 类型,而long没法转换为int。
成员内部类,局部内部类,匿名内部类和静态内部类。
内部类的优点:
匿名内部类在生成字节码阶段,会把涉及的变量作为构造函数的参数,这样使得在匿名内部类中修改的数据无法传递到外部,因此不能是final。
参考资料 https://cuipengfei.me/blog/2013/06/22/why-does-it-have-to-be-final/
所有的异常类都有个共同祖先 Throwable。
Error一般是指程序无法处理的错误或者不应该处理的错误。
Exception是程序可以处理的异常。
Exception分为runtime exception和非运行时异常;或者分为受检异常和非受检异常。受检异常是指编译器必须处理的异常
Java容器分为collection和map两大类。
主要包括list*包括ArrayList,LinkedList,Vector)和set(主要有HashSet,LinkedSet和TreeSet)
这是通过modCount实现的。遍历器在遍历访问集合中的内容时,会维护modCount和expectedModCount。当modCount不等于expoectedModCount时,就会抛出异常。迭代器在调用next()、remove()方法时都是调用checkForComodification()方法来进行检测modCount是否等于expectedModCount。具体来说:在创建迭代器的时候会把对象的modCount的值传递给迭代器的expectedModCount,如果创建多个迭代器对一个集合对象进行修改的话,那么就会有一个modCount和多个expectedModCount,且modCount的值之间也会不一样,这就导致了moCount和expectedModCount的值不一致,从而产生异常。
那么为什么在下列代码中即使单线程,也会出现快速失败呢:
for( Integer i:list){
list.remove(i);
}
因为当使用for each时,会生成一个iterator来来遍历该list,同时这个list正在被iterator.remove修改。
解决办法:
简单来说就是当前容量*1.5+1,具体可以参考https://blog.csdn.net/qq_26542493/article/details/88873168
Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。
###LinkedList
LinkedList是用双向链表结构存储数据的,并且通过first和last引用分别指向链表的第一个和最后一个元素,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。
HashSet首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals方法 如果 equls结果为true ,HashSet就视为同一个元素。如果equals 为false就不是同一个元素。
HashMap中实现了一个Entry[]数组,数组的每个item是一个单项链表的结构,当我们put(key, value)的时候,HashMap首先会newItem.key.hashCode()作为该newItem在Entry[]中存储的下标,要是对应的下标的位置上没有任何item,则直接存储上去,要是已经有oldItem存储在了上面,那就会判断oldItem.key.equals(newItem.key),那么要是我们把上面的Person作为key进行存储的时候,重写了equals()方法,但没重写hashCode()方法,当我们去put()的时候,首先会通过hashCode() 计算下标,由于没有重写hashCode(),那么实质是完整的Object的hashCode(),会受到Object多个属性的影响,本来equals的两个Person对象,反而得到了两个不同的下标。
同样的,HashMap在get(key)的过程中,也是首先调用hashCode()计算item的下标,然后在对应下标的地方找,要是为null,就返回null,要是 != null,会去调用equals()方法,比较key是否一致,只有当key一致的时候,才会返回value,要是我们没有重写hashCode()方法,本来有的item,反而会找不到,返回null结果。
所以,要是你重写了equals()方法,而你的对象可能会放入到散列(HashMap,HashTable或HashSet等)中,那么还必须重写hashCode(), 如果你的对象有可能放到有序队列(实现了Comparable)中,那么还需要重写compareTo()的方法。
与HashSet和HashMap关系类似,TreeSet是基于TreeMap实现的。他采用红黑树来保存map的每一个Entry。
HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。我们用下面这张图来介绍 HashMap 的结构。
JDK7:
大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。
1. capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
2. loadFactor:负载因子,默认为 0.75(负载因子设置为0.75是为了在大量空间浪费和大量hash冲突之间取得一个平衡)。计算HashMap的实时装载因子的方法为:size/capacity
3. threshold:扩容的阈值,等于 capacity * loadFactor
4. size:size表示HashMap中存放KV的数量(为链表和树中的KV的总和)
JDK8:
Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。 根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树 (将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
HashMap是线程不安全的,其主要体现:
1.在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据覆盖。
参考资料:https://blog.csdn.net/qq_21993785/article/details/80384250
2.在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
参考资料:https://blog.csdn.net/javageektech/article/details/116101276
HashMap扩容的时候为什么是2的n次幂?
数组下标的计算方法是(n - 1) & hash,取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
HashMap的put方法:
1.根据key通过哈希算法与与运算得出数组下标
2.如果数组下标元素为空,则将key和value封装为Entry对象(JDK1.7是Entry对象,JDK1.8是Node对象)并放入该位置。
3.如果数组下标位置元素不为空,则要分情况
(i)如果是在JDK1.7,则首先会判断是否需要扩容,如果要扩容就进行扩容,如果不需要扩容就生成Entry对象,并使用头插法添加到当前链表中。
(ii)如果是在JDK1.8中,则会先判断当前位置上的TreeNode类型,看是红黑树还是链表Node
(a)如果是红黑树TreeNode,则将key和value封装为一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value。
(b)如果此位置上的Node对象是链表节点,则将key和value封装为一个Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历过程中会判断是否存在当前key,如果存在则更新其value,当遍历完链表后,将新的Node插入到链表中,插入到链表后,会看当前链表的节点个数,如果大于8,则会将链表转为红黑树
(c)将key和value封装为Node插入到链表或红黑树后,在判断是否需要扩容,如果需要扩容,就结束put方法。
HashMap源码中在计算hash值的时候为什么要右移16位?
当数组的长度很短时,只有低位数的hashcode值能参与运算。而让高16位参与运算可以更好的均匀散列,减少碰撞,进一步降低hash冲突的几率。并且使得高16位和低16位的信息都被保留了。
而在这里采用异或运算而不采用& ,| 运算的原因是 异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值的二进制会向1靠拢,采用|运算计算出来的值的二进制会向0靠拢。
计算方式:hash实际上是object.hashCode() ^ object.hashCode() >> 16。数组下标index = (table.length-1)&hash。table.length-1的目的是把结果限制在0-table.length-1里面,因为这个值通常不大,所以会丢失高位信息(因为是&操作)。
参考资料:https://www.cnblogs.com/skyvalley/p/14227702.html
Segment段:
ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了“槽”来代表一个 segment。
线程安全(Segment 继承 ReentrantLock 加锁)
简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
concurrencyLevel: 并行级别、并发数、Segment 数。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。
和HashMap一样,ConcurrentHashMap的JDK7和JDK8实现也有区别。如下图所示:
可以看到Java8中的ConcurrentHashMap和Java8的HashMap很像,那么他是怎么保证线程安全的呢。java8实现了粒度更细的加锁,去掉了segment数组,直接使用synchronized锁住hash后得到的数组下标位置中的第一个元素 ,如下图,这样加锁比segment加锁能支持更高的并发量。
Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
名称 | 倍数 | 备注 |
---|---|---|
HashMap | 2倍 | jdk1.7中,扩容后重新计算hash,1.8中,根据,是否仍然在同一个桶中判断,即e.hash()& oldCap,oldCap为旧容量,当你给定了初始容量值时,会将其扩充到2的幂,参考资料https://blog.csdn.net/lkforce/article/details/89521318 |
HashTable | 2倍+1 | 无 |
HashMap 、LinkedHashMap 的 key 和 value 都允许为 null。
而ConcurrentHashMap、ConcurrentSkipListMap、Hashtable、TreeMap 的 key 不允许为 null。
参考资料:https://blog.csdn.net/vsmybits/article/details/120496341
当需要读两个以上的I/O的时候,如果使用阻塞式的I/O,那么可能长时间的阻塞在一个描述符上面,另
外的描述符虽然有数据但是不能读出来,这样实时性不能满足要求,大概的解决方案有以下几种:
它们的区别主要有三点:
共同点 :
ThreadLocal为变量在每个线程都创建了一个副本。每个线程可以访问自己内部的副本变量。首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
threadLocaMap为什么是弱引用呢?
每个Thread内部都维护一个ThreadLocalMap字典数据结构,字典的Key值是ThreadLocal,那么当某个ThreadLocal对象不再使用(没有其它地方再引用)时,每个已经关联了此ThreadLocal的线程怎么在其内部的ThreadLocalMap里做清除此资源呢?JDK中的ThreadLocalMap又做了一次精彩的表演,它没有继承java.util.Map类,而是自己实现了一套专门用来定时清理无效资源的字典结构。其内部存储实体结构Entry
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?
其实实现的思路很简单:在ThreadLocal类中有一个static声明的Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。
小于返回<0,等于返回0,大于返回大于0
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
NIO基于Reactor。
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。如下图所示:
NIO和传统IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。
Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓冲导向方法不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
下面说明下一些名词的含义:
与NIO不同,当进行读写操作时,只需要调用API的Read和Write方法即可这两个都是异步的,完成后会调用回调函数。
主要在java.nio.channels包下增加了下面四个异步通道:
AsynchronousSocketChannel
AsynchronousServerSocketChannel
AsynchronousFileChannel
AsynchronousDatagramChannel
其中,对于AsynchronousSocketChannel而言,linux和windows实现方式并不一致,windows上通过IOCP实现,即WindowsAsynchronousSocketChannelImpl,实现接口为Iocp.OverlappedChannel;而在Linux上是UnixAsynchronousSocketChannelImpl,实现接口为Port.PollableChannel。
AIO不需要对selector进行轮询。
例如从文件中读取数据并将其通过网络传输给其他应用程序的的操作需要经历四次内核态和用户态的切换。
步骤如下:
线程池的创建方法总共有 7 种,但总体来说可分为 2 类:
一类是通过 ThreadPoolExecutor 创建的线程池;另一个类是通过 Executors 创建的线程池。
线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors 创建的,1 种是通过ThreadPoolExecutor 创建的):
Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置。
参数 | 说明 |
---|---|
corePoolSize | 核心线程数量,线程池维护线程的最少数量 |
maximumPoolSize | 线程池维护线程的最大数量 |
keepAliveTime | 线程池除核心线程外的其他线程的最长空闲时间,超过该时间的空闲线程会被销毁 |
unit | keepAliveTime的单位,TimeUnit中的几个静态属性:NANOSECONDS(纳秒)、MICROSECONDS(微秒)、MILLISECONDS(毫秒)、SECONDS(秒)、MINUTES、HOURS、DAYS |
workQueue | 线程池所使用的任务缓冲队列,有如下几种: 1. ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。2.- LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。3. SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。 4. PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。5 . DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。6. LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。 |
threadFactory | 线程工厂,用于创建线程,一般用默认的即可 |
handler | 线程池对拒绝任务的处理策略,包括如下几种:1. AbortPolicy:拒绝并抛出异常。2.CallerRunsPolicy:使用当前调用的线程来执行此任务。3. DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。4. DiscardPolicy:忽略并抛弃当前任务。 |
一般来说,推荐使用ThreadPoolExecutor创建线程池(因为Executor创建的很多线程池,Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
四者之间的关系:
Callable是Runnable封装的异步运算任务。
Future用来保存Callable异步运算的结果
FutureTask封装Future的实体类
简单来说,提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种。
什么是僵死进程:
僵死进程就是指子进程退出时,父进程并未对其发出的SIGCHLD信号进行适当处理,导致子进程停留在僵死状态等待其父进程,这个状态下的子进程就是僵死进程。这个僵死进程不占有内存,也不会执行代码,更不能被调用,他只是在进程列表中占了个地位而已。
如何结束僵死进程:
SynchronousQueue 是一个队列,但它的特别之处在于它内部没有容器。其中的一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)。SynchronousQueue的实现并不依赖AQS(AbstractQueuedSynchronizer)而是使用CAS。
SynchronousQueue的内部实现了两个类,一个是TransferStack类,使用LIFO顺序存储元素,这个类用于非公平模式;还有一个类是TransferQueue,使用FIFI顺序存储元素,这个类用于公平模式。这两个类继承自"Nonblocking Concurrent Objects with Condition Synchronization"算法,此算法是由W. N. Scherer III 和 M. L. Scott提出的,关于此算法的理论内容在这个网站中:http://www.cs.rochester.edu/u/scott/synchronization/pseudocode/duals.html。两个类的性能差不多,FIFO通常用于在竞争下支持更高的吞吐量,而LIFO在一般的应用中保证更高的线程局部性。
参考资料:
https://blog.csdn.net/yanyan19880509/article/details/52562039
https://www.jianshu.com/p/d5e2e3513ba3
https://www.jianshu.com/p/95cb570c8187
JVM包括五块数据区域:
参数 | 说明 |
---|---|
方法区 | 方法区是各个线程共享的内存区域,用于存储被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据,其中包括运行时常量池(用于存放编译器生成的各种字面量和符号引用 ) |
虚拟机栈 | 线程私有,描述的是Java方法执行的线程内存模型,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量,操作数栈,动态连接,方法出口等信息。每一个方法被调用直到执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程,其中含有局部变量表,存放Java基本数据类型,对象引用和returnAddress(指向了一条字节码指令的地址) |
本地方法栈 | 结构与虚拟机栈相似,但是是服务于本地方法的 |
程序计数器 | 线程私有,可以看成当前线程所执行的字节码的行号指示器。 |
JAVA堆 | 线程共享,此内存区域用于存放对象实例 |
直接内存 | 直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用DirectByteBuffer 对象作为这块内存的引用进行操作(详见: Java I/O 扩展), 这样就避免了在 Java堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。 |
其中,JVM堆从GC角度看看能被分为新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。如下图所示:
名词解释:
元数据的定义:
这部分参考资料:https://developers.redhat.com/blog/2018/02/14/java-class-metadata
GC过程:
新生代GC采用复制算法。称为Minor GC。
一般来说,线程模型有三种,分别是:
Hotspot JVM 后台运行的系统线程主要有下面几个:
线程名 | 功能 |
---|---|
虚拟机线程(VM thread) | 这个线程等到 JVM 到达安全点操作出现。这些操作的类型有:stop-the-world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。具体来说:The VMThread spends its time waiting for operations to appear in the VMOperationQueue, and then executing those operations. Typically these operations are passed on to the VMThread because they require that the VM reach a safepoint before they can be executed. In simple terms, when the VM is at safepoint all threads inside the VM have been blocked, and any threads executing in native code are prevented from returning to the VM while the safepoint is in progress. This means that the VM operation can be executed knowing that no thread can be in the middle of modifying the Java heap, and all threads are in a state such that their Java stacks are unchanging and can be examined.The most familiar VM operation is for garbage collection, or more specifically for the “stop-the-world” phase of garbage collection that is common to many garbage collection algorithms. But many other safepoint based VM operations exist, for example: biased locking revocation, thread stack dumps, thread suspension or stopping (i.e. The java.lang.Thread.stop() method) and numerous inspection/modification operations requested through JVMTI.Many VM operations are synchronous, that is the requestor blocks until the operation has completed, but some are asynchronous or concurrent, meaning that the requestor can proceed in parallel with the VMThread (assuming no safepoint is initiated of course).Safepoints are initiated using a cooperative, polling-based mechanism. In simple terms, every so often a thread asks “should I block for a safepoint?”. Asking this question efficiently is not so simple. One place where the question is often asked is during a thread state transition. Not all state transitions do this, for example a thread leaving the VM to go to native code, but many do. The other places where a thread asks are in compiled code when returning from a method or at certain stages during loop iteration. Threads executing interpreted code don’t usually ask the question, instead when the safepoint is requested the interpreter switches to a different dispatch table that includes the code to ask the question; when the safepoint is over, the dispatch table is switched back again. Once a safepoint has been requested, the VMThread must wait until all threads are known to be in a safepoint-safe state before proceeding to execute the VM operation. During a safepoint the Threads_lock is used to block any threads that were running, with the VMThread finally releasing the Threads_lock after the VM operation has been performed. |
周期性任务线程 | 这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。 |
GC线程 | 这些线程支持 JVM 中不同的垃圾回收活动。 |
编译器线程 | 这些线程在运行时将字节码动态编译成本地平台相关的机器码。 |
信号分发线程 | 这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。 |
此部分参考资料: https://openjdk.java.net/groups/hotspot/docs/RuntimeOverview.html
可达性分析中GC Roots对象包括下列几种:
由于目前主流Java虚拟机使用的都是准确式垃圾收集(指虚拟机可以准确知道一段值是代码还是数据),因此虚拟机并不需要一个不漏的检查完所有执行上下文和全局的位置,而是有办法直接得到哪些地方存着对象的引用。Hotspot使用OopMap来完成任务,一旦类加载完成,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来。
OopMap 记录了栈上本地变量到堆上对象的引用关系。其作用是:垃圾收集时,收集线程会对栈上的内存进行扫描,看看哪些位置存储了 Reference 类型。如果发现某个位置确实存的是 Reference 类型,就意味着它所引用的对象这一次不能被回收。但问题是,栈上的本地变量表里面只有一部分数据是 Reference 类型的(它们是我们所需要的),那些非 Reference 类型的数据对我们而言毫无用处,但我们还是不得不对整个栈全部扫描一遍,这是对时间和资源的一种浪费。
一个很自然的想法是,能不能用空间换时间,在某个时候把栈上代表引用的位置全部记录下来,这样到真正 gc 的时候就可以直接读取,而不用再一点一点的扫描了。事实上,大部分主流的虚拟机也正是这么做的,比如 HotSpot ,它使用一种叫做 OopMap 的数据结构来记录这类信息。
我们知道,一个线程意味着一个栈,一个栈由多个栈帧组成,一个栈帧对应着一个方法,一个方法里面可能有多个安全点。 gc 发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的 OopMap ,记下栈上哪些位置代表着引用。枚举根节点时,递归遍历每个栈帧的 OopMap ,通过栈中记录的被引用对象的内存地址,即可找到这些对象( GC Roots )。
可以把oopMap简单理解成是调试信息。在源代码里面每个变量都是有类型的,但是编译之后的代码就只有变量在栈上的位置了。oopMap就是一个附加的信息,告诉你栈上哪个位置本来是个什么东西。 这个信息是在JIT编译时跟机器码一起产生的。因为只有编译器知道源代码跟产生的代码的对应关系。 每个方法可能会有好几个oopMap,就是根据safepoint把一个方法的代码分成几段,每一段代码一个oopMap,作用域自然也仅限于这一段代码。 循环中引用多个对象,肯定会有多个变量,编译后占据栈上的多个位置。那这段代码的oopMap就会包含多条记录。
通过上面的解释,我们可以很清楚的看到使用 OopMap 可以避免全栈扫描,加快枚举根节点的速度。但这并不是它的全部用意。它的另外一个更根本的作用是,可以帮助 HotSpot 实现准确式 GC 。
但是随着而来的又有一个问题,就是在方法执行的过程中, 可能会导致引用关系发生变化,那么保存的OopMap就要随着变化。如果每次引用关系发生了变化都要去修改OopMap的话,这又是一件成本很高的事情。所以这里就引入了安全点的概念。
什么是安全点?OopMap的作用是为了在GC的时候,快速进行可达性分析,所以OopMap并不需要一发生改变就去更新这个映射表。只要这个更新在GC发生之前就可以了。所以OopMap只需要在预先选定的一些位置上记录变化的OopMap就行了。这些特定的点就是SafePoint(安全点)。由此也可以知道,程序并不是在所有的位置上都可以进行GC的,只有在达到这样的安全点才能暂停下来进行GC。
既然安全点决定了GC的时机,那么安全点的选择就至为重要了。安全点太少,会让GC等待的时间太长,太多会浪费性能。所以安全点的选择是以程序“是否具有让程序长时间执行的特征”为标准的,所以我们这里了解一下结果就行了。一般会在如下几个位置选择安全点:
安全点另一方面问题:如何让垃圾收集时所有线程都到达安全点:主要有两种方案:抢先式中断和主动式中断。
抢断式中断就是在GC的时候,让所有的线程都中断,如果这些线程中发现中断地方不在安全点上的,就恢复线程,让他们重新跑起来,直到跑到安全点上。
主动式中断在GC的时候,不会主动去中断线程,仅仅是设置一个标志,当程序运行到安全点时就去轮训该位置,发现该位置被设置为真时就自己中断挂起。所以轮训标志的地方是和安全点重合的,另外创建对象需要分配内存的地方也需要轮询该位置。
但是安全点也不是完美的,对于处于Sleep或者block的线程,无法到达安全点,因此引入了安全区域进行处理。
此部分参考资料:
https://blog.csdn.net/dyingstarAAA/article/details/88559806,https://my.oschina.net/u/1757225/blog/1583822
目前大部分 JVM 的 GC 对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Survivor , To Survivor ),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中,Eden:From Survivor:To Survivor =8:1:1。而老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老生代中。
目前的分代回收器中,新生代占总空间的1/3,老年代占2/3.。
实际上,并不是内存被耗空的时候才抛出OutOfMemoryException,而是JVM98%的时间都花费在内存回收,每次回收的内存小于2%满足时就抛出异常。
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),从网络中获取(Web Applet),可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类),从数据库读取(例如某些中间件服务器),从加密文件中获取(用来防止Class文件被窥探)。用户可以通过自定义的ClassLoader来完成类的加载。
对于数据类来说,其空间是Java直接在内存中动态构造的,但是类加载器还是得加载其中的元素类型。其遵循一下规律。
这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证主要包括:
准备阶段是正式为类变量(被static修饰的变量)分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用主要有CONSTANT_Class_info, Constant_Fieldref_Info,Constant_Methodref_Info等类型的常量。
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
此部分参考资料:https://www.cnblogs.com/shinubi/articles/6116993.html
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。类初始化主要执行类构造器
以下几种情况不会进行初始化:
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。 采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
双亲委派模型的好处:
一些特点:
1.全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
2.父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时,才使用本类加载器从自己的类路径中加载该类。
3.缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
类加载完成后会在java堆中划分区域分配给对象。分配包含两种方式:
选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所 采用的垃圾收集器是否带有压缩整理功能决定。
那怎么解决内存分配的并发问题呢?
一方面对分配内存空间的行为进行同步处理(采用CAS+失败重试来保证同步);
另一方面给每个进程在java堆中预分配一小块空间(TLAB,Thread Local Allocation Buffer),对象现在tlab上分配空间而TLAB的分配才需要用到同步锁。
从分区角度看,内存一般在eden区分配,如果空间不够进行一次minor GC,如果还不够则启用担保机制在老年代分配。特别的,对于大对象,直接进入老年代。
目前有两种方式:句柄访问和直接指针。
句柄访问是指:Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据(实例池)与对象类型数据(方法区)各自的具体地址信息。这种方法的优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
直接指针:如果使用直接指针访问,引用中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot中采用的就是这种方式。
JVM调优可以考虑在以下几个方面进行:
线程池:解决用户响应时间长的问题
连接池
JVM启动参数:调整各代的内存比例和垃圾回收算法,提高吞吐量
程序算法:改进程序逻辑算法提高性能
在GC调优之前,我们需要记住下面的原则:
GC优化的目的有两个:
为了达到上面的目的,一般地,你需要做的事情有: