Java 面试题中级篇(二)
因为篇幅限制,本篇接着Java 面试题中级篇 (一)总结
Callable接口
1 | class MyThread implements Callable<Integer> { |
FutureTask同时实现了Runnable和Callable接口所以这里可以用FutureTask
1 | FutureTask<Integer> futureTask = new FutureTask<>(new MyThread()); |
线程池
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
它的主要特点为: 线程复用,控制最大并发数,管理线程。
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到下岸城创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行同一的分配,条友和监控。
三种常用的方法
- Executors.newFixedThreadPool
- 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue
- Executors.newSingleThreadExecutor()
- 创建一个单线程的线程,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
- newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue
- Executors.newCachedThreadPool()
- 创建一个可缓存线程池,如果线程长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
线程池的七大参数
- corePoolSize: 线程池中常驻核心线程数
- 在创建了线程池后,当有请求任务后你,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
- 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
- maximumPoolSize: 线程池能够容纳同时执行的最大线程数,此值必须大于等于1
- keepAliveTime: 多余的空闲线程存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
- unit: keepAliveTime的单位
- workQueue: 任务队列,被提交但尚未被执行的任务
- threadFactory: 表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可
- handler: 拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)如何来拒绝
线程池底层工作原理
- 在创建了线程后,等待提交过来的任务请求
- 当调用execute()方法添加一个请求任务时,线程池会做如下判断
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
- 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
- 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
- 当一个线程完成任务时,它会从队列中取下一个任务来执行
- 当一个线程无事可做超过一定时间时,线程池会判断
- 如果当前训醒的线程数大于corePoolSize,那么这个线程就会被停掉
- 所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小
线程池的拒绝策略
等待队列已经排满了,再也塞不下新任务了,同时,线程池中的max线程也达到了,无法继续为新任务服务,这时候我们就需要拒绝策略机制合理地处理这个问题
JDK内置的拒绝策略
- AbortPolicy(默认): 直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy: 调用者运行一种调节机制,该策略即不会抛弃任务,也不会抛出异常,而是将某些任务会退到调用者,从而降低新任务的流量
- DiscardOldestPolicy: 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
- DiscardPolicy: 直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案
创建线程池代码演示
这里的maximumPoolSize主要有两种算法(我们这里主要是以测试为主,所以就填的5):
- CPU密集型: maximumPoolSize = CPU核数 + 1个线程的线程池
- IO密集型: maximumPoolSize = CPU核数 * 2 或者 CPU核数 / 1 - 阻塞系数(阻塞系数在0.8~0.9之间)
- CPU核数可以用Runtime.getRuntime().availableProcessors()获取
1 | ExecutorService executorService = new ThreadPoolExecutor( |
死锁
死锁是指两个或两个以上的进程在执行过程中,因抢夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
产生死锁的主要原因:
- 资源系统不足
- 进程运行推进的顺序不合适
- 资源分配不当
代码演示
1 | class HoldLockThread implements Runnable { |
1 | String lockA = "lockA"; |
解决办法
- jps命令定位进程号
- jstack找到死锁查看
JVM
GC的作用域
作用域是方法区和堆同时也是线程共享的
常见的垃圾回收算法(重点💢)
- 引用计数
- 每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗
- 较难处理循环引用
- JVM的实现一般不采用这种方式
- 复制
- eden SurvivorFrom复制到SurvivorTo,年龄+1
- 清空eden SurvivorFrom
- SurvivorTo和SurvivorFrom互换
- 标记清除
- 主要用于老年代
- 标记整理
- 主要用于老年代
GC Roots
- 枚举根结点做可达性分析(根搜索路径)
- 为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法
- 所谓GC Roots 或者说tracing GC 的根集合,就是一组必须活跃的引用
- 基本思路就是通过一系列名为GC Roots的对象作为起点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,没有被遍历到的就自然被判定为死亡。
- 可被作为GC Roots的对象有虚拟机栈(栈桢中的局部变量区,也叫做局部变量表)中引用的对象,方法区中的类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI(Native方法)引用的对象
JVM参数
- -Xms = -XX:InitialHeapSize初始内存大小,默认为物理内存的1/64
- -Xmx = -XX:MaxHeapSize最大分配内存,默认为物理内存的1/4
- -Xss = -XX:ThreadStackSize设置单个线程栈的大小,一般默认为512k~1021k
- -Xmn 设置年轻代大小
- -XX:MetaspaceSize 设置元空间大小元空间并不在虚拟机中,而是使用本地内存元空间大小仅受本地内存大小限制
- -XX:SurvivorRatio 设置新生代中eden和s0/s1空间的比例,默认是8:1:1
- -XX:NewRation 配置年轻代与老年代在堆结构的占比,默认是2,年轻代占1,老年代占2
- -XX:MaxTenuringThreshold 设置垃圾的最大年龄( must be between 0 and 15 )
- -XX:+PrintGCDetails 输出GC的详细收集日志信息
- -XX:+PrintFlagsInitial盘点JVM初始参数
- -XX:+PrintFlagsFinal主要查看修改更新 := 说明是修改过的值
- -XX:+PrintCommandLineFlags打印命令行参数
强引用(默认支持)
当内存不足时,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还活着,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应的引用赋值为null,一般认为就是可以被垃圾收集的了(当然具体回收时还是要看垃圾收集策略)。
软引用
软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
- 点那个系统内存充足时它不会被回收
- 点那个系统内存不足时它会被回收
软引用通常在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就会回收!
弱引用
弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存其更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
软引用和弱引用的使用场景
假如有一个应用场景需要读取大量的图片:
- 如果每次读取图片都从硬盘读取则会严重影响性能
- 如果一次性全部加载到内存中有可能会造成内存溢出
此时使用软引用可以解决这个问题。设计思路是用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。
1 | Map<String, SoftReference<Bitmap>> imageCache = new HashMap<>(); |
虚引用
虚引用需要java.lang.ref.PhantomReference类来实现。顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。
虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。Java计数允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
java提供了4种引用类型,在垃圾回收的时候,都有自己的特点。ReferenceQueue是用来
OOM
StackOverflowError
Exception in thread main java.lang.StackOverflowErrorjava.lang.Object
java.lang.Throwable
java.lang.Error
java.lang.VirtualMachineError
java.lang.StackOverflowError
由此可见StackOverflowError是一个错误
OOM之Java heap space
Exception in thread main java.lang.OutOfMemoryError: Java heap spacejava.lang.Object
java.lang.Throwable
java.lang.Error
java.lang.VirtualMachineError
java.lang.OutOfMemoryError
由此可见OutOfMemoryError是一个错误
OOM之GC overhead limit exceeded
Exception in thread main java.lang.OutOfMemoryError: GC overhead limit exceededGC回收时间过长时会抛出OutOfMemoryError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC都只回收了不到2%的极端情况下才会抛出。假如不抛出GC overhead limit 错误会发生什么情况呢?那就是GC清理的这么点内存很快会再次填满,迫使GC再次执行,这样就形成了恶性循环,CPU使用率一直是100%,而GC却没有任何成果
OOM之Direct buffer memory
Exception in thread main java.lang.OutOfMemoryError: Direct buffer memory写NIO程序经常会使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
- ByteBuffer.allocation(capability)第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢
- ByteBuffer.allocateDirect(capability)第一种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快
但如果不分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了
OOM之unable to create new native thread
高并发请求服务器时,经常出现如下异常:java.lang.OutOfMemoryError: Unable to create new native thread准确的讲该native thread异常与对应的平台有关
导致原因:
- 你的应用创建了太多线程了,一个应用进程创建多个线程,超过系统承载极限
- 你的服务器不允许你的应用程序创建这么多线程,linux系统默认允许单个进程可以创建的线程数是1024个,你的应用创建超过这个数量,就会报java.lang.OutOfMemoryError: Unable to create new native thread
解决办法:
- 想办法降低你应用程序创建的线程数量,分析应用是否真的需要创建这么多线程,如果不是,该代码将线程数降到最低
- 对于有的应用,确实需要创建很多线程,远超过linux系统的默认1024个线程的限制,可以通过修改linux服务器配置,扩大linux默认限制
OOM之Metaspace
java.lang.OutOfMemoryError: MetaspaceJava8及之后的版本使用Metaspace来代替永久代。Metaspace是方法区在HotSpot中的实现,它与持久化最大的区别在于: Metaspace并不在虚拟机中内存中而是使用本地内存,也即在Java8中,class metadata被存储在叫做Metaspace的native memory
永久代(java8后被元空间取代了)存放了一下信息:
- 虚拟机加载的类信息
- 常量池
- 静态变量
- 即时编译后的代码
垃圾回收器
GC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现。因为目前为止还没有完美地收集器出现,更加没有万能的收集器,只是针对具体应用最合适地收集器,进行分代收集。
- Serial(串行)
它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境
- Parallel(并行)
多个垃圾收集线程并行工作,此时用户线程是暂停的,使用与科学计算/大数据处理等弱交互场景
- CMS(并发)
用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程。互联网公司多用它,适用对响应时间有要求的场景
- G1
G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收
那么如何查看默认的垃圾收集器呢?其实也很简单 java -XX:+PrintCommandLineFlags -version
java的GC回收的了类型主要有几种:
UseSerialGC,UseSerialOldGC(串行老年代GC,已废弃),UseParallelGC,UseConcMarkSweepGC,UseParNewGC(年轻代的串行回收方式),UseParallelOldGC(老年代的并行回收方式),UseG1GC
垃圾收集器就来具体实现这些GC算法并实现内存回收。不同厂商 不同版本的虚拟机实现差别很大,HotSpot中包含的收集器如下图所示,红色表示java8版本开始,对应的垃圾收集器Deprecated,不推荐使用。
GC之Serial收集器
一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束。串行收集器是最古老,最稳定以及效率最高地收集器,只使用一个线程去回收但其进行垃圾收集过程中可能会产生较长的停顿(Stop-The-World状态),虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。
对应的JVM参数是: -XX:+UseSerialGC
开启后会使用: Serial(Young区用) + Serial Old(Old区用)的收集器组合
表示:新生代,老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法
GC之ParNew收集器
使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-World暂停其他所有工作线程知道它收集结束。ParNew收集器其实就是Serial收集器新生代的并行多线程版本,最常见的应用场景时配合老年代的CMS GC工作,其余行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是很多java虚拟机运行在Server模式下新生代的默认垃圾收集器。
常用对应JVM参数: -XX:UseParNewGC
启用ParNew收集器,只影响新生代的收集,不影响老年代,开启上述参数后,会使用ParNew(Young 区用) + Serial Old 的收集器组合,新生代使用复制算法,老年代采用标记整理算法,但是,ParNew + Tenured这样的搭配,java8已经不再被推荐。
备注: -XX:ParallelGCThreads 限制线程数量,默认开启和CPU数目相同的线程数
GC之Parallel收集器
Parallel收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。一句话: 串行收集器在新生代和老年代的并行化。
它关注的重点是:
- 可控制的吞吐量(Thoughput=运行用户代码时间/(运行用户代码时间 + 垃圾收集时间),也即比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99%)。高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。
- 自适应调节策略也是Parallel收集器与ParNew收集器的一个重要区别。(自适应调节策略: 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量)。
- 常用JVM参数: -XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)使用Parallel收集器
- 开启该参数后: 新生代使用复制算法,老年代使用标记-整理算法
GC之ParallelOld收集器
ParallelOld收集器是Parallel的老年代版本,使用多线程的标记-整理算法,ParallelOld收集器在JDK1.6才开始提供。在JDK1.6之前,新生代使用Parallel收集器只能搭配老年代的SerialOld收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,在JDK1.6之前(Parallel _ SerialOld)。ParallelOld正式为了在老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,JDK1.8后可以考虑新生代Parallel和老年代ParallelOld收集器的搭配策略。在JDK1.8及后(Parallel + ParallelOld)。
JVM常用参数: -XX:+UseParallelOldGC使用ParallelOld收集器,设置该参数后,新生代Parallel + 老年代ParallelOld
GC之CMS收集器
CMS收集器(Concurrent Mark Sweep: 并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。适合应用与在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的相应速度,希望系统停顿时间最短。CMS非常适合堆内存大,CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。Concurrent Mark Sweep并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行。
开启该收集器的JVM参数:-XX:+UseConcMarkSweepGC开启该参数后会自动将-XX:+UseParNewGC打开。开启该参数后,使用ParNew(Young区用) + CMS(Old区用) + SerialOld的收集器组合,SerialOld将作为CMS出错的后被收集器。
工作流程:
- 初始标记(CMS initial mark)只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
- 并发标记(CMS concurrent mark)和用户线程一起。进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象。
- 重新标记(CMS remark)为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分的标记记录,仍然需要暂停所有的工作线程。由于并发标记时,用户线程仍然运行,因此在正式清理前,再做修正。
- 并发清除(CMS concurrent sweep)和用户线程一起。清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象,由于耗时最长地并发标记和并发清除过程中,垃圾收集小城可以和用户一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行
优缺点:
- 并发收集停顿低
- 并发执行对CPU的压力比较大。由于并发执行,CMS在收集与应用线程会同时增加堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大的停顿时间
- 采用标记清除算法会导致大量的碎片。标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFullGCBeForeCompaction(默认0,即每次都进行内存中整理)来指定多少次CMS收集之后,进行一次压缩的Full GC
GC之SerialOld收集器
SerialOld是Serial垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器主要是运行在Client默认的java虚拟机默认的老年代垃圾收集器。
在Server模式下,主要有两个用途(了解,版本已经到8及以后)
- 在JDK1.5之前版本中与新生代的Parallel收集器搭配使用。(Parallel + SerialOld)
- 作为老年代版本中使用CMS收集器的后备垃圾收集方案
如何选择合适的垃圾收集器
- 单CPU或小内存,单机程序-XX:+UseSerialGC
- 多CPU,需要最大吞吐量,如后台计算应用-XX:+UseParallelGC或者-XX:+UseParallelOldGC
- 多CPU,追求低停顿时间,需快速响应如互联网应用-XX:+UseConcMarkSweepGC或者-XX:+ParaNewGC
总结
参数 | 新生代垃圾收集器 | 新生代算法 | 老年代收集器 | 老年代算法 |
---|---|---|---|---|
-XX:UseSerialGC | SerialGC | 复制 | SerialOldGC | 标记整理 |
-XX:UseParNewGC | ParNew | 复制 | SerialOldGC | 标记整理 |
-XX:ParallelGC | Parallel | 复制 | ParallelOld | 标记整理 |
-XX:UseConcMarkSweepGC | ParNew | 复制 | CMS + SerialOld(备用) | 标记清除 |
-XX:UseG1GC | G1整体上采用标记-整理算法 | 局部通过复制算法。不会产生内存碎片 | - | - |
GC之G1收集器
以前收集器的特点
- 年轻代和老年代是各自独立且连续的内存块
- 年轻代收集使用单eden + s0 + s1进行复制算法
- 老年代收集必须扫描整个老年代区域
- 都是以尽可能少而快地执行GC为设计原则
G1(Garbage-First)收集器,是一款面向服务端应用的收集器。应用在多处理和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。另外它还具有以下特性:
- 像CMS收集器一样,能与应用程序线程并发执行
- 整理空闲空间更快
- 需要更多的时间来预测GC停顿时间
- 不希望牺牲大量的吞吐性能
- 不需要更大的Java Heap
- G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片
- G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间
CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题。于是,为了取出内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器G1垃圾收集器。G1是在2012年才在JDK1.7u4中可用,Oracle官方计划在JDK9中将G1变成默认的垃圾收集器以替代CMS。它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换JAVA8以前的CMS收集器。主要改变是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域。
- G1能充分利用多CPU,多核环境硬件优势,尽量缩短STW
- G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片
- 宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋棋盘
- G1收集器里面讲整个的内存区都混合在一起了,但其本身依然在小范围内要进行年轻代合并老年代的区分,保留了新生代和老年代,但他们不再是物理隔离的,而是一部分Region的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域
- G1虽然也是分带收集器,但整个内存分区不再是物理上的年轻代与老年代,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑伤的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换
常用的配置参数
- -XX:+UseG1GC
- -XX:G1HeapRegionSize=n设置的G1区域的大小,值是2的幂,范围是1MB到32MB。目标是根据最小的Java堆大小划分出约2048个区域
- -XX:MaxGCPauseMillis=n最大GC停顿时间,这是个软目标,JVM将金肯呢个(但不保证)停顿小于这个时间
- -XX:InitiatingHeapOccupancyPercent=n堆占用了多少的时候就出发GC,默认为45%
- -XX:ConcGCThreads=n并发GC使用的线程数
- -XX:G1ReservePercent=n设置作为空闲的预留内存百分比,以降低目标空间溢出的风险,默认是10%
和CMS相比的优势
- G1不会产生内存碎片
- 是可以精确控制停顿,该停顿是把整个堆(新生代,老年代)划分为多个固定大小的区域,每次根据允许的停顿时间去收集垃圾最多的区域
底层原理
区域化内存划片Region,整体编为了一些列不连续的内存区域,避免了全内存区的GC操作。核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小,在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为: 32MB * 2048 = 65535Mb = 64G内存。
G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另一个区域,完成了清理工作。这意味着,在正常地处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。
在G1中,还有一种特殊的区域,叫Humongous(巨大的)区域,如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会堆垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
回收步骤
G1收集器下的YoungGC,针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集 + 形成连续的内存块,避免内存碎片
- Eden区的数据移动到Survivor区,假如出现Survivor空间不够,Eden区数据会晋升到Old区
- Survivor区的数据移动到新的Survivor区
- 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行
Linux
生产服务器变慢,诊断思路和评估性能分析命令。
top
显示系统各个进程的资源占用情况
1 | top |
- 系统当前时间:top - 09:15:52
- 系统运行时间:up 0 min
- 当前登录用户:1 user
- 负载均衡情况:load average: 1.78,0.49,0.17(average后面的三个数分别是1分钟,5分钟,15分钟的负载情况。)
vmstat
一般vmstat工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数单位是秒,第二个参数是采样的次数
1 | vmstat -n 2 3 |
- procs
- r: 运行和等待CPU时间片的进程数,原则上单核的CPU的运行队列不要超过2,整个系统的运行队列不能超过总核数的2倍,否则代表系统压力过大
- b: 等待资源的进程数,比如正在等待磁盘I/O,网络I/O等
- cpu
- us: 用户进程消耗CPU时间占比,us值高,用户进程消耗CPU时间多,如果长期大于50%,优化程序
- sy: 内核进程消耗的CPU时间百分比
- us + sy参考值为80%,如果us + sy 大于80%,说明可能存在CPU不足
- id: 处于空闲的CPU百分比
- wa: 系统等待IO的CPU时间百分比
- st: 来自于一个虚拟机偷取的CPU时间的百分比
free
查看应用程序可用内存数
1 | free -m |
df
查看磁盘剩余空间数
1 | df -h |
iostat
磁盘I/O性能评估
1 | iostat -xdk 2 3 |
- rkB/s每秒读取数据量kB
- wkB/s每秒写入数据量kB
- svctm I/O请求的平均服务时间,单位毫秒
- await I/O请求的平均等待时间,单位毫秒,值越小,性能越好
- util 一秒中有百分几的时间用于I/O操作。接近100%时,表示磁盘带宽跑满,需要优化程序或者增加磁盘
ifstat
查看网络I/O
1 | ifstat 1 |
CPU占用过高的分析思路
- 先用top命令找出CPU占比最高的
- ps -ef或者jps进一步定位,得知是一个怎么样的一个后台程序给我们惹事💢
- 定位到具体线程或者代码 ps -mp | 进程 -o THREAD,tid,time
- -m显示所有的线程
- -p pid进程使用CPU的时间
- -o 该参数后是用户自定义格式
- 将需要的线程ID转换为16禁止格式(英文小写格式) printf”%x\n” 有问题的线程ID
- jstack进程ID | grep tid(16进制线程ID小写英文) -A60