1. Java & JVM

1.1. 简介

报考岗位为 2025年 信息支援部队某单位-数据分析与管理 岗位。

1.2. 详细内容

1.2.1. JDK常见集合类源码

jdk1.8新特性:Lambda表达式、函数式接口、default 关键字、方法引用构造器调用、Stream API、Date API

jdk1.9新特性:平台级模块系统、Jshell、改进Javadoc、集合工厂方法、Http2、多版本兼容jar

ArrayList

底层是数组默认大小是10,每次达到上限,扩容1.5倍(old+old>>1),优点随机位置查找快,插入、删除随机位置数据效率低,线程不安全,Vector线程安全,底层是通过synchronized锁定数组来保证线程安全的。

LinkedList

底层是双向链表,随机位置插入删除数据性能高,查找性能差,线程不安全。

HashMap

底层数据结构是数组+链表+红黑树,默认大小是16,负载因子是0.75,当数据量达到大小*负载因子时,进行扩容,扩容2倍(size<<1,当数组大小小于Integer最大值时),然后rehash;hash算法,通过&(数组大小-1)的方式计算数组位置,效果和取模一样,效率高(每次扩容2倍的原因);减少hash碰撞的优化,将高低16位都参与计算,通过异或方式计算出来的值(充分使用全部数据位),再计算数组位置,如果位置为空,直接放置元素,如果不为空长度不大于8,结构是链表,把新元素挂在链表上,如果大于8将链表转化为红黑树,然后将数据设置到树上,红黑树是为了尽量保证树的平衡,避免出现极端情况(单链表);扩容后rehash优化,因为扩容是2倍,所以原来位置的元素调整有两种情况,还在原位,或者在oldSize+原位,当hash&old数组大小不为0时就代表不在原位;线程不安全(HashTable线程安全,通过synchronized加锁;ConcurrentHashMap线程安全,通过cas分段加锁实现)

LinkedHashMap

底层是有序链表+HashMap,map等操作都是调用HashMap的接口,重写一些方法,当调用map方法时,会调用到重写的方法,来设置有序链表,从而维护有序性。当遍历LinkedHashMap时,是按照链表的顺序进行遍历。

Set相关

底层都是对应Map,使用的是key,value放置的是New Object。

fail_fast机制

当map等操作时,修改,新增、删除操作都会自增modCount,遍历时,通过迭代器获取迭代器对象时,会把modCount赋值到迭代器中,每次获取元素时,判断该值与map等数据对像的modCount是否一致,如果不一致抛异常,因为线程不安全,通过该机制限制并发问题。

1.2.2. JDK并发包源码

线程相关

线程创建方式,继承Thread、实现Runnable、Future和Callable、线程池;daemon守护线程,不能阻止jvm进程结束,如果所有工作线程都结束,只剩守护线程时,程序就会结束;线程组,每个线程都有一个线程组,默认使用父线程的线程组,可以通过线程组对一组线程统一进行管理和操作;yield,告诉cpu先别执行这个线程了,cpu就会切换别的线程执行,一般测试或者debug时使用;join,阻塞主线程,等待线程执行完后继续执行;interrupt,打断,不会直接结束线程,会修改标识,根据标识可以判断是否想要结束线程。

java内存模型

java程序在机器上执行时,每个线程有自己的工作内存,使用数据时会从机器内存拷贝一份数据到工作内存,计算好之后再写回带机器内存,从机器内存读取数据的步骤是read,把读到的数据放到工作内存步骤是load,线程使用工作内存的数据步骤是use,计算处理好后,写回到工作内存步骤是assign,从工作内存获取数据步骤是store,然后写到内存的步骤是write。

硬件CPU多级缓存模型

机器组成有内存、高速缓存、cpu,总线,当执行代码指令的时候,会先从内存中加载数据到总线,然后从总线获取数据到cpu高速缓存(每个cpu都有自己的高速缓存),计算时从高速缓存获取数据,执行完指令计算好数据后,再写回高速缓存,满足某些条件之后会把数据刷回主内存?

volatile

