【面试题2】线程池、JVM

一、线程池

优秀的博文
https://www.cnblogs.com/dolphin0520/p/3932921.html

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。

为什么用线程池

1、创建和销毁线程消耗系统资源,我们的应用都是多线程处理业务,所以频繁创建和销毁线程销毁处理影响效率

2、线程的并发数过多,抢占系统资源导致阻塞

3、对线程进行一些简单的管理(线程的生命周期)

线程池的原理

1、保持线程处于存活状态(就绪,运行或者阻塞)

2、控制并发的数量()

3、设置线程的状态,管理线程

线程池 最小的线程数量

​ 最大的线程数量

​ 闲置时间 有单位的

​ 任务队列

​ 拒绝

常见四种线程池
1、可缓存线程池cachedThreadPool

corePoolSize没有核心线程数量;maxPoolSize最大线程数量为int最大值

适用:短期异步小程序或者负载较轻服z务器

2、定长线程池FixedThreadPool

核心线程数量等于最大线程数量

适用:执行长期的任务

3、SingleThreadPool单例线程池

核心线程数量等于最大线程数量 都等于1

适用:一个任务一个任务执行

4、ScheduelThreadPool定时线程池

最大线程数是int最大值

适用:周期性执行任务的场景(定期同步数据)

5、ThreadPoolExecutor线程池构造器

参数:corePoolSize:核心线程数(最小存活的工作线程数量)

​ maxPoolSize:最大线程数

​ keepAliveTime:存活时间

​ timeUnit:单位

​ workQueue:阻塞队列

​ threadFactory线程工厂

​ handler:拒绝

handler拒绝策略

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:

``ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 ``ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 ``ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)``ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 
使用场景
配置线程数量原则

本节来讨论一个比较重要的话题:如何合理配置线程池大小,仅供参考。

一般需要根据任务的类型来配置线程池大小:

如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1

如果是IO密集型任务,参考值可以设置为2**N*CPU

当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。

二、JVM

JVM的组成
类加载器子系统、运行时数据区(元空间、本地方法栈、虚拟机栈、堆、程序计数器)、本地方法库、执行引擎

在这里插入图片描述

类加载的5个过程:

1、加载:找到字节码文件,读取到内存中。有隐式加载,new的方式,显示加载通过反射的方式

2、验证:验证此字节码文件是不是一个真的字节码文件。

3、准备:为类中static修饰的变量分配内存空间并设置其初始值为0或null

4、解析:将Java代码中的符号引用替换为直接引用

5、初始化:如刚才准备阶段所说的static修饰的变量,这个阶段就是堆变量赋值的阶段

类加载器一般有4种

1、启动类加载器:jdk环境变量的lib包下

2、扩展类加载器:jdk环境变量下的lib/ext下

3、应用程序类加载器:加载classpath下的

4、自定义类加载器。

双亲委派机制:

应用程序加载器举例,它现在需要加载一个类,不会直接的去尝试加载,而是委托上级的扩展类加载器去加载,而扩展类加载器也是委托上级启动类加载器加载。

启动类加载器在自己的搜索范围内找到这么一个类,表示自己无法加载,就再让扩展类加载器去加载,同样的,扩展类加载器再自己的搜索范围内找一遍,如果还是没有找到,就委托应用类加载器去加载,如果最终还是没有找到,那就会直接抛出异常了。

为什么要这么麻烦由上而下,再由下而上呢?

比如:自己自定义一个类java.lang.Object的类,放到自己的classpath中,没有这种优先级的话,应用程序加载器就把它当作Object加载到内存中了,从而会引发一片混乱。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AhxGxT44-1572016478040)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\1571985167311.png)]

运行时数据区:元空间、本地方法栈、虚拟机栈、堆、程序计数器;元空间和堆 是所有线程共享

1、程序计数器:确定指令的执行顺序的。为了保证每个线程都能按正常顺序执行,所以程序计数器是私有的,程序计数器是唯一一个JVM没有规定任何OOM的区块。

OOM out of memory内存溢出。

2、Java虚拟机栈:方法执行的过程就是一个入栈和出栈的过程。每个方法的执行都会创建一个栈帧。stackOverFloeError栈溢出错误,因为线程请求栈深度超出虚拟机所允许的范围。

设置每个线程的栈大小:-Xss256k

递归没有出口

oom动态扩展栈的大小的时候,申请不到足够的内存空间。

3、本地方法栈:本地方法栈执行本地方法。

4、元空间:虚拟机加载的类信息,class常量池、编译后的代码数据。直接占用本地内存

