java JVM详解(持续更新)

JVM定义

JVM结构

类装载子系统

双亲委派模型

运行时数据区

[方法区(Method Area)](#方法区(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)组成,对应着每次调用方法时所占用的内存。
每个线程只能有一个活动栈帧,对应着当前正在执行的方法

程序计数区

用于保存当前线程所正在执行的字节码指令的地址(行号)

执行引擎子系统
垃圾回收机制

  1. 目的

    程序运行过程中会产生大量的内存垃圾,为了确保程序运行时的性能,JVM会在过程中不断地进行自动垃圾回收

  2. 关注对象

    程序计数器、虚拟机栈、本地方法栈是线程私有的,所以会随着线程结束而消亡。
    Java 堆和方法区是线程共享的,在程序处于运行期才知道哪些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收所关注的就是这部分内存。

  3. 判断对象可以回收

    在进行内存回收之前要做的事情就是判断那些对象是'死'的,哪些是'活'的

    • 引用计数法:当一个对象被引用时,就当引用对象的值加一,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。
    • 可达性分析

  4. 垃圾回收算法

    算法 速度 碎片 空间
    标记清除算法 较快 产生内存碎片
    标记整理算法 没有内存碎片
    复制算法 没有内存碎片 占用两倍内存空间
    分代垃圾回收算法

内存分代机制

方法区即被称为永久代,而堆中存放的是对象实例,为了回收的时候对不同的对象采用不同的方法,又将堆分为新生代和老年代,默认情况下新生代占堆的1/3,老年代占堆的2/3。

• 新生代(Young):HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。
• 老年代(Old):在新生代中经历了多次GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
• 永久代(Permanent):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,一般而言不会进行垃圾回收。
• 元空间(metaspace):从JDK 8开始,Java开始使用元空间取代永久代,元空间并不在虚拟机中,而是直接使用本地内存。那么,默认情况下,元空间的大小仅受本地内存限制。当然,也可以对元空间的大小手动的配置。

JVM调优

  1. JDK自带工具

    JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是jconsole和jvisualvm这两款视图监控工具。

    • jconsole:用于对JVM中的内存、线程和类等进行监控;
    • jvisualvm:JDK自带的全能分析工具,可以分析:内存快照、线程快照、程序 死锁、监控内存的变化、gc变化等。

  2. 第三方工具

    • MAT(MemoryAnalyzer Tool):基于Eclipse的内存分析工具
    • GChisto:专业分析gc日志的工具

  3. 调优参数

    -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面试题

  1. 垃圾回收是否涉及栈内存?

    不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。

  2. 栈内存分配越大越好吗?

    物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。

  3. 栈内存溢出的原因

    栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError,使用 -Xss256k 指定栈内存大小

  4. 堆内存调整参数

    可以使用 -Xmx8m 来指定堆内存大小。

  5. 堆内存诊断方法

    jps -》查看当前系统中有哪些 java 进程
    jmap -》查看堆内存占用情况 jmap - heap 进程id

  6. Java中创建的对象是存储在JVM中的哪个区域的?

    例如,这里,我们简单的列举一行代码,如下所示。

    java 复制代码
    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的逃逸分析情况

相关推荐
芊寻(嵌入式)9 分钟前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
WaaTong10 分钟前
《重学Java设计模式》之 原型模式
java·设计模式·原型模式
m0_7430484410 分钟前
初识Java EE和Spring Boot
java·java-ee
AskHarries12 分钟前
Java字节码增强库ByteBuddy
java·后端
一颗松鼠18 分钟前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
有梦想的咸鱼_19 分钟前
go实现并发安全hashtable 拉链法
开发语言·golang·哈希算法
海阔天空_201325 分钟前
Python pyautogui库:自动化操作的强大工具
运维·开发语言·python·青少年编程·自动化
天下皆白_唯我独黑32 分钟前
php 使用qrcode制作二维码图片
开发语言·php
小灰灰__32 分钟前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭36 分钟前
Java中的动态代理
java·开发语言·aop·动态代理