Java——JVM

本文详细介绍了JVM虚拟机的相关概念,包括JDK、JRE和JVM之间的关系,JVM内存空间的组成,如栈、堆、元空间等,以及类加载的过程和类加载器的层次结构。此外,还探讨了垃圾回收的原理,如可达性分析法和各种垃圾回收算法,并提到了JVM性能调优的相关参数和常见问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JVM虚拟机

一:JDK、JRE、JVM,是什么关系?

  • JDK(Java Development Kit Java开发工具包),JDK是提供给Java开发人员使用的,其中包含了java的开发工具,也包括了JRE。所以安装了JDK,就不用在单独安装JRE了。其中的开发工具包括编译工具(javac.exe) 打包工具(jar.exe)等。
  • JRE(Java Runtime Environment Java运行环境) 是 JDK 的子集,也就是包括 JRE 所有内容,以及开发应用程序所需的编译器和调试器等工具。JRE 提供了库、Java 虚拟机(JVM)和其他组件,用于运行 Java 编程语言、小程序、应用程序。
  • JVM(Java Virtual Machine Java虚拟机),JVM可以理解为是一个虚拟出来的计算机,具备着计算机的基本运算方式,它主要负责把 Java 程序生成的字节码文件,解释成具体系统平台上的机器指令,让其在各个平台运行。

JDK 是 JRE 的超集,JDK 包含了 JRE 所有的开发、调试以及监视应用程序的工具。以及如下重要的组件:

  • java – 运行工具,运行 .class 的字节码
  • javac– 编译器,将后缀名为.java的源代码编译成后缀名为.class的字节码
  • javap – 反编译程序
  • javadoc – 文档生成器,从源码注释中提取文档,注释需符合规范
  • jar – 打包工具,将相关的类文件打包成一个文件
  • jdb – debugger,调试工具
  • jps – 显示当前java程序运行的进程状态
  • appletviewer – 运行和调试applet程序的工具,不需要使用浏览器
  • javah – 从Java类生成C头文件和C源文件。这些文件提供了连接胶合,使 Java 和 C 代码可进行交互。
  • javaws – 运行 JNLP 程序
  • extcheck – 一个检测jar包冲突的工具
  • apt – 注释处理工具
  • jhat – java堆分析工具
  • jstack – 栈跟踪程序
  • jstat – JVM检测统计工具
  • jstatd – jstat守护进程
  • jinfo – 获取正在运行或崩溃的java程序配置信息
  • jmap – 获取java进程内存映射信息
  • idlj – IDL-to-Java 编译器. 将IDL语言转化为java文件
  • policytool – 一个GUI的策略文件创建和管理工具
  • jrunscript – 命令行脚本运行
  • appletviewer:小程序浏览器,一种执行HTML文件上的Java小程序的Java浏览器

RE 本身也是一个运行在 CPU 上的程序,用于解释执行 Java 代码。 这是运行 Java 程序的最低要求。

JVM 就是运行 Java 字节码的虚拟机,JVM 是一种规范。

在 JVM 中有两种不同风格的启动模式, Client模式、Server模式。

  • Client模式:加载速度较快。可以用于运行GUI交互程序。
  • Server模式:加载速度较慢但运行起来较快。可以用于运行服务器后台程序。
  • 如果需要调整,可以把 client 设置为 KNOWN,并调整到 server 前面。
  • JVM 默认在 Server模式下,-Xms128M、-Xmx1024M
  • JVM 默认在 Client 模式下,-Xms1M、-Xmx64M

二:JVM内存空间,各空间都存放什么

  • JDK 1.8:无永久代,运行时常量池、类常量池,都保存在元数据区,也就是常说的元空间。但字符串常量池仍然存放在堆上。

1. 程序计数器

  • 较小的内存空间、线程私有,记录当前线程所执行的字节码行号。
  • 如果执行 Java 方法,计数器记录虚拟机字节码当前指令的地址,本地方法则为空。
  • 这一块区域没有任何 OutOfMemoryError 定义。