5、堆内存:字符串常量池、静态变量,所有线程共享的,主要存放对象和数组的区域,也是GC的主要区域,GC分区分为:新生代和老年代。新生代中又细分为一个Eden(伊甸园),两个survivor区。Eden中存放的是通过new或者newInstance方法创建出来的对象,绝大多数都是很短命的,正常的话,经历过一次gc过后,存活的对象会转入其中一个survivor区,然后再经历15次的gc,把存活的转入老年代。这时常规状态,当survivor区已经满了,JVM会依据担保机制将一些对象直接放入到老年代。

设置堆空间大小

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FHlduaqR-1572016478041)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\1571987524535.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z7tRX0Zw-1572016478044)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\1571987539318.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zwCvcNl2-1572016478046)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\1571987552679.png)]

设置最大堆最小堆:-Xms20m -Xmx20m

堆和方法区一样(确切来说JVM规范中方法区就是堆的一个逻辑分区),就是一个所有线程共享的,存放对象的区域,也是GC的主要区域.其中的分区分为新生代,老年代.新生代中又可以细分为一个Eden,两个Survivor区(From,To).Eden中存放的是通过new 或者newInstance方法创建出来的对象,绝大多数都是很短命的.正常情况下经历一次gc之后,存活的对象会转入到其中一个Survivor区,然后再经历默认15次的gc,就转入到老年代.这是常规状态下,在Survivor区已经满了的情况下,JVM会依据担保机制将一些对象直接放入老年代。

新生代使用时minor gc 老年代使用的full gc

l 大多数情况下,新的对象都分配在Eden区,当Eden区没有空间进行分配时,将进行一次Minor GC,清理Eden区中的无用对象。清理后,Eden和From Survivor中的存活对象如果小于To Survivor的可用空间则进入To Survivor,否则直接进入老年代);Eden和From Survivor中还存活且能够进入To Survivor的对象年龄增加1岁(虚拟机为每个对象定义了一个年龄计数器,每执行一次Minor GC年龄加1),当存活对象的年龄到达一定程度(默认15岁)后进入老年代,可以通过-XX:MaxTenuringThreshold来设置年龄的值。

l 当进行了Minor GC后,Eden还不足以为新对象分配空间(那这个新对象肯定很大),新对象直接进入老年代。

l 占To Survivor空间一半以上且年龄相等的对象,大于等于该年龄的对象直接进入老年代,比如Survivor空间是10M,有几个年龄为4的对象占用总空间已经超过5M,则年龄大于等于4的对象都直接进入老年代,不需要等到MaxTenuringThreshold指定的岁数。

l 在进行Minor GC之前,会判断老年代最大连续可用空间是否大于新生代所有对象总空间,如果大于,说明Minor GC是安全的,否则会判断是否允许担保失败,如果允许,判断老年代最大连续可用空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则执行Minor GC,否则执行Full GC。

l 当在java代码里直接调用System.gc()时,会建议JVM进行Full GC,但一般情况下都会触发Full GC,一般不建议使用,尽量让虚拟机自己管理GC的策略。

本地执行引擎

主要包含了即时编译器(JIT)和垃圾回收器(GC)

JVM内存溢出

1、堆内存溢出 OOM

堆内存主要是存放对象和数组的,只要不断的创建对象,并且避免垃圾回收机制清除这些对象,当这些对象所占空间查过最大堆内存容量时,就会出现堆内存溢出的错误。

如何避免垃圾回收机制清除对象??

保证GC Roots到对象之间又可达路径。

新生代的对象最初分配到新生代,新生代满后进行一次Minor GC,如果Minor GC后空间不足会把存活对象放入老年代,老年代空间不足时会进行Full GC,之后如果空间还不足以存放新对象就抛出OOM的错误。

死循环产生过多重复对象;

堆内存分配不合理;

网络连接问题;

数据库问题等

判断对象是否为垃圾

1、引用计数算法:给每一个对象添加一个引用计数器,每当有引用,计数器就加1,每当不再引用它,计数器就减1,这样只要计数器的值不为0,就说明还有地方引用它,为零就是无用的对象了。

2、可达性分析算法:我们先了解一下GC Roots,垃圾收集的起点。可以作为GC Roots的有:虚拟机栈中本地变量表中引用的对象方法区中静态属性引用的对象方法区中常量引用的对象本地方法栈中JNI(Native方法)引用对象

当一个对象到GC Roots没有任何引用链相连,就说明此对象是不可用的,是死对象。还可以拯救,看是否有finalize()方法。

垃圾回收算法

1、标记清楚算法:首先标记所有要回收的对象,标记完后统一回收所有标记的对象。会造成不连续的内存空间。

2、标记整理算法:先对可用的对象进行标记,然后所有被标记的对象向一端移动,最后清除可用对象边界以外的内存。

