1、JVM原理,常见垃圾回收算法,垃圾回收器及JVM调优;
JVM原理:
JVM包含两个子系统和两个组件,两个子系统为Class loader-类装载,Execution engine-执行引擎,两个组件Runtime data area-运行时数据区,Native Interface-本地接口;
Class loader:根据给定的全限定名类名(带包路径的类名,如java.lang.Object,非限定类名即类名)来装载class文件到Runtime data are中的method area;
Execution engine:执行classes中的指令;
Native Interface:与native libraries交互,是其他编程语言交互的接口;
Runtime data area:即我们常说的JVM内存;
流程:首先通过编译器把Java代码转换成字节码,类加载器ClassLoader再把字节码加载到内存中,将其放在运行时数据区Runtime data are的方法区内,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎Execution engine,将字节码翻译成底层系统指令,再交由CPU去执行,这个过程需要调用其他语言的本地库接口Native Interface来实现整个程序的功能;
JVM运行时数据区:
程序计数器:Program Counter Register,当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下条需要执行的字节码命令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
Java虚拟机栈:Java Virtual Machine Stacks,用于存储局部变量表,操作数栈,动态链接,方法出口等信息;
本地方法栈:Native Method Stack,与虚拟机栈的作用是一样的,只不过虚拟机栈是服务Java方法的,而本地方法栈是为虚拟机调用Native方法服务的;
Java堆:Java Heap,Java虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都是在这里分配内存;
方法区:Method area,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据;
常见垃圾回收算法:
标记清除算法:标记无用对象,然后进行清除回收,缺点:效率不高,无法清除垃圾碎片
复制算法:按照容量划分两个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉,缺点:内存使用率不高,只有原来的一半
标记整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存
分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
垃圾回收器:
新生代回收器:Serial,ParNew,Parallel,Scavenge
老年代回收器:Serial Old,Parallel Old,CMS
整堆回收器:G1
JVM调优工具:
JDK自带监控工具,位于JDK的bin目录下
jconsole:用于对JVM中的内存,线程和类等进行监控
jvisualvm:JDK自带的全能分析工具,可以分析内存快照,线程快照,程序死锁,监控内存的变化,GC变化等;
常用的 JVM 调优的参数都有哪些?
-Xms2g:初始化堆大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。
2、java并发编程,了解各种锁机制,线程池机制,并在项目中合理应用;
悲观锁:总是架设最坏的情况,每次去拿数据的时候都认为别人会修改,所有每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞知道它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁,都是在做操作之前先上锁。再比如同步语句Synchronized关键字的实现也是悲观锁;
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
CAS:
CAS是compare and swap的缩写,即我们所说的比较交换;
CAS是基于乐观锁的操作,包含三个操作数:内存位置(V),预期原值(A)和新值(B),如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B,并修改版本标识;
CAS是通过无限循环来获取数据的,如果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才可能有机会执行;
CAS会产生什么问题?
--ABA问题:线程1从内存中取出A,但是它阻塞了,线程2也取出了A,做了一些操作变成了B,然后又将B变成了A(或其他线程抢占成功,将B变成了A),这时候线程1进行CAS操作发现内存中仍然是A,然后操作成功;
使用版本号解决,版本号一致且期望值一致才会更新。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
--循环时间长开销大:对于资源竞争严重的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于Synchronized。
--只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
AQS:是一个用来构建锁和同步器的框架
AQS核心思想是,如果被请求的共享资源贡献,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待一级被唤醒时分配锁的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中;
AQS定义两种资源共享方式
Exclusive独占:只有一个线程能执行,如ReentrantLock,又可分为公平锁和非公平锁:
--公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
--非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share共享:多个线程可同时执行
可重入锁(ReentrantLock):是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞;
在java中,synchronized通过获取自增,释放自建的方式实现重入
读写锁(ReentrantReadWriteLock):ReadWriteLock是一个读写锁接口,读写锁时用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock是ReadWriteLock接口的具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能;
并发容器:
ConcurrentHashMap:分段锁。并发度16(SynchronizedMap,一次锁住整张表来保证线程安全,所以每次只能有一个线程来访问map)
CopyOnWriteArrayList:免锁容器,适合读多写少的场景;
ThreadLocal:线程变量;
BlockingQueue:阻塞队列;
ConcurrentLinkedQueue:非阻塞线程安全的队列;
ArrayBlockingQueue:用数组实现的有界阻塞队列
LinkedBlockingQueue:链表阻塞队列
线程池
顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
优点:
--降低资源消耗:重用存在的线程,减少对象创建销毁的开销
--提高响应速度:可有效的空值最大并发线程数,提高系统资源的使用率,同事避免过多资源竞争,避免堵塞,当任务到达时,任务可以不需要等待线程创建就能立即执行
--提高线程的可管理性:线程时稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以进行统一的分配,调优和监控。
--附加功能:提供定时执行,定期执行,单线程,并发数控制等功能;
线程池状态:
--RUNNING:正常的状态,接受新的任务,处理等待队列中的任务;
--SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务;
--STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程;
--TIDYING:所有的任务都销毁了,workCount为0,线程池的状态在转换为TIDYING状态时,会执行钩子方法terminated();
--TERMINATED:terminated()方法结束后,线程池的状态;
常用的两种线程池:ThreadPoolExecutor,ScheduledThreadPoolExecutor
ThreadPoolExecutor构造函数重要参数分析
3个最重要的参数
--corePoolSize:核心线程数,线程数定义了最小可以同时运行的线程数量;
--maximumPoolSize:线程池中允许存在的工作线程的最大数量;
--workQueue:当心任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就被存放在队列中
4个常见参数
--keepAliveTime:线程池中的线程数量大于corePoolSIze的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁;
--unit:keepAliveTime参数的时间单位;
--threadFactory:为线程池提供创建新线程的线程工厂;
--handler:线程池任务队列超过maxinumPoolSize之后的拒绝策略;
ThreadPoolExecutor的饱和策略(拒绝策略)
--ThreadPoolExecutor.AbortPolicy:抛出RejectExecutionException来拒绝新任务的处理;
--ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,这种策略会降低对于新任务提交速度,影响程序的整体性能,另外,这个策略喜欢增加队列容量,如果你的应用程序可以承受此延迟并且你不能丢弃任何一个任务请求的话,可以选择这个策略;
--ThreadPoolExecutor.DiscardPolicy:不处理新的任务,直接丢弃掉;
--ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求;