-
运行时数据区域
线程私有:程序计数器、虚拟机栈、本地方法栈。
线程共享:堆、方法区、直接内存
-
程序计数器:
程序计数器是一块较小的内存空间。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令 ,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储 ,我们称这类内存区域为"线程私有"的内存。
作用:
1 字节码解释器通过程序计数器来依次读取指令,实现代码流程控制
2 多线程情况下,线程私有,方便线程上下文切换回来能够知道该线程上次运行到哪里了。
-
Java虚拟机栈
Java 虚拟机栈(后文简称栈)也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。
方法调用的数据需要通过栈进行传递 ,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。 -
本地方法栈
虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息
-
堆
Java 虚拟机所管理的内存中最大的一块,**Java 堆是所有线程共享的一块内存区域,**在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例 ,几乎所有的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆
由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代 ;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:新生代内存(Young Generation)老生代(Old Generation)永久代(Permanent Generation)
JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。 -
方法区
方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。
当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
-
运行时常量池
用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的 常量池表(Constant Pool Table) 。常量池表会在类加载后存放到方法区的运行时常量池中。
-
字符串常量池
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
-
直接内存
直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。
-
JVM垃圾回收
堆空间基本结构:
新生代内存(Young Generation)
老生代(Old Generation)
永久代(Permanent Generation)
Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代。
JDK 8 版本之后 PermGen(永久) 已被 Metaspace(元空间) 取代,元空间使用的是直接内存
-
内存分配和回收原则
对象优先在Eden区分配 。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC
大对象直接进入老年代 。大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。大对象直接进入老年代的行为是由虚拟机动态决定的,它与具体使用的垃圾回收器和相关参数有关。大对象直接进入老年代是一种优化策略,旨在避免将大对象放入新生代,从而减少新生代的垃圾回收频率和成本。
长期存活的对象将进入老年代虚拟机给每个对象一个对象年龄(Age)计数器,
对象都会首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间(s0 或者 s1 )中,并将对象年龄设为 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。
对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁 ),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。默认晋升年龄并不都是 15,这个是要区分垃圾收集器的,CMS 就是 6. -
主要进行GC的区域
针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:
部分收集 (Partial GC):
新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个 Java 堆和方法区。
-
空间分配担保
空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC
-
死亡对象判断方法
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象 )
引用计数法:给对象添加一个引用计数器。每当有一个地方引用,计数器加1,引用失败,计数器减1,任何时候计数器为0的对象就不可能再被使用。该方法虽然效率高,实现简单,但无法解决对象之间循环引用的问题
可达性分析算法
通过一系列的称为 "GC Roots " 的对象作为起点,**从这些节点开始向下搜索,节点所走过的路径称为引用链,**当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
GC Roots :
虚拟机栈(栈帧中的局部变量表)中引用的对象
本地方法栈(Native 方法)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
所有被同步锁持有的对象
JNI(Java Native Interface)引用的对象
**对象可以被回收不代表着一定会被回收。**要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,(对象没有覆盖finalize方法或finalize方法已经被调用过了,就没必要执行)。把认为需要的对象放入到一个队列,如果没有其他对象和它建立联系,则被清理回收。
-
引用类型
无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与"引用"有关
强引用 :那就类似于必不可少的生活用品,垃圾回收器绝不会回收它 。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。软引用:可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
弱引用:弱引用与软引用的区别在于:**只具有弱引用的对象拥有更短暂的生命周期。**在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存 。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。(使用软引用情况更多) -
如何判断一个常量是废弃常量
运行时常量池主要回收的是废弃的常量
假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池了
-
如何判断一个类是无用的类
方法区主要回收的是无用的类
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
满足上面条件可以回收但不是一定会回收
-
垃圾回收算法
标记清除算法:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
效率不高、标记清除后会产生大量不连续的内存碎片
复制算法:将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。
可用内存变小、不适合老年代
标记整理算法:标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
效率不高(存在整理的步骤)
分代收集算法:当前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,根据各个年代的特点选择合适的垃圾收集算法。
新生代:标记-复制算法 老年代:标记整理 标记清除
-
垃圾回收器
Serial(串行)收集器是最基本的。
它的 "单线程" 的意义不仅仅意味着它只会使用一条垃圾收集线程 去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程 ( "Stop The World" ),直到它收集结束。
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器。
Serial old serial 收集器的老年代版本,它同样是一个单线程收集器。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
并发收集、低停顿.对CPU资源敏感、无法处理浮动垃圾、采用的是标记清除算法,会产生空间碎片
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
初始标记
并发标记
最终标记
筛选回收
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 RegionZGC 也采用标记-复制算法。ZGC 可以将暂停时间控制在几毫秒以内,且暂停时间不受堆内存大小的影响,但牺牲了一些吞吐量。
-
JVM参数
-
堆内存相关
堆内存唯一目的就是存储对象实例(new之后的)
-Xms(最小) -Xmx(最大)显式指定堆内存(最小和最大堆大小)
-Xms2G -Xmx5G
显式新生代内存
-XX:NewSize
-XX:MaxNewSize
-XX:NewSize=256m
-XX:MaxNewSize=1024m
或者可以用-Xmn[unit]
-Xmn256m
-
显式指定永久代/元空间的大小
-XX:MetaspaceSize=N #设置 Metaspace 的初始大小
-XX:MaxMetaspaceSize=N #设置 Metaspace 的最大大小
Metaspace 的初始容量并不是 -XX:MetaspaceSize 设置,无论 -XX:MetaspaceSize 配置什么值,对于 64 位 JVM 来说,Metaspace 的初始容量都是 21807104(约 20.8m) -
垃圾回收器
JVM 具有四种类型的 GC 实现:
串行垃圾收集器
并行垃圾收集器
CMS 垃圾收集器
G1 垃圾收集器
可以使用以下参数声明这些实现:-XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+UseParNewGC
-XX:+UseG1GC
GC日志
-
处理OOM
内存不足错误
这些参数将堆内存转储到一个物理文件中,以后可以用来查找泄漏
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./java_pid.hprof
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >"
-XX:+UseGCOverheadLimit
-
JDK监控和故障处理工具
-
JDK命令行工具
jps:查看所有的Java进程的启动类、传入参数和Java虚拟机参数等信息
jstat:收集hotspot虚拟机各方面的运行数据
jinfo:显示虚拟机配置信息
jmap:生成堆转储快照
jhat:分析heapdump文件,建立一个http/html服务器,让用户可以在浏览器查看分析结果
jstack:生成虚拟机当前时刻的线程快照,线程快照
-
jps:
显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID
-q:只输出本地虚拟机唯一ID
-l:输出主类的全名,如果进程执行的是jar包,输出jar的路径
-v:输出虚拟机进程启动时JVM的参数
-m:输出传递给Java进程main函数的参数
-
jstat
用于监视虚拟机各种运行状态信息的命令行工具。显示本地或者远程(需要远程主机提供 RMI 支持)虚拟机进程中的类信息、内存、垃圾收集、JIT 编译等运行数据。是运行期间定位虚拟机性能问题的首选工具
jstat - [-t] [-h] [ []]
比如 jstat -gc -h3 31736 1000 10表示分析进程 id 为 31736 的 gc 情况,每隔 1000ms 打印一次记录,打印 10 次停止,每 3 行后打印指标头部。
常见option:
加上一个-t参数可以显示程序的运行时间
-
jinfo:实时的查看和调整虚拟机各项参数
jinfo vmid :输出当前 jvm 进程的全部参数和系统属性 (第一部分是系统的属性,第二部分是 JVM 的参数)
jinfo -flag name vmid :输出对应名称的参数的具体值。比如输出 MaxHeapSize、查看当前 jvm 进程是否开启打印 GC 日志 ( -XX:PrintGCDetails :详细 GC 日志模式,这两个都是默认关闭的)。
C:\Users\SnailClimb>jinfo -flag MaxHeapSize 17340-XX:MaxHeapSize=2124414976
C:\Users\SnailClimb>jinfo -flag PrintGC 17340
-XX:-PrintGC
使用Jinfo可以在不重启虚拟机的情况下动态修改jvm的参数
jinfo -flag [+|-]name vmid 开启或者关闭对应名称的参数
C:\Users\SnailClimb>jinfo -flag PrintGC 17340\查看printGC参数状态,关闭状态,不会打印垃圾回收信息
-XX:-PrintGC
C:\Users\SnailClimb>jinfo -flag +PrintGC 17340\打开了printGC参数,表示虚拟机会打印垃圾回收信息
C:\Users\SnailClimb>jinfo -flag PrintGC 17340\查看printGC参数状态
-XX:+PrintGC
-
jmap生成堆转储快照,不仅可以获取到dump文件,还可以查询finalizer执行队列、java堆和永久代的详细信息,如空间使用率、当前使用的哪种回收器。
-
jhat:分析heapdump文件。它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果
-
jsatck:生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合.
生成线程快照的目的主要是定位线程长时间出现停顿的原因 ,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的原因。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者在等待些什么资源。
-
jdk可视化分析工具
JConsole:java监视与管理控制台
Visual VM:多合一故障处理工具
-
类加载过程
-
类的生命周期
系统加载 Class 类型的文件主要三步:加载->连接->初始化 。连接过程又可分为三步:验证->准备->解析
加载通过类加载器完成,
验证 是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
准备 阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配(方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据 )
解析阶段 是虚拟机将常量池内的符号引用替换为直接引用的过程。解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量
初始化 阶段是执行初始化方法 ()方法的过程,是类加载的最后一步 ,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。**卸载类即该类的 Class 对象被 GC。**在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
-
类加载器
赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
每个 Java 类都有一个引用指向加载它的 ClassLoader。
数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
类加载器的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象) -
类加载器规则
JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。
对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。
-
类加载器总结
JVM 中内置了三个重要的 ClassLoader:
BootstrapClassLoader(启动类加载器 ):最顶层的加载类,由 C++实现,通常表示为 null (Java中没有与之对应的类,所以是null ),并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jar、resources.jar、charsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
ExtensionClassLoader(扩展类加载器 ):主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
AppClassLoader(应用程序类加载器 ):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader抽象类。 (都有父类)
-
双亲委派模型
ClassLoader 类使用委托模型来搜索类和资源。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器 。
ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载 (JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。
-
打破双亲委派模型方法
自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。
Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。可以让高层的类加载器(SharedClassLoader)借助子类加载器( WebAppClassLoader)来加载业务类,破坏了 Java 的类加载委托机制,让应用逆向使用类加载器。
JVM总结
buptlzl2024-04-04 20:10