保证变量的可见性,有序性,不能保证原子性。结合java内存模型来说一下可见性,在内存模型中线程计算好数据写到工作内存后,如果变量被volatile修饰,通过MESI协议和嗅探机制(发送lock前缀指令),会强制把修改的数据刷回内存,让其他使用这个变量的工作内存数据变为失效,如果其他线程从工作内存使用这个变量,会发现已经失效了,会重新加载这个数据,不能保证原子性的原因是线程读取数据的时候才会判断是否过期,如果已经计算好的数据写回工作内存是不会判断过期的,直接覆盖写,所以不能保证原子性。java中指令重排时会遵守happen-before原则,不能胡乱的排序,volatile通过内存屏障保证有序性,写操作会在前面加StoreStore屏障禁止与前面的普通写和他发生重排,写操作后面会加StroeLoad屏障,禁止跟下面的volatils读写发生重排;读操作会在后面加LoadLoad(禁止与下面的普通读和volatile读发生重排),LoadStore(禁止与下面的普通写和volatile写发生重排)。

synchronized

加锁方式,对对象加锁,对class对象加锁;每个synchronized都会对应一个monitor对象,对象中有state,wait_set,保存上锁线程信息的变量,当线程加锁的时候,判断如果state不为零加锁失败,再判断是不是当前线程加的锁,如果是可以自增state,加可重入锁,否则加锁失败阻塞等待,如果为0,直接加锁成功,自增state,底层加锁思想还是cas,因为state自增使用的就是cas操作(底层jvm编译后加锁时会执行monitorenter指令,解锁会执行monitorexit指令),当释放锁时state会自减1,减为0时,阻塞线程会尝试加锁;线程wait必须要放在synchronized中,因为wait线程会放到wait_set中,然后释放锁(清空state),其他线程加锁成功后,可以notity唤醒wait的线程,会从wait_set中取出阻塞线程唤醒。

很可能唤醒的线程不满足继续执行的条件了,所以放到循环中不断判断,满足条件才会继续执行;

多个线程同时被唤醒,如果不在循环中判断,其中一个处理后,其他唤醒的不做判断继续处理可能会出错。

1)原子性:加锁和释放锁,ObjectMonitor

2)可见性:加了Load屏障和Store屏障,释放锁flush数据,加锁会refresh数据

3)有序性:Acquire屏障和Release屏障,保证同步代码块内部的指令可以重排,但是同步代码块内部的指令和外部的指令是不能重排的

线程安全相关数据结构

ConcurrentHashMap,加锁采用分段加锁方式,判断数组位置如果为空,cas设置数据,如果不为空,加锁设置数据(synchronized),如果加锁成功设置数据,和hashmap类似,jdk1.7设置16个分段,当扩容后锁竞争会增加,jdk1.8做了优化,数组每个位置就是一个段;

CopyOnWriteArrayList,写时复制机制,保证线程安全,修改相关操作都会ReentrantLock加锁,保证只有一个线程可以操作,底层被volatile修饰,修改了,可以立即被其他线程看到,修改、插入、删除都是基于复制的方式实现(数组拷贝),读数据时不需要加锁,直接读就可以,因为修改相关操作都是基于写时复制,会不断的创建新数组,修改新数组,然后再用新数组替换老数组,迭代数据的时候,迭代器会有一个快照数组,使用修改等操作也不会影响迭代(读操作不加锁保证读的并发很强,适合写少读多的地方);

ConcurrentLinkedQueue,无界队列,线程安全,与LiinkedList类似,offer操作,for+cas,尾节点为空cas赋值,否则for+cas方式把数据挂到链表上,poll出队也是for+cas操作,保证只有一个线程可以操作成功,如果发现头节点有值,直接移除相关操作,不会移动值为null的头节点,peek操作,如果头节点为null,但是next不为null,移动头节点,for循环再次获取头节点,遍历时使用的是快照机制,可能会有不一致的情况;

LinkedBlockingQueue,有界队列,数据超过指定大小,再往里放数据就会阻塞住,避免无限大,两把独占锁分别锁队头和队尾,当队列为空时,获取数据的获取线程就会被condition阻塞住,如果不为空,获取元素时会唤醒阻塞的放入数据的线程,放入元素时会唤醒阻塞的获取元素的线程,获取元素时判断如果队列满了,就会阻塞线程,队列加了两把独占锁,可以同时出队入队,提高性能,阻塞时会被contition放到aqs等待队列,满足条件后会唤醒阻塞线程继续操作(根据count和capacity判断),遍历的时候会把两把锁都锁掉,相当于锁掉整个queue;

ArrayBlockingQueue,基于数组实现的阻塞有界队列,采用一把独占锁,同时只能一个线程来出队或者入队,然后通过数组下标来控制队头和队尾,当达到最大值时(数组长度),就重新从0开始循环往复的使用一个数组达到出队入队的效果,当数据量count为0时阻塞获取的线程,如果count为数组长度时阻塞放入的线程,每次放入数据时count++,每次获取数据时count--;

