ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JVM内存模型分析

JVM内存模型分析 一、JVM概念1、什么是JVMJVM是一种用于计算设备的规范它是一个虚构出来的机器是通过在实际的计算机上仿真模拟各种功能实现的。JVM虚拟机由类装载子系统、字节码执行引擎、运行时数据区组成。JVM屏蔽了与具体操作系统平台相关的信息使Java程序只需生成在Java虚拟机上运行的目标代码字节码,就可以在多种平台上不加修改地运行。2、JVM与JDK和JRE的关系1JRE(Java Runtime Environment)也就是java平台。所有的java程序都要在JRE环境下才能运行。2JDK(Java Development Kit)是开发者用来编译、调试程序用的开发包。JDK也是JAVA程序需要在JRE上运行。3JVM(Java Virtual Machine)是JRE的一部分。它是一个虚构出来的计算机是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关实现跨平台。二、JVM虚拟机1、JVM组成结构JVM虚拟机主要包含三部分 类装载子系统、字节码执行引擎、运行时数据区。程序在JVM虚拟机中执行过程是先通过类装载子系统将类、变量、以及类相关信息放到运行时数据区中。通过字节码执行引擎执行.class文件字节码中的代码。通过运行时数据区中的相关内存来计算运行具体行数代码。操作系统装入JVM是通过jdk中Java.exe来完成通过下面4步来完成JVM环境创建JVM装载环境和配置装载JVM.dll初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例调用JNIEnv实例装载并处理class类。2、JVM的生命周期JVM实例对应了一个独立运行的java程序它是进程级别。JVM执行引擎实例则对应了属于用户运行程序的线程它是线程级别的。1. 启动启动一个Java程序时一个JVM实例就产生了任何一个拥有public static voidmain(String[] args)函数的class都可以作为JVM实例运行的起点2. 运行main()作为该程序初始线程的起点任何其他线程均由该线程启动。JVM内部有两种线程守护线程和非守护线程main()属于非守护线程守护线程通常由JVM自己使用java程序也可以表明自己创建的线程是守护线程3. 消亡当程序中的所有非守护线程都终止时JVM才退出若安全管理器允许程序也可以使用Runtime类或者System.exit()来退出三、JVM内存模型JVM内存模型也称为JVM运行时数据区。由堆、栈、本地方法栈、方法区元空间以及程序计数器五部分组成。我们借助代码实例对JVM内存模型运行时数据进行分析1、代码运行时数据流转代码示例package com.test.jvm; /** * 分析JVM内存模型运行 */ public class Math { private final static int initData 666; private static User user new User(); public int compute(){ //一个方法对应一个块栈帧内存区域 int a 1; int b 2; int c (a b) * 10; return c; } public static void main(String[] args) { Math m new Math(); int result m.compute(); System.out.println(result); } }内存模型中数据运行以上面代码为例代码在内存模型中数据运行图如下2、内存模型各区域功能1. 栈虚拟机栈栈又被称为虚拟机栈主要存储方法、方法中局部变量以及运行时数据。也可叫做线程栈每个线程执行时都会从栈中划分出一块给线程单独使用所以栈是线程私有的。栈中存放的是一个个的栈帧程序中每个方法都有对应的一块栈帧内存区域。栈帧由局部变量表、操作数栈、动态链接、方法出口四部分组成。局部变量表存放方法中的局部变量默认有一个this变量。如compute()方法中a、b、c等变量操作数栈是程序运行中操作数进行运算的临时的中转存放内存空间。如进行a1、b2、ab、以及*10等计算操作在操作数栈中进行计算计算过程是在CPU的寄存器中进行。可以在.class字节码文件中查看操作数栈运行步骤。动态链接在程序运行过程中将符号引用转换为直接引用。如compute()方法 程序初始化时将符号放到了常量池Constant Pool中。程序初始化加载时只会解析静态方法将符号引用转换为直接引用称为静态链接。在程序运行时需要找到compute()符号对应的代码那么就需要对compute符号进行解析。解析compute()符号就是找compute()符号在方法区对应的直接地址方法实际代码在加载时 加载到了方法区 有对应的地址。这个过程就是将符号引用转换为直接引用被称为动态链接。方法出口当前方法执行完需要回到执行这个方法后面的位置接着往下执行那么这个方法调用地方后面的位置信息就存储在方法出口中。如调用compute()方法时已经将main方法运行的线程信息存放到了方法出口区域了。线程在栈帧中运行如下通过javap -c 对代码进行反汇编生成Math.txt文件就可以查看文件在栈帧中执行过程。如下打开Math.txt文件就可看到compute()执行过程。可以通过官方文档查看命令对应的具体操作2. 堆Heap堆是用来存储对象和数组数组引用是存放在Java栈中的。在JVM中只有一个堆。只要是堆中的对象就可以被所有线程共享静态变量、静态常量、字符串存储在堆中的老年代里。对于基本数据类型对象如byte、short、int、long、float、double、char)在方法体内声明时会直接分配在栈中,其它情况都会分配在堆中。对于普通对象来说JVM 会首先在堆上创建对象然后在其他地方使用它的引用。如main()方法中Math对象的引用保存在栈帧的局部变量表中。但是在开启了逃逸分析时如果发现某个对象只会在方法内部使用则可能会将该对象经过标量替换后也存在栈中。堆的几个重要参数-Xms堆的最小值初始值默认单位是字节要求是1024的整数倍-Xmx堆的最大值-Xmn新生代的大小-XX:NewSize新生代最小值初始值-XX:MaxNewSize新生代最大值堆的组成堆由年轻代和老年代组成两者默认比例为1:2。年轻代包含eden区和两个Survivor区它们的比例默认 8:1:1。年轻代中的对象经过多次mintor GC回收后若一直存活当对象年龄达到15时就会将对象移动到老年代中。因为对象年龄是在对象头中用4个bit存储所以最大值为15这就是-XX:MaxTenuringThreshold选项最大值为15的原因。Mintor GC当eden区内对象存满时就会触发mintor GC这时会对eden区和其中一个survivor区进行GC对垃圾对象死亡对象进行回收将非垃圾对象存活对象采用复制算法放到另一个空的Survivor区。这时存活的对象年龄1。Full GC当老年代中对象存储满时就会触发Full GCFull GC是对整个堆内存和方法区内存空间进行垃圾回收。当老年代中的对象还是被GC root引用无法进行回收时再往老年代中放对象时就会触发OOMSTW机制STW机制全称叫Stop the World机制该机制是在堆发生GCmintor、Full过程中会暂停用户线程不让线程继续执行。凡是由用户发起的线程都被称为用户线程。STW机制的核心目的是确保垃圾回收过程中内存分析的一致性。在进行可达性分析算法时JVM需要确保整个堆内存的状态在某一时刻是静态的、一致的以便准确地判断哪些对象是可达的哪些对象是不可达的。为什么设计STW机制不使用STW机制不行吗不行若没有STW机制无法确保堆内存中某一时刻是静态的。无法判断出哪些对象是非垃圾对象那些是垃圾对象这样就会出现判断不准问题若是这样就无法确保内存分析的一致性且会大大增加垃圾回收算法的复杂度和实现难度。如执行main线程时 在执行c (a b) *10 时发生了GC在GC过程中对堆内存中对象进行了可达性分析判断出math变量是非垃圾对象。继续进行分析时若没有STW机制那么线程就会继续执行 线程结束时 意味着线程中局部变量如math出栈进行销毁。意味着math变量就成了垃圾对象与之前分析的冲突了需要重新判断。3. 程序计数器每个线程都有一个自己的程序计数器用来存放线程正在运行或马上运行的哪一行代码的位置或行号。字节码运行引擎执行程序每执行一行代码就会对程序计数器进行修改。由于在JVM中多线程是通过线程轮流切换来获得CPU执行时间的因此在任一具体时刻一个CPU的内核只会执行一个线程中的指令。因此为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置每个线程都需要有自己独立的程序计数器并且不能互相被干扰。所以程序计数器是每个线程所私有的。由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变因此对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。4. 方法区元空间方法区与堆一样是被线程共享的区域。在方法区中存储了每个类的信息包括类的名称、方法信息、字段信息、静态变量、常量以及编译器编译后的代码等。在Class文件中除了类的字段、方法、接口等描述信息外还有一项信息是常量池用来存储编译期间生成的字面量和符号引用。运行时常量池在方法区中有一个非常重要的部分就是运行时常量池它是每一个类或接口的常量池的运行时表示形式在类和接口被加载到JVM后对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池在运行期间也可将新的常量放入运行时常量池中比如String的intern方法。若静态变量是Object对象那么静态变量在方法区中存放的是对象在堆中的内存指针地址如Math.class文件中User变量。元空间大小参数设置jdk1.7 及以前-XX:PermSize-XX:MaxPermSizejdk1.8 以后-XX:MetaspaceSize -XX:MaxMetaspaceSize JDK1.8的MetaspaceSize大小默认21M。不是初始内存是触发Full GC扩容阈值方法区使用的是物理内存直接内存若不设置元空间上限元空间默认可以无限扩容受限于物理内存。注意MetaspaceSize256M不是JVM启动就直接占用256M本地内存而是当元空间使用达到256M触发Full GC进行元空间扩容。大的项目程序启动慢的情况下可看下是否设置了方法区的配置。因为若是不设置在程序启动时可能就会频发达到扩容阈值扩容就会发生full GC 通过动态扩容方式再对方法区进行扩容这样就会大大影响程序启动。这也是JVM优化的一点。5. 本地方法栈属于线程私有内存区域一个线程对应一份本地方法栈和虚拟机栈成对出现。 虚拟机栈执行 Java 方法字节码本地方法栈执行 Native 本地方法JNIC/C 写的方法。1. 什么是 native 本地方法方法带native关键字没有 Java 实现底层由 C/C 实现通过 JNI 调用操作系统底层能力。public class Object { // native本地方法没有方法体 public native void wait(); public native Object clone(); }还有System.currentTimeMillis()、线程 start () 底层、文件 IO、操作系统调用都是 native 方法。Java 方法 → 执行栈帧放在虚拟机栈native 方法 → 执行栈帧放在本地方法栈2. 本地方法栈里面存什么每调用一个 native 方法就压入一个本地方法栈帧栈帧保存 C/C 方法的局部变量、返回地址、操作系统调用上下文native 方法执行完毕栈帧弹出销毁。和 Java 虚拟机栈结构很像只不过栈帧是给 C/C 本地代码使用不是给字节码使用。3. 结合你之前那张 JVM 内存图图左侧本地方法栈和虚拟机栈并列属于 main 线程私有。当 Java 代码调用obj.wait()字节码执行引擎不再跑 java 字节码切换到本地方法栈执行 C 代码。native 方法执行完切回虚拟机栈继续运行 Java 代码。4. 会发生什么异常StackOverflowError本地方法递归调用栈深度超过上限栈溢出。OutOfMemoryError线程创建过多本地方法栈内存耗尽。✨对比虚拟机栈 Java 虚拟机栈-Xss 参数控制大小 本地方法栈栈大小通常和 - Xss 共用同一个设置HotSpot 中不提供独立参数。‑Xss同时影响虚拟机栈 本地方法栈。5. 典型工作流程举例obj.wait();Java 线程虚拟机栈当前栈帧执行wait()字节码发现这是native标记的本地方法切换在本地方法栈压入 native 栈帧执行 C 实现的 wait 逻辑C 调用操作系统内核让线程阻塞被唤醒后本地方法栈帧弹出回到 Java 虚拟机栈继续执行后面 Java 代码。6. 面试高频问题Q1本地方法栈和虚拟机栈区别虚拟机栈执行 Java 字节码存放 Java 方法栈帧。本地方法栈执行 JNI Native (C/C) 方法存放本地方法栈帧。 两者都是线程私有每个线程一份栈溢出都会抛出 StackOverflowError。Q2什么场景会用到本地方法栈调用native方法Object#wait ()/notify ()、clone、System.arraycopy、Thread.start 底层、文件、网络、操作系统调用。普通业务代码没有调用 native则本地方法栈几乎没有栈帧。Q3HotSpot 中本地方法栈可以单独设置大小吗HotSpot 没有专门参数跟随-Xss和 Java 虚拟机栈共用栈大小。修改 - Xss两者同时生效。Q4本地方法栈属于堆吗不属于。线程私有属于线程栈内存不在堆不在元空间。根据上面分析可知栈、本地方法栈、程序计数器是线程私有的堆和元空间是线程共享的。
返回列表