JVM原理

1 类的初始化过程

在这里插入图片描述

  • 加载
    • 通过一个类的全限定名来获取定义此类的二进制字节流。
    • 将这个字节流所代表的静态存储(Class文件本身)结构转化为方法区的运行时数据结构。
    • 在内存中生成这个类的java.lang.Class对象,作为方法区这个类的各种数据访问的入口。
  • 链接-验证:文件格式验证、元数据验证、字节码验证、符号引用验证。
  • 链接-准备:为类中静态变量分配内存并设置初始值。
  • 链接-解析:Java虚拟机将常量池内的符号引用替换为直接引用。
  • 初始化:初始化类变量,静态语句块等。其实就是执行类构造器()方法的过程。

创建子类对象的时候类的加载顺序

父类的静态字段——>父类静态代码块——>子类静态字段——>子类静态代码块——>父类成员变量(非静态字段)——>父类非静态代码块——>父类构造器——>子类成员变量——>子类非静态代码块——>子类构造器。

1.1 类进行初始化的时机

  • 1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发
    其初始化阶段。能够生成这四条指令的典型Java代码场景有:
    • 使用new关键字实例化对象的时候。
    • 读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
    • 调用一个类的静态方法的时候。
  • 2)使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
  • 3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  • 6)当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

2 双亲委派模型

在这里插入图片描述

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

2.1 双亲委派的原因

  • 相同对象无需重复加载,避免资源浪费。
  • 安全问题,避免已加载的类被恶意替换。

3 运行时栈帧的结构

在这里插入图片描述

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

  • 局部变量表:用于存放方法参数和方法内部定义的局部变量。
  • 操作数栈:也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。算术运算的时候是通过将运算涉及的操作
    数栈压入栈顶后调用运算指令来进行的,又譬如在调用其他方法的时候是通过操作数栈来进行方法参数的传递。
  • 动态连接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
  • 方法返回:返回上层方法调用的位置,恢复上层方法的执行状态。(恢复上层方法局部变量表,操作数栈)

3.1 方法调用的两种形式

一种形式是解析;另外一种形式是分派。

解析
所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析(Resolution)。

调用不同类型的方法,字节码指令集里设计了不同的指令。在Java虚拟机支持以下5条方法调用字节码指令:

  • invokestatic。用于调用静态方法。
  • invokespecial。用于调用实例构造器()方法、私有方法和父类中的方法。
  • invokevirtual。用于调用所有的虚方法。
  • invokeinterface。用于调用接口方法,会在运行时再确定一个实现该接口的对象。
  • invokedynamic。先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为“非虚方法”(Non-Virtual Method),与之相反,其他方法就被称为“虚方法”(VirtualMethod)。

分派
分派又分为静态分派动态分派

所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的,这点也是为何一些资料选择把它归入“解析”而不是“分派”的原因。

动态分派的实现过程,它与Java语言多态性的另外一个重要体现——重写(Override)有着很密切的关联。动态分派其实就是动态定位到实现类的方法进行调用。

4 运行时数据区存储结构

在这里插入图片描述

  • 程序计数器:记录正在执行的虚拟机字节码指令的地址。
  • 方法区:存储类的元数据,常量池,静态变量,即时编译器编译的代码缓存等。
  • :存放实例对象,对象的内存布局分为: 对象头、实例数据、对齐填充。
  • 虚拟机栈:存放局部变量表、操作数栈、动态链接、方法返回。
  • 本地方法栈:和虚拟机栈一样,只不过是给本地方法使用的。

5 对象如何被线程访问定位

通过栈上的reference数据来操作堆上的具体对象。但是它只是个对象的引用,该引用通过句柄或者使用直接指针方式去定位、访问到堆中对象的具体位置。由虚拟机实现而定。

5.1 句柄访问

在这里插入图片描述

5.2 直接指针访问

在这里插入图片描述

6 JVM 垃圾回收

6.1 对象是否存活-可达性分析法

Object没有到GCRoost的引用链,故可回收。

GCRoots包含

  1. 虚拟机栈中(栈帧中的本地变量表)引用的对象。
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  5. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  6. 所有被同步锁(synchronized关键字)持有的对象。
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

6.2对象的引用类型

