赞
踩
目录
复制算法(Copying Garbage Collection):
分代算法(Generational Garbage Collection):
JVM(Java虚拟机)是Java编程语言的关键组成部分,它是一种虚拟计算机环境,用于执行Java程序。JVM的主要作用是将Java源代码编译成与特定计算机硬件无关的字节码,并在运行时将这些字节码转换为机器码,以便在不同平台上运行Java应用程序。
在JVM的运行环境中,Java程序能够实现跨平台的特性,因为它们不需要直接与底层操作系统进行交互,而是依赖JVM来处理与硬件的交互。这使得Java成为一种高度可移植和可扩展的编程语言。
JVM的关键功能包括:
- 类加载:JVM负责加载Java类的字节码文件,通过类加载器实现这一任务。
- 内存管理:JVM自动管理内存分配和垃圾回收,以确保应用程序不会出现内存泄漏和溢出。
- 字节码执行:JVM解释或编译Java字节码,将其转换为本地机器码以执行应用程序。
- 多线程支持:JVM提供多线程支持,允许并发执行Java应用程序的部分或全部代码。
- 垃圾回收:JVM使用垃圾回收机制来自动释放不再被引用的内存,以提高内存利用率。
JVM 是 Java 运行的基础,也是实现一次编译到处执行的关键,那么 JVM 是如何执行的呢?
程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码 文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调 用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。
总结来看, JVM 主要通过分为以下 4 个部分,来执行 Java 程序的,它们分别是:
1. 类加载器(ClassLoader)
2. 运行时数据区(Runtime Data Area)
3. 执行引擎(Execution Engine)
4. 本地库接口(Native Interface)
JVM 运行时数据区域也叫内存布局,但需要注意的是它和 Java 内存模型((Java Memory Model,简称JMM)完全不同,属于完全不同的两个概念,它由以下5大部分组成:
堆的作用:程序中创建的所有对象都在保存在堆中。
我们常见的 JVM 参数设置 -Xms10m 最小启动内存是针对堆的,-Xmx10m 最大运行内存也是针对堆的。
ms 是 memory start 简称,mx 是 memory max 的简称。
堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象会放入老生代。新生代还有 3 个区域:一个 Endn + 两个 Survivor(S0/S1)。
垃圾回收的时候会将 Eden 中存活的对象放到一个未使用的 Survivor 中,并把当前的 Endn 和正在使用的 Survivor 清楚掉。
Java 虚拟机栈的作用:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的 内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈。
Java 虚拟机栈中包含了以下 4 部分:
1. 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量 表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局 部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数 和局部变量。
2. 操作栈:每个方法会生成一个先进后出的操作栈。
3. 动态链接:指向运行时常量池的方法引用。
4. 方法返回地址:即在方法执行完成后将控制返回到调用方法的指令位置。方法出口通常用于支持方法调用的返回操作。
由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器 (多核处理器则指的是一个内核) 都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们就把类似这类区域称之为"线程私有"的内存。
程序计数器的作用:用来记录当前线程执行的行号的。
程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是一个Native方法,这个计数器值为空。 程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM情况的区域!
方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。
在《Java虚拟机规范中》把此区域称之为“方法区”,而在 HotSpot 虚拟机的实现中,在 JDK 7 时此区域叫做永久代(PermGen),JDK 8 中叫做元空间(Metaspace)。
汽车(Java虚拟机规范):在这个比喻中,汽车代表了Java虚拟机规范,它定义了Java虚拟机的基本结构和功能,其中包括了方法区的概念。
动能提供装置(方法区):这相当于汽车的一个重要部分,就像Java虚拟机中的方法区一样。方法区是用于存储类的结构信息、静态变量、常量池、方法的字节码等的内存区域。它提供了Java应用程序所需的关键信息。
发动机和电机(永久代和元空间):在不同类型的汽车中,动能提供装置可以有不同的实现。对于燃油车,它使用发动机作为动能提供装置,而电动汽车使用电机。同样地,Java虚拟机可以使用永久代或元空间来实现方法区。
永久代(PermGen):就像燃油车使用汽油发动机一样,一些早期版本的Java虚拟机使用永久代作为方法区的实现。永久代有一些限制,如固定大小,可能会导致内存溢出问题。
元空间(Metaspace):与之不同,元空间是Java虚拟机规范的一种新实现方式。它更灵活,不再受到永久代的限制,可以动态调整大小,避免了一些与永久代相关的问题。
总之,永久代和元空间都是Java虚拟机规范中对方法区的不同实现方式,就像汽油发动机和电动机都是动能提供装置的不同实现一样。选择使用哪种实现方式取决于Java虚拟机的版本和配置,以及应用程序的需求。这个比喻很好地概括了它们之间的关系。
1. 对于 HotSpot 来说,JDK 8 元空间的内存属于本地内存,这样元空间的大小就不在受 JVM 最大内存的参数影响了,而是与本地内存的大小有关。
2. JDK 8 中将字符串常量池移动到了堆中。
运行时常量池是方法区的一部分,存放字面量与符号引用。
字面量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免来 GC清除这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常。
我们可以设置JVM参数-Xms:设置堆的最小值、-Xmx:设置堆最大值。下面我们来看一个 Java堆OOM的测试,测试以下代码之前先设置 Idea 的启动参数,如下图所示:
JVM 参数为:-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError
示例:
- public class Main {
- static class OOMObject {
-
- }
- public static void main(String[] args) {
- List<OOMObject> list =
- new ArrayList<>();
- while(true) {
- list.add(new OOMObject());
- }
- }
- }
以上程序的执行结果如下:
Java堆内存的OOM异常是实际应用中最常见的内存溢出情况。当出现Java堆内存溢出时,异常堆栈信 息"java.lang.OutOfMemoryError"会进一步提示"Java heap space"。当出现"Java heap space"则很明 确的告知我们,OOM发生在堆上。
此时要对Dump出来的文件进行分析,以MAT为例。分析问题的产生到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
内存泄漏 : 内存泄漏是指在程序中分配了一些内存(通常是用于存储对象或数据),但在后续的程序执行中,无法释放或回收这些内存,导致内存消耗不断增加,最终可能导致程序性能下降或崩溃。
内存溢出 : 内存溢出(Memory Overflow)是指在程序运行时,尝试分配的内存超出了可用的物理内存或虚拟内存的范围,导致程序无法继续正常执行的情况。内存溢出通常是由于程序内存管理不当或程序本身存在缺陷引起的。内存溢出会导致程序崩溃或异常终止。
由于我们HotSpot虚拟机将虚拟机栈与本地方法栈合二为一,因此对于HotSpot来说,栈容量只需要由-Xss参数来设置。
关于虚拟机栈会产生的两种异常:
1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverFlow异常
2. 如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM异常
示例:
观察StackOverFlow异常(单线程环境下)
-
- /**
- * JVM参数为:-Xss128k
- */
- public class Test {
- private int stackLength = 1;
-
- public void stackLeak() {
- stackLength++;
- stackLeak();
- }
-
- public static void main(String[] args) {
- Test test = new Test();
- try {
- test.stackLeak();
- } catch (Throwable e) {
- System.out.println("Stack Length: " + test.stackLength);
- throw e;
- }
- }
- }
会出现如下运行结果:
出现StackOverflowError异常时有错误堆栈可以阅读,比较好找到问题所在。如果使用虚拟机默认参数,栈深度在多多数情况下达到1000-2000完全没问题,对于正常的方法调用(包括递归),完全够用。
如果是因为多线程导致的内存溢出问题,在不能减少线程数的情况下,只能减少最大堆和减少栈容量的 方式来换取更多线程。
- /**
- * JVM参数为:-Xss2M
- */
- public class Test {
-
- private void dontStop() {
- while(true) {
-
- }
- }
- public void stackLeakByThread() {
- while(true) {
- Thread thread = new Thread(new Runnable() {
- @Override
- public void run() {
- dontStop();
- }
- });
- thread.start();
- }
- }
-
- public static void main(String[] args) {
- Test test = new Test();
- test.stackLeakByThread();
- }
- }
以上代码运行电脑可能会崩,记得保存所有工作~~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。