Java中的自增是线程安全的吗,如何实现线程安全的自增

  • 自增会带来线程安全问题吗?为什么?
    是线程不安全的
    1.i++在字节码层面分为三步:保存当前值,执行添加操作,更新新值
    2.多线程操作时,可能会同时获取到旧值(假设为1),添加操作后为2,第一个线程刷新新值为3,第二个刷新还是3。

  • volatile可以保证线程安全吗?
    不可以!
    volatile只能保证可见性,以及顺序性
    但是不能保证多个线程同时操作

  • 如何保证线程安全?
    1.增加synchronized进行线程同步
    2.使用lock、unlock处理Reetrantent 锁进行锁定
    3.使用JVM封装类AtomicInteger
    AtomicInteger >>> Unsafe >>> cas >>> aba
    首先说明,此处 AtomicInteger,一个提供原子操作的 Integer 的类,常见的还有AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他们的实现原理相同,区别在与运算对象类型的不同。令人兴奋地,还可以通过 AtomicReference将一个对象的所有操作转化成原子操作。

    我们知道,在多线程程序中,诸如+i 或 i++等运算不具有原子性,是不安全的线程操作之一。通常我们会使用 synchronized 将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger 的性能是 ReentantLock 的好几倍。

Jdk1.8中的stream有用过吗,详述一下stream的并行操作原理,stream并行的线程池是从哪里来的

Stream作为Java8的一大亮点,它与java.io包里的InputStream和OutputStream是完全不同的概念。它是对容器对象功能的增强,它专注于对容器对象进行各种非常便利、高效的聚合操作或者大批量数据操作。

Stream API借助于同样新出现的Lambda表达式,极大的提高编程效率和程序可读性。同时,它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用fork/join并行方式来拆分任务和加速处理过程。所以说,Java8中首次出现的 java.util.stream是一个函数式语言+多核时代综合影响的产物。

Stream有如下三个操作步骤:

一、创建Stream:从一个数据源,如集合、数组中获取流。

二、中间操作:一个操作的中间链,对数据源的数据进行操作。

