有头发且有趣的码农万里挑一:06有料叔——程序猿解析JVM内存结构

有头发且有趣的码农万里挑一~

有头发且有趣的码农万里挑一:06有料叔——程序猿解析JVM内存结构

[id_413266381]

有料叔 | 一位有故事的程序猿

JVM内存结构

可以看出,JVM 从宏观角度来看,可以划分成“内部”以及“外部”这两个部分,这样便于记忆和理解。

内部包含以下两个部分:一是线程共享(公有)的数据区;二是线程隔离(私有)的数据区。

“外部”包含以下这些方面:有类加载子系统;还有垃圾回收器;以及执行引擎;包含本地库接口;并且有本地方法库。

以上部件构成了整个jvm,接下来我们一个一个零件拆开了看。

class文件

一个 java 文件能够借助编译工具(javac)被编译成 class 字节码文件,接着通过 jvm 来进行加载和运行。由于 jvm 把底层操作系统的差异给屏蔽了,具备平台无关性,所以能够一次编译然后在各处运行。

类加载子系统

类加载子系统的职责是查找并装载 class 文件,然后把 class 文件中的二进制数据加载到 jvm 当中。

字节码被加载,接着进行验证,然后是准备,之后进行解析,最后是初始化。

通过类的完全限定名能够找到类文件所在的位置,依据其中的字节码可以创建 java.lang.Class 对象,正因如此才会有万物皆对象的说法,我们还可以继承 ClassLoader 并重写 findClass 方法,以此来自定义实现类加载器。在默认情况下,我们都使用 AppClassLoader。

验证的目的是确保加载的字节码符合虚拟机的要求,这是 Java 提供的一种自我保护机制,目的是防止字节码危害虚拟机安全。这种验证主要包括四种,分别是字节码验证、文件格式验证、元数据验证和符号引用验证。

准备:为类变量分配地址,同时进行初始化值的设定。类变量会被分配到方法区(元空间)里。这里所说的初始化,指的是该数据类型的默认初始值,像 int 对应的是 0,long 对应的是 0L。只有在初始化这个阶段,才会进行显示赋值的动作。

解析:将类中的二进制数据所含的符号引用转化为直接引用;比如我们借助 user.getInfo(),这里的.getInfo()属于符号引用,在解析阶段会让它指向实际的内存位置,此即为直接引用。

初始化主要做的事情是给类的静态变量赋予正确的值,例如将 int num 赋值为 10,此时 num 的值会从准备阶段的 0 转变为 10;倘若该类存在父类,那么就会对父类进行初始化操作;要是类中存在初始化语句,系统会依照顺序来进行初始化。

双亲委派模式

双亲委派:自底向上检查是否加载成功,自顶向下尝试加载。

当一个类加载器收到类加载请求时,它不会自行进行加载。它会把这个请求丢给父类加载器。如果父类加载器还有父类,就会依次向上请求。一直到到达顶级加载器。如果父类加载器能够完成加载,就会返回加载成功。否则,子类加载器才会自己尝试进行加载。

System 输出 Test 类的类加载器。

System.out.println(Test 类的类加载器的父类加载器);

System.out.println(获取 Test 类的类加载器的父类加载器的父类加载器());

System 输出类 String 的类加载器。

通过代码进行验证,能够较为容易地知晓 AppClassLoader 与 ExtClassLoader 以及 BootstrapClassLoader 这三层之间的关系。

类加载的三种方式

1. new关键字加载

User user =newUser();

静态加载,在运行时候通过new关键字创建类实例

2.Class.forName()加载

通过 Class.forName("User") 来获取 Class 实例,将其赋值给 clazz 这个变量。

创建一个类的实例,这个实例是通过 clazz 类的 newInstance()方法创建出来的。

动态加载的方式是通过 Class.forName() 来加载类,接着调用类的 newInstance() 方法以实现对象的实例化。

ClassLoader 实例的这个方法叫做 loadClass()

Object user=clazz.newInstance();

动态加载的方式是可以通过继承 ClassLoader 来实现自定义类加载器。

线程私有和线程公有

JVM内存区从宏观上可以分为线程私有和线程公有两块。

线程私有部分

这部分不存在线程安全方面的问题,它会随着线程的执行完毕而结束。它包含着程序计数器、虚拟机栈以及本地方法栈这三个部件。

程序计数器:

程序计数器又被称作 PC 寄存器。它的作用在于,当 cpu 进行切换时,能够指向当前时刻需要获取指令的那个位置。

特点:

线程私有

一块较小的区域

记录程序执行的位置

不存在内存溢出OutOfMemoryError

虚拟机栈:

栈数据结构的实现中,入口和出口都只有一个。这两个操作分别被称为入栈和出栈。其特点是先进后出(FILO)。

栈的主要作用是执行方法。最先执行的方法处于最下方,接着依次进行放置。当方法执行完毕后,会从上到下依次退出。因此,方法的执行就是向栈中压入元素,而方法的结束就是从栈中弹出元素(销毁栈帧)。

