1 JVM 加载 class 文件的原理机制
JVM(Java Virtual Machine,Java虚拟机)加载class文件的原理机制主要包括以下几个步骤:
- 加载(Loading):JVM通过类的全名获取定义此类的二进制字节流,然后将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,最后在堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。加载过程主要由类加载器完成,包括启动类加载器、扩展类加载器和应用程序类加载器等。类加载器采用双亲委派模型,即先让父类加载器尝试加载,只有当父类加载器无法完成时才由自己加载。
- 链接(Linking):链接包括验证、准备和解析三个阶段。验证是为了确保被加载的类文件信息符合JVM规范,无安全方面的问题;准备是给类的静态变量分配内存,并设置默认的初始值;解析是将符号引用转换为直接引用。
- 初始化(Initialization):初始化阶段是执行类构造器方法(())的过程。此方法由编译器自动收集类中的所有类变量的赋值动作和静态代码块集合来的。类构造器方法中的指令按语句在源文件中出现的顺序执行。
在JVM中,类的加载过程具有动态性,即并不是一次性加载所有类,而是按需加载。当一个类被使用时,才会被加载到内存中。这种动态加载的方式有助于节省内存资源,提高程序的运行效率。
JVM(Java虚拟机)加载class文件的原理机制是一个复杂的过程,它确保了Java应用程序的正常运行。以下是该过程的基本概述:
- 装载工作:Java中的所有类,在能够被JVM执行之前,必须被装载到JVM中。这个装载工作是由JVM中的类装载器(ClassLoader)及其子类来完成的。类装载器的主要工作就是将类文件从硬盘读取到内存中。
- 类装载器的层级结构:在JVM中,类装载器之间存在层级关系。当JVM需要加载某个类时,会首先委托给父类装载器去尝试加载。如果父类装载器无法加载这个类,那么子类装载器才会尝试去加载。这就是类装载器的“委托机制”。此外,子类装载器可以看见所有父类装载器加载的类,但父类装载器无法看见子类装载器加载的类,这是“可见性原理”。同时,由于委托机制的存在,一个类只会被加载一次,这保证了类的“单一性”。
- 类的加载方式:类的加载方式主要有两种:隐式加载和显式加载。隐式加载通常发生在程序运行过程中,当碰到需要通过new等方式生成对象时,JVM会隐式调用类装载器加载对应的类。而显式加载则是通过诸如
Class.forName()
等方法,显式地请求加载某个类。 - 类的加载过程:当JVM加载class文件时,首先会读取文件中的数据,通常是通过创建一个字节数组来读入.class文件,然后产生与所加载类对应的Class对象。此时,Class对象还不完整,需要经过连接阶段,包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后,JVM对类进行初始化,包括初始化父类(如果存在)以及执行类中的初始化语句。
- 动态性体现:Java程序在启动时,并不是一次性将所有类全部加载到JVM中,而是先将保证程序运行的基础类加载进来,其他类则等到JVM在运行时用到的时候再进行加载。这种按需加载的方式,有效地节省了内存开销,体现了JVM加载class文件的动态性。
总的来说,JVM加载class文件的原理机制是一个涉及类装载器、类的层级结构、加载方式、加载过程以及动态性等多个方面的复杂过程。这个过程的目的是确保Java程序能够在JVM上正确、高效地运行。
2 JVM运行时五大数据区
JVM(Java虚拟机)运行时数据区主要分为以下五个区域:
-
方法区(Method Area):
- 方法区是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 这个区域又被称为非堆(Non-Heap),目的是与Java堆进行区分。
- 方法区无法满足内存分配需求时,会抛出
OutOfMemoryError
异常。
-
堆(Java Heap):
- 堆也是所有线程共享的一块内存区域,它是JVM所管理的最大一块内存区域。
- 堆主要用于存放对象实例,几乎所有的对象实例都会在这里分配内存。
- Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。
- 堆可以细分为新生代和老年代;其中新生代又可以进一步细分为Eden区和Survivor区(包括From Survivor和To Survivor)。
-
虚拟机栈(Java Virtual Machine Stacks):
- 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。
- 每一个方法被调用直至执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 栈帧中存储了局部变量表、操作数栈、动态链接、方法出口等信息。
- 每一个方法被调用时都会创建一个新的栈帧,用于存储局部变量、操作栈、常量池引用等信息。
- 每一个方法调用完成后,对应的栈帧随之销毁。
-
本地方法栈(Native Method Stacks):
- 本地方法栈与虚拟机栈所发挥的作用非常相似,其区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
- 在HotSpot虚拟机中,本地方法栈和虚拟机栈是合二为一的。
-
程序计数器(Program Counter Register):
- 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
- 字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器的值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何
OutOfMemoryError
情况的区域。
这些区域各自承载着不同的任务,协同工作以支持Java应用程序的运行。
3 JVM垃圾回收算法
JVM(Java Virtual Machine)垃圾回收算法是Java内存管理的重要组成部分,用于自动回收不再使用的对象,释放内存空间。以下是JVM中常见的几种垃圾回收算法:
-
标记-清除算法(Mark-Sweep Algorithm)
这是最基本的垃圾回收算法。它分为两个阶段:标记阶段和清除阶段。在标记阶段,垃圾回收器会从根对象(如静态变量、栈中的引用等)开始,递归地访问所有可达的对象,并标记它们为存活。在清除阶段,垃圾回收器会遍历整个堆内存,回收未被标记(即不可达)的对象。优点:实现简单,不需要移动对象,因此效率较高。
缺点:会产生内存碎片,导致分配大对象时可能需要提前触发垃圾回收。 -
复制算法(Copying Algorithm)
为了解决标记-清除算法的内存碎片问题,复制算法将内存划分为两个等大小的区域。在任意时间点,只有其中一个区域被使用,另一个区域是空闲的。当进行垃圾回收时,存活的对象会被复制到空闲区域,然后整个使用过的区域会被一次性清理掉。优点:不会产生内存碎片,且对象复制过程中可以按内存块顺序进行,效率较高。
缺点:内存使用效率较低,因为只有一半的内存是可用的。另外,当存活对象较多时,复制开销会较大。 -
标记-整理算法(Mark-Compact Algorithm)
标记-整理算法是标记-清除算法的改进版。它在标记存活对象的基础上,将所有存活的对象都移动到一端,然后直接清理掉边界以外的内存。这样可以避免内存碎片问题,同时也不需要像复制算法那样浪费一半的内存空间。优点:解决了内存碎片问题,且内存使用效率较高。
缺点:对象移动的开销较大,尤其是在存活对象较多的情况下。此外,如果大多数对象都是存活的,那么这种算法的效率会较低。 -
分代收集算法(Generational Collection Algorithm)
分代收集算法是根据对象存活周期的不同将内存划分为几块的思想。一般将堆区划分为老年代和新生代,新生代中的对象大多数很快就会成为垃圾被回收,而老年代中的对象存活时间较长。针对不同区域可以使用最适合的收集算法以提高效率。例如,新生代可以使用复制算法,而老年代可以使用标记-清除或标记-整理算法。优点:可以根据不同区域的对象特性采用最合适的垃圾回收算法,从而提高垃圾回收效率。
缺点:需要维护多个内存区域,增加了内存管理的复杂性。同时,对于跨代引用(如老年代对象引用新生代对象)的处理也会增加一定的开销。
4 JVM垃圾收集器
JVM(Java Virtual Machine)常用的垃圾回收器有以下几种:
-
Serial收集器:
- 最古老的垃圾回收器,采用单线程进行垃圾回收。
- 在回收过程中会暂停所有的应用线程(Stop-The-World),因此适用于小型的应用场景或者对停顿时间没有严格要求的情况。
-
Parallel收集器(也称为Parallel Scavenge):
- 是Serial收集器的改进版,使用多线程进行垃圾回收。
- 同样会在回收过程中暂停所有的应用线程,但利用多核CPU的并行处理能力来提高回收效率,适用于多核CPU的服务器应用场景。
-
CMS(Concurrent Mark Sweep)收集器:
- 是一种以获取最短回收停顿时间为目标的垃圾回收器,使用多线程进行垃圾回收。
- 在回收过程中只暂停少量的应用线程,大部分回收工作与应用线程并发进行,适用于响应时间敏感的应用场景。
- 缺点是会产生内存碎片,并且无法处理浮动垃圾(在并发标记阶段新产生的垃圾对象)。
-
G1(Garbage-First)收集器:
- 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。
- 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
- G1收集器将整个Java堆划分为多个独立的块(Region),根据各个块中垃圾对象数量的多少和回收预期停顿时间的长短来优先回收垃圾对象最多的块,从而达到高效回收的目的。
这些垃圾回收器各有优缺点,适用于不同的应用场景和需求。在选择垃圾回收器时,需要根据具体的应用特点、性能要求和硬件环境来进行权衡和选择。
5 JVM垃圾回收对应算法
当然,不同的垃圾回收器在JVM中采用了不同的回收算法。以下是常见垃圾回收器及其所采用的回收算法的简要概述:
-
Serial收集器:
- 回收算法:复制算法。
- 特点:Serial收集器是一个单线程的收集器,它在进行垃圾收集时,必须暂停所有用户线程。它主要针对于新生代的垃圾收集,老年代则使用其他收集器(如Serial Old)。
-
Parallel Scavenge收集器:
- 回收算法:复制算法。
- 特点:Parallel Scavenge是一个新生代的多线程收集器,与Serial收集器类似,但它利用多个线程并行地进行垃圾收集,从而提高了效率。此收集器的主要目标是达到一个可控的吞吐量。
-
ParNew收集器:
- 回收算法:复制算法。
- 特点:ParNew收集器是Serial收集器的多线程版本。它在进行垃圾收集时,也是使用多个线程,从而减少了垃圾收集时的停顿时间。ParNew收集器通常与CMS收集器一起搭配使用,用于新生代的垃圾收集。
-
CMS(Concurrent Mark Sweep)收集器:
- 回收算法:标记-清除算法。
- 特点:CMS收集器是一个老年代的并发收集器,它使用多线程进行垃圾收集,并且尽可能地减少停顿时间。CMS收集器分为初始标记、并发标记、重新标记和并发清除四个步骤。其中,初始标记和重新标记阶段会暂停用户线程,但这两个阶段的时间相对较短。
-
G1(Garbage-First)收集器:
- 回收算法:G1收集器采用了一种混合的收集策略,它结合了标记-清除、标记-整理和复制算法。
- 特点:G1收集器将整个堆内存划分为多个独立的块(Region),并且根据每个块中垃圾对象的数量和回收预期停顿时间的长短来优先回收垃圾对象最多的块。G1收集器旨在满足高吞吐量的同时,也提供可预测的停顿时间。
需要注意的是,垃圾回收器的选择和配置应根据具体的应用场景、性能要求和硬件环境来进行权衡和选择。不同的垃圾回收器和算法在不同的场景下可能表现出不同的性能和效果。
6 JVM参数
标准参数
1 ‐version
2 ‐help
3 ‐server
4 ‐cp
些参数是JVM各版本中基本不会发生变化的参数,例如-version、-help等。它们提供了基本的运行时选项和功能,且相对稳定。
非标准参数
-X
1 ‐Xint 解释执行
2 ‐Xcomp 第一次使用就编译成本地代码
3 ‐Xmixed 混合模式,JVM自己来决定
非标准参数(-X参数):这类参数的变化比较小,并且默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容。这些参数主要用于JVM的调试和优化,如-Xint、-Xcomp和-Xmixed等。其中,-Xint表示解释执行,-Xcomp表示第一次使用就编译成本地代码,而-Xmixed表示采用混合模式,由JVM自动选择解释执行或编译成本地代码。
-XX
1 a.Boolean类型
2 格式:‐XX:[+‐]<name> +或‐表示启用或者禁用name属性
3 比如:‐XX:+UseConcMarkSweepGC 表示启用CMS类型的垃圾回收器
4 ‐XX:+UseG1GC 表示启用G1类型的垃圾回收器
5 b.非Boolean类型
6 格式:‐XX<name>=<value>表示name属性的值是value
7 比如:‐XX:MaxGCPauseMillis=500
-XX:NewRatio:指定新生代和老年代的比例,例如-XX:NewRatio=4表示新生代和老年代的比例为1:4。(hotspot默认2)
-XX:SurvivorRatio:指定新生代中Eden区和Survivor区的比例,例如-XX:SurvivorRatio=8表示Eden区和Survivor区的比例为8:2(hostspot默认8)
-XX:+PrintGC:开启打印GC日志信息,以便分析和调优垃圾收集性能。
-Xloggc:<filename> 参数可以指定 GC 日志的输出文件。例如:-Xloggc:/path/to/gc.log
在较新的 JVM 版本中(如 Java 9 及以后),可以使用统一的日志系统参数 -Xlog 来配置日志,包括 GC 日志。例如:-Xlog:gc*:file=/path/to/gc.log。这将配置 GC 日志并指定输出文件。
-XX:+HeapDumpOnOutOfMemoryError:当发生OutOfMemoryError时,自动生成堆转储文件,以便分析内存泄漏等问题。
-XX:HeapDumpPath=<path> 参数可以指定堆转储文件的输出路径。例如:-XX:HeapDumpPath=/path/to/dumps/。
非Stable参数(-XX参数):这类参数主要用于JVM的高级调优和Debug,变化相对较大,不建议在生产环境中使用。这类参数主要可以分为Boolean类型和非Boolean类型。Boolean类型的参数格式如-XX:+<option>或者-XX:-<option>,分别表示打开或关闭某个选项。非Boolean类型的参数则通过-XX:<option>=<number/string>的形式设定,例如设定堆的初始值和最大值等。
-D
-D 是一个 Java 虚拟机(JVM)的非标准参数,它用于设置系统属性值。这些属性可以在 Java 程序中通过 System.getProperty() 方法进行访问。属性名通常是一些有特定意义的字符串,属性值可以是任意字符串或其他数据类型。通过 -D 参数,我们可以在启动 Java 应用程序时为其配置一些运行时的环境或行为。
例如,我们可以使用 -Dfile.encoding=UTF-8 来设置 Java 程序的默认文件编码为 UTF-8。这将在程序运行期间影响文件的读写操作。需要注意的是,-D 参数设置的属性值对整个 Java 应用程序都是可见的,因此应该谨慎使用,避免引入不必要的副作用或安全隐患。
其他参数
1 ‐Xms1000M等价于 ‐XX:InitialHeapSize=1000M
2 ‐Xmx1000M等价于 ‐XX:MaxHeapSize=1000M
3 ‐Xss100等价于 ‐XX:ThreadStackSize=100
常见参数 官网: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html