三、终止操作:一个终止操作,执行中间操作链,并产生结果。

  • Stream流的入门
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class StreamDemo1 {
    public static void main(String[]args){
    //外部迭代
    int [] nums = {1,2,3};
    int sum = 0;
    for (int num : nums) {
    sum += num;
    }
    System.out.println("结果是:" + sum);

    //使用Strem进行内部迭代
    int sum2 = IntStream.of(nums).map( i -> i *2).sum();
    System.out.println("结果为:" + sum2);

    System.out.println("惰性求值是终止没有调用的情况下,中间的操作不会执行");

    IntStream.of(nums).map(StreamDemo1::doubNum);
    }

    public static int doubNum(int i){
    System.out.println("执行了乘以二");
    return i * 2;
    }
    }
  • Stream流的创建
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class StremDemo2 {
    public static void main(String[]args){
    List<String> list = new ArrayList<>();

    //从集合中创建
    list.stream();
    list.parallelStream();

    //从数组中创建
    Arrays.stream(new int [] {1,2,3});

    //使用rondom创建无线流
    new Random().ints().limit(10);

    //创建数字流
    IntStream.of(1,2,3);
    IntStream.rangeClosed(1,10);
    Random r = new Random();

    //自己产生流
    Stream.generate(()-> r.nextInt()).limit(20).forEach(System.out::println);
    }
    }
  • Strem流中常用的几个方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class StreamDemo3 {
    public static void main(String[]args){
    String str = "i want to be a software enginner";

    Stream.of(str.split(" ")).map( s -> s.length()).forEach(System.out::println);

    Stream.of(str.split(" ")).filter(s -> s.contains("a")).map(s -> s.length()).forEach(System.out::println);

    Stream.of(str.split(" ")).flatMap(s -> str.chars().boxed()).forEach(i -> System.out.println((char)i.intValue()));

    Stream.of(str.split(" ")).peek(System.out::println).forEach(System.out::println);

    //limit 使用 ,主要用于无线流
    new Random().ints().filter(i -> i > 100 && i < 1000).limit(10).forEach(System.out::println) ;
    }
    }
    }
  • 流的操作二
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    public class StreamDemo4 {
    public static void main(String[]args){
    String str = "hello lambda hello";

    //使用并行流
    str.chars().parallel().forEach( s -> System.out.println((char)s));
    //保证顺序
    str.chars().parallel().forEachOrdered(i -> System.out.println((char)i));

    //收集器
    List<String> list = Stream.of(str.split(" ")).collect(Collectors.toList());
    System.out.println("这是一个list:" + list);

    //使用reduce拼接字符串
    Optional<String> letters = Stream.of(str.split(" ")).reduce((s1,s2) -> s1 + "|" + s2);

    System.out.println(letters.orElse(""));

    //计算所有单词总长度
    Integer lent = Stream.of(str.split(" ")).map(s -> s.length()).reduce(0,(s1,s2) -> s1 + s2);

    System.out.println("单词的总长度是:" + lent);

    //max
    Optional<String> max = Stream.of(str.split(" ")).max((s1,s2) -> s1.length() - s2.length());
    System.out.println("单词最长的是:" + max.get());

    OptionalInt findFirst = new Random().ints().findFirst();
    System.out.println("短路操作" + findFirst.getAsInt());
    }
  • 串行流和并行流以及线程池
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
       public class StreamDemo5 {
    public static void main(String[]args){

    IntStream.range(1,100).parallel().peek(StreamDemo5::debug).count();

    //parallel 并行流
    IntStream.range(1,100).parallel().peek(StreamDemo5::debug).
    //sequential串行流
    sequential().peek(StreamDemo5::debug2).count();

    ForkJoinPool pool = new ForkJoinPool(20);
    pool.submit(() -> IntStream.range(1,100).parallel().peek(StreamDemo5::debug).count());
    pool.shutdown();

    synchronized (pool){
    try {
    pool.wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

    public static void debug(int i){
    System.out.println("debug" + i);
    System.out.println("线程" + Thread.currentThread().getName() + ":" + i);
    try {
    TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }

    public static void debug2(int i){
    System.err.println("debug" + i);
    try {
    TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

请聊一下java的集合类,以及在实际项目中你是如何用的

参照java集合一章

注意说出集合体系,常用类 接口 实现类

加上你所知道的高并发集合类,JUC 参照集合增强内容

在实际项目中引用, 照实说就好了

问集合的引子… …


集合类型主要有3种:set(集)、list(列表)和map(映射)。

集合接口分为:Collection和Map,list、set实现了Collection接口

  • List总结:

可以重复,通过索引取出加入数据,顺序与插入顺序一致,可以含有null元素

ArrayList:底层数据结构是数组结构array,查询速度快,增删改慢,因为是一种类似数组的形式进行存储,因此它的随机访问速度极快;

Vector:底层是数组结构array,与ArrayList相同,查询速度快,增删改慢;

LinkedList:底层使用链表结构,增删速度快,查询稍慢;

ArrayList与Vector的区别:

1.如果集合中的元素数量大于当前集合数组的长度时,Vector的增长率是目前数组长度的100%,而ArryaList增长率为目前数组长度的50%。所以,如果集合中使用数据量比较大的数据,用Vector有一定优势

2.线程同步ArrayList是线程不同步,所以Vector线程安全,但是因为每个方法都加上了synchronized,所以在效率上小于ArrayList

  • Set总结:
    数据无序且唯一,实现类都不是线程安全的类,解决方案:Set set = Collections.sysnchronizedSet(Set对象);

HashSet:是Set接口(Set接口是继承了Collection接口的)最常用的实现类,顾名思义,底层是用了哈希表(散列/hash)算法。其底层其实也是一个数组,存在的意义是提供查询速度,插入的速度也是比较快,但是适用于少量数据的插入操作。

LinkedHashSet:继承了HashSet类,所以它的底层用的也是哈希表的数据结构,但因为保持数据的先后添加顺序,所以又加了链表结构,但因为多加了一种数据结构,所以效率较低,不建议使用,如果要求一个集合急要保证元素不重复,也需要记录元素的先后添加顺序,才选择使用LinkedHashSet

TreeSet:Set接口的实现类,也拥有set接口的一般特性,但是不同的是他也实现了SortSet接口,它底层采用的是红黑树算法(红黑树就是满足一下红黑性质的二叉搜索树:①每个节点是黑色或者红色②根节点是黑色的③每个叶子结点是黑色的④如果一个节点是红色的,那么他的两个子节点是黑色的⑤对每个节点,从该节点到其所有的后代叶子结点的简单路径上,仅包含相同数目的黑色结点,红黑树是许多“平衡”搜索树的一种,可以保证在最坏情况下的基本操作集合的时间复杂度为O(lgn)。

  • Map总结:
    java的Map(映射)是一种把键对象和值对象进行映射的集合,其中每一个元素都包含了键对象和值对象,其中值对象也可以是Map类型的数据,因此,Map支持多级映射,Map中的键是唯一的,但值可以不唯一,Map集合有两种实现,一种是利用哈希表来完成的叫做HashMap,它和HashSet都是利用哈希表来完成的,区别其实就是在哈希表的每个桶中,HashSet只有key,而HashMap在每个key上挂了一个value;另一种就是TreeMap,它实现了SortMap接口,也就是使用了红黑树的数据结构,和TreeSet一样也能实现自然排序和客户化排序两种排序方式,而哈希表不提供排序。

HashMap:哈希表的实现原理中,先采用一个数组表示位桶,每个位桶的实现在1.8之前都是使用链表,但当每个位桶的数据较多的时候,链表查询的效率就会不高,因此在1.8之后,当位桶的数据超过阈值(8)的时候,就会采用红黑树来存储该位桶的数据(在阈值之前还是使用链表来进行存储),所以,哈希表的实现包括数组+链表+红黑树,在使用哈希表的集合中我们都认为他们的增删改查操作的时间复杂度都是O(1)的,不过常数项很大,因为哈希函数在进行计算的代价比较高,HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,即null value和null key。,但是将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销和HashMap 的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者load factor过低。

TreeMap:TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。TreeMap 实现了Cloneable接口,意味着它能被克隆。TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。

TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。

HashTable:Hashtable继承Map接口,实现一个key-value映射的哈希表。任何非空(non-null)的对象都可作为key或者value,线程安全。

Hashmap为什么要使用红黑树?

在jdk1.8版本后,java对HashMap做了改进,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度

红黑树虽然本质上是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。加快检索速率。

集合类是怎么解决高并发中的问题?

思路 先说一下那些是非安全

​ 普通的安全的集合类

​ JUC中高并发的集合类

线程非安全的集合类 ArrayList LinkedList HashSet TreeSet HashMap TreeMap 实际开发中我们自己用这样的集合最多,因为一般我们自己写的业务代码中,不太涉及到多线程共享同一个集合的问题

线程安全的集合类 Vector HashTable 虽然效率没有JUC中的高性能集合高,但是也能够适应大部分环境

高性能线程安全的集合类

 1.ConcurrentHashMap

 2.ConcurrentHashMap和HashTable的区别

 3.ConcurrentHashMap线程安全的具体实现方式/底层具体实现

 4.说说CopyOnWriteArrayList

ConcurrentHashMap 在JDK1.7的时候,ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争 JDK1.8 ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N))) synchronized只锁定当前链表或红黑二叉树的首节点,这样只 要hash不冲突,就不会产生并发,效率又提升N倍。

ConcurrentSkipListMap是线程安全的有序的哈希表(相当于线程安全的TreeMap); 它继承于AbstractMap类,并且实现ConcurrentNavigableMap接口。ConcurrentSkipListMap是通过“跳表”来实现的

ConcurrentSkipListSet是线程安全的有序的集合(相当于线程安全的TreeSet)它继承于AbstractSet,并实现了NavigableSet接口。ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,它也支持并发。

CopyOnWriteArraySet addIfAbsent和 CopyOnWriteArrayList(写入并复制)也是juc里面的,它解决了并发修改异常,每当有写入的时候,就在底层重新复制一个新容器写入,最后把新容器的引用地址赋给旧的容器,在别人写入的时候,其他线程读数据,依然是旧容器的线程。这样是开销很大的,所以不适合频繁写入的操作。适合并发迭代操作多的场景。只能保证数据的最终一致性

简述一下自定义异常的应用场景?

借助异常机制,我们可以省略很多业务逻辑上的判断处理,直接借助java的异常机制可以简化业务逻辑判断代码的编写

1当你不想把你的错误直接暴露给前端或者你想让前端从业务角度判断后台的异常,这个时候自定义异常类是你的不二选择

2 虽然JAVA给我们提供了丰富的异常类型,但是在实际的业务上,还有很多情况JAVA提供的异常类型不能准确的表述出我们业务上的含义

3 控制项目的后期服务 … …

描述一下Object类中常用的方法

Object类中的toString()方法

1.object 默认方法toString方法,toString() 输出一个对象的地址字符串(哈希code码)!
2.可以通过重写toString方法,获取对象的属性!

Object类中的equals()方法

1.Object类equals()比较的是对象的引用是否指向同一块内存地址!
2.重写equals()方法比较俩对象的属性值是否相同

Object()

默认构造方法

clone()

创建并返回此对象的一个副本。

finalize()

当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。

getClass()

返回一个对象的运行时类。

hashCode()

返回该对象的哈希码值。

为什么wait notify会放在Object里边?

wait(),notify(),notifyAll()用来操作线程为什么定义在Object类中?
1、这些方法存在于同步中;
2、使用这些方法必须标识同步所属的锁;
3、锁可以是任意对象,所以任意对象调用方法一定定义在Object类中。

wait(),sleep()区别?

wait():释放资源,释放锁
sleep():释放资源,不释放锁

notify()

唤醒在此对象监视器上等待的单个线程。

notifyAll()

唤醒在此对象监视器上等待的所有线程。

wait()

导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。

wait(long timeout)

导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。

wait(long timeout, int nanos)

导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。

1.8的新特性有了解过吗? (注意了解其他版本新特征) +JDK更新认识

· Lambda表达式

· 函数式接口 函数式编程

· 方法引用和构造器调用

· Stream API

· 接口中的默认方法和静态方法

· 新时间日期API

新的日期类

属性 含义
Instant 代表的是时间戳
LocalDate 代表日期,比如2020-01-14
LocalTime 代表时刻,比如12:59:59
LocalDateTime 代表具体时间 2020-01-12 12:22:26
ZonedDateTime 代表一个包含时区的完整的日期时间,偏移量是以UTC/ 格林威治时间为基准的
Period 代表时间段
ZoneOffset 代表时区偏移量,比如:+8:00
Clock 代表时钟,比如获取目前美国纽约的时间

简述一下Java面向对象的基本特征,继承、封装与多态,以及你自己的应用?

知识参照面向对象章节

注意单独解释 继承 封装 多态的概念

继承 基本概念解释 后面多态的条件

封装 基本概念解释 隐藏实现细节,公开使用方式

多态 基本概念解释 就是处理参数 提接口 打破单继承

设计模式 设计原则

Java中重写和重载的区别?

联系: 名字相似 都是多个同名方法

重载 在同一个类之中发生的

重写 继承中,子类重写父类方法

1 目的差别

2 语法差别

怎样声明一个类不会被继承,什么场景下会用?

final修饰的类不能有子类 大部分都是出于安全考虑

String举例

什么是ForkJoin框架 适用场景

虽然目前处理器核心数已经发展到很大数目,但是按任务并发处理并不能完全充分的利用处理器资源,因为一般的应用程序没有那么多的并发处理任务。基于这种现状,考虑把一个任务拆分成多个单元,每个单元分别得到执行,最后合并每个单元的结果。

Fork/Join框架是JAVA7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干小任务,最终汇总每个小任务结果得到大任务结果的框架。

img

img

2. 工作窃取算法(work-stealing)

一个大任务拆分成多个小任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列中,并且每个队列都有单独的线程来执行队列里的任务,线程和队列一一对应。

但是会出现这样一种情况:A线程处理完了自己队列的任务,B线程的队列里还有很多任务要处理。

A是一个很热情的线程,想过去帮忙,但是如果两个线程访问同一个队列,会产生竞争,所以A想了一个办法,从双端队列的尾部拿任务执行。而B线程永远是从双端队列的头部拿任务执行。

img

注意:线程池中的每个线程都有自己的工作队列(PS,这一点和ThreadPoolExecutor不同,ThreadPoolExecutor是所有线程公用一个工作队列,所有线程都从这个工作队列中取任务),当自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源。

工作窃取算法的优点:

​ 利用了线程进行并行计算,减少了线程间的竞争。

工作窃取算法的缺点:

​ 任务争夺问题

Java种的代理有几种实现方式?

动态代理

JDK >>> Proxy

​ 1 面向接口的动态代理 代理一个对象去增强面向某个接口中定义的方法

​ 2 没有接口不可用

​ 3 只能读取到接口上的一些注解

MyBatis

DeptMapper dm=sqlSession.getMapper(DeptMapper.class)

第三方 CGlib

​ 1 面向父类的动态代理

​ 2 有没有接口都可以使用

​ 3 可以读取类上的注解

​ AOP 日志 性能检测 事务

MyBatis 源码 spring源码

happens-before规则

​ 先行发生原则(Happens-Before)是判断数据是否存在竞争、线程是否安全的主要依据。
​ 先行发生是Java内存,模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,那么操作

A产生的影响能够被操作B观察到。

口诀:如果两个操作之间具有happen-before关系,那么前一个操作的结果就会对后面的一个操作可
见。是Java内存模型中定义的两个操作之间的偏序关系。

常见的happen-before规则:

1.程序顺序规则:

一个线程中的每个操作,happen-before在该线程中的任意后续操作。(注解:如果只有一个线程的操
作,那么前一个操作的结果肯定会对后续的操作可见。)
程序顺序规则中所说的每个操作happen-before于该线程中的任意后续操作并不是说前一个操作必须要
在后一个操作之前执行,而是指前一个操作的执行结果必须对后一个操作可见,如果不满足这个要求那就不允许这两个操作进行重排序

2.锁规则:

对一个锁的解锁,happen-before在随后对这个锁加锁。(注解:这个最常见的就是synchronized方法和
syncronized块)

3.volatile变量规则:

对一个volatile域的写,happen-before在任意后续对这个volatile域的读。该规则在CurrentHashMap
的读操作中不需要加锁有很好的体现。

4.传递性:

如果A happen-before B,且B happen-before C,那么A happen - before C.

5.线程启动规则:

Thread对象的start()方法happen-before此线程的每一个动作。

6.线程终止规则:

线程的所有操作都happen-before对此线程的终止检测,可以通过Thread.join()方法结束,
Thread.isAlive()的返回值等手段检测到线程已经终止执行。

7.线程中断规则:

对线程interrupt()方法的调用happen-before发生于被中断线程的代码检测到中断时事件的发生。

jvm监控系统是通过jmx做的么?

​ 一般都是,但是要是记录比较详细的性能定位指标,都会导致进入 safepoint,从而降低了线上应用性

​ 例如 jstack,jmap打印堆栈,打印内存使用情况,都会让 jvm 进入safepoint,才能获取线程稳定状态
从而采集信息。
​ 同时,JMX暴露向外的接口采集信息,例如使用jvisualvm,还会涉及rpc和网络消耗,以及JVM忙时,无
法采集到信息从而有指标断点。这些都是基于 JMX 的外部监控很难解决的问题。
​ 所以,推荐使用JVM内部采集 JFR,这样即使在JVM很忙时,也能采集到有用的信息

jvm有哪些垃圾回收器

img

图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用。虚
拟机所处的区域则表示它是属于新生代还是老年代收集器。
新生代收集器(全部的都是复制算法):Serial、ParNew、Parallel Scavenge
老年代收集器:CMS(标记-清理)、Serial Old(标记-整理)、Parallel Old(标记整理)
整堆收集器: G1(一个Region中是标记-清除算法,2个Region之间是复制算法)
同时,先解释几个名词:

1,并行(Parallel):多个垃圾收集线程并行工作,此时用户线程处于等待状态

2,并发(Concurrent):用户线程和垃圾收集线程同时执行

3,吞吐量:运行用户代码时间/(运行用户代码时间+垃圾回收时间)

1.Serial收集器是最基本的、发展历史最悠久的收集器。

特点:单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器
由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收
时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
应用场景:适用于Client模式下的虚拟机。

Serial / Serial Old收集器运行示意图

img

2.ParNew收集器其实就是Serial收集器的多线程版本。

除了使用多线程外其余行为均和Serial收集器一模一样(参数控制、收集算法、Stop The World、对象
分配规则、回收策略等)。
特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以
使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
和Serial收集器一样存在Stop The World问题
应用场景:ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了
Serial收集器外,唯一一个能与CMS收集器配合工作的。
ParNew/Serial Old组合收集器运行示意图如下:

img

3.Parallel Scavenge 收集器与吞吐量关系密切,故也称为吞吐量优先收集器。

特点:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类
似)。
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与
ParNew收集器最重要的一个区别)
GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不
需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代
的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动
态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

Parallel Scavenge收集器使用两个参数控制吞吐量:

​ XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
​ XX:GCRatio 直接设置吞吐量的大小。

4.Serial Old是Serial收集器的老年代版本。

特点:同样是单线程收集器,采用标记-整理算法。
应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。
Server模式下主要的两大用途(在后续中详细讲解···):

  1. 在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用。
  2. 作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。
    Serial / Serial Old收集器工作过程图(Serial收集器图示相同):

img

5.Parallel Old是Parallel Scavenge收集器的老年代版本。

特点:多线程,采用标记-整理算法。
应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收
集器。
Parallel Scavenge/Parallel Old收集器工作过程图:

6.CMS收集器是一种以获取最短回收停顿时间为目标的收集器。

特点:基于标记-清除算法实现。并发收集、低停顿。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如
web程序、b/s服务。
CMS收集器的运行过程分为下列4步:
初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记
录。仍然存在Stop The World问题。
并发清除:对标记的对象进行清除回收。
CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器的工作过程图:

img

CMS收集器的缺点:

​ 对CPU资源非常敏感。

​ 无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生。
​ 因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发
一次Full GC。

img

7.G1收集器一款面向服务端应用的垃圾收集器。

特点如下:
并行与并发:

​ G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿
时间。部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序
继续运行。

​ 分代收集:

​ G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果

​ 空间整合:

​ G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。

​ 可预测的停顿:

            G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长

度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。

G1收集器运行示意图:

img

关于gc的选择

​ 除非应用程序有非常严格的暂停时间要求,否则请先运行应用程序并允许VM选择收集器(如果没有特别
要求。使用VM提供给的默认GC就好)。
​ 如有必要,调整堆大小以提高性能。 如果性能仍然不能满足目标,请使用以下准则作为选择收集器的起
点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	如果应用程序的数据集较小(最大约100 MB),则选择带有选项-XX:+ UseSerialGC的串行
收集器。
如果应用程序将在单个处理器上运行,并且没有暂停时间要求,则选择带有选项-XX:+
UseSerialGC的串行收集器。
如果(a)峰值应用程序性能是第一要务,并且(b)没有暂停时间要求或可接受一秒或更长
时间的暂停,则让VM选择收集器或使用-XX:+ UseParallelGC选择并行收集器 。
如果响应时间比整体吞吐量更重要,并且垃圾收集暂停时间必须保持在大约一秒钟以内,则选
择具有-XX:+ UseG1GC。(值得注意的是JDK9中CMS已经被Deprecated,不可使用!移除
该选项)
如果使用的是jdk8,并且堆内存达到了16G,那么推荐使用G1收集器,来控制每次垃圾收集
的时间。
如果响应时间是高优先级,或使用的堆非常大,请使用-XX:UseZGC选择完全并发的收集
器。(值得注意的是JDK11开始可以启动ZGC,但是此时ZGC具有实验性质,在JDK15中
[202009发布]才取消实验性质的标签,可以直接显示启用,但是JDK15默认GC仍然是G1)

​ 这些准则仅提供选择收集器的起点,因为性能取决于堆的大小,应用程序维护的实时数据量以及可用处
理器的数量和速度。
​ 如果推荐的收集器没有达到所需的性能,则首先尝试调整堆和新生代大小以达到所需的目标。 如果性能
仍然不足,尝试使用其他收集器
​ 总体原则:减少STOP THE WORD时间,使用并发收集器(比如CMS+ParNew,G1)来减少暂停时间,
加快响应时间,并使用并行收集器来增加多处理器硬件上的总体吞吐量。

pc计数器

​ pc计数器是一种指针,也叫bcp(bytecode pointer)字节码指针 ,当栈帧成立,读取字节码文件时,你需要知道它读取到哪里了,pc计数器就有着这么一个作用,用来标记读取字节码的当前位置。

介绍一下垃圾回收算法

老年代和新生代

内存溢出的原因

内存溢出的原因
java.lang.OutOfMemoryError: ……java heap space….. 堆栈溢出,代码问题的可能性极大
java.lang.OutOfMemoryError: GC over head limit exceeded 系统处于高频的GC状态,而且
回收的效果依然不佳的情况,就会开始报这个错误,这种情况一般是产生了很多不可以被释放
的对象,有可能是引用使用不当导致,或申请大对象导致,但是java heap space的内存溢出
有可能提前不会报这个错误,也就是可能内存就直接不够导致,而不是高频GC.
java.lang.OutOfMemoryError: PermGen space jdk1.7之前才会出现的问题 ,原因是系统的
代码非常多或引用的第三方包非常多、或代码中使用了大量的常量、或通过intern注入常量、
或者通过动态代码加载等方法,导致常量池的膨胀
java.lang.OutOfMemoryError: Direct buffer memory 直接内存不足,因为jvm垃圾回收不
会回收掉直接内存这部分的内存,所以可能原因是直接或间接使用了ByteBuffer中的
allocateDirect方法的时候,而没有做clear
java.lang.StackOverflowError - Xss设置的太小了
java.lang.OutOfMemoryError: unable to create new native thread 堆外内存不足,无法为
线程分配内存区域
java.lang.OutOfMemoryError: request {} byte for {}out of swap 地址空间不够

线上Gc频繁

  1. 查看监控,以了解出现问题的时间点以及当前FGC的频率(可对比正常情况看频率是否正常)
  2. 了解该时间点之前有没有程序上线、基础组件升级等情况。
  3. 了解JVM的参数设置,包括:堆空间各个区域的大小设置,新生代和老年代分别采用了哪些垃
    圾收集器,然后分析JVM参数设置是否合理。
  4. 再对步骤1中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法
    比较容易排查。
  5. 针对大对象或者长生命周期对象导致的FGC,可通过 jmap -histo 命令并结合dump堆内存文
    件作进一步分析,需要先定位到可疑对象。
  6. 通过可疑对象定位到具体代码再次分析,这时候要结合GC原理和JVM参数设置,弄清楚可疑