2. Java虚拟机栈

  • 每一个方法在执行的同时,都会创建出一个栈帧,用于存放局部变量表、操作数栈、动态链接、方法出口、线程等信息。
  • 方法从调用到执行完成,都对应着栈帧从虚拟机中入栈和出栈的过程。
  • 最终,栈帧会随着方法的创建到结束而销毁。

3. 本地方法栈

  • 本地方法栈与Java虚拟机栈作用类似,唯一不同的就是本地方法栈执行的是Native方法,而虚拟机栈是为JVM执行Java方法服务的。
  • 另外,与 Java 虚拟机栈一样,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
  • JDK1.8 HotSpot虚拟机直接就把本地方法栈和虚拟机栈合二为一。

4. 堆和元空间 

  • JDK 1.8 JVM 的内存结构主要由三大块组成:堆内存、元空间和栈,Java 堆是内存空间占据最大的一块区域。
  • Java 堆,由年轻代和年老代组成,分别占据1/3和2/3。
  • 而年轻代又分为三部分,EdenFrom SurvivorTo Survivor,占据比例为8:1:1,可调。
  • 另外这里我们特意画出了元空间,也就是直接内存区域。在 JDK 1.8 之后就不在堆上分配方法区了。
  • 元空间从虚拟机Java堆中转移到本地内存,默认情况下,元空间的大小仅受本地内存的限制,说白了也就是以后不会因为永久代空间不够而抛出OOM异常出现了。jdk1.8以前版本的 class和JAR包数据存储在 PermGen下面 ,PermGen 大小是固定的,而且项目之间无法共用,公有的 class,所以比较容易出现OOM异常。

5. 常量池

  • 从 JDK 1.7开始把常量池从永久代中剥离,直到 JDK1.8 去掉了永久代。而字符串常量池一直放在堆空间,用于存储字符串对象,或是字符串对象的引用。

三:类加载

1.类的生命周期

  • 加载:Java 虚拟机规范对 class 文件格式进行了严格的规则,但对于从哪里加载 class 文件,却非常自由。Java 虚拟机实现可以从文件系统读取、从JAR(或ZIP)压缩包中提取 class 文件。除此之外也可以通过网络下载、数据库加载,甚至是运行时直接生成的 class 文件。
  • 链接:包括了三个阶段;
    • 验证,确保被加载类的正确性,验证字节流是否符合 class 文件规范,例魔数 0xCAFEBABE,以及版本号等。
    • 准备,为类的静态变量分配内存并设置变量初始值等
    • 解析,解析包括解析出常量池数据和属性表信息,这里会包括 ConstantPool 结构体以及 AttributeInfo 接口等。
  • 初始化:类加载完成的最后一步就是初始化,目的就是为标记常量值的字段赋值,以及执行 <clinit> 方法的过程。JVM虚拟机通过锁的方式确保 clinit 仅被执行一次
  • 使用:程序代码执行使用阶段。
  • 卸载:程序代码退出、异常、结束等。

2.java类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现 ,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”

 从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader ),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java 语言实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。

1)启动类加载器(Bootstrap ClassLoader):前面已经介绍过,这个类加载器负责将存放在< JAVA_HOME>\lib目录中的,或者被-Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用

2)扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher .ExtClassLoader实现,它负责加载<JAVA_HOME> \lib\ext目录中的,或者被java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器

3)应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher.AppClassLoader 实现。由于这个类加载器是ClassLoader中的 getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(Class Path)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

 图中展示的类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承( Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。

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

先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。双亲委派的具体逻辑就实现在这个loadClass()方法之中,JDK 1.2之后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。

3.打破双亲委派模型

在自定义ClassLoader的子类时,两种方式:
重写引padClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)
重写findClass方法(推荐)

4.引用

强引用:强引用是我们使用最广泛的引用,如果一个对象具有强引用,那么垃圾回收期绝对不会回收它,当内存空间不足时,垃圾回收器宁愿抛出OutOfMemoryError,也不会回收具有强引用的对象;我们可以通过显示的将强引用对象置为null,让gc认为该对象不存在引用,从而来回收它;