引用类型描述垃圾回收
强引用程序代码中,传统的引用赋值例如:Object obj = new Object()垃圾收集器不会回收此引用的对象
软引用有用但非必须的对象系统将要发生内存溢出前会将此引用的对象列入回收范围之中进行第二次回收,如果这次回收还是没有足够内存空间才抛出内存溢出异常
弱引用非必须的对象,比软引用更弱一些只能生存到下一次垃圾回收发生为止
虚引用最弱的一种引用关系。无法通过这个找到对象实例唯一目的是在回收的时候收到系统通知
  • 强引用:程序代码中,传统的引用赋值,例如:Object obj = new Object()。垃圾收集器不会回收此引用的对象。
  • 软引用:有用但非必须的对象。系统将要发生内存溢出前会将此引用的对象列入回收范围之中进行第二次回收,如果这次回收还是没有足够内存空间才抛出内存溢出异常。
  • 弱引用:非必须的对象。比软引用更弱一些。只能生存到下一次垃圾回收发生为止。
  • 虚引用:最弱的一种引用关系。无法通过这个找到对象实例,唯一目的是在回收的时候收到系统通知。

6.3 回收时机

  1. 对象不可达,则标记。进行一次筛选后放入F-Queue队列。
  2. 虚拟机建立的低调度优先级的Finalizer线程去执行队列中对象的finalize()方法。

6.4 垃圾回收算法

算法描述
标记-清除标记可回收对象,清除对象。大量对象时效率低且会产生内存碎片。
标记-复制分为两个半区,对象只存放一个半区,还存活的对象复制到另一半区,然后清除本身这半区。大量需要清除对象时效率高且没有内存碎片,但是内存空间缩小了一半。
标记-整理标记对象,存活的对象往内存空间一端移动,然后清掉边界外的内存。
  • 标记-清除:标记可回收对象,清除对象。大量对象时效率低且会产生内存碎片。
  • 标记-复制:分为两个半区,对象只存放一个半区,还存活的对象复制到另一半区,然后清除本身这半区。大量需要清除对象时效率高且没有内存碎片,但是内存空间缩小了一半。
  • 标记-整理:标记对象,存活的对象往内存空间一端移动,然后清掉边界外的内存。

6.5 JVM的安全点

  • 根节点枚举:根节点枚举需要暂停用户线程。HotSpot编译的时候用OopMap记录栈里和寄存器里哪些位置是引用。
    导致OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外存储空间。
    实际上HotSpot也的确没有为每条指令都生成OopMap,只是在“特定的位置”记录了这些信息,这些位置被称为安全点(Safepoint)。

  • 安全的位置选取:是否具有让程序长时间执行的特征,也即指令序列的复用。例如方法的调用循环跳转异常跳转

  • 回收时如何让所有线程跑到最近的安全点

    1. 抢先式中断:系统把所有用户线程中断,不在安全点上则恢复执行,直到跑到安全点。
    2. 主动式中断:设置一个中断标志位,线程执行过程时不断去轮询这个标志位,如果标志位为true,则自己在最近的安全点上主动中断挂起。
  • 安全区域:有些线程处于sleep状态或者blocked状态,线程无法响应系统的中断请求,不能走到安全点去中断挂起自己,虚拟机也不会去等待它。这个时候就引入安全区域来解决。

  • 安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,在这个区域中任何地方开始垃圾收集都是安全的。

6.6 跨代引用收集

场景:假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。

并不只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集(Partial GC)行为的垃圾收集器,典
型的如G1、ZGC和Shenandoah收集器,都会面临相同的问题。

JVM 为了用尽量少的资源消耗解决跨代引用下的垃圾回收问题,引入了记忆集

在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节。目前最常用的一种记忆集实现形式种称为“卡表”,卡表中的每个记录精确到一块内存区域(每块内存区域称之为卡页),该区域内有对象含有跨代指针。

6.7 垃圾收集器

在这里插入图片描述

  • Serial:新生代单线程收集器,标记复制算法。
  • ParNew:多线程并行收集器,注重缩短垃圾收集时间。
  • Parallel Scavenge:注重吞吐量的新生代多线程并行收集器,采用标记-复制算法。吞吐量 = 运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)
  • Serial Old:年老代单线程收集器,标记整理算法。
  • Parallel Old:注重吞吐量的年老代多线程并行收集器,采用标记-整理算法。
  • CMS:一种以获取最短回收停顿时间为目标的收集器(并发收集、低停顿)。吞吐量低。标记-清除算法。Full GC再处理内存碎片。JDK1.9之后废弃了。
  • G1:基于Region的内存布局实现面向局部收集的设计思路。
    • 把连续的Java堆划分为多个大小相等的独立区域(Region)。
    • 每一个 Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
    • 回收最有价值的空间(即回收所获得的空间以及回收所需要时间的经验值)。
    • 后台维护一个优先级队列,记录哪些Region最有回收价值。
    • Humongous区域存储大对象(大小超过整个Region)。

6.7.1 parNew 垃圾收集过程

