内存管理 内存管理也称为垃圾回收(GarbageCollection),指的是虚拟机在应用程序运行时管理应用程序使用的内存。Java代码中只需要分配内存而不需要考虑释放内存,内存释放的工作交由虚拟机处理。虚拟机在内存管理中通常要做以下4方面的事情。 1)分配(Allocate):从OS请求内存,虚拟机需要考虑何时请求内存,请求内存的粒度。 2)使用(Use):针对应用程序的请求,设计连续内存或者非连续内存的管理,为应用程序提供高速内存分配。 3)回收(Recycle):当虚拟机管理的内存都被使用时,需要识别内存中的活跃对象,对活跃对象保留或者对非活跃对象释放,完成非活跃对象占用内存的回收,并将回收后的内存重新用于应用程序的分配。 4)释放(Free):向OS归还内存,虚拟机需要考虑何时释放内存,释放内存的粒度等。 不是所有的虚拟机都包含分配和释放这两个步骤,主要原因是虚拟机在实现时可以借助一些内存管理库来代替自己提供这些功能。 另外,需要指出的是,这里所说的使用和回收是大家常提到的分配和回收。本节使用分配和使用来区别虚拟机向OS请求内存及应用程序向虚拟机请求内存。为了保持阅读的一致性,后文统一使用分配和回收替代此处的使用和回收。 然而设计和实现一款垃圾回收器并不容易,不同的应用场景对于垃圾回收的诉求也不相同。一款垃圾回收器主要从以下几个方面衡量。 1)吞吐量:指的是在一段时间内回收的内存量。吞吐量越大说明垃圾回收器的效率越高。 2)停顿时间:指的是垃圾回收器在垃圾回收过程中可能会要求应用暂停以配合垃圾回收的工作。停顿时间越长,则说明垃圾回收器对应用的影响越大,停顿时间越短,说明垃圾回收器对应用的影响越小。 3)数据访问的局部性:垃圾回收器在进行垃圾回收时可能会调整内存中活跃对象的位置,当对象的位置发生变化后会影响应用访问内存的速度,从而影响应用程序执行的效率。 4)额外资源消耗:垃圾回收器实现时都需要额外的内存管理其内部数据结果。不同的垃圾回收器采用的算法不同,使用的数据结果也不同,占用的额外资源也不同。通常来说,额外资源消耗越少,说明垃圾回收器越优秀。 本文后面将详细介绍JVM中实现的垃圾回收器,读者在阅读相关章节时可以从这个几个方面思考垃圾回收器实现的优劣。 线程管理 通常高级语言都支持多线程,所以虚拟机需要考虑如何高效地支持多线程,例如高级语言的线程和虚拟机的线程以及操作系统的线程关系是什么?是否可以支持协程?这些内容都非常复杂,部分内容也和垃圾回收密切相关,但限于篇幅,本文不展开介绍。 以JVM为例,JVM为了执行字节码或者编译代码,需要为代码准备执行的线程和线程栈。例如当启动JVM后,启动线程将变成执行Java的main线程,如果在Java代码中产生新的线程,则由OS产生线程。 所以,从这个角度来说Java字节码或者编译代码的执行和C语言的执行完全一致。但是JVM为了更好地管理和执行代码,实现了线程对象和线程栈对象,线程对象和线程栈对象也是分配在JVM的本地堆中。线程对象和线程栈对象除了会关联真正底层OS的线程之外,还会存储一些额外的信息,这些信息用于描述当前线程和线程栈的信息,比如线程属于哪个Java线程对象、关联哪个类加载器、线程栈的调用链信息等。 另外,高级语言通常会支持多言语的互操作,当进行互操作时,需要考虑不同语言线程执行的约定,例如参数和返回值如何组织,内存是否可以互访问等。在JVM中支持通过JNI(JavaNativeInterface)的方式调用CC代码,但是这样的互操作除了要考虑线程管理以外,还要考虑内存的影响,特别是垃圾回收的影响。例如JVM在执行一些JNI时通常会阻塞垃圾回收的执行(例如调用JNI的CriticalAPI),当然阻塞与否还与垃圾回收器的实现有关。在第2章中介绍安全点相关的知识时会进一步展开介绍。扩展阅读:JIT概述 虚拟机的实现通常可以划分为3部分:运行时(RunTime)、编译优化(JIT)和垃圾回收。已经有较多的书籍和文章介绍了运行时,本文不再介绍。垃圾回收是本文的重点,后面会详细介绍。关于JIT的相关介绍并不多,同时JIT也非常复杂,特别是编译优化的相关知识。 本节在LinuxAArch64平台的基础上,通过一个简单的例子演示JIT的基本概念。 首先从一个简单的C代码例子出发,如下所示:includestdio。h intadd(inta,intb){ returnab; } intmain(){ printf(d,add(4,5)); return0; } 该代码片段的功能非常简单,其中函数add实现加法功能。这个add例子和1。4。1节中Java的add功能完全相关,都是完成两个整数的加法计算并返回结果。 本节构造C的add函数就是为了让读者可以方便地理解在编译优化时Java的函数(字节码片段)可以被一个CC的函数替代。当然,这里省略了JVM构造这个C语言的add函数的过程,这本质上就是编译优化要做的工作。 使用gcc进行编译,这里先使用O2的编译优化级别,命令如下: gccO2otesttest。c 编译后使用objdump命令查看add函数的反汇编代码:0000000000400650: 400650:0b010000addw0,w0,w1 400654:d65f03c0ret 注意:在AArch64平台中有31个通用寄存器,其中x0~x7用于传递参数和返回值。 w0~w7是x0~x7的低32位,用于传递32位的参数,当函数的参数个数超过8个时,通过栈传递。 在这个例子中,add的两个参数通过w0和w1传入,通过add指令完成加法,结果存放在寄存器w0中,通过ret返回函数的执行结果。 假设JVM识别Java的add函数为热点,现在也知道add函数对应的汇编代码,那么还有一个问题,就是如何让JVM替换原来的add函数而执行编译后的代码。下面通过一个例子演示CC代码直接执行编译后代码的过程。首先将编译后的代码作为输入数据,表示待执行的函数,然后通过mmap函数将数据加载到内存区,并设置内存区可以执行(PROTEXEC),最后再通过函数调用执行相关代码。代码示例如下:includestdio。h includememory。h include typedefint(addfunc)(inta,intb); intmain(){ charcode〔〕{ 0x00,0x00,0x01,0x0b,0x0b010000,等价于指令addw0,w0,w1 0xc0,0x03,0x5f,0xd60xd65f03c0,等价于指令ret };参考objdump对add函数的反汇编代码 voidcodecachemmap(NULL,sizeof(code),PROTWRITEPROTEXEC, MAPANONYMOUSMAPPRIVATE,1,0); memcpy(codecache,code,sizeof(code)); addfuncpadd(addfunc)codecache; printf(d,padd(4,5)); return0; } 示例中通过一个函数调用完成汇编代码的执行。实际上除了使用函数调用以外,还可以直接通过jmp完成相关的调用(函数调用的本质是通过call指令完成控制流的转移)。JVM执行编译后的代码原理和示例介绍基本类似,通过识别热点代码(例如Java中的add函数),并对热点代码进行编译优化,产生目标机器代码(类似于此处C代码中add函数的反汇编代码),然后执行目标机器代码。 在add函数的编译过程中直接使用了O2的编译优化级别,gcc默认的编译优化级别为O0。下面是使用默认编译优化级别产生的目标文件反汇编的结果。0000000000400624: 400624:d10043ffsubsp,sp,0x10 400628:b9000fe0strw0,〔sp,12〕 40062c:b9000be1strw1,〔sp,8〕 400630:b9400fe1ldrw1,〔sp,12〕 400634:b9400be0ldrw0,〔sp,8〕 400638:0b000020addw0,w1,w0 40063c:910043ffaddsp,sp,0x10 400640:d65f03c0ret 比较O2和O0的编译优化结果可以发现,O2的代码质量远高于O0的代码质量(指令明显少了很多)。那么O2采用的编译优化会更加复杂,编译耗时也更多。JVM中C1和C2编译器的目的也是生成不同指令的编译代码,可以简单理解为gcc不同编译级别产生的代码。当然JVM中C1和C2采用了不同的技术,使用的IR和编译优化手段都不相同。本文给大家讲解的内容是Java虚拟机和垃圾回收基础知识:内存线程JIT概述下篇文章给大家讲解的内容是JVM中垃圾回收相关的基本知识:GC算法分类觉得文章不错的朋友可以转发此文关注小编;感谢大家的支持!