软引用:软应用是用来描述一些有用但不是必须的对象,在java中用SoftReference来表示,当一个对象只有软应用时,只有当内存不足时,才会回收它; 软引用可以和引用队列联合使用,如果软引用所引用的对象被垃圾回收器所回收了,虚拟机会把这个软引用加入到与之对应的引用队列中;

弱引用:弱引用是用来描述一些可有可无的对象,在java中用WeakReference来表示,在垃圾回收时,一旦发现一个对象只具有软引用的时候,无论当前内存空间是否充足,都会回收掉该对象; 弱引用可以和引用队列联合使用,如果弱引用所引用的对象被垃圾回收了,虚拟机会将该对象的引用加入到与之关联的引用队列中;

虚引用:虚引用就是一种可有可无的引用,无法用来表示对象的生命周期,任何时候都可能被回收,虚引用主要使用来跟踪对象被垃圾回收的活动,虚引用和软引用与弱引用的区别在于:虚引用必须和引用队列联合使用;在进行垃圾回收的时候,如果发现一个对象只有虚引用,那么就会将这个对象的引用加入到与之关联的引用队列中,程序可以通过发现一个引用队列中是否已经加入了虚引用,来了解被引用的对象是否需要被进行垃圾回收;

四:垃圾回收

1.如何判断对象以死

1.引用计数器

  1. 为每一个对象添加一个引用计数器,统计指向该对象的引用次数。
  2. 当一个对象有相应的引用更新操作时,则对目标对象的引用计数器进行增减。
  3. 一旦当某个对象的引用计数器为0时,则表示此对象已经死亡,可以被垃圾回收。

2.可达性分析法

通过定义一系列称为 GC Roots 根对象作为起始节点集,从这些节点出发,穷举该集合引用到的全部对象填充到该集合中(live set)。这个过程教过标记,只标记那些存活的对象 ,那么现在未被标记的对象就是可以被回收的对象了。

GC Roots 包括;

  1. 全局性引用,对方法区的静态对象、常量对象的引用
  2. 执行上下文,对 Java方法栈帧中的局部对象引用、对 JNI handles 对象引用
  3. 已启动且未停止的 Java 线程

2.垃圾回收算法

标记-清除算法(mark-sweep):
  • 标记无引用的死亡对象所占据的空闲内存,并记录到空闲列表中(free list)。
  • 当需要创建新对象时,内存管理模块会从 free list 中寻找空闲内存,分配给新建的对象。
  • 这种清理方式其实非常简单高效,但是也有一个问题内存碎片化太严重了。
  • Java 虚拟机的堆中对象,必须是连续分布的,所以极端的情况下可能即使总剩余内存充足,但寻找连续内存分配效率低,或者严重到无法分配内存。重启汤姆猫!
  • 在CMS中有此类算法的使用,GC暂停时间短,但存在算法缺陷。

 标记-复制算法(mark-copy) 

  • 从图上看这回做完垃圾清理后连续的内存空间就大了。
  • 这种方式是把内存区域分成两份,分别用两个指针 from 和 to 维护,并且只使用 from 指针指向的内存区域分配内存。
  • 当发生垃圾回收时,则把存活对象复制到 to 指针指向的内存区域,并交换 from 与 to 指针。
  • 它的好处很明显,就是解决内存碎片化问题。但也带来了其他问题,堆空间浪费了一半。

标记-压缩算法(mark-compact) 

  • 标记的过程和标记清除算法一样,但在后续对象清理步骤中,先把存活对象都向内存空间一端移动,然后在清理掉其他内存空间。
  • 这种算法能够解决内存碎片化问题,但压缩算法的性能开销也不小。

3.回收过程

  • Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。(一般采用复制算法回收垃圾)
  • Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。(可采用标记清楚法和标记整理法)
  • Full GC是清理整个堆空间,包括年轻代和老年代。
  • Minor GC 触发条件一般为:

    1. eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。
    2. 新创建的对象大小 > Eden所剩空间时触发Minor GC
  • Major GC和Full GC 触发条件一般为: Major GC通常是跟full GC是等价的

    1. 每次晋升到老年代的对象平均大小>老年代剩余空间

    2. MinorGC后存活的对象超过了老年代剩余空间

    3. 永久代空间不足

    4. 执行System.gc()

    5. CMS GC异常

    6. 堆内存分配很大的对象