PriorityBlockigQueue,设置数据的时候会设置优先级,出队的时候会按照优先级进行出队,底层是通过二叉树来存储的,根据优先级进行排队;

DelayQueue,给每个压入队列的数据设置一个delay延迟时间,在延迟时间之后才可以从队列里获取到,底层时PriorityQueue,会按照时间进行排序,获取数据的时候,判断队头是否到达延迟的时间,如果没到达则返回null,如果到达了则出队(循环的方式,没到达延迟时间阻塞等待延迟的时间,超时之后再循环尝试获取数据)

cas

核心思想是比较和赋值,如果多线程要为一个变量赋值,首先获取当前值,然后计算操作得到新值,通过java底层方法Unsafe方式(通过变量地址偏移量操作变量,原子操作),比较旧值和当前是否一致,如果一致,则替换为新值。存在问题是ABA问题,仅支持简单的数据类型。AtimicInteger等原子类都是基于cas思想实现的(如果设置值失败,会循环再设置,直到成功)。乐观锁的思想,性能相对较高,但是高并发情况下,竞争锁失败很多,性能不一定就好,针对这个问题,LongAddr采用分段锁的思想,内部有base+cell数组,当并发操作修改时,如果几次失败后就会切换cell,降低竞争锁导致的失败次数,获取值时,是通过base+cell数组求和的方式。复杂对象通过AtomicReference处理,AtomicStampedReference,保存数据时会设置一个数字,不断的自增,类似数据版本号,可以解决ABA的问题。

aqs

抽象队列同步器,有state和双向链表的队列(阻塞就会加入到队列),定义两种资源Exclusive独占(例如ReentrantLock)和share共享(Semaphore/CountDownLatch);

ReentrantLock为例,当线程加锁时,先cas操作state,如果从0变为1,代表加锁成功,否则加锁失败,执行acquire(tryAcquire),判断加锁线程是不是当前线程,如果是的话则可以可重入加锁,自增state,否则执行acquireQueued,创建一个独占类型资源,挂到aqs的队列上,尝试加锁失败会park挂起线程,当锁都释放时(执行release),会unpark唤醒阻塞线程,唤醒的线程会尝试加锁,不一定成功(如果此时有其他线程来操作,先加锁成功,此时会加锁失败,接着park挂起线程),如果加锁成功,就会把队列中的节点移除(ReentrantLock默认是非公平锁,可以设置为公平锁,加锁的时候会判断等待队列中是否有等待线程,如果有则进入队列排队,如果没有则尝试加锁);

ReentrantReadWriteLock,读写锁,写锁与写锁、写锁/读锁(不是同一个线程)互斥,读锁与读锁不互斥,加锁时,先获取state,读写锁根据高低16位来管理读写锁状态的,低16位代表写锁,高16位代表读锁,加写锁时,判断如果state不为零,如果是写锁判断是否为自己加的,因为可重入加锁,不是自己加的写锁,或者是读锁则加锁失败,进入等待队列等待,创建节点的类型是独占类型,释放锁的时候和ReentrantLock类似,如果读锁加锁,会判断如果加的是写锁,不是自己加的写锁直接返回失败,会创建shard类型node加入等待队列,读锁可以可重入加锁,每次加锁会state会增加65563,相当于高16位加1,释放锁也是同理,如果其他线程来加读锁失败,会循环不断的重试加读锁,直到加锁成功退出,释放锁高16位会自减1,当减为0时,会唤醒等待队列的线程尝试加锁;

Condition,流程,线程1->加锁->释放锁&阻塞挂起->线程2->加锁->唤醒线程1->释放锁->线程1->加锁->再次释放锁;condition.await,将自己加入conditon等待队列,然后释放锁,挂起自己;condition.signal,加锁后,把阻塞的线程1节点挂到等待队列,唤醒线程1并释放锁,线程1跟之前ReentrantLock唤醒一样,尝试加锁。

CountDownLatch,创建对象时设置一个数字,就是aqs的state,然后主线程调用await方法,判断state不为0,就会阻塞线程并挂起,放到队列中等待,执行countDown时state会自减,当减到0的时候会唤醒阻塞等待的线程,判断state为0,退出结束。

