Java:跨平台的语言
write once, run anywhere
JVM:跨语言的平台
Java虚拟机根本不关心运行在其内部的程序到底是使用何种编程语言编写的,它只关心“字节码”文件。
Java不是最强大的语言,但是JVM是最强大的虚拟机。
JVM的整体结构
这个架构可以分成三层看:
- 最上层:javac编译器将编译好的字节码class文件,通过java 类装载器执行机制,把对象或class文件存放在 jvm划分内存区域。
- 中间层:称为Runtime Data Area,主要是在Java代码运行时用于存放数据的,从左至右为方法区(永久代、元数据区)、堆(共享,GC回收对象区域)、栈、程序计数器、寄存器、本地方法栈(私有)。
- 最下层:解释器、JIT(just in time)编译器和 GC(Garbage Collection,垃圾回收器)
常见的JVM
- SUN公司的 HotSpot VM
- BEA 的 JRockit --> 不包含解释器,服务器端,JMC
- IBM 的 J9
类的加载
ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
类的加载过程
见https://blog.csdn.net/weixin_44743245/article/details/129231581
类的加载器
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)

这里的四者之间的关系是包含关系。不是上层下层,也不是子父类的继承关系。
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。为什么要自定义类加载器?
- 隔离加载类,避免类冲突
- 修改类加载的方式,根据实际情况在某个时间点按需动态加载
- 扩展加载源:网络、数据库、机顶盒
- 防止源码泄漏
双亲委派机制
工作原理
1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
优势
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
自定义类:java.lang.String
自定义类:java.lang.ShkStart
内存结构篇
程序计数器
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
虚拟机栈
栈桢(Stack Frame)
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
局部变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量
这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈
又称为表达式栈,后进先出。在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。并非采用索引访问。
动态链接
- 每一个栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
- 在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
本地方法栈
- Java 虚拟机栈用于管理Java 方法的调用,而本地方法栈用于管理本地方法的调用。
- 本地方法是使用C语言实现的。
- 它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
堆
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)。
在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间。
默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
对象分配过程
n欸村
内存分配策略
-
优先分配到Eden
-
大对象直接分配到老年代
-
长期存活的对象分配到老年代
-
空间分配担保(-XX:HandlePromotionFailure)
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,
如果大于,则此次Minor GC是安全的
如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。
如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。在JDK 6 Update 24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察OpenJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
-
动态对象年龄判断
方法区
对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。所以,方法区看作是一块独立于Java 堆的内存空间。
垃圾回收篇
垃圾回收算法
垃圾标记阶段算法
引用计数算法
原理:
对于一个对象A,只要有任何一个对象引用了A ,则A 的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A 的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
- 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java 的垃圾回收器中没有使用这类算法。
可达性分析算法
原理:
其原理简单来说,就是将对象及其引用关系看作一个图,选定活动的对象作为 GC Roots,然后跟踪引用链条,如果一个对象和GC Roots之间不可达,也就是不存在引用链条,那么即可认为是可回收对象。
优点:
实现简单,执行高效 ,有效的解决循环引用的问题,防止内存泄漏。
垃圾清除阶段算法
标记-清除算法
标记:对存活的对象进行标记。
清除:清除没有标记的,也就是垃圾对象。
缺点:
1、效率比较低:递归与全堆对象遍历两次
2、在进行GC的时候,需要停止整个应用程序,导致用户体验差
3、这种方式清理出来的空闲内存是不连续的,产生内存碎片。
复制算法
优点:
没有标记和清除过程,实现简单,运行高效
复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点:
此算法的缺点也是很明显的,就是需要两倍的内存空间。
另外,对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,这个开销也不小,不管是内存占用或者时间开销。
应用场景:
在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
比如:IBM 公司的专门研究表明,新生代中 80% 的对象都是“朝生夕死”的。
标记-压缩算法
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM 的设计者需要在此基础之上进行改进。标记 - 压缩(Mark - Compact)算法由此诞生。
优点:(此算法消除了“标记-清除”和“复制”两个算法的弊端。)
消除了标记/清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
消除了复制算法当中,内存减半的高额代价。
缺点:
从效率上来说,标记-压缩算法要低于复制算法。
效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
对于老年代每次都有大量对象存活的区域来说,极为负重。
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
移动过程中,需要全程暂停用户应用程序。即:STW
分代收集算法
目前几乎所有的GC都是采用分代收集(Generational Collecting)算法执行垃圾回收的。
在HotSpot 中,基于分代的概念, GC 所使用的内存回收算法必须结合年轻代和老年代各自的特点。
年轻代(Young Gen)
年轻代特点是区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
老年代(Tenured Gen)
老年代的特点是区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
分区算法
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
4种引用
- 强引用:不回收
- 软引用:内存不足即回收
- 弱引用:发现即回收
- 虚引用:对象回收跟踪
垃圾回收器
-
按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。
-
按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。
并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
独占式垃圾回收器( Stop the world)一旦运行,就停止应用程序中的其他所有线程,直到垃圾回收过程完全结束。 -
按碎片处理方式可分为压缩式垃圾回收器和非压缩式垃圾回收器。
压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
非压缩式的垃圾回收器不进行这步操作。 -
按工作的内存区间,又可分为年轻代垃圾回收器和老年代垃圾回收器。
GC评估指标
- 吞吐量:程序的运行时间(程序的运行时间+内存回收的时间)。
-
垃圾收集开销:吞吐量的补数,垃圾收集器所占时间与总时间的比例。
-
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
-
收集频率:相对于应用程序的执行,收集操作发生的频率。
-
内存占用: Java 堆区所占的内存大小。
-
快速: 一个对象从诞生到被回收所经历的时间。
吞吐量优先:单位时间内,STW的时间最短:0.2 + 0.2 = 0.4
响应时间优先:尽可能让单次STW的时间最短:0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5
吞吐量、暂停时间、内存占用共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。
这三项里,低延迟的重要性日益凸显。
Serial GC:串行回收
Serial 收集器采用复制算法、串行回收和“ Stop-the-World ”机制的方式执行内存回收。
**优势:**简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
在HotSpot 虚拟机中,使用 -XX: +UseSerialGC 参数可以指定使用年轻代串行收集器和老年代串行收集器。
等价于 新生代用Serial GC,且老年代用Serial Old GC
ParNew GC:并行回收
如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew 收集器则是Serial 收集器的多线程版本。
Par是Parallel的缩写,New:只能处理的是新生代
ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别,因为ParNew 收集器在年轻代中同样也是采用复制算法、“ Stop-the-World"机制。
在程序中,开发人员可以通过选项“ -XX:+UseParNewGC ”手动指定使用ParNew 收集器执行内存回收任务。它表示年轻代使用并行收集器。
Parallel GC:吞吐量优先
HotSpot 的年轻代中除了拥有ParNew 收集器是基于并行回收的以外, Parallel收集器同样也采用了复制算法、并行回收和“ Stop the World ”机制。
那么Parallel收集器的出现是否多此一举?和ParNew 收集器不同,Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。
“ -XX:+UseParallelGC ”手动指定年轻代使用Parallel 并行收集器执行内存回收任务。
“ -XX:+UseParallelOldGC” 手动指定年轻代和老年代都是使用并行回收收集器。
CMS:低延迟
在 JDK 1.5 时期,HotSpot 推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器——CMS (Concurrent-Mark-Sweep)收集器,这款收集器是 HotSpot 虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
在G1出现之前,CMS使用还是非常广泛的。
-XX :+UseConcMarkSweepGC:手动指定使用CMS 收集器执行内存回收任务
G1 GC:区域化分代式
G1(Garbage-First)是一款面向服务端应用的垃圾收集器,兼顾吞吐量和停顿时间的GC实现。
在JDK1.7版本正式启用,是JDK 9以后的默认GC选项,取代了CMS 回收器。
各GC使用场景
- Serial收集器:串行运行;作用于新生代;复制算法;响应速度优先;适用于单CPU环境下的client模式。
- ParNew收集器:并行运行;作用于新生代;复制算法;响应速度优先;多CPU环境Server模式下与CMS配合使用。
- Parallel Scavenge收集器:并行运行;作用于新生代;复制算法;吞吐量优先;适用于后台运算而不需要太多交互的场景。
- Serial Old收集器:串行运行;作用于老年代;标记-整理算法;响应速度优先;单CPU环境下的Client模式。
- Parallel Old收集器:并行运行;作用于老年代;标记-整理算法;吞吐量优先;适用于后台运算而不需要太多交互的场景。
- CMS收集器:并发运行;作用于老年代;标记-清除算法;响应速度优先;适用于互联网或B/S业务。
)]
各GC使用场景
- Serial收集器:串行运行;作用于新生代;复制算法;响应速度优先;适用于单CPU环境下的client模式。
- ParNew收集器:并行运行;作用于新生代;复制算法;响应速度优先;多CPU环境Server模式下与CMS配合使用。
- Parallel Scavenge收集器:并行运行;作用于新生代;复制算法;吞吐量优先;适用于后台运算而不需要太多交互的场景。
- Serial Old收集器:串行运行;作用于老年代;标记-整理算法;响应速度优先;单CPU环境下的Client模式。
- Parallel Old收集器:并行运行;作用于老年代;标记-整理算法;吞吐量优先;适用于后台运算而不需要太多交互的场景。
- CMS收集器:并发运行;作用于老年代;标记-清除算法;响应速度优先;适用于互联网或B/S业务。
- G1收集器:并发运行;可作用于新生代或老年代;标记-整理算法+复制算法;响应速度优先;面向服务端应用。