publicvoidstart(){

say();

run();

虚拟机栈如何执行

栈帧

栈帧位于 Java 虚拟机栈内,它是虚拟机栈里的基本单位元素。当方法开始执行时,就会创建栈帧,每一个方法对应着一个栈帧,并且一个栈帧包含四个部分:

1. 局部变量表

存放方法参数,或者是内部定义的一组变量列表;例如在方法中声明的对象:

创建了一个名为 user 的局部变量,其类型为 User,即 newUser() 所代表的类型。

2. 操作数栈

在执行字节码指令时使用,通俗来讲,方法的执行是在操作数栈中进行的,通过压栈和出栈来进行访问。

3. 动态链接

Java运行期间是动态链接的,需要把指向方法的符号引用转变为直接引用(即内存地址);在类加载解析阶段,把符号引用转变为直接引用这种行为被称作静态解析。而这里恰恰就是动态链接。

找到这个 getInfo()方法所在的内存位置。

4. 返回地址

方法不管正常执行结束还是异常退出,需要返回方法被调用的位置

以上四个部分对应着方法执行的过程。在虚拟环境中存在着许多个栈帧,并且每个方法都对应着一个栈帧。

使用 bin/javap.exe 文件对一个 class 文件进行反汇编,能够查看出上述四个部分。

栈溢出是指当栈的深度超过虚拟机所允许的范围时,就会报 StackOverflowError。而 -Xss 这个参数可以用来设置栈的大小。

10

11

12

13

/** 递归演示如何栈溢出 */

publicstaticintnum =0;

publicstaticvoida(){

num++;

a();

公共静态 void 主函数(String 数组 args) {

try{

a();

}catch(Exception ex){

System.out 会打印出“调用次数:”加上 num 的值。

内存溢出的情况是,当栈需要进行扩展时却无法申请到空间,此时就会报 OutOfMemoryError 。

本地方法栈

本地方法栈和虚拟机栈有相似之处。其区别在于,虚拟机栈主要是为 jvm 执行字节码提供服务,而本地方法栈则是为 Native 方法服务,也就是为本地方法提供服务。因此,本地方法栈同样是一块内存私有区域,并且和虚拟机栈一样,也会存在同样的异常问题。

特点:

与虚拟机栈基本类似

区域是本地方法栈会为 Native 方法提供服务,比如在 Windows 系统下会调用 dll 文件。

Sun HotSpot将虚拟机栈和本地方法栈合并

存在 StackOverflowError 以及 OutOfMemoryError

线程公有部分

这部分存在线程安全方面的问题。平常我们所提及的内存优化问题以及溢出等问题,都需要对这个区域予以关注。这个区域包含堆以及方法区(也叫元空间)这两个部件。

方法区(元空间)

类加载器在加载类的时候,会把一些类的元数据信息(字节码)留存于这个区域。比如类变量、静态方法以及普通方法等。并且,方法区是线程共享的,多个线程能够使用同一个类。

jdk1.7合并方法区到了堆里面

从逻辑上可以认为它是在堆中。

特点:

线程共享

存储类信息、常量、静态变量、方法描述等信息

HotSpot虚拟机中称之为永久代

GC很少回收这个区域

存在 OutOfMemoryError 这种情况,能够通过 -XX:MaxPermSize 来设置其大小。

堆

堆用于存放所有实例化对象和数组,堆中的信息是线程共享的。它是所有 jvm 部件中分配内存最大的区域,在虚拟机启动时就被创建。垃圾回收器主要对该区域进行管理。堆分为新生代和老年代,新生代占堆内存的 1/3,老年代占堆内存的 2/3。新生代更为细致,可分为 Eden、From Survivor、To Survivor 空间,比例为 8:1:1。可以通过 -Xmx、-Xms 设置堆的大小。

在堆中生成了一个实例对象或者数组。可以在栈中声明一个变量,此变量用来指向堆中的对象。该变量的取值与堆中对象的内存地址相等。因此,当我们打印变量名时,显示的是一串内存地址。

Test test =newTest();

System.out 会输出 test,输出的内容为 Test@1b6d3586

万物皆为对象。在实际开发过程中,当我们创建了众多对象时,为避免内存泄露,java 会确保对内存进行有效使用,并且由 java 虚拟机的自动垃圾回收器来进行管理。同时,java 会把堆划分成新生代和老年代进行管理。

新生代与老年代

新生代是 Java 对象诞生的场所,也是新对象分配内存的地方。大多数对象的存活时间并不长,所以这个区域会常常触发 MinorGC 来进行垃圾回收。

老年代存放的对象存活时间较久或者内存较大。因此,Full GC 不会频繁执行。

Minor GC

新生代中发生的垃圾回收机制采用复制算法,该算法会扫描存活对象并将其复制到新的内存空间中。From Survivor 和 to Survivor 是相对的概念。当 Minor GC 发生时,Eden 区和其中一个 Survivor 区会把一些仍然存活的对象放置到另一个 Survivor 区,接着清理 Eden 区和之前的 Survivor 区。下次 Minor GC 时也是如此。当对象达到一定“年龄”后,新生代会将其放入老年代,每发生一次 Minor GC,对象的年龄就增加 1 岁,默认情况下对象达到 15 岁时会进入老年代。

Full GC

老年代中发生的垃圾回收机制是标记 - 清除,也就是标记存活的对象,清除未标记的对象,也就是需要回收的对象。由于老年代中的对象比较稳定,所以 Full GC 发生的频率相较于 Minor GC 要少,不过一次回收所花费的时间会比 Minor GC 更长。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

26阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

26阅读