CyclicBarrier,让一组线程在到达某个屏障点(Barrier)时相互等待,直到所有线程都到达屏障点后,再一起继续执行。它的名字中的 "Cyclic" 表示它可以重复使用,而 "Barrier" 表示它是一个屏障点。与 CountDownLatch 不同,CyclicBarrier 可以重复使用,即所有线程到达屏障点后,屏障会重置,可以再次使用。

Semaphore,创建对象设置state为0,然后会执行acquire,把state设置为负数,会把当前线程封装为一个节点,进入等待队列,然后挂起等待,每次有线程执行release后state会加1,唤醒阻塞线程,当满足数量时退出程序。

ThreadLocal:线程本地副本,线程拷贝变量在自己线程内部维护一份。Thread本地有一个ThreadLocalMap,存放线程自己的数据的;可能存在内存泄露的情况,ThreadLocalMap的key是弱引用值是强引用,当内存不足时发生回收key会被回收掉,value不会,如果是线程池中的线程会一直存在,value也就一直存在,导致内存泄露,用完之后调用remove,jdk1.8做了优化,在set之后会循环判断如果有内存泄露的情况就移除掉value。

线程池

频繁的创建线程销毁线程,系统的开销会很大,线程池是,需要的时候创建线程使用,空闲后放到一个公共池子里,其他任务需要线程时,直接从池子里获取空闲的线程,避免频繁的创建和销毁;

线程池种类,fixed(固定线程数量,无界队列)、cached(不固定线程数量,尽快的执行任务,来多少任务使用空闲线程,如果没有则创建线程)、single(单线程的线程池)、scheduled(调度任务的线程池);

所有线程池都是基于ThreadPoolExecutor来实现的,方法参数说明,corePoolSize(线程池线程数量),maximumPoolSize(当达到最大值,等待队列也满了,最大可以创建线程的数量),keepTimeAlive+timeUnit(超出corePoolSize的线程空闲多久被回收),woreQueue(当达到corePoolSize时,任务加入的等待队列),threadFactory(创建线程的工厂),rejectExceptionHandler(当线程数量达到maximumPoolSize,并且等待队列也满了,就会执行这个逻辑);

32位integer,前3位表示线程池当前状态,后29位表示线程数量,创建好线程池后,首先加入任务时,如果没达到corePoolSize,直接创建新线程(加入hashSet保存worker<将任务和线程封装在里面>),执行worker,会调用线程执行任务,当线程达到corePoolSize后,再加入任务直接加入到等待队列,之前创建好的线程执行时会循环的判断如果队列有任务就会循环的取出来执行,如果保错,会移除worker,如果线程数量想要corePoolSize再重新添加worker创建线程,当队列为空的时候就会阻塞住。

fixed参数:corePoolSize,corePoolSize,0,TimeUnit,LinkedBlockingQueue;不会拒绝,无界队列

cached参数:corePoolSize(0),Integer.MAX_VALUE,60,秒,SynchronousQueue;core线程数量为零,不允许加入等待队列(没有线程等待获取任务,不让入队),每次来任务直接创建非core线程,当非core线程空闲60s后就回收掉,不会拒绝,可以无限制的创建线程

single线程池:1,1,0,TimeUnit,LinkedBlockingQueue;只是一个线程在执行任务,新来的任务不断的加入等待队列,不会拒绝,无界队列

scheduled参数:corePoolSize,Integer.MAX_VALUE,0,TimeUnit,DelayWorkerQueue;执行时延时队列,指定时间之后才会被获取到,定时执行时,调用方法时又重新压入到队列,一段时间之后才能被获取到。

1.2.3. JVM

JVM类加载机制

加载到使用经历步骤,加载、验证、准备、解析、初始化、使用、卸载;类加载器,启动类加载器、扩展类加载器、应用类加载器、自定义类加载器;双亲委派机制,加载时交给上级加载,上级加载不到让下级再加载,避免重复加载。

内存区域划分

堆、方法区是共享的,栈、程序计数器是线程独有的

JVM分代模型

年轻代、老年代、永久代,只要对象被引用了就不能被回收。

-XX:PermSize和-XXMaxPermSize设置永久带的参数,
-Xss设置线程栈大小的参数,
-Xms堆内存最大小,
-Xmx堆内存最大值,
-Xmn新生代大小

垃圾回收算法

复制算法(年轻代垃圾回收算法MinorGC,Eden区和Survivor区,新对象放到Eden区+S1区,当达到上限了,发生垃圾回收,将有GC Root的对象移动带S2区,然后整个E区和另一个S区垃圾回收掉)

