目录
JVM
一、JVM 组成部分(Java内存布局)(5部分)
- 线程私有区域:程序计数器、Java虚拟机栈、本地方法栈
- 线程共享区域:Java堆、方法区
1. 堆(线程共享)
所有创建的对象信息都放在这个区域中。堆也是JVM中最大的一块内存
堆的数据划分:
a> 新生代:新创建的数据会在新生代中,当经历了一定次数的GC(垃圾回收),依然存活下来的数据,会移动到老年代(HotSpot默认的垃圾回收次数是15)。
新生代又有三个区域:
b>老生代:存放的是经过一定次数GC还存活的对象和大对象
面试题:为什么大对象会直接存放在老年代中?
答:因为大对象的创建和销毁所需要的时间比较多,所以性能也比较慢,如果存到新生代中,那可能导致频繁的创建和销毁大对象,从而导致JVM运行效率的降低,所以就直接存到老年代中
2. JVM栈(先进后出)(线程私有)
栈帧内容:
1>局部变量表:8大基础类型和对象的引用信息
- 如果线程请求的栈深度大于虚拟机所允许的深度(-Xss设置栈容量),将会抛出StackOverFlowError异常。
- 虚拟机在动态扩展时无法申请到足够的内存,会抛出OOM(OutOfMemoryError)异常
2>操作栈:每个方法都会生成的栈
3>动态连接:指向字符串常量池
4>方法返回地址:PC 寄存器的地址
关于虚拟机栈会产生的两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverFlow异常
- 如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM异常
3. 程序计数器(线程私有)
用来记录线程执行的行号
4. 本地方法栈(线程私有)
和JVM栈类似,不过本地方法栈是给本地方法(c/c++)使用的,JVM栈是给java程序使用。
5. 元空间(JDK 1.7 方法区)(线程共享)
JDK 1.8 元数据区的改变:
- 元空间:1.8 之后才有的,将自己的内容(比如静态属性,常量,类元信息)放入本地内存。优点:JVM会有内存大小限制,这一部分内容会占用JVM的内容,元空间就不会受JVM最大运行内存的限制了,只和本地内存大小有关。
- JDK 1.8 将字符串常量池放到了堆中
二、JVM 类加载过程 Class Loading
JVM 类加载过程要符合双亲委派模型。
类加载双亲委派模型:
JVM 加载类的时候并不会直接进行类的加载,而是将任务交给父类,一层一层的传递。如果找不到父类或者在父类中找不到此类,才会自己尝试加载。
优点;
- 可以保证类的唯一性
- 安全性
破坏双亲委派模型:
第一次: JDK 1.2 引入双亲委派模型时,为了兼容老代码,出现了第一次破坏双亲委派模型
第二次:是双亲委派模型自身的缺点所导致的第二次破坏,比如当父类出现了调用子类的方法的时候
第三次:人们对于热更新和热加载的追求,导致了第三次双亲委派模型的破坏
类加载过程:
1. 加载 Loading:将全限类名的二进制流加载到内存中
- 通过完整的类路径,找到二进制字节流的类
- 将静态字节流转换成方法区的数据
- 在内存中生成方法入口
2. 验证:就是用来验证加载的信息是否符合JVM规范
- 文件格式
- 字节码
- ···
3. 准备
将类中的静态变量初始化到内存中
例如:public static int num1 =123;(会在内存中存储 num1 = 0 )
4. 解析:
常量池中的符号引用直接替换为直接引用的过程
5. 初始化
此步骤才将程序的执行权交给程序,也就是说此步骤就开始执行类的相应方法(构造方法)
三、JVM 垃圾回收
1. 判断死亡对象
① 引用计数器算法
给每个对象生成一个对应的计数器,每次在进行引用的时候这个计数器+1,如果撤销引用的时候计数器-1,GC 会根据计数器的值,当此值为 0 的时候就可以判定此对象为死亡对象
缺点:无法解决对象的循环引用问题
② 可达性分析算法(目前JVM使用判断对象生死的算法)
此算法的核心思想为 : 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象
不可达)时,证明此对象是不可用的。
在Java语言中,可作为GC Roots的对象包含下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
Java对引用分为以下的四种类型:
- 强引用:Object obj = new Object(); 即使发生 OOM 垃圾回收器也不会进行垃圾回收
- 软引用:它的引用关系仅次于强引用,如果内存够用,那么垃圾回收不会考虑回收此引用,将要发生OOM 的时候才会回收此引用
- 弱引用:不管内存够不够用,下一次回收都会将此引用的对象回收
- 虚引用:创建即回收,它可以触发一个垃圾回收的回调函数
为什么 ThreadLocal 会将 key 设置为弱引用?
答:ThreadLocal 为了更大的避免OOM
2.垃圾回收算法
• 标记-清除算法
算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足主要有两个 :
- 效率问题 : 标记和清除这两个过程的效率都不高
- 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
• 复制算法(新生代回收算法)
"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再
把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。
将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。当回收时,将Eden和
Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
这是新生代主流的垃圾回收算法,改良版:Eden 80%、S0 10%、S1 10%
优点:性能比较快
缺点:内存利用率不高
• 标记-整理算法(老年代回收算法)
标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
可以解决内存碎片问题
3.垃圾回收器
3.1 Serial 收集器(新生代收集器,串行GC)
Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯 一选择。
特性:
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它
收集结束(Stop The World)。
应用场景:
Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。
优势:
简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。 实际上到现在为止 : 它依然是虚拟机运行在Client模式下的默认新生代收集器
3.2 ParNew收集器(新生代收集器,并行GC)
特性 :
Serial收集器的多线程版本
应用场景 :
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。
作为Server的首选收集器之中有一个与性能无关的很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。
在JDK 1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器——CMS收 集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线
程与用户线程同时工作。
不幸的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge 配合工作,所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中
的一个。
对比分析:
与Serial收集器对比:
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。然
而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。
3.3 Parallel Scavenge收集器(新生代收集器,并行GC)
特性:
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
Parallel Scavenge收集器使用两个参数控制吞吐量:
XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
XX:GCRatio 直接设置吞吐量的大小
```
直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停
顿70毫秒。停顿时间下降的同时,吞吐量也下降了。
应用场景:
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
对比分析:
Parallel Scavenge收集器 VS CMS等收集器:
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),牺牲了STW的时间为代价,比较适合纯后端系统。由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。
Parallel Scavenge收集器 VS ParNew收集器:
Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略。
GC自适应的调节策略:
Parallel Scavenge收集器有一个参数- XX:+UseAdaptiveSizePolicy 。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前
系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。
3.4 Serial Old收集器(老年代收集器,串行GC)
特性:
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
应用场景:
- Client模式 Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。
- Server模式 如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生 Concurrent Mode Failure时使用。
3.5 Parallel Old收集器(老年代收集器,并行GC)
特性:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
应用场景:
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
3.6 CMS收集器(老年代收集器,并发GC)
特性:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前 很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统
停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
CMS收集器是基于“标记—清除”算法实现的,整个过程分为4个步骤:
- 初始标记(CMS initial mark):初始标记仅仅只是标记一下GC Roots 能直接关联到的对象,速度很快,需要“Stop The World”。
- 并发标记(CMS concurrent mark):并发标记阶段就是进行GC Roots Tracing 的过程。
- 重新标记(CMS remark):重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”。
- 并行清除(CMS concurrent sweep) 并行清除阶段会清除对象。
3.7 G1收集器(JDK 11 默认的垃圾回收器)
G1(Garbage First)垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的region块,然后并行的对其进行垃圾回收。
G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩。
G1垃圾回收器回收region的时候基本不会STW(Stop The World),而是基于 most garbage优先回收(整体来看是基于"标记-整理"算法,从局部(两个region之间)基于"复制"算法) 的策略来对region进行垃圾回收的。无论如何,G1收集器采用的算法都意味着一个region有可能属于Eden,Survivor或者Tenured内存区域。
四、JMM(Java 内存模型)
JVM定义了一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
4.1 主内存与工作内存
Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存
进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
4.2内存间交互操作
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存中拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了如下8种操作来完成。
- lock(锁定) : 作用于主内存的变量,它把一个变量标识为一条线程独占的状态
- unlock(解锁) : 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取) : 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入) : 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用) : 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
- assign(赋值) : 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。
- store(存储) : 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便后续的write操作使用。
- write(写入) : 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
Java内存模型的三大特性 :
- 原子性 : 由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和read。大致可以认为,基本数据类型的访问读写是具备原子性的。如若需要更大范围的原子性,需要synchronized关键字约束。(即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行)
- 可见性 : 可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。volatile、synchronized、final三个关键字可以实现可见性。
- 有序性 : 如果在本线程内观察,所有的操作都是有序的;如果在线程中观察另外一个线程,所有的操作都是无序的。前半句是指"线程内表现为串行",后半句是指"指令重排序"和"工作内存与主内存同步延迟"现象。