实质上是Serial收集器的多线程并行版本。
在这里插入图片描述
-XX:ParallelGCThreads 参数来限制垃圾收集的线程数。

6.7.2 Parallel Scavenge 垃圾收集过程

Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
在这里插入图片描述
-XX:MaxGCPauseMillis 最大垃圾收集停顿时间;
-XX:GCTimeRatio 设置吞吐量大小。
-XX:+UseAdaptiveSizePolicy 自适应的调节策略(GC Ergonomics)。

6.7.3 CMS垃圾收集过程

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。采用标记-清除算法。

垃圾收集过程

  • 1)初始标记(CMS initial mark)
  • 2)并发标记(CMS concurrent mark)
  • 3)重新标记(CMS remark)
  • 4)并发清除(CMS concurrent sweep)
    在这里插入图片描述

6.7.4 G1 垃圾回收过程

在这里插入图片描述

  1. 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
  2. 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
  3. 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
  4. 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

备注:跨代或者跨Region引用的解决方案:记忆集。需要耗费额外内存收。

7 JVM工具

  • jps:查看进程状况。
  • jstat(JVM Statistics Monitoring Tool):监视虚拟机各种运行状态信息。例如:类加载、内存、垃圾收集、即时编译等运行时数据。是运行期定位虚拟机性能问题的常用工具。
  • jinfo(Configuration Info for Java):实时查看和调整虚拟机各项参数。
  • jmap(Memory Map for Java):用于生成堆转储快照(一般称为heap dump或dump文件)。
  • jhat(JVM Heap Analysis Tool):与jmap搭配使用,来分析jmap生成的堆转储快照。
  • jstack(Stack Trace for Java):用于生成虚拟机当前时刻的线程快照(一般称为thread dump或者javacore文件)。

7.1 内存泄漏分析

  • 内存泄漏:对象存在无效引用导致不能被回收。
  • 分析
  1. jmap生成heap dump文件。
  2. jhat、VisualVM、Eclipse Memory Analyzer、IBM HeapAnalyzer分析heap dump文件。
  3. 分析占用空间比较多的class对象,检查该对象的instances以及reference引用。
  4. 最终定位程序代码。

常见内存泄漏场景:各种链接没有释放,例如:数据库链接,rpc连接;文件IO流没有关闭,ThreadLocal线程变量设置的变量使用完之后没有remove调。

8 Jvm调优

正常情况
■ Minor GC执行时间不到50ms;
■ Minor GC执行不频繁,约10秒一次;
■ Full GC执行时间不到1s;
■ Full GC执行频率不算频繁,不低于10分钟1次。

调优目标:在系统可接受的情况下达到一个合理的MGC和FGC的频率以及可接受的回收 时间(减少GC的频率和Full GC的次数)。

监控工具:jstact、Java VisualVM。

场景1:大并发场景下,Minor GC比较频繁。
分析:并发高,对象创建频繁。可以适当调整Eden区大小降低Minor GC频率,但会增大用户停顿时间,在可接受范围内调整。

场景2:Minor GC频繁且容易引发 Full GC.
分析:每次Minor GC 存活的对象大小是否能够全部移动到S1区,如果不能则会进入年老代。监控Minor GC存活的对象大小,调整Eden和S区的大小以及比例。

场景3:大对象创建频繁,导致Full GC频繁。
分析:找出业务中大对象创建的场景;业务优化,拆分大对象。或者调整JVM参数 -XX:PretenureSizeThreshold 设置进入年老代的对象大小(调大)。

场景4:MGC 与 FGC 停顿时间长导致影响用户体验。
分析:内存过大导致垃圾收集时间过长。减少堆内存大小,包括新生代和老年代,比如之前使用16G的堆内存,可以考虑将16G 内存拆分为4个4G的内存区域,可以单台机器部署JVM逻辑集群,也可以为了降低GC回收时间进行4节点的分布式部署,这里的分布式部署是为了降低GC垃圾回收时间。

其他JVM参数:

-Xms -Xmx:设置堆的最小和最大值。
-Xmn :新生代大小。
-XX:newSize -XX:MaxNewSize:新生代大小。
-XX:SurvivorRatio:Eden与Survivor区的比例。默认81
-XX:NewRadio:新生代和年老代的比例。
-XX:PretenureSizeThreshold:晋升年老代对象大小。
-XX:MaxTenuringThreshold:晋升年老代的年龄阈值,默认15-XX:MaxGCPauseMillis:最大停顿时长。
-XX:GCTimeRatio:设置吞吐量大小。
-XX:+UseAdaptiveSizePolicy:垃圾收集的自适应的调节策略。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值