五:JVM性能调优

1.jvmc常见的问题

  • Heap内存(老年代)持续上涨达到设置的最大内存值;
  • Full GC 次数频繁;
  • GC 停顿时间过长(超过1秒);
  • 应用出现OutOfMemory 等内存异常;
  • 应用中有使用本地缓存且占用大量内存空间;
  • 系统吞吐量与响应性能不高或下降。

整体耗时、执行路径、参数信息、异常结果、GC次数、堆栈数据、分代内容等等

2.调优参数数据

jvm调优参数:

#常用的设置
-Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。 

-Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。 

-Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。 

-XX:NewSize=n 设置年轻代初始化大小大小 

-XX:MaxNewSize=n 设置年轻代最大值

-XX:NewRatio=n 设置年轻代和年老代的比值。如: -XX:NewRatio=3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代+年老代和的 1/4 

-XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。8表示两个Survivor :eden=2:8 ,即一个Survivor占年轻代的1/10,默认就为8

-Xss:设置每个线程的堆栈大小。JDK5后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。

-XX:ThreadStackSize=n 线程堆栈大小

-XX:PermSize=n 设置持久代初始值	

-XX:MaxPermSize=n 设置持久代大小
 
-XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。

#下面是一些不常用的

-XX:LargePageSizeInBytes=n 设置堆内存的内存页大小

-XX:+UseFastAccessorMethods 优化原始类型的getter方法性能

-XX:+DisableExplicitGC 禁止在运行期显式地调用System.gc(),默认启用	

-XX:+AggressiveOpts 是否启用JVM开发团队最新的调优成果。例如编译优化,偏向锁,并行年老代收集等,jdk6纸之后默认启动

-XX:+UseBiasedLocking 是否启用偏向锁,JDK6默认启用	

-Xnoclassgc 是否禁用垃圾回收

-XX:+UseThreadPriorities 使用本地线程的优先级,默认启用

 JVM的GC收集器设置

  • -xx:+Use xxx GC
    • xxx 代表垃圾收集器名称
-XX:+UseSerialGC:设置串行收集器,年轻带收集器 

-XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。

-XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量

-XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0 支持对年老代并行收集。 

-XX:+UseConcMarkSweepGC:设置年老代并发收集器

-XX:+UseG1GC:设置 G1 收集器,JDK1.9默认垃圾收集器

3.调优工具

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

jdk命令: 

 linux命令:

命令说明
top实时显示正在执行进程的 CPU 使用率、内存使用率以及系统负载等信息
vmstat对操作系统的虚拟内存、进程、CPU活动进行监控
pidstat监控指定进程的上下文切换
iostat监控磁盘IO
jconsole:用于对 JVM 中的内存、线程和类等进行监控

jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

第三方的监控工具,同样是性能分析和故障排查的利器,如MATGChistoJProfilerarthas

4.优化方案

JVM调优应该是Java性能优化的最后一颗子弹,一般项目加个xms和xmx参数就够了。在没有全面监控、收集性能数据之前,调优就是瞎调。主要还是以优化代码为主。

吞吐量、延迟、内存占用三者类似CAP,构成了一个不可能三角,只能选择其中两个进行调优,不可三者兼得。

  • 延迟:GC低停顿和GC低频率;
  • 低内存占用;
  • 高吞吐量;
  • 分析系统系统运行情况:分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点;
  • 确定JVM调优量化目标;
  • 确定JVM调优参数(根据历史JVM参数来调整);
  • 依次确定调优内存、延迟、吞吐量等指标;
  • 对比观察调优前后的差异;
  • 不断的分析和调整,直到找到合适的JVM参数配置;
  • 找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。

常用调优策略 

 1.选择合适的垃圾回收器

2.调整内存大小

3.设置符合预期的停顿时间

4.调整内存区域大小比率

5.调整对象升老年代的年龄

6.调整大对象的标准

7.调整GC的触发时机

8.调整 JVM本地内存大小

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值