多线程
为什么不建议用Executors启动线程池
*队列LinkedBlockingQueue是没有边界的队列,请求多会造成OOM
*建议使用ThreadPoolExecutors
线程池中提交一个任务的流程?
1先判断线程池是否有线程,如果与就直接执行,没有就放队列
2如果队列满了,就拒绝(代码要做处理)
线程池有的状态
1. Running(运行状态):线程池已经创建并且正在运行,可以接受新的任务并处理已提交的任务。
2. Shutdown(关闭状态):线程池不再接受新的任务,但会继续处理已提交的任务,直到所有任务完成。在关闭状态下,线程池不会创建新的线程。
3. Stop(停止状态):线程池不再接受新的任务,并且会中断正在执行的任务。它会尝试终止所有的工作线程。
4. Tidying(整理状态):所有的任务都已经终止,工作线程数量已经降为零。线程池会进行一些清理工作,例如关闭底层线程池等。
5. Terminated(终止状态):线程池已经完全终止,不再接受任务,也不会处理已提交的任务。
优雅的停止线程
*stop(),释放synchronized,不会释放RreetranLock
*建议用字段(再线程做判断)
*用全局变量控制
tomcat是如何定义线程的
*tomcat中线程池走构造就会启动所有核心线程池
*tomcat的思想优先启动线程,线程达到上线才加入队列
线程池怎么设置核心线程数和最大线程数
*先看是CPU密集型 还是IO密集型,或者是混合型
*CUP密集可以+1
*IO密集可以X2
*核心业务可以多,非核心可以少,
*最终还是要压测得到合适的
volatile修饰的变量可以多线程可见
并发中的原则性要加锁实现
产生死锁的原因
1资源只能被一个线程使用
2线程阻塞 不释放被占用资源
3若干线程头尾相连的循环等待资源关系
建议:加锁顺序 。 设置超时时间,死锁检查
sychronize和ReetrantLock的区别
重量锁-轻量锁-偏向锁
Java 中主要有以下几种锁:
*互斥锁(synchronized)**:互斥锁是 Java 中最常用的锁,它可以保证同一时间只有一个线程可以访问某个资源。
*读写锁(ReentrantReadWriteLock)**:读写锁可以同时允许多个线程读取一个资源,但只有一个线程可以写入该资源。
*公平锁(FairLock)**:公平锁保证等待时间最长的线程可以最先获得锁。
*非公平锁(NonfairLock)**:非公平锁不保证等待时间最长的线程可以最先获得锁。
*自旋锁(SpinLock)**:自旋锁是不需要操作系统参与的锁,它可以提高锁的性能。 耗cpu 建议在时间短的用
*乐观锁(OptimisticLock)**:乐观锁假设在大多数情况下,多个线程不会同时访问同一个资源,因此它不会对资源进行加锁。
如果需要保证同一时间只有一个线程可以访问某个资源,那么可以使用互斥锁。如果需要同时允许多个线程读取一个资源,但只有一个线程可以写入该资源,那么可以使用读写锁。如果需要保证等待时间最长的线程可以最先获得锁,那么可以使用公平锁。如果不需要保证等待时间最长的线程可以最先获得锁,那么可以使用非公平锁。如果需要提高锁的性能,那么可以使用自旋锁。如果需要在读操作时不加锁,那么可以使用乐观锁
from
https://www.yuque.com/tulingzhouyu/sfx8p0/dr3wi1f7uxi961uo?singleDoc# 密码:bghr
JVM
类什么时候被加载?
*Hotspot虚拟机是按需加载的,在需要用到该类的时候加载
在Java中,类在以下几种情况下会被加载:
1. 创建对象:当使用 `new` 关键字创建类的实例时,该类会被加载。此时会先检查类是否已经加载,如果没有加载则会触发类的加载过程。
2. 访问静态成员:当访问类的静态成员(静态变量或静态方法)时,类会被加载。静态成员属于类级别的,需要类加载后才能访问。
3. 调用静态方法:当调用类的静态方法时,类会被加载。静态方法属于类级别的,需要类加载后才能调用。
4. 反射操作:使用反射机制(如 `Class.forName()` )加载类时,会触发类的加载过程。
5. 启动类(Main Class):当执行Java程序时,JVM会首先加载指定的启动类,即包含 `main()` 方法的类。
需要注意的是,类的加载过程是由Java虚拟机(JVM)负责的,它按需加载类并进行必要的链接和初始化操作。类的加载是一个动态的过程,可以在运行时根据需要进行加载,而不是一次性加载所有的类。
值得一提的是,类的加载过程包括加载、链接和初始化三个阶段。加载阶段是将类的字节码文件加载到内存中;链接阶段包括验证、准备和解析等操作;初始化阶段是对类的静态变量赋初值,并执行静态代码块。
jvm 一个类的加载过程
JVM(Java虚拟机)在运行Java程序时,通过类加载器(ClassLoader)来加载类。类的加载过程包括加载、链接和初始化三个阶段。
1. 加载(Loading):加载阶段是将类的字节码文件(通常是.class文件)从文件系统、网络等位置读入到内存中。类加载器负责查找并加载类的字节码文件。加载阶段包括以下步骤:
(加载到元空间/方法区)
(可以自定义类加载器-extends ClassLoader),可以干预
- 通过类的全限定名获取字节码文件的二进制数据。
- 将字节码数据转换为方法区内部的数据结构(运行时常量池、字段、方法等)。
- 在内存中生成一个代表该类的Class对象,作为程序访问类的入口。
2. 链接(Linking):链接阶段是对加载的类进行校验、准备和解析等操作。链接阶段包括以下步骤:
- 验证(Verification):验证字节码文件的结构、语义以及符合Java虚拟机规范的约束。
- 准备(Preparation):为类的静态变量分配内存,并设置默认初始值。常量赋值
- 解析(Resolution):将符号引用替换为直接引用,即将类、字段、方法等符号引用解析为实际内存地址。
3. 初始化(Initialization):初始化阶段是对类的静态变量进行赋值,并执行静态代码块。初始化阶段会按照类的初始化顺序依次执行静态变量的赋值和静态代码块的代码。初始化阶段是类加载过程中的最后一个阶段。
需要注意的是,类的加载过程是按需进行的,即当使用到某个类时才会进行加载。而且在整个加载过程中,JVM会对类进行缓存,避免重复加载。
类加载过程的细节和具体实现可能会因为不同的JVM实现而有所差异,但大体上遵循上述的加载、链接和初始化三个阶段。
4.使用
5.销毁(一般不会销毁)
一个类的初始化过程
1静态(常量,变量,初始化快)
2普通(变量,初始化快)
构造方法。
继承的时候 先父子类的静态 再其他
什么是类加载器
通过一个类名 来获取该类的二进制字节流,这个动作的代码,被吃完类加载器,可以自定义实现
有两种1:jvm自带的 启动类加载器
2:java实现的继承java.lang.ClassLoader
jdk层面: 1启动类加载器
它负责加载Java运行时环境所需的类。 JDK启动类加载器主要加载以下几类核心类库:
1. java.lang包中的类:如Object、String等。
2. java.util包中的类:如List、Map等。
3. java.io包中的类:如File、InputStream等。
4. java.net包中的类:如URL、URLConnection等。
5. java.util.concurrent包中的类:如Executor、ConcurrentHashMap等。
2 扩展类加载器(加载仓库)
JRE的扩展目录是指JRE安装目录下的 lib/ext
目录,该目录用于存放JDK或其他第三方库的扩展类库。
3 应用程序类加载器,(加载用户类下的应用)
jvm类加载的双亲委派模型
* 一个类加载器收到请求 先尝试让父类加载,
* 应用类加载器 委派给 启动类和扩展类去加载 (从低向顶委派)
* 启动类先尝试加载 然后扩展类尝试,最好应用类尝试,没有就异常(从顶向下尝试)
好处
*确保安全,避免核心类被修改
*避免重复加载,保证类的唯一
Tomcat 打破双亲委派机制的原因是,它需要加载一些由第三方提供的类,而这些类的父类是由 Tomcat 自己提供的。
例如,Tomcat 需要加载 JSP 页面中的类,而 JSP 页面中的类是由第三方提供的。如果 Tomcat 不打破双亲委派机制,那么 Tomcat 就无法加载 JSP 页面中的类。
另外,Tomcat 还需要加载一些由第三方提供的 JAR 包,而这些 JAR 包中的类的父类是由 Tomcat 自己提供的。如果 Tomcat 不打破双亲委派机制,那么 Tomcat 就无法加载这些 JAR 包中的类。
因此,Tomcat 需要打破双亲委派机制,以便能够加载由第三方提供的类和 JAR 包。
但是,Tomcat 打破双亲委派机制可能会带来安全风险。因此,Tomcat 在打破双亲委派机制时,会采取一些措施来降低安全风险。
例如,Tomcat 会在启动时,将所有由第三方提供的类和 JAR 包都放在一个单独的目录中。这样,Tomcat 就可以在加载这些类和 JAR 包时,进行严格的安全检查。
另外,Tomcat 还会在加载这些类和 JAR 包时,使用一个单独的类加载器。这样,如果这些类和 JAR 包中存在恶意代码,那么这些恶意代码就无法影响 Tomcat 的其他部分。
总之,Tomcat 打破双亲委派机制是为了能够加载由第三方提供的类和 JAR 包,但是这可能会带来安全风险。因此,Tomcat 在打破双亲委派机制时,会采取一些措施来降低安全风险。
jvm的内存怎么划分
1方法区(元空间):存储虚拟机加载的字节码数据,静态变量常量,运行时常量池(公有)
2栈stack:栈帧,局部变量,操作数据,动态链接(找到方法区),方法出口
深度可以通过递归查看,stackOverFlowError
oom很少,hotspot不会(提前申请,不会扩),可能在申请的时候报错,
-Xss1M 默认1M(一般不设置)
3本地栈:native的方法
和栈相似,不过只是运行native的
Thread类的本地方法 System类的本地方法
hotspot将和本地方法栈和栈合并了 内存区域
4程序计数器:记录线程运行的状态(记录执行到哪一行,
不存在内存溢出(很小)
无gc)
5堆heap,创建所有对象(公有)
*线程共享
*虚拟器启动创建,最大的一块区域
*存对象,数组
*GC主要回收
*分新生代。老年代 1:2
*新生代细化:eden,from Survivor,to Survivor 8:1:1
*Xmx最大堆内存, Xms初始化堆内存 调整
*可以分配出各个线程缓冲区 TLAB ,以提示对象分配的效率(没有锁)-先放私有的再放公共
jvm中对象如何在堆内存分配
1指针碰撞(在内存整齐,连续,情况下)
* 向数组移动指针一样
2空闲列表(在内存不规则情况下)
*记录有空闲列表
!选哪种方式是有java堆决定的,空间是否规则是由垃圾收集器是否带有压缩功能compact决定的
serial,parNew(有压缩整理),CMS基于清除算法(sweep)(没有)
3本地线程分配缓存tlab:
-xx:useTLAB (默认大小512k)
给每个线程分配小内存,(使用完才会去使用公共的内存)
解决多线程指向相同对象问题
1同步锁定,采用CAS配上失败重试
2TLAB
堆对象的组成
1对象头
1自身运行的数据: 哈希码,GC分代年龄,锁状态标志,偏向锁线程id
2类型指针:指向元空间的类
2实例数据
程序代码中的各种成员变量(包含父类的)
3对齐填充
不是必须的,占位,保证是对象某个字节的整数倍 (hotspot是8字节的整数倍)
*对齐填充的主要目的是为了提高 CPU 访问内存的效率,如果不进行对齐填充,对象头之后的内存空间可能不是 8 字节的倍数,这将导致 CPU 在访问这部分内存时需要进行额外的操作
jvm如何判断对象可以回收
*可达性分析法。判断算法有GC root
哪些对象可以作为GC root
*栈/本地方法栈中引用的对象 局部变量,参数。临时变量等
*方法区/元空间 静态属性引 或者常量引用对象
堆回收过程
*每次回收eden区的,和有数据的 s区,将不能回收的放到另外一个没有数据的S区
*15岁 /6 CMS的对象就放到老年代
动态年龄判断:
survivor从小到大累加年龄超过空间的50%(默认) ,后面的会直接晋升老年代
什么是老年代空间担保机制-避免频繁的full gc
1每次minor GC的时候 检查一下 老年代剩余空间是否大于新生代对象大小
2.1空间足够 就直接minor GC
2.2空间不足就判断大于之前minor GC后接入老年代的平均大小
2.2.1空间够居直接minor GC,如果minor GC后老年代装不下就 触发Full GC
2.2.2空间不足就直接 full GC
如果full GC 之后还是放不下minor GC的存活对象就会oom
什么情况对象会进入老年代
1满15岁
2动态年龄判断
3老年代担保机制
4大对象直接进入老年代
java的不同引用类型
1强引用:(new Object())不回收
2软引用 :内存够就保留,内存不足的时候就回收,(用在缓存里面)例如mybatis的缓存
3若引用:GC运行就回收
4虚引用:其作用就是被GC回收的时候触发一个系统通知(几乎不用)
JVM中的CAS(Compare and Swap)是一种并发编程的原子操作,用于实现多线程环境下的线程安全操作。CAS操作包括三个参数:内存位置(通常是一个变量的内存地址)、期望值和新值。它的执行过程如下:
1. 首先,JVM会读取内存位置的当前值(期望值)。
2. 然后,JVM会比较期望值和内存位置的当前值是否相等。如果相等,则说明该位置的值没有被其他线程修改,可以进行更新操作。
3. 如果相等,JVM会将新值写入内存位置,并返回更新成功;如果不相等,则说明该位置的值已经被其他线程修改,CAS操作失败,需要重新尝试。
CAS操作是一种乐观锁的实现方式,它避免了使用传统的锁机制带来的线程阻塞和上下文切换的开销。CAS操作在并发环境下可以实现高效的数据共享和同步。
然而,CAS操作也存在一些问题。例如,CAS操作在忙等待的过程中可能会消耗大量的CPU资源。此外,由于CAS操作的原子性只能保证单个变量的操作,对于多个变量之间的复合操作,需要额外的措施来保证原子性。
选择垃圾回收器
选择垃圾回收器时,需要根据应用程序的需求、硬件资源、内存大小等因素进行综合考虑。
-
Serial GC 适用于资源受限的计算机,最早的,(新生代)
*因为它是一个单线程的垃圾回收器应用程序的执行会stw。
*简单易用的特点,适用于简单的应用程序和开发环境。
*是client模式下的默认收集器(现在服务器一般是service模式)
*Serial old GC配合使用 -
Serial old GC(老年代)
可以成为CMS的备用 -
ParNew GC 适用于资源受限的计算机,(新生代)
*因为它是一个多线程的垃圾回收器,可以提高垃圾回收的效率。
*但是,ParNew GC 需要与应用程序同步,因此在应用程序执行时,可能会受到影响。
*CMS配合使 -
Parallel GC (新生代) 15岁
自动配置
*jvm默认的垃圾 回收器
*可以配置吞吐量,(预期停顿时间)(就是GC执行时间,例如1)
*开启自适应新生代大小策略(新生代大小,eden区和S区比例,直接晋升老年代对象大小)
*适合不怎么会调试的
*配合Parallel Old GC (老生代) -
Parallel Old GC (老生代)
-
CMS GC 适用于需要高吞吐量的场景,(老年代)
*追求最短停顿时间
*常用标记-清除,和标记-整理
*因为它是一个低延迟、高吞吐量的垃圾回收器,并且提供了可预测的垃圾回收延迟。
*适用于需要高吞吐量的应用程序,例如 Web 应用程序和大数据处理应用程序。6岁
*线程数默认是cpu的核心数
*默认配合ParNew GC
*可以结合Serial old GC(老年代)(备用)
运作过程
1初始标记-会STW ,标记Gc Root直接管理的对象-存活对象
2并发标记-不会STW,追踪整个GC root 链路
3重新标记-会STW, 对2阶段变化的对象标记
4并发清除-不会STW,清除标记阶段死亡的对象
缺点:
*并发清除会占用CPU
*会产生浮动垃圾
*不能填满老年代(jdk1.6 默认92%,jdk1.8默认 80%) 因为要和程序并发执行
*空间预留不够就会出现 并发清除失败concurrent Mode failure
*然后就启动Serial old GC配合使用
*会产生内存碎片
- G1 GC 适用于需要高吞吐量的场景,新老年代)不能搭配使用--
-
*目前最先进
*jdk9 默认 ,也不推荐CMS
*可以设置预期停顿时间(默认200ms)核心(在指定时间回收更多对象)
*因为它是一个低延迟、高吞吐量的垃圾回收器。G1 GC *适用于需要高吞吐量的应用程序,例如 Web 应用程序和大数据处理应用程序。
*怎么实现可预测停顿时间
1,将堆划分为大小相等的区(region)(有:eden区,s区,old区,H大对象区)
1M-32M 最多2048个
2,计算每个区的回收价值,和回收时间
3,将区作为最小回收单位,维护应该优先级(回收价值)列表 -
Eden区到60%才开始回收(复制回收)
大对象:超过region的一半,可以存放在多个region里面
老年代运作过程
1初始标记-会STW ,标记Gc Root直接管理的对象-存活对象
2并发标记-不会STW,追踪整个GC root 链路
3重新标记-会STW, 对2阶段变化的对象标记
4筛选回收-会STW,清除标记阶段死亡的对象
复制算法
*minor GC
*major GC 回收老年区
*full GC 区别回收 堆/元空间
Mixde GC 混合回收 目前只有G1)
混合回收(特有)
-
什么情况下使用
*大内存堆大小6G
*G1 GC *适用于需要高吞吐量的应用程序,例如 Web 应用程序和大数据处理应用程序
-
ZGC实验阶段
-
总结:根据应用程序的需求、硬件资源、内存大小等因素,可以选择合适的垃圾回收器进行使用。对于资源受限的计算机,可以考虑使用 Serial GC 和 ParNew GC;对于需要高吞吐量的场景,可以考虑使用 CMS GC 和 G1 GC。
jvm本机直接内存的作用
*不属于jvm运行时数据区,是本机直接内存
*通过NIO基于通道和缓存的方式可以直接对内存进行操作,提高性能 如零拷贝
jvm内存相关的核心参数
*-Xms 堆内存大小
*-Xmx 堆内存最大值
*-Xmn 堆内存新生代大小 -扣除剩下的就是老年代
*-XX: survi 设置eden和s区的大小比例 默认 8:1:1
*-XX:Met.. 元空间大小
*-XX:Met.. 元空间最大值
*-Xss 每个线程栈大小
-XX :+ useConcMarkSeepGc 设置CMS 垃圾收集器
-XX:+UseG1GC 设置G1 垃圾收集器
垃圾回收类型
*minor GC
*major GC 回收老年区
*full GC 区别回收 堆/元空间
Mixde GC 混合回收 目前只有G1
jvm垃圾回收算法
*复制算法(标记复制算法) -(新生代
*标记-清除算法 (老年代
*标记-整理算法(老年代
*分代收集算法 (思想
(都是基于最基础的可达性算法)
*标记-清除算法(老年代)
*分标记和清除两个阶段
*标记:标记出要回收的对象,不移动存活对象
*清除:清理被标记的对象
优点:基于最基础的可达性算法,实现简单
缺点:
*产生大量不连续的内存碎片(如果又有大对象放不下就是触发下一次垃圾回收)
*导致执行效率不稳定-大量的标记和清除消耗性能
*复制算法 (标记复制算法) (一般适合新生代)
*将内存复制成大小相同的两块,1:1
*每次只使用一块
*使用完就将存活(标记)的复制到另一块上面,然后把其他的一次性删除
优点:实现简单,效率高,解决了内存碎片的问题
缺点:*浪费空间(缩小一半),
*对象存活高的时候 复制比较多,效率会低 (不适合老年代,一般适合新生代)
jvm优化这个算法 改比例 8:1 就是 Eden区和S区
*标记-整理算法 (一般适合老年代)
标记-清除算法的优化
*标记
*整理:整理存活的对象,移动到一端,然后清除边界外的内存,移动的时候会STW
虽然影响性能,但总比有内存碎片好
CMS垃圾收集器平时一般用标记-清除,当碎片到一定程度才会用 标记-整理
优点:提高空间利用率,没有内存碎片
效率:效率低 会STW,除了标记还要整理
*分代收集算法(思想)
*一般jvm都会采用这种 思想
*就是将内存分为新生代和老年代,然后各年代采用不同的收集算法
内存溢出和内存泄漏
*内存溢出:申请空间不足的时候 oom
*内存泄漏:程序运行后没有释放,不能被回收,Memory Leak
例如单例对象(),网络连接,io连接 socket连接
线上环境JVM设置多大
栈:1M 默认300线程 300M
堆:4G
CMS 1/3 2/3
元空间 默认或者513
运行时内存飙升怎么排查
运行时CPU飙升怎么排查
jvm堆溢出之后是否可以继续工作
*可以工作
情况1:溢出之后释放
情况2:溢出之后不释放 可以访问 慢,也可能不能访问