标记整理算法(发生条件:MinorGC之前,发现可能要进入老年代的对象太多,可能放不下、或者MinorGC之后发现存活对象太多,老年代放不下;老年代垃圾回收算法FullGC,把存活的对象挪动到一起,剩下的垃圾对象一起回收掉)

年轻代进入老年代的条件

1)年龄超过15的;

2)大对象;

3)动态年龄判断(按照年龄从小到大类加对象大小,如果大小大于S区一半了,大于等于当前对象年龄的所有对象进入老年代);

4)MinorGC之后S区放不下进入老年代(MinorGC之前会判断如果整体对象大小小于老年代大小,放心的进行MinorGC;如果不是,判断历届存活对象大于老年代,老年代会尝试进行一次垃圾回收;判断历届MinorGC的存活对象大小小于老年代大小,尝试MinorGC,如果小于S区则进入S区,如果小于老年代大小进入老年代,如果大于老年代,老年代会进行一次垃圾回收,如果还放不下就OOM;)。

cglib和java动态代理

cglib通过增强字节码方式,功能强大,直接对字节码操作,生成的类会在永久堆,java动态代理方式是通过反射机制,只能针对接口进行代理,不能针对普通类。

Stop The World

停止工作线程,系统不再运行,专心的进行垃圾回收。

垃圾回收器

Serial和SerialOld垃圾回收器

(后台基本不用,单线程回收年轻代和老年代);

ParNew和CMS

ParNew新生代垃圾回收器,ParNew垃圾回收器,在回收期间进入Stop The World,多线程进行回收;CMS老年代垃圾回收器<标记清理算法,默认达到92%就进行垃圾回收>,

1、初始标记,进入STW,标识GC Root引用的对象,

2、并发标记,不进入STW,对已有的GC Root进行追踪<最耗时>,

3、重新标记,第二阶段会产生新的对象和垃圾,此时会进入STW,重新标记产生的新对象的状态,

4、并发清理,不进入STW,清理掉垃圾对象<最耗时>,默认参数,执行完并发清理会将存活对象移动到一起,避免有内存碎片;执行CMS垃圾回收期间,如果要进入老年代的对象大于剩余空间,就会并发垃圾回收失败,进入STW,使用SerialOld方式单线程进行垃圾回收,回收完垃圾系统再继续运行<痛点,STW,各种优化都为为了减少STW时间>;

G1

统一回收新生代和老年代,最大特点将堆内存拆分为多个大小相等的Region,设置预期停顿时间,系统自动根据Region的情况进行选择性回收,选择最少回收时间,最多回收对象的Region进行回收;之前的E区S区老年代,在G1中也存在就是不同的Region来代表,进入老年代的判断规则也和之前差不多,但是大对象会单独放Region,如果一个Region放不下,可以横跨多个Region来存放;

1、初始标记,进入STW,过程很快,标记GC Root,

2、并发标记,不进入STW,追踪GC Root引用的对象,

3、重新标记,进入STW,标记那些对象存活那些对象是垃圾,

4、混合回收。进入STW,根据标记等信息、预期停留时间等,选择部分Region进行回收;如果没有可用的Region就会单线程进行回收

“-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+DisableExplicitGC -XX:+PrintGCDetails -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/app/oom”

优化FullGC

查看GC日志,判断垃圾回收时间,及各个区的情况;jstat -gc 进程id 1000(时间ms) 100(打印几次),查看正在运行的情况E区,S区,及老年代的情况。让年轻代回收的存活对象落在S区,尽量少进老年代,降低老年代垃圾回收的频率。(jmap可以查看运行的对象分布情况,jhat可以用浏览器查看dump文件)

优化OOM

Metaspace区域、栈、堆都可能发生内存溢出;

Metaspace区域内存溢出

1)使用默认值,太小;
2)使用cglib动态生成一些类导致内存溢出

栈内存溢出

线程栈空间有限的,如果无限制调用就会导致溢出

堆内存溢出

1)有内存泄露,对象一直被引用,无限的生成新的对象;
2)高并发,瞬间产生大量对象;最终堆空间被占满就会内存溢出

优化: (栈的没办法看,只能看普通日志来判断)gc日志可以看到是什么地方内存溢出,然后获取内存快照文件,工具打开可以看到最多的对象,看详细信息可以找到对应代码大概位置

results matching ""

    No results matching ""