JVM
java虚拟机(java virtual machine,JVM),一种能够运行java字节码的虚拟机。JVM不只是专用于java语言,只要生成的编译文件匹配JVM对加载编译文件格式要求,任何语言都可以由JVM编译运行。如kotlin、scala等。
jvm有很多,不只是Hotspot,还有JRockit、J9等。
一、JVM的基本结构
JVM由三个主要的子系统构成
- 类加载子系统
- 运行时数据区(内存结构)
- 执行引擎
二、类加载子系统
1、类的生命周期
加载->连接(验证,准备,解析)->初始化->使用->卸载
1.加载
将.class文件从磁盘读到内存
2.连接
2.1验证
验证字节码文件的准确性
2.2准备
给类的静态变量分配内存,并赋予默认值
2.3解析
类装载器装入类所引用的其他所有类
3.初始化
为类的静态变量赋予正确的初始值,上述的准备阶段为静态变量赋予的是虚拟机默认的初始值,此处赋予的才是编程编写者为变量分配的真正的初始值,执行静态代码块。
4.使用
5.卸载
2、类的加载器种类
启动类加载器(Bootstrap ClassLoader)
负责加载JRE的核心类库,如JRE目标下的rt.jar,charsets.jar等
扩展类加载器(Extension ClassLoadEer)
负责加载JRE扩展目录ext中jar类包
系统类加载器(Application ClassLoader)
负责加载ClassPath路径下的类包
用户自定义加载器(User ClassLoader)
负责加载用户自定义路径下的类包
2、类加载机制
全盘负责委托机制
当一个ClassLoader加载一个类的时候,除非显示的使用另一个ClassLoader,该类所依赖和引用的类也由这个ClassLoader载入
双亲委派机制
指先委托父类加载器寻找目标类,在找不到的情况下载自己的路径中查找并载入目标类
双亲委派模式的优势
- 沙箱安全机制:比如自己写的String.class类不会被加载,这样可以防止核心库被随意篡改
- 避免类的重复加载:当父ClassLoader已经加载了该类的时候,就不需要子ClassLoader再加载一次
GC算法和收集器
新生代:轻GC
老年代:full GC
如何判断对象可以被回收
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能被任何途径使用的对象)
引用计数法
给对象添加一个引用计数器,每当有一个地方引用,计数器就加1。当引用失效,计数器就减1。任何时候计数器为0的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中没有选择这个算法来管理内存,最主要的原因是它很难解决对象之前相互循环引用的问题。
可达性分析算法!!
这个算法的基本思想就是通过一系列的称为“GC Roots”的对象作为起点,这些节点开始向下搜索,节点走过的路径与当一个对象到GC Roots没有任何引用链相连的话,则证明此对象不可用的。
GC Roots根结点:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等等。
对象的引用:
强引用 软引用 弱引用 虚引用
如何判断一个常量是废弃常量
运行时常量池主要回收的是废弃的常量。
如果在常量池中存在字符串“abc”,如果当前没有任何String对象引用该字符串常量的话,就说明常量“abc”就是废弃常量。
如何判断一个类是无用的类
需要满足以下三个条件:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾回收算法
标记-清除算法
它是最基础的收集算法,这个算法分为两个阶段,“标记”和“清除”。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它有两个不足的地方:
1.效率问题,标记和清除两个过程的效率都不高。
2.空间问题,标记清除后会产生大量不连续的碎片。
复制算法
为了解决效率问题,复制算法出现了。它可以把内存分为大小相同的两块,每次只使用其中一块。当这一块的内存使用完后,就将还存活的对象复制到另一块区,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对 内存区间的一般进行回收。
标记-整理算法
根据老年代的特点提出的一种标记算法,标记过程和“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行回收,而是让所有存活的对象向一段移动然后直接清理掉边界以外的内存。
分代收集算法
现在的商用虚拟机的垃圾收集器基本都采用“分代收集”算法,这种算法就是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法,
在新生代中,每次收集都有大量对象死去,所以可以选择复制算法,只要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,就必须选择“标记-清除”或者“标记-整理”算法进行垃圾收集。
三、垃圾收集器
Java虚拟机规范对垃圾收集器应该如何实现没有任何规定,因为没有所谓最好的垃圾收集器出现,只能根据具体的应用场景选择合适的垃圾收集器。
Serial收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器。是一个单线程收集器。它的“单线程”的意义不仅仅意味着它只会使用一条垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有线程(“Stop The World”),直到它收集结束。
新生代采用复制算法,老年代采用标记-整理算法。
应用程序线程——GC线程应用程序暂停——应用程序线程
Serial收集器相比其他垃圾收集器更加简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial收集器对于运行在Client模式下的虚拟机来说是不错的选择。
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等)和Serial收集器完全一样。
新生代采用复制算法,老年代采用标记-整理算法。
应用程序线程——GC线程 多线程并发 应用程序暂停—— 应用程序线程
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器)配合工作。
Parallel Scavenge收集器
Parallel Scavenge收集器类似于ParNew收集器。
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成。
新生代采用复制算法,老年代采用标记-整理算法。
应用程序线程——GC线程 多线程并发 应用程序暂停—— 应用程序线程
Serial Old收集器
Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种是JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。
Parallel Old收集器
Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以用Scavenge收集器和Parallel Old收集器。
CMS收集器
并行和并发概念补充:
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU上。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集器同线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种”标记-清除”算法实现的,它的运作过程分为四个步骤:
- 初始标记(CMS initial mark):暂停所有的其他线程,并记录下直接与root相连的对象,速度很快。
- 并发标记(CMS concurrent mark):同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记(CMS remark):重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
- 并发清除(CMS concurrent sweep):开启用户线程,同时GC线程开始对为标记的区域做清扫。
CMS主要优点:并发收集、低停顿。
缺点:
- 对CPU资源敏感;
- 无法处理浮动垃圾;
- 它使用的回收算法“标记-清除”算法导致收集结束时会有大量空间碎片产生。
G1收集器
G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足GC停顿时间要求的同时,还具备高吞吐性能特征。(被视为JDK1.7中HotSpot虚拟机的一个重要进化特征)
- 并行和并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
- 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但还是保留了分代的概念。
- 空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器,从局部上来看是基于“”
- 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
G1收集器的运作步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率。
怎么选择垃圾收集器?
1、优先调整堆的大小让服务自己来选择
2、如果内存小于100m,使用串行收集器
3、如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
4、如果允许停顿时间超过1秒,选择并行或者JVM自己选
5、如果响应时间最重要,并且不能超过1秒,使用并发收集器
官方推荐G1,性能高
调优
JVM调优主要是调整下面两个指标
停顿时间:垃圾收集器做垃圾回收中断应用执行时间。-XX:MaxGCPauseMillis
吞吐量:垃圾收集的时间和总时间的占比:1/(1+n),吞吐量为1-1/(1+n)。-XX:GCTimeRatio=99
GC调优步骤 !!
1、打印GC日志
1 | idea |
测试:代码调用system.gc后输出以下内容:
Tomcat可以直接加载Java_OPTS变量里
2、分析日志得到关键性指标
3、分析GC原因,调优JVM参数
使用GCeasy分析
四、运行时数据区
1、内存私有
程序计数器
较小的内存空间,线程所执行的字节码的行号指示器,线程私有
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。每个线程都有一个独立的程序计数器。
1、如果执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址
2、如果正在执行的是Native方法,这个计数器值则为空。
Java虚拟机栈
线程私有的,生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型。
每个方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
通常我们讲的栈就是局部变量表部分。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char…..)、对象引用(reference类型)、returnArrdress类型(指向了一条字节码指令的地址)
本地方法栈
本地方法栈则为虚拟机使用到的Native方法服务。
本地方法栈区域也会抛出StackOverflowError的OutOfMemoryError异常
Java堆
是JVM所管理的内存最大的一块。
唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
java堆是垃圾收集器管理的主要区域,有时候也称为GC堆
Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
如果在堆中没有完成实例分配,并且堆也无法再扩展时。会抛出OutOfMemoryError异常
方法区
方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池是方法区的一部分
执行引擎
新生代 老年代
Java堆是Java虚拟机管理的最大的一块内存空间,主要存放对象实例。
在Java中,堆被分为两块区域:新生代、老年代。
堆大小=新生代+老年代。(分别占堆空间为1/3、2/3)
新生代
新生代又被分为Eden、from survivor、to survivor(8:1:1)
新生代这样划分是为了更好的管理堆内存中的对象,方便GC算法–>“复制算法”来进行垃圾回收。
JVM每次只会使用Eden和其中一块survivor来为对象服务,所以无论什么时候,都会有一块survivor空间,因此新生代实际可用空间为90%。
新生代GC(minor gc):指发生在新生代的垃圾回收动作,因为Java对象大多数都是“朝生夕死”的特性,所以minor GC非常频繁,使用复制算法快速的回收。
新生代几乎是所有Java对象出生的地方,Java对象申请的内存和存放都是在这个地方。
当对象在Eden(包括一个survivor,假如是from),当此对象经过一次minor GC后仍然存活,并且能够被另一块survivor所容纳(这里的survivor则是to),则使用复制算法将这些仍然存活的对象复制到to survivor区域中,然后清理掉Eden和from survivor区域,并将这些存活的对象年龄+1,以后对象在survivor中每熬过一次则+1,当达到某个值(默认为15),这些对象会成为老年代!
事情不是绝对,有些较大的对象(需要分配连续的内存空间),则直接进入老年代。
老年代
老年代GC(major GC):指发生在老年代的垃圾回收动作,所采用的的是“标记–整理”算法。
老年代几乎都是从survivor中熬过来的,不会轻易“死掉”,因此major GC不会像minor GC那样频繁
什么叫复制算法
两块survivor,每次使用其中的块。当这一块使用完了,就将还存储着的对象复制到另一块survivor上面,然后再把已经使用过的内存空间一次清理掉,下图为示意图。
优点:不用考虑内存碎片问题,实现简单,运行效率高。
缺点:当对象存活较高(PS:老年代)时,就要进行较多的复制操作,效率会很低。
复制算法示意图
什么叫标记–整理算法
与“标记-清理”算法相似,只是后续不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,下图为示意图。