JVM虚拟机
JVM虚拟机
FANSEAJVM虚拟机
一个典型的 Java 程序运行过程是下面这样的:
通过 Java 命令启动 JVM,JVM 的类加载器根据 Java 命令的参数到指定的路径加载.class 类文件,类文件被加载到内存后,存放在专门的方法区。然后 JVM 创建一个主线程执行这个类文件的 main 方法,mian 方法的输入参数和方法内定义的变量被压入 Java 栈。如果在方法内创建了一个对象实例,这个对象实例信息将会被存放到堆里,而对象实例的引用,也就是对象实例在堆中的地址信息则会被记录在栈里。堆中记录的对象实例信息主要是成员变量信息,因为类方法内的可执行代码存放在方法区,而方法内的局部变量存放在线程的栈里。
面试题
JVM内存结构:程序计数器、虚拟机栈、本地方法栈、方法区和堆。
-
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
java虚拟机栈
栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作
局部变量表 主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
操作数栈 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
动态链接 主要服务一个方法需要调用其他方法的场景。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态链接 。l
什么是方法区
他是java虚拟机规范定义的一个虚拟的概念,保存了类的元信息,运行时常量池,字符串常量池。是各线程共享的。
方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
- 方法区和永久代以及元空间有什么关系?
是方法区的不同实现,遵循同一规范
- 方法区常用参数有哪些?
1
2 -XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 常:java.lang.OutOfMemoryError: PermGen
- 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
- 永久代脱离堆内存管理,内存只受系统内存限制,不容易内存溢出
1 -XX:MaxMetaspaceSize //用此方法配置
- 能加载更多类
- 什么是运行时常量池?
保存了字节码文件中的常量池内容
- 字符串常量池有什么作用?
避免字符串重新创建
- JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
- GC是什么时候触发的?
- Scavenge GC(Minor GC)
Minor GC触发条件:当Eden区满时,触发Minor GC。
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来(复制算法)。
- Full GC触发条件
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
JVM调优
常用命令:
1、jps:查看进程及其相关去信息
2、jmap:用来生成dump文件和查看堆相关的各类信息的命令。
3、jstat:查看jvm运行时的状态信息
4、jstack:查看jvm线程快照的命令
5、jinfo:查看jvm参数和动态修改部分jvm参数
常用参数:
1、-Xms:初始化堆大小
2、-Xmx:最大堆大小
3、-Xmn:新生代的内存空间大小
4、-XX:SurvivorRatio 设置新生代中Eden区与 Survivor 区的比例
5、-Xss:每个线程的堆栈大小
6、-XX:PermSize:设置永久代初始值
7、- XX:MaxPermSize:设置永久代最大值.
1 | java -XX:SurvivorRatio=4 -jar myapp.jar |
当老年代空间已满,也就是无法将新生代中多次复制后依然存活的对象复制进去的时候,就会对新生代和老年代的内存空间进行一次全量垃圾回收,即 Full GC。所以根据应用程序的对象存活时间,合理设置老年代和新生代的空间比例对 JVM 垃圾回收的性能有很大影响,JVM 设置老年代新生代比例的参数是 -XX:NewRatio
以下是您提到的各个命令的使用案例:
- jps:查看进程及其相关信息
jps
命令可以列出正在运行的Java进程的PID和主类名(或JAR文件名)。使用案例:
1 jps输出可能如下:
1
2 23456 MyApp
23457 AnotherApp这里,
23456
和23457
是Java进程的PID,MyApp
和AnotherApp
是相应Java进程的主类名称。
- jmap:用来生成dump文件和查看堆相关的各类信息的命令
jmap
命令可以用来生成堆的转储快照(heap dump)以及查看堆内存的使用情况。使用案例:
生成堆转储文件:
1 jmap -dump:live,format=b,file=heapdump.bin <pid>这里,
<pid>
是Java进程的PID,heapdump.bin
是输出的堆转储文件名。查看堆的直方图信息:
1 jmap -histo <pid>
- jstat:查看jvm运行时的状态信息
jstat
命令可以实时监控JVM的各类运行状态信息,如类加载、垃圾收集、即时编译等。使用案例:
查看类加载统计信息:
1 jstat -class <pid>查看垃圾收集统计信息:
1 jstat -gc <pid>
1
2
3 PS D:\桌面\Git\algorithm-training-camp> jstat -gc 14092
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT
0.0 5120.0 0.0 5120.0 44032.0 28672.0 208896.0 4840.7 25088.0 24338.6 2816.0 2498.4 2 0.014 0 0.000 2 0.002 0.016
- jstack:查看jvm线程快照的命令
jstack
命令用于生成Java虚拟机当前时刻的线程快照。使用案例:
1 jstack <pid>输出将包含所有线程的堆栈跟踪信息,这对于诊断线程死锁、竞争条件等问题非常有用。
- jinfo:查看jvm参数和动态修改部分jvm参数
jinfo
命令可以打印出JVM的系统属性和VM标志信息,也可以动态地修改部分JVM参数。使用案例:
查看JVM的系统属性和VM标志:
1 jinfo <pid>修改JVM启动参数(例如启用PrintGC日志):
1 jinfo -flag +PrintGC <pid>这里,如果成功修改了参数,通常不会有输出。要验证参数是否已修改,你可以再次使用
jinfo
查看特定标志的状态。请注意,
jmap
、jstat
、jstack
和jinfo
都是JDK的一部分,通常位于bin
目录下,确保该目录在您的系统PATH
环境变量中,以便您可以从任何位置运行这些命令。此外,执行这些命令通常需要足够的权限来访问目标Java进程。在某些系统上,您可能需要以root用户或具有相应权限的用户身份运行这些命令。
JVM特性
即时编译
自动对热点代码进行优化,转化成机器码之后保存在内存中,再次执行可以直接调用
电商领域高性能虚拟机
JVM运行流程
字节码
类字节码对比
1 | package org.example; |
文件内容
字节码阅读方法
- 使用jclasslib
- javap -v 命令远程查看字节码文件(企业调试常用)
将对应class的字节码信息保存到txt文件
1 | javap -v test.class > test.txt |
arthas工具
运行arthas工具:
1 | java -jar arthas-boot.jar |
生成字节码文件并保存指定文件夹:
1 | dump -d D:/app/arthas-packaging-3.7.1-bin/log org.example.test |
查看运行面板:
1 | dashboard -i 2000 -n 1 //2000为刷新时间,1为刷新次数 |
将对应字节码文件转化成java代码:
1 | jad org.example.test |
- dashboard:这个命令用于展示当前系统的实时数据面板,包括CPU使用率、内存使用情况、线程数等指标。通过dashboard,开发人员可以直观地了解应用程序的运行状态。
- thread:用于查看Java应用程序中的线程信息,包括线程ID、线程状态、线程堆栈等。通过thread命令,开发人员可以定位线程问题,如死锁、线程阻塞等。
- class:用于查看Java应用程序中的类信息,包括类加载器、类签名等。class命令可以帮助开发人员了解类的加载情况,以及排查类加载相关的问题。
- watch:用于监控指定方法的调用情况,包括方法的参数、返回值以及调用耗时。通过watch命令,开发人员可以实时观察方法的执行过程,以便进行性能优化和问题排查。
- trace:用于展示方法内部调用路径,并输出方法路径上的每个节点上的耗时。trace命令可以帮助开发人员分析方法的执行流程,找出性能瓶颈。
- stack:用于展示一个方法的被调用链路。通过stack命令,开发人员可以了解方法的调用层次和调用关系,有助于排查调用链上的问题。
- redefine:用于动态修改Java应用程序的类文件。通过redefine命令,开发人员可以在运行时重新定义类的行为,实现热更新和动态修复。
除了以上重点指令外,Arthas还提供了其他丰富的命令和功能,如反编译已加载类源码(
jad
命令)、内存诊断(memory
命令)等。这些命令和功能共同构成了Arthas强大的Java诊断能力,帮助开发人员快速定位和解决问题。
Arthas的指令功能强大且多样,下面我将为您提供几个具体的使用示例,帮助您更好地理解和使用这些指令。
1. dashboard命令
dashboard
命令用于展示当前系统的实时数据面板,可以观察到 Java 服务的配置信息,如 CPU 使用率、内存使用情况、GC 情况、线程数等。使用方法如下:
1 | java -jar arthas-boot.jar |
执行后,Arthas 将在终端上显示一个实时更新的数据面板,您可以根据这些数据了解应用程序的运行状态。
2. thread命令
thread
命令用于查看当前线程信息,包括线程ID、线程状态、线程堆栈等。例如,您可以使用 thread -n 2
命令显示当前最忙的2个线程,并打印出线程栈。具体使用方法如下:
1 | java -jar arthas-boot.jar |
执行后,Arthas 将显示最忙碌的两个线程及其堆栈信息,这有助于您定位线程相关的问题,如死锁、线程阻塞等。
3. watch命令
watch
命令用于方法执行过程中的数据观测,可以观测的数据包括入参、返回值、抛出的异常等。例如,如果您想观测某个方法的入参和返回值,可以使用如下命令:
1 | java -jar arthas-boot.jar |
在这个示例中,com.example.MyClass
是类名,myMethod
是要观测的方法名,'{params,returnObj}'
表示要观测的参数和返回值,-x 2
表示执行两次后停止观测。执行后,Arthas 将显示每次方法调用时的入参和返回值,帮助您了解方法的执行过程和数据变化。
这些只是Arthas的一些基本命令示例,实际上它提供了更多高级功能和选项,您可以根据具体需求选择使用。请注意,在使用Arthas时,您需要确保有足够的权限来附加到目标Java进程,并且需要谨慎操作以避免对应用程序造成不必要的影响。
类加载器
加载阶段:
加载是类加载过程的第一步,主要完成下面 3 件事情:
1 | Class class = Class.forname("java.fansea.juc.Test") |
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在堆中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口
原文链接:https://javaguide.cn/java/jvm/classloader.html
双亲委派模型:当一个类加载器需要加载一个类时,它首先把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。只有当父类加载器无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
优势:
- 避免重复加载同一个类,保证类的唯一性,父类加载过就不会交给子类加载
- 保证Java程序的安全性,因为Java程序中不同的类加载器可能会加载同名但不同版本的类,对应层级的加载器只会加载自己范围内的类,避免用户自定义类与java自带的类冲突
为什么要打破双亲委派?
双亲委派无法满足实际情况的要求,这就需要打破,而自定义加载器:
JSP文件修改热部署(双亲委派机制无法做到),自定义热部署类加载器并通过监听器实现重新加载
类加载原理
每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。
1 | protected Class<?> loadClass(String name, boolean resolve) |
查看类加载器的的加载内容:
1 | cd C:\\Users\\ASUS\\.jdks\\corretto-1.8.0_352\\lib |
Bootstrap启动类是由hotspot虚拟机提供的,他是底层的类加载器,用于加载每个java项目常用的核心类,比如String类
Maven的jar包加载都依赖于Application加载器
类的生命周期
类的初始化
执行static代码块,给不是final的静态变量初始化赋值
- 当类被加载之后会触发一次初始化,但是在同一个类下多 new新对象就不会触发类加载,也不会触发初始化动作,不会执行静态代码块。
- 代码块在执行类构造器的时候会把代码合并在构造方法里面,并比构造方法更先执行
类的卸载
类被卸载需要满足三种情况,即使满足一般也不会被卸载
- 无该类的实例、加载该类的ClassLoader被卸载、该类对应的
java.lang.Class
对象没有在任何地方被引用
JVM内存结构
.png)
栈(运行时数据区)
栈帧的组成
局部变量表
位于栈帧,用于保存方法中的所有局部变量(包括:局部变量,方法参数,this(非静态方法))
操作数栈
用于保存运行时的临时数据
帧数据
保存方法出口(保存了上一个方法的地址,以便程序计数器将方法跳出)
内存溢出
堆
用于保存加载的对象信息,栈中局部变量创建对象其实是保存了堆中对象的引用地址
静态变量有什么作用?
静态变量也就是被
static
关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。Java 世界中“几乎”所有的对象都在堆中分配,但是从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
堆结构
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
来设置。
字符串常量池
JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
堆内存设置
- 设置堆内存
1 | -Xms<heap size>[unit] |
- 设置新生代内存
1 | -XX:NewSize=<young size>[unit] |
GC 调优策略中很重要的一条经验总结是这样说的:
将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。
新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快
另外,你还可以通过 -XX:NewRatio=<int>
来设置老年代与新生代内存的比值。
比如下面的参数就是设置老年代与新生代内存的比值为 1。也就是说老年代和新生代所占比值为 1:1,新生代占整个堆栈的 1/2。
1 | -XX:NewRatio=1 |
方法区
方法区的实现就是永久代和元空间是在堆里面的,在jdk7之前为永久代,jdk8之后为MetaSpace(元空间)
方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据
运行时常量池和常量池
- 常量池:数据的直接存储地址,一个java源文件中的类、接口,编译后产生一个字节码文件。而java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池
- 运行时常量池:一个方法的引用另外一个方法是建立在运行时常量池实现的,运行时常量池保存了方法的地址,可以将常量池中指向方法的符号引用转化为其在内存地址中的直接引用
运行时常量池
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的 常量池表(Constant Pool Table) 。
字面量:Class文件中的常量池表中存储的字面量确实可以被看作是常量,它们在编译时期就已经确定下来,并在运行时被直接引用
字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量。常见的符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号。
方法区内存设置
元空间初始和最大空间的设置
1 | -XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小) |
方法区的内部结构
- 类型信息
方法区需要存储每个加载的类(类,接口,枚举,注解)的以下类型信息:
- 完整名称(包类.类名)
- 这个类的直接父类的完整名称(接口和java.long.object没有父类)
- 这个类型的修饰符(public,abstract,final的某个子集)
- 这个类型直接接口的一个有序列表
- 域(属性)信息
JVM需要保存类型的域信息和域的声名顺序
域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
- 方法信息
JVM需要保存所有方法的信息及其声明的顺序
方法的名称,返回类型,参数(数量类型,按顺序),修饰符
方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
异常表(abstract和native方法除外)
- 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
- non-final的类变量(static)
静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
类变量被类的所有实例共享,即使没有类实例时,你也可以访问它
- 补充说明:全局常量(static final)被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
直接内存
垃圾回收
总结
Java的垃圾回收(Garbage Collection, GC)机制是自动管理内存的一部分,用于自动检测并回收那些不再使用的对象所占用的内存空间。下面我将从触发条件、分类以及常用的垃圾回收器来解释这一过程。
触发条件
垃圾回收通常在以下几种情况下被触发:
- 堆内存不足:当应用程序尝试创建新的对象,而堆内存已满无法分配更多空间时,会触发垃圾回收来释放不再使用的对象所占据的空间。
- **显式调用System.gc()**:虽然可以显式地调用
System.gc()
请求进行垃圾回收,但这并不保证JVM会立即执行GC操作。通常建议让JVM自动管理垃圾回收,因为显式调用可能会影响性能。分类
Java的垃圾回收可以分为不同的代:
- 年轻代(Young Generation):包括Eden区和两个Survivor区(S0, S1)。新创建的对象首先放置在Eden区,经过多次GC后仍然存活的对象会被移动到Survivor区或直接晋升到老年代。
- 老年代(Old Generation):存放长时间存活的对象,通常对象在年轻代经过多次GC后仍存活,则会被晋升到老年代。
- 永久代(Permanent Generation):在Java 7及以前版本,用于存放类的信息、常量池等数据。从Java 8开始,永久代被元空间(Metaspace)取代,元空间使用本地内存而非堆内存。
垃圾回收器
Java中有多种不同的垃圾回收器,每种都有自己的特点,适用于不同的应用场景:
- Serial Collector:单线程的垃圾回收器,适合于单CPU环境下的客户端应用。
- **Parallel Collector (又称Throughput Collector)**:多线程的垃圾回收器,专注于提高吞吐量(通过并行处理来减少总的运行时间)。
- **CMS Collector (Concurrent Mark Sweep)**:并发标记清除算法的垃圾回收器,尽量减少停顿时间,适合对延迟敏感的应用。
- **G1 Collector (Garbage First)**:目标是最小化停顿时间,并且能够预测停顿时间,适合大堆内存环境。
- ZGC (Z Garbage Collector) 和 Shenandoah:这两种垃圾回收器都设计为提供更短的停顿时间,适用于具有大量内存的系统。
选择哪种垃圾回收器取决于应用程序的需求,例如是否需要最小化停顿时间、最大化吞吐量等。每个垃圾回收器都有相应的JVM参数来启用或配置它们的行为。
方法区垃圾回收
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的
ClassLoader
已经被回收。- 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
堆内存垃圾回收算法
- 引用计数法
当对象被引用时维护一个引用计数器,被引用加一,解除引用则减一,当为0时回收对象
- 可达性分析算法
GC Root对象:
虚拟机栈引用对象(在虚拟机栈中代表对象肯定还在引用)
方法区常量、方法区静态变量(方法区加载的变量也一定在应用)
线程、本地方法栈引用的对象
虚拟机栈(栈帧中的局部变量表)中引用的对象
本地方法栈(Native 方法)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
所有被同步锁持有的对象
JNI(Java Native Interface)引用的对象
软引用
空间不足时进行垃圾回收,在这种情况下仍然不能解决内存不足的问题,才回收软引用的对象!
如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中
弱引用
只要垃圾回收直接被回收
虚引用与终结器引用
虚引用:和没有一样,不会决定对象的生命周期,在任何时候都可能被垃圾回收。
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
总结
- 软引用:空间不足时进行垃圾回收,在这种情况下仍然不能解决内存不足的问题,才回收软引用的对象!
- 弱引用:只要垃圾回收直接被回收
- 虚引用:和没有一样,不会决定对象的生命周期,在任何时候都可能被垃圾回收。
垃圾回收算法
垃圾回收衡量标准
- 吞吐量
- 最大暂停时间
- 堆使用效率
标记清除算法
复制算法
付出空间代价而获得效率,提高吞吐量
使用堆内存越小,处理垃圾回收的单位时间里越少,吞吐量大
标记整理算法
分代GC
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集
young区死亡对象多,可以着重提升吞吐量,使用复制算法,降低用户等待时间
old区存活时间长,空间大,单位时间死亡对象少,可以使用标记清除算法!
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
延伸面试问题: HotSpot 为什么要分为新生代和老年代?
垃圾回收器
垃圾收集器参数
- 设置回收器
JVM 具有四种类型的 GC 实现:
- 串行垃圾收集器 Serial -> ParNew(多线程)
- 并行垃圾收集器 Parallel Scavenge
- CMS 垃圾收集器
- G1 垃圾收集器
可以使用以下参数声明这些实现:
1 | -XX:+UseSerialGC |
- GC 日志记录
生产环境上,或者其他要测试 GC 问题的环境上,一定会配置上打印 GC 日志的参数,便于分析 GC 相关的问题。
1 | # 必选 |
Serial垃圾回收器
CMS回收器
老年代GC 本质采用 标记清除 算法,会产生很多的内存碎片,很容易导致 FullGC
- 初始标记(将GC Root能直接关联的对象标记出来,耗时极短)
- 并发标记(用户线程不需要暂停)对CPU消耗很大
- 重新标记(并发标记时会产生新的gc对象,需要重新标记)
为什么这里不直接就重新标记,是因为并发标记这个过程中产生的新垃圾是很少的,所以这个时候做重新标记性能消耗是很低的
- 并发清理(清理死亡对象,不需要停顿) 产生浮动垃圾,只能等下次清理
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
缺点:
- 对 CPU 资源敏感;
- 无法处理浮动垃圾;
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
ParNew
标记复制、并行化、与 CMS 收集器的配合
与 CMS 收集器的配合:ParNew 经常与 CMS(Concurrent Mark Sweep)收集器一起使用,后者用于老年代(Old Generation)的垃圾回收。在这种组合下,ParNew 负责年轻代的垃圾回收,而 CMS 负责老年代的垃圾回收。
parallel回收器(JDK1.8默认)
并行收集、可以手动设置吞吐量、最大暂停时间
JDK1.8默认Parallel Scavenge
(年轻代)+Parallel Old
(老年代)
Parallel Scavenge
:复制算法Parallel Old
:标记-整理算法
介绍一下 CMS,G1 收集器!
G1回收器(JDK1.8之后默认)
- 内部:标记复制
- 外部:标记整理
- CMS的并发标记
- 可预测停顿
- 并行垃圾回收
优势:
并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
1 | --XX:+UseG1GC // 打开G1开关,JDK9之后默认打开 |
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。 从 JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器
G1在收集过程中采用了标记-整理算法。标记阶段,G1会遍历对象图,找出所有存活的对象,并给它们打上标记。整理阶段,G1会移动存活的对象,使得它们连续排列在一起,同时回收掉所有的垃圾对象。这种算法的好处是,它可以避免内存碎片的产生,使得内存分配更加高效。
大对象(大于单个Region的一半的对象)直接保存在 Humongous 区,相当于老年代
GC垃圾回收的两种方式:
- 年轻代回收(Young GC)
执行动机:当年轻区(伊甸园区和幸存者区)占用空间大于60%时触发
执行过程:标记 Eden 和 Survivor 区,根据上次记录的region清理平均耗时时间来选择清理的数量;之后进行清理时将 Survivor 区清理搬运到其他区,判断是否搬运次数达到15次,将其搬运到Old区;
Tip:当一个存储内容大于region的一半就会将其直接存放到Humorous区(老年区)
- 混合回收(Mixed GC)
执行动机:当 Old 区占用超过了设定阈值(默认45%,可以设置-XX:InitiatingHeapOccupancyPercent)
处理对象: 所有年轻代、Humongous 区,部分老年代
执行流程:如下图
- FullGC
执行动机:当老年代空间不足以容纳新生成的对象或者大对象、大数组时,就可能触发Full GC
处理对象: 所有年轻代、Humongous 区、老年代
ZGC回收器
与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。
ZGC 可以将暂停时间控制在几毫秒以内,且暂停时间不受堆内存大小的影响,出现 Stop The World 的情况会更少,但代价是牺牲了一些吞吐量。ZGC 最大支持 16TB 的堆内存。
总结
Serial
:单线程,硬件条件不好的可以选择parallel
:多线程,增加吞吐量CMS
:并发标记,减少Stop The World时间,但是会有浮动垃圾G1
:分代GC、并行与并发、空间整合、控制Stop The World
时间ZGC
:标记复制、极大降低暂停时间,暂停时间不受堆内存大小的影响
对象
对象创建
- 加载类信息
- 分配内存
- 碰撞指针
- 空闲列表
- 初始化0值
- 设置对象头
- init对象
对象的内存布局
在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。
Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象的访问定位
建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有:使用句柄、直接指针。
句柄
如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
直接指针
如果使用直接指针访问,reference 中存储的直接就是对象的地址。
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
HotSpot 虚拟机主要使用的就是这种方式来进行对象访问。
面试题
JVM运行流程
一个典型的 Java 程序运行过程是下面这样的。
通过 Java 命令启动 JVM,JVM 的类加载器根据 Java 命令的参数到指定的路径加载.class 类文件,类文件被加载到内存后,存放在专门的方法区。然后 JVM 创建一个主线程执行这个类文件的 main 方法,mian 方法的输入参数和方法内定义的变量被压入 Java 栈。如果在方法内创建了一个对象实例,这个对象实例信息将会被存放到堆里,而对象实例的引用,也就是对象实例在堆中的地址信息则会被记录在栈里。堆中记录的对象实例信息主要是成员变量信息,因为类方法内的可执行代码存放在方法区,而方法内的局部变量存放在线程的栈里。
Java的各种变量在内存的区域?
1 | public class RunoobTest { |
在Java中,变量的存储位置取决于其作用域和生命周期。根据您提供的代码片段,我们可以分析如下:
private int i = 0;
:这是一个实例变量,它属于类的实例(对象)。每次创建一个新的RunoobTest
对象时,这个变量就会被分配在堆内存中的该对象实例区域里。当对象不再被引用时,这个变量所占的空间可能会被垃圾回收器回收。private static int m = 0;
:这是一个静态变量(也称为类变量),它属于类本身,而不是类的某个特定实例。所有的对象共享这个变量的一个副本。静态变量是在类加载的时候初始化,并且在整个程序执行期间存在,存储在方法区的静态变量区中。int j = 0;
:这是定义在main
方法内的局部变量。局部变量存在于栈内存中,它们随着方法的调用而创建,随着方法的结束而销毁。因此,当main
方法执行完毕后,这个变量就不再存在了。
总结一下:
- 实例变量(如
i
)保存在堆内存的对象实例中。 - 静态变量(如
m
)保存在方法区的静态变量区中。 - 局部变量(如
j
)保存在栈内存中。