JVM(Java Virtual Machine)内存区域主要分为以下几个部分:
程序计数器(Program Counter Register):是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,用来记录正在执行的虚拟机字节码指令的地址。
Java虚拟机栈(Java Virtual Machine Stacks):每个线程在执行Java程序时都会创建一个对应的Java虚拟机栈,用于存储局部变量、操作数栈、动态链接、方法出口等信息。Java虚拟机栈可以细分为Java栈(Java Stack)和本地方法栈(Native Method Stack)。
堆(Heap):是Java虚拟机管理的最大一块内存区域,被所有线程共享,用于存储对象实例和数组。堆是垃圾收集器的主要作用区域,可以细分为新生代和老年代等不同区域。
元空间(Metaspace):用于存储类信息、常量、静态变量、即时编译器编译后的代码等数据。
直接内存(Direct Memory):在Java堆外直接分配内存,由Java NIO引入,通过一种名为“内存映射文件”的机制,可以在堆外分配内存,提高数据读写效率。
元空间存储了运行时常量池和类常量池。在Java 8及其之前的版本中,JVM的方法区(Permanent Generation)主要用于存储类的元数据信息,例如类名、访问修饰符、字段、方法、接口等信息,这些信息对应着Java语言规范中的描述。而在Java 8之后,方法区被彻底移除,而元数据信息则被移到了一个新的内存区域——元空间(Metaspace)中。
元空间主要存储如下内容:
与方法区相比,元空间的内存管理方式有所不同。元空间中的类元数据信息是使用本地内存来存储的,而不是使用虚拟机的堆内存,因此可以更加灵活地分配和回收内存,而不受堆内存大小的限制。另外,元空间还提供了一些额外的功能,如支持动态地调整元数据空间大小,以及可以使用外部存储来保存元数据信息等。
在Java中,每个类都有一个运行时常量池(Runtime Constant Pool),它是一种特殊的数据结构,用于存储常量,例如字符串常量、类和接口的符号引用、字段和方法的符号引用等。
具体来说,运行时常量池包含以下信息:
虽然运行时常量池是每个类的一部分,但它并不是类加载器的一部分,而是在类加载后由虚拟机创建和维护的。在内存中,运行时常量池通常是以一个Java对象的形式存在,每个类都有一个对应的运行时常量池对象。
Java中的类常量池(Class Constant Pool)是每个类文件中的一个数据结构,用于存储常量、符号引用和其他元数据信息。与运行时常量池不同,类常量池是每个类文件的一部分,由编译器在编译期间生成并打包进类文件中。
具体来说,类常量池包含以下信息:
类常量池与运行时常量池不同,它是每个类文件的一部分,由编译器在编译期间生成并打包进类文件中,而运行时常量池则是在类加载后由虚拟机创建和维护的。同时,类常量池中的符号引用需要在类加载时进行解析,并转换为实际的直接引用,而运行时常量池中的符号引用则可以在运行时动态解析。
JVM虚拟机栈中的操作数栈是一种后进先出(LIFO)的数据结构,用于存储方法执行时的操作数(数值或对象引用)。当方法被调用时,虚拟机会为该方法创建一个新的栈帧,并将该栈帧压入虚拟机栈顶。栈帧包含了方法的局部变量表、操作数栈、方法返回地址等信息。
在执行方法时,所有的操作数都被存储在该方法的操作数栈中。当方法执行过程中需要进行一些计算操作时,比如加减乘除、取余等,虚拟机会将操作数从操作数栈中弹出,进行相应的计算操作,并将计算结果再次压入操作数栈中。
操作数栈的大小在编译时就已经确定,其最大容量也是固定的。当操作数栈的空间不足时,虚拟机会抛出StackOverflowError异常。因此,在编写Java程序时需要特别注意方法中操作数栈的大小,避免出现栈溢出的情况。
在方法执行完毕后,虚拟机会将该方法的栈帧弹出虚拟机栈,并将执行结果返回给调用方。如果方法是非void类型的,那么执行结果将被压入调用方的操作数栈中。
JVM(Java Virtual Machine)虚拟机栈中的动态链接(Dynamic Linking)是指在方法调用过程中,虚拟机通过符号引用来动态确定方法的直接引用地址。动态链接在Java程序的执行过程中起到了非常重要的作用。
在Java程序中,方法调用是通过符号引用(Symbolic Reference)来实现的。符号引用是一种用来描述方法、变量等符号信息的数据类型,它包括符号名称和符号所在的类、方法等信息。在Java程序编译时,所有的符号引用都被保存在类的常量池中。但是,符号引用并不包含方法的直接引用地址,因此在方法调用时需要进行动态链接来确定方法的直接引用地址。
在执行方法调用时,虚拟机会从方法的符号引用中获取到方法所在的类,并通过该类的方法表找到对应的方法。如果该方法在类中是静态方法或者非私有实例方法,那么它的直接引用地址就可以直接确定。但是,如果该方法是私有实例方法或者虚方法,那么它的直接引用地址需要在方法调用时进行动态链接来确定。
动态链接的实现方式是通过虚方法表(Virtual Method Table)来实现的。虚方法表是一种数据结构,用于存储类中所有的虚方法信息。每个类都有一个虚方法表,其中存储了该类中所有的虚方法信息,包括方法名称、参数类型、返回类型等信息。在动态链接的过程中,虚拟机会根据对象的实际类型,在虚方法表中查找到对应的方法信息,并确定方法的直接引用地址。
垃圾收集器主要的算法有:标记清除,标记复制,标记整理等。
G1(Garbage First)收集器是Java虚拟机提供的一种新型垃圾收集器,它的堆空间是由多个大小相等的Region(区域)组成的。与传统的垃圾收集器不同,G1收集器不是将堆空间划分为年轻代和老年代,而是将整个堆空间划分为多个大小相等的Region,每个Region可以是年轻代或老年代,也可以是空闲的。
G1收集器采用的是分代收集的思想,但是它的实现与传统的分代收集器有所不同。G1将堆空间划分为多个Region,每个Region可以是年轻代或老年代,这些Region之间是没有固定的边界的,而是通过指针相连。在G1收集器中,为了避免Full GC的出现,G1会根据堆中各个Region的垃圾情况,动态地决定哪些Region需要进行垃圾回收,将这些Region标记为收集集合(Collection Set),然后对它们进行垃圾回收。这个过程称为Mixed GC,因为它同时包括了年轻代和老年代的垃圾回收。
G1收集器的堆空间划分如下:
在G1收集器中,每次垃圾回收都会选择一些Region作为收集集合,这些Region会被标记为可回收的,然后通过多线程并发地进行垃圾回收。G1收集器的目标是在可接受的停顿时间内,尽可能地回收垃圾,并保证应用程序的吞吐量。
目前 Java 虚拟机的主流垃圾回收器采取的是可达性分析算法。这个算法的实质在于将一系列 GC Roots 作为初始的存活对象合集(Gc Root Set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。
在Java语言中,被视为垃圾的对象必须满足两个条件中的至少一个,即对象不可达或对象无法继续使用。而GC Root(垃圾回收根节点)就是一组被定义为不可回收的对象,它们直接或间接引用了所有的活动对象,并形成了整个对象图的根。以下是一些常见的GC Root类型:
对于以上这些对象,它们都被认为是不可回收的,并且所有活动的对象都可以通过它们直接或间接引用到。因此,在垃圾回收时,垃圾收集器会首先从这些根节点开始遍历对象图,标记所有与之相关的对象,并将其保留下来,未标记的对象则被判定为垃圾对象,并被回收。
GC Root只是垃圾回收的一个概念,不会实际存在于内存中,而是由垃圾收集器来维护。垃圾收集器会根据不同的GC算法和具体的实现方式来选择和维护GC Root,并在垃圾回收过程中对其进行遍历和处理。
Java类加载是指将Java类的字节码文件加载到内存中,并经过验证、准备、解析、初始化等过程,最终将该类的Class对象作为Java程序访问和使用该类的入口。
Java类的加载过程可以分为以下三个步骤:
在Java程序运行时,所有的类、接口、方法、字段等信息都被存储在Class文件中,这些信息被称为符号(Symbol)。符号引用(Symbolic Reference)就是指向这些符号的引用,它包含了被引用的符号名称以及对符号的描述。
符号引用是一种用来描述引用其他类、接口、字段、方法等的方式,它并不直接指向具体的内存地址,而是通过类似字符串的方式,记录了被引用符号的名称和描述信息。在Java程序执行过程中,虚拟机需要将符号引用转换为实际的内存地址,这个过程称为符号解析。
例如,在Java程序中使用一个类的某个方法,会生成对该方法的符号引用,包括方法名称、参数类型等信息。在程序运行时,虚拟机需要根据这个符号引用,找到该方法在内存中的实际地址,才能够执行这个方法。
符号引用的作用是将程序中的引用与实际的内存地址分离开来,使得Java程序可以具有跨平台的特性,同时也方便了Java程序的动态加载和卸载。
Java Class文件的结构可以分为三个主要部分:文件头、常量池和类信息。
Java Class文件的头部固定为16个字节,包含了一些基本的信息,如文件的魔数(cafebabe)、版本号等。
Java Class文件的常量池(Constant Pool)是Java语言中的常量池表现形式,是Class文件中最大的部分。常量池是一个表格,里面存放着编译时生成的各种字面量和符号引用,如类名、方法名、字段名、字符串常量、接口方法句柄等等。
常量池的主要作用是为Java代码提供了一种基础数据类型的表达方式,同时也为类的加载和解析提供了必要的信息。在运行时,JVM会将常量池中的符号引用转换为实际的内存地址,从而实现程序的执行。
类信息部分包含了类的访问标志、类名、父类名、接口列表、字段表、方法表、属性表等信息,它们描述了类的各种特征和行为。其中,字段表和方法表分别描述了类中的字段和方法,包含了访问标志、名称、描述符等信息。
在Java Class文件中,各个部分的顺序是固定的,同时也是紧密相连的。当JVM加载一个Class文件时,它会按照文件结构解析文件,并将解析出的信息保存在内存中,供程序执行时使用。
主要有四种类加载器:
双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
jstat是一个强大的工具。它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据。
jmap命令用于生成堆转储快照(一般称为heapdump或dump文件)。
jmap的作用并不仅仅是为了获取堆转储快照,它还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。
JDK提供jhat命令与jmap搭配使用,来分析jmap生成的堆转储快照。 jhat内置了一个微型的HTTP/Web服务器,生成堆转储快照的分析结果后,可以在浏览器中查看。
jstack命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者 javacore文件)。
JConsole( Java Monitoring and Management Console),是一款基于 JMX( Java Manage-ment Extensions) 的可视化监视管理工具。
它的功能主要是对系统进行收集和参数调整,不仅可以用在虚拟机本身的管理上,还可以用于运行于虚拟机之上的软件中。
使用JConsole连接了一个本地程序,在概述可以看到Java程序运行的概览信息,包括堆内存使用情况、线程、类、CPU使用情况四项信息的曲线图。
内存的作用相当于可视化的jstat命令,用于监视被收集器管理的虚拟机内存。
它不仅包含堆内存的整体信息,更细化到eden区、suvivior区、老年代的使用情况。
JConcole还可以监控线程,相当于可视化的jstack命令,JConcole显示了系统内的线程数量,并在屏幕下方显示了程序中所有的线程。单击线程名称,就可以查看线程的栈信息。
其他信息还包含如类加载情况,虚拟机信息等。
一般情况下,JVM调优可通过以下步骤进行:
现象:垃圾收集频率非常频繁。
原因:如果内存太小,就会导致频繁的需要进行垃圾收集才能释放出足够的空间来创建新的对象,所以增加堆内存大小的效果是非常显而易见的。
注意:如果垃圾收集次数非常频繁,但是每次能回收的对象非常少,那么这个时候并非内存太小,而可能是内存泄露导致对象无法回收,从而造成频繁GC。
参数配置:
//设置堆初始值
指令1:-Xms2g
指令2:-XX:InitialHeapSize=2048m
//设置堆区最大值
指令1:`-Xmx2g`
指令2: -XX:MaxHeapSize=2048m
//新生代内存配置
指令1:-Xmn512m
指令2:-XX:MaxNewSize=512m
现象:程序间接性的卡顿
原因:如果没有确切的停顿时间设定,垃圾收集器以吞吐量为主,那么垃圾收集时间就会不稳定。
注意:不要设置不切实际的停顿时间,单次时间越短也意味着需要更多的GC次数才能回收完原有数量的垃圾.
参数配置:
//GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间
-XX:MaxGCPauseMillis
现象:某一个区域的GC频繁,其他都正常。
原因:如果对应区域空间不足,导致需要频繁GC来释放空间,在JVM堆内存无法增加的情况下,可以调整对应区域的大小比率。
注意:也许并非空间不足,而是因为内存泄造成内存无法回收。从而导致GC频繁。
参数配置:
//survivor区和Eden区大小比率
指令:-XX:SurvivorRatio=6 //S区和Eden区占新生代比率为1:6,两个S区2:6
//新生代和老年代的占比
-XX:NewRatio=4 //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2
现象:老年代频繁GC,每次回收的对象很多。
原因:如果升代年龄小,新生代的对象很快就进入老年代了,导致老年代对象变多,而这些对象其实在随后的很短时间内就可以回收,这时候可以调整对象的升级代年龄,让对象不那么容易进入老年代解决老年代空间不足频繁GC问题。
注意:增加了年龄之后,这些对象在新生代的时间会变长可能导致新生代的GC频率增加,并且频繁复制这些对象新生的GC时间也可能变长。
配置参数:
//进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,默认值7
-XX:InitialTenuringThreshol=7
现象:老年代频繁GC,每次回收的对象很多,而且单个对象的体积都比较大。
原因:如果大量的大对象直接分配到老年代,导致老年代容易被填满而造成频繁GC,可设置对象直接进入老年代的标准。
注意:这些大对象进入新生代后可能会使新生代的GC频率和时间增加。
配置参数:
//新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。
-XX:PretenureSizeThreshold=1000000
现象:CMS,G1 经常 Full GC,程序卡顿严重。
原因:G1和CMS 部分GC阶段是并发进行的,业务线程和垃圾收集线程一起工作,也就说明垃圾收集的过程中业务线程会生成新的对象,所以在GC的时候需要预留一部分内存空间来容纳新产生的对象,如果这个时候内存空间不足以容纳新产生的对象,那么JVM就会停止并发收集暂停所有业务线程(STW)来保证垃圾收集的正常运行。这个时候可以调整GC触发的时机(比如在老年代占用60%就触发GC),这样就可以预留足够的空间来让业务线程创建的对象有足够的空间分配。
注意:提早触发GC会增加老年代GC的频率。
配置参数:
//使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小
-XX:CMSInitiatingOccupancyFraction
//G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
-XX:G1MixedGCLiveThresholdPercent=65
参考: