JVM定义
JVM结构
类装载子系统
双亲委派模型
运行时数据区
方法区(Method Area)
堆区(Heap)
虚拟机栈区
程序计数区
执行引擎子系统
垃圾回收机制
内存分代机制
JVM调优
JVM面试题
JVM定义
JVM它是jre的一部分,也java程序最终运行的地方。jdk默认虚拟机HotSpot。
我们编写的java代码,编译后会生成对应的.class文件,这个文件是字节码文件,紧接着JVM 通过类装载子系统将字节码文件装载到运行时数据区,该区域将字节码内容拆分分别装载到JVM运行时数据区的方法区、堆、栈、本地方法栈和程序计数器 几个部分,最终送到执行引擎子系统配合本地接口然后运行。
JVM结构
JVM由两个子系统和两个组件构成。
子系统:
子系统 | 内容 |
---|---|
类装载子系统(ClassLoader) | 根据给定的全限定类名称装载class文件到运行时数据区的方法区 |
执行引擎子系统(Execution engine) | 包含即时编译器(JITCompiler)和垃圾回收器(Garbage Collector);用于执行class文件中的命令 |
组件:
组件 | 内容 |
---|---|
运行时数据区(Runtime data Area) | jvm的内存,包含方法区、虚拟机栈、本地方法栈、堆、程序计数器 |
本地接口(Native Interface) | 与本地方法库交互,与其他变成语言交互的接口 |
类装载子系统
类的加载是通过双亲委派模型来完成的
类的生命周期:加载、验证、准备、解析、初始化、使用、卸载
双亲委派模型
加载器找到.class文件找到并读取,双亲委派模型描述的是加载器找到.class文件得基本过程。
即加载类时先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器. 父类加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载.
运行时数据区
JVM在执行Java程序时,会把它管理的内存划分为若干个的区域,每个区域都有自己的用途和创建销毁时间。
区域 | 线程属性 | 内存占比 |
---|---|---|
堆区 | 共享 | 最大 |
虚拟机栈 | 私有 | |
方法区 | 共享 | |
程序计数器 | 私有 | 较小 |
方法区(Method Area)
线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
例如,在程序中声明的常量、静态变量和有关于类的信息等的引用,都会存放在方法区,而这些引用所指向的具体对象 一般都会在堆中开辟单独的空间进行存储,也可能会在直接内存中进行存储。
堆区(Heap)
通过new关键字创建的对象都会被放在堆内存,由于堆是线程共享的,所以堆内存中的对象都需要考虑线程安全问题。
堆中的数据不需要事先明确生存期,可以动态的分配内存,不再使用的数据和对象由JVM中的GC机制自动回收。对JVM的性能调优一般就是对堆内存的调优。
Java中基本类型的包装类:Byte、Short、Integer、Long、Float、Double、Boolean、Character类型的数据是存储在堆中的。
堆区内存分为两个不同区域:新生代(Young)和老年代(Old)。
年轻代又会被进一步分为1个Eden区和2个Survivor区。在内存分配上,如果保持默认配置的话,年轻代和老年代的内存大小比例为1 : 2,年轻代中的1个Eden区和2个Survivor区的内存大小比例为:8 : 1 : 1。
虚拟机栈区
每个线程运行需要的内存空间,用于保存方法执行的内存模型。
每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存。
每个线程只能有一个活动栈帧,对应着当前正在执行的方法
程序计数区
用于保存当前线程所正在执行的字节码指令的地址(行号)
执行引擎子系统
垃圾回收机制
-
目的
程序运行过程中会产生大量的内存垃圾,为了确保程序运行时的性能,JVM会在过程中不断地进行自动垃圾回收
-
关注对象
程序计数器、虚拟机栈、本地方法栈是线程私有的,所以会随着线程结束而消亡。
Java 堆和方法区是线程共享的,在程序处于运行期才知道哪些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收所关注的就是这部分内存。 -
判断对象可以回收
在进行内存回收之前要做的事情就是判断那些对象是‘死’的,哪些是‘活’的
• 引用计数法:当一个对象被引用时,就当引用对象的值加一,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。
• 可达性分析 -
垃圾回收算法
算法 速度 碎片 空间 标记清除算法 较快 产生内存碎片 标记整理算法 慢 没有内存碎片 复制算法 没有内存碎片 占用两倍内存空间 分代垃圾回收算法
内存分代机制
方法区即被称为永久代,而堆中存放的是对象实例,为了回收的时候对不同的对象采用不同的方法,又将堆分为新生代和老年代,默认情况下新生代占堆的1/3,老年代占堆的2/3。
• 新生代(Young):HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。
• 老年代(Old):在新生代中经历了多次GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
• 永久代(Permanent):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,一般而言不会进行垃圾回收。
• 元空间(metaspace):从JDK 8开始,Java开始使用元空间取代永久代,元空间并不在虚拟机中,而是直接使用本地内存。那么,默认情况下,元空间的大小仅受本地内存限制。当然,也可以对元空间的大小手动的配置。
JVM调优
-
JDK自带工具
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是jconsole和jvisualvm这两款视图监控工具。
• jconsole:用于对JVM中的内存、线程和类等进行监控;
• jvisualvm:JDK自带的全能分析工具,可以分析:内存快照、线程快照、程序 死锁、监控内存的变化、gc变化等。 -
第三方工具
• MAT(MemoryAnalyzer Tool):基于Eclipse的内存分析工具
• GChisto:专业分析gc日志的工具 -
调优参数
-Xms2g:初始化推大小为2g
-Xmx2g:堆最大内存为2g (为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值)
-XX:NewSize=m;设置年轻代大小
-XX:NewRatio=4:设置年轻代的和老年代的内存比例为 1:4; 年轻代和年老代将根据默认的比例(1:2)分配堆内存.
-XX:SurvivorRatio=8:设置新生代Eden和Survivor比例为 8:1
在JVM中,主要是对堆(新生代)、方法区和栈进行性能调优。各个区域的调优参数如下所示。
• 堆:-Xms、-Xmx
• 新生代:-Xmn
• 方法区(元空间):-XX:MetaspaceSize、-XX:MaxMetaspaceSize
• 栈(线程):-Xss
为了更加直观的表述,我们可以将JVM的内存区域和对应的调优参数总结成下图所示。
在设置JVM启动参数时,需要特别注意方法区(元空间)的参数设置。
关于方法区(元空间)的JVM参数主要有两个:-XX:MetaspaceSize和-XX:MaxMetaspaceSize。
-XX:MetaspaceSize:指的是方法区(元空间)触发Full GC的初始内存大小(方法区没有固定的初始内存大小),以字节为单位,默认为21M。达到设置的值时,会触发Full GC,同时垃圾收集器会对这个值进行修改。
如果在发生Full GC时,回收了大量内存空间,则垃圾收集器会适当降低此值的大小;如果在发生Full GC时,释放的空间比较少,则在不超过设置的-XX:MetaspaceSize值或者在没设置-XX:MetaspaceSize的值时不超过21M,适当提高此值。
-XX:MaxMetaspaceSize:指的是方法区(元空间)的最大值,默认值为-1,不受堆内存大小限制,此时,只会受限于本地内存大小。
最后需要注意的是:调整方法区(元空间)的大小会发生Full GC,这种操作的代价是非常昂贵的。如果发现应用在启动的时候发生了Full GC,则很有可能是方法区(元空间)的大小被动态调整了。
所以,为了尽量不让JVM动态调整方法区(元空间)的大小造成频繁的Full GC,一般将-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置成一样的值。例如,物理内存8G,可以将这两个值设置为256M
最后,我们一起看下在物理内存8G的情况下,启动应用程序时,可以设置的JVM参数。当然,我这里给出的是一些经验值,实际部署到生产环境时,需要经过压测找到最佳的参数值。
• 启动SpringBoot:
java ‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐jar xxx.jar
• 启动Tomcat(Linux):
在Tomcat bin目录下catalina.sh文件里配置。
‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M
• 启动Tomcat(Windows)
在Tomcat bin目录下catalina.bat文件里配置。
‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M
JVM面试题
-
垃圾回收是否涉及栈内存?
不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。
-
栈内存分配越大越好吗?
物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
-
栈内存溢出的原因
栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError,使用 -Xss256k 指定栈内存大小
-
堆内存调整参数
可以使用 -Xmx8m 来指定堆内存大小。
-
堆内存诊断方法
jps -》查看当前系统中有哪些 java 进程
jmap -》查看堆内存占用情况 jmap - heap 进程id -
Java中创建的对象是存储在JVM中的哪个区域的?
例如,这里,我们简单的列举一行代码,如下所示。
User user = new User();
关于上面的代码,不少小伙伴都知道,创建出来的User对象是放在JVM中的堆区域的,而User对象的引用是放在栈中的。但如果你只是了解到这种程度,那面试官就会认为你了解的太浅显了,可能就会达不到他们的要求。其实面试官想要了解你是否对JVM有一个更深入的认识。
站在面试官的角度来看这个问题时,回答创建出来的User对象是放在JVM的堆区,也并没有错。但是JVM的堆内存区域又会分为年轻代和老年代,而年轻代又会分为Eden区和Survivor区。JVM堆空间的逻辑结构如下图所示。
而面试官更想了解的是你能不能说出来创建的对象具体是存放在JVM堆空间的哪个区域。在JVM内部,会将整个堆空间划分成年轻代和老年代,年轻代默认会占整个堆内存空间的1/3,老年代默认会占整个堆内存空间的2/3。年轻代又会划分为Eden区和两个Survivor区,它们之间的默认比例是Eden:Survivor1:Survivor2 = 8:1:1。
如果你能回答出 新创建的User对象是存放在JVM堆空间中年轻代的Eden区,那面试官就会对你刮目相看了。当然,这里没有考虑JVM的逃逸分析情况