3、赋值算法:把存分为大小相等的两块,每次存储只用一块,当一块用完了,就把存活的对象全部复制到另一块。把失去引用的对象全部清理掉,就是一个全新的内存空间,往复适用。

缺点可使用空间缩减为原来的一半

4、分代收集算法:把堆内存分为年轻代和老年代。

·

常见的垃圾收集器

新生代收集器:Serial(单线程、复制算法 适用:桌面应用、单核服务器)、ParNew(多线程版本 多核服务器)、Parallel Scavenge(多线程 效率高,尽可能缩短垃圾收集时用户线程的停顿时间)

老年代收集器:Serial Old(单线程 标记整理算法)、CMS(以最短用户停顿时间为目标的收集器)、Parallel Old(多线程 标记整理算法)

堆内存垃圾收集器:G1 jdk1.7才正式商用,jdk1.9默认的收集器。对整个堆内存进行垃圾收集。

内存占用:

  • 程序正常运行需要的内存大小

延迟:

  • 由于垃圾收集导致的程序停顿的时间。

吞吐量:

  • 用户程序运行时间(此刻占用)/垃圾收集占用时间;他们的比值就是吞吐量
JVM的内存优化
调优工具:
  • jps
  • jstat
  • jmap
  • 利用jvisualvm可视化 分析内存信息(各个区如:Eden、Survivor、Old等内存的变化)、
  • 分析堆转储快照
jvm调优经验

堆大小设置:老年带存活对象的3-4倍。

新生代Xmn的设置为老年代存活对象的1-1.5倍

老年代的内存大小设置为老年代存活对象的2-3倍。

JVM配置方面,一般情况可以先用默认配置(基本的一些初始参数可以保证一般的应用跑的比较稳定了),在测试中根据系统运行状况(会话并发情况、会话时间等),结合gc日志、内存监控、使用的垃圾收集器等进行合理的调整

当老年代内存过小时可能引起频繁Full GC,当内存过大时Full GC时间会特别长。

常用JVM参数参考:

参数 说明 实例

-Xms 初始堆大小,默认物理内存的1/64 -Xms512M

-Xmx 最大堆大小,默认物理内存的1/4 -Xms2G

-Xmn 新生代内存大小,官方推荐为整个堆的3/8 -Xmn512M

-Xss 线程堆栈大小,jdk1.5及之后默认1M,之前默认256k -Xss512k

-XX:NewRatio=n 设置新生代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 -XX:NewRatio=3

-XX:SurvivorRatio=n 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:8,表示Eden:Survivor=8:1:1,一个Survivor区占整个年轻代的1/8 -XX:SurvivorRatio=8

-XX:PermSize=n 永久代初始值,默认为物理内存的1/64 -XX:PermSize=128M

-XX:MaxPermSize=n 永久代最大值,默认为物理内存的1/4 -XX:MaxPermSize=256M

-verbose:class 在控制台打印类加载信息

-verbose:gc 在控制台打印垃圾回收日志

-XX:+PrintGC 打印GC日志,内容简单

-XX:+PrintGCDetails 打印GC日志,内容详细

-XX:+PrintGCDateStamps 在GC日志中添加时间戳

-Xloggc:filename 指定gc日志路径 -Xloggc:/data/jvm/gc.log

-XX:+UseSerialGC 年轻代设置串行收集器Serial

-XX:+UseParallelGC 年轻代设置并行收集器Parallel Scavenge

-XX:ParallelGCThreads=n设置Parallel Scavenge收集时使用的CPU数。并行收集线程数。 -XX:ParallelGCThreads=4

-XX:MaxGCPauseMillis=n 设置Parallel Scavenge回收的最大时间(毫秒) -XX:MaxGCPauseMillis=100

-XX:GCTimeRatio=n设置Parallel Scavenge垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) -XX:GCTimeRatio=19

-XX:+UseParallelOldGC 设置老年代为并行收集器ParallelOld收集器

-XX:+UseConcMarkSweepGC 设置老年代并发收集器CMS

-XX:+CMSIncrementalMode 设置CMS收集器为增量模式,适用于单CPU情况。

设置jvm参数的几种方式

1、集成开发环境下启动并使用JVM,如eclipse需要修改根目录文件eclipse.ini;

2、Windows服务器下安装版Tomcat,可使用Tomcat7w.exe工具(tomcat目录下)和直接修改注册表两种方式修改Jvm参数;

3、Windows服务器解压版Tomcat注册Windows服务,方法同上;

4、解压版本的Tomcat, 通过startup.bat启动tomcat加载配置的,在tomcat 的bin 下catalina.bat 文件内添加;

5、Linux服务器Tomcat设置JVM,修改TOMCAT_HOME/bin/catalina.sh;

6、windows环境下配置JAVA_OPTS

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值