Java性能优化系列之-JIT即时编译器与Java内存管理机制
一、JIT编译器1. JIT编译器JIT(just in time)即时编译器。其存在的目的:为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler),简称 JIT 编译器。2. 即时编译器和Java虚拟机的关系即时编译器并不是虚拟机必需的部分,Java 虚拟机规范
目录
8. 为何 HotSpot 虚拟机要使用解释器与编译器并存的架构?
一、JIT编译器
1. JIT编译器
JIT(just in time)即时编译器。其存在的目的:为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler),简称 JIT 编译器。
2. 即时编译器和Java虚拟机的关系
即时编译器并不是虚拟机必需的部分,Java 虚拟机规范并没有规定 Java 虚拟机内必须要有即时编译器的存在,更没有限定或指导即时编译器应该如何去实现。
但是,即时编译器编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一。它也是虚拟机中最核心且最能体现虚拟机技术水平的部分。
3. 即时编译器的分类
即时编译器的类别为:
- Client Compiler - C1编译器
- Server Compiler - C2编译器
目前主流的 HotSpot 虚拟机(JDK1.7 及之前版本的虚拟机)默认采用一个解释器和其中一个编译器直接配合的方式工作,程序使用哪个编译器,取决于虚拟机运行的模式。
在 HotSpot 中,解释器和 JIT 即时编译器是同时存在的,他们是 JVM 的两个组件。对于不同类型的应用程序,用户可以根据自身的特点和需求,灵活选择是基于解释器运行还是基于 JIT 编译器运行。
HotSpot 为用户提供了几种运行模式供选择,可通过参数设定,分别为:解释模式、编译模式、混合模式,HotSpot 默认是混合模式,需要注意的是编译模式并不是完全通过 JIT 进行编译,只是优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程。
4.什么是字节码、机器码和本地码
字节码是指平常所了解的 .class 文件,Java 代码通过 javac 命令编译成字节码
机器码和本地代码都是指机器可以直接识别运行的代码,也就是机器指令
字节码是不能直接运行的,需要经过 JVM 解释或编译成机器码才能运行
Java源码转换成字节码的过程:
Java 字节码的执行是由 JVM 执行引擎来完成,流程图如下所示:
此时你要问了,为什么 Java 不直接编译成机器码,这样不是更快吗?一下是原因解释,JIT的出现必定有其的实际应用性。
JIT的出现的原因:
1. 机器码是与平台相关的,也就是操作系统相关,不同操作系统能识别的机器码不同,如果编译成机器码那岂不是和 C、C++差不多了,不能跨平台,Java 就没有那响亮的口号 “一次编译,到处运行”;
2.之所以不一次性全部编译,是因为有一些代码只运行一次,没必要编译,直接解释运行就可以。而那些“热点”代码,反复解释执行肯定很慢,JVM 在运行程序的过程中不断优化,用JIT编译器编译那些热点代码,让他们不用每次都逐句解释执行;
3.还有一方面的原因是后文讲解的解释器与编译器共存的原因。
5. 类加载和执行机制
1) JVM 的类加载
通过 ClassLoader 及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
1)Bootstrap ClassLoader
负责加载$JAVA_HOME中jre/lib/rt.jar
里所有的 class,由 C++ 实现,不是 ClassLoader 子类。
2)Extension ClassLoader
负责加载Java平台中扩展功能的一些 jar 包,包括$JAVA_HOME中jre/lib/*.jar
或-Djava.ext.dirs
指定目录下的 jar 包。
3)App ClassLoader
负责记载 classpath 中指定的 jar 包及目录中 class。
4)Custom ClassLoader
属于应用程序根据自身需要自定义的 ClassLoader,如 Tomcat、jboss 都会根据 J2EE 规范自行实现 ClassLoader。
加载过程中会先检查类是否被已加载,检查顺序是自底向上,从 Custom ClassLoader 到 BootStrap ClassLoader 逐层检查,只要某个 Classloader 已加载就视为已加载此类,保证此类只所有 ClassLoade r加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
2)类执行机制
JVM 是基于栈的体系结构来执行 class 字节码的。线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。栈的结构如下图所示:
6. 什么是解释和编译
上述第二小节,提到的JIT出现的原因的第二条解释,出现了编译与解释的概念,这个小节将解释一下。
编译器:把源程序的每一条语句都编译成机器语言,并保存成二进制文件,这样运行时计算机可以直接以机器语言来运行此程序,速度很快;
解释器:只在执行程序时,才一条一条的解释成机器语言给计算机来执行,所以运行速度是不如编译后的程序运行的快的;
通过javac
命令将 Java 程序的源代码编译成 Java 字节码,即我们常说的 class 文件。这是我们通常意义上理解的编译。
字节码并不是机器语言,要想让机器能够执行,还需要把字节码翻译成机器指令。这个过程是Java 虚拟机做的,这个过程也叫编译。是更深层次的编译。(实际上就是解释,引入 JIT 之后也存在编译)
此时又有疑惑了,Java 不是解释执行的吗?
没错,Java 需要将字节码逐条翻译成对应的机器指令并且执行,这就是传统的 JVM 的解释器的功能,正是由于解释器逐条翻译并执行这个过程的效率低,引入了 JIT 即时编译技术。
必须指出的是,不管是解释执行,还是编译执行,最终执行的代码单元都是可直接在真实机器上运行的机器码,或称为本地代码
附一张图来理解:
7. 什么是HotSpot热点代码
我们通过javac将java的源程序编译成java字节码后,并不能自己在计算机上执行,还需要将字节码转换成机器码。
上图表明java并不完全是通过编译来生成机器码的,还结合了解释执行,那如何判断那些代码是使用编译执行还是解释执行呢?
定义:当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。
热点代码的分类:
- 被多次调用的方法
一个方法被调用得多了,方法体内代码执行的次数自然就多,成为“热点代码”是理所当然的。
- 被多次执行的循环体
一个方法只被调用过一次或少量的几次,但是方法体内部存在循环次数较多的循环体,这样循环体的代码也被重复执行多次,因此这些代码也应该认为是“热点代码”。
如何检测热点代码:
判断一段代码是否是热点代码,是否需要触发即使编译,这样的行为称为热点探测,热点探测并不一定知道方法具体被调用了多少次。
目前主要的热点探测判定方式有两种:
第一种:
- 基于采样的热点探测:采用这种方法的虚拟机会周期性地检查各个线程的栈顶如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”
优点:实现简单高效,容易获取方法调用关系(将调用堆栈展开即可)
缺点:不精确,容易因为因为受到线程阻塞或别的外界因素的影响而扰乱热点探测
第二种:
- 基于计数器的热点探测:采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果次数超过一定的阈值就认为它是“热点方法”
优点:统计结果精确严谨
缺点:实现麻烦,需要为每个方法建立并维护计数器,不能直接获取到方法的调用关系
HotSpot使用第二种 - 基于计数器的热点探测方法。
方法调用计数器触发即时编译的流程:
计数器的种类(两种共同协作)
- 方法调用计数器:这个计数器用于统计方法被调用的次数。默认阈值在 Client 模式下是 1500 次,在 Server 模式下是 10000 次
- 回边计数器:统计一个方法中循环体代码执行的次数
了解了热点代码和计数器有什么用呢?即时编译是需要达到某种条件才会触发的。
两个计数器的协作(这里讨论的是方法调用计数器的情况):当一个方法被调用时,会先检查该方法是否存在被 JIT 编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器加 1,然后判断方法调用计数器与回边计数器之和是否超过方法调用计数器的阈值。如果已经超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。
当编译工作完成之后,这个方法的调用入口地址就会被系统自动改成新的,下一次调用该方法时就会使用已编译的版本。
8. 为何 HotSpot 虚拟机要使用解释器与编译器并存的架构?
解释器与编译器两者各有优势
解释器:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。
编译器:在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。
两者的协作:在程序运行环境中内存资源限制较大时,可以使用解释执行节约内存,反之可以使用编译执行来提升效率。当通过编译器优化时,发现并没有起到优化作用,,可以通过逆优化退回到解释状态继续执行。
9. 分层编译
产生的原因:由于即时编译器编译本地代码需要占用程序运行时间,要编译出优化程度更高的代码,所花费的时间可能更长;而且要想编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释执行的速度也有影响。为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot 虚拟机启用分层编译的策略
分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次:
- 第 0 层:程序解释执行,解释器不开启性能监控功能,可触发第 1 层编译。
- 第 1 层:也称为 C1 编译,将字节码编译为本地代码,进行简单,可靠的优化,如有必要将加入性能监控的逻辑。
- 第 2 层(或 2 层以上):也称为 C2 编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
实施分层编译后,Client Compiler 和 Server Compiler 将会同时工作,许多代码都可能会被多次编译看,用 Client Compiler 获取更高的编译速度,用 Server Compiler 获取更好的编译质量,在解释执行的时候也无须再承担收集性能监控信息的任务。
10.编译器的优化
Java程序员有一个共识,以编译方式执行本地代码比解释方式更快,之所以有这样的共识,除去虚拟机解释执行字节码时额外消耗时间的原因外,还有一个很重要的原因就是虚拟机设计团队几乎把代码的所有优化措施都集中在了即时编译器之中,因此一般来说,即时编译器生成的本地代码比Javac产生的字节码更加优秀!
1)方法内联优化
内联优化是:一是去除方法调用的成本(如建立栈帧等),二是为了其他优化建立良好的基础
方法的调用过程:
(1) 首先会有个执行栈,存储目前所有活跃的方法,以及它们的本地变量和参数;
(2) 当一个新的方法被调用了,一个新的栈帧会被加到当前线程的栈顶,分配的本地变量和参数会存储在这个栈帧中;
(3) 跳到目标方法代码执行;
(4) 方法返回的时候,本地方法和参数会被销毁,栈顶被移除;
(5) 返回原来地址执行;
方法内联的原理:
方法内联就是把被调用方函数代码”复制”到调用方函数中,来减少因函数调用开销的技术。
一个简单的两数相加程序,被内联前的代码:
private int add1(int a, int b, int c, int d){
return add2(a + b) + add2(c + d);
}
private int add2(int x, int y){
return x + y;
}
运行一段时间后JVM会把add2方法去掉,并把你的代码翻译成:
private int add1(int a, int b, int c, int d){
return a + b + c + d;
}
注意点:
1)方法内联的其他隐含条件:
-
虽然JIT号称可以针对代码全局的运行情况而优化,但是JIT对一个方法内联之后,还是可能因为方法被继承,导致需要类型检查而没有达到性能的效果
-
想要对热点的方法使用上内联的优化方法,最好尽量使用
final、private、static
这些修饰符修饰方法,避免方法因为继承,导致需要额外的类型检查,而出现效果不好情况。
2)如果想要知道方法被内联的情况,可以使用下面的JVM参数来配置:
-XX:+PrintCompilation //在控制台打印编译过程信息
-XX:+UnlockDiagnosticVMOptions //解锁对JVM进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对JVM进行诊断
-XX:+PrintInlining //将内联方法打印出来
3)编译器在进行内联时,如果是非虚方法,那么直接进行内联就可以了,这时候的内联是有稳定前提保障的。(虚方法即为子类继承/实现父类,重写的方法)
2)公共子表达式消除
什么是公共子表达式消除:
如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式!
例如:int d = (c + b) * 12 + a + (a + b * c);
1)代码交给Javac编译器则不会进行任何优化:(Javac编译器编译后的字节码展示)
2)代码进入即时编译器JIT中:
int d = E * 12 + a + (a + E);
3) 这时即时编译器有可能进行另外一种优化:代数简化
int d = E * 13 + a * 2;
3) 数组边界检查消除
Java语言是一门动态安全的语言。如果有一个数组foo[],在Java语言中访问数组元素foo[i]的时候系统将会自动进行上下界的范围检查,即检查i必须满足i >=0 && i < foo.length这个条件,否则将抛出一个运行时异常:java.lang.ArrayIndexOutOfBoundsException。
无论如何,为了安全,数组便捷检查肯定是必须做的,但数组边界检查是不是必须在运行期间一次不漏的检查则是可以“商量”的事情。例如:数组下标是一个常量,如 foo[3],只要在编译期根据数据流分析来确定foo.length的值,并判断下标“3”没有越界,执行的时候就无需判断了。
4)逃逸分析
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定以后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至还有其可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸!
11.Java编译器与C/C++编译器的区别
二、Java内存管理机制
Java内存模型结构分为
- 线程共享区:堆、方法区
- 线程私有区:虚拟机栈、本地方法栈、程序计数器
1. 线程私有内存区
1)程序计数器
在多线程情况下,当线程数超过CPU数量或CPU内核数量时,线程之间就要根据时间片轮询抢夺CPU时间资源。也就是说,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器去记录其正在执行的字节码指令地址
字节码解释器工作时通过改变该计数器的值来选择下一条需要执行的字节码指令,分支、跳转、循环等基础功能都要依赖它来实现。每条线程都有一个独立的的程序计数器,各线程间的计数器互不影响,因此该区域是线程私有的。
程序计数器是线程私有的一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器
- 如果线程正在执行的是一个 Java 方法,计数器记录的是正在执行的字节码指令的地址
- 如果正在执行的是 Native 方法,则计数器的值为空
- 程序计数器是唯一一个没有规定任何 OutOfMemoryError 的区域
2)本地方法栈
本地方法栈用于支持native方法的执行,存储了每个native方法调用的状态。本地方法栈和虚拟机方法栈运行机制一致,它们唯一的区别就是,虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,
在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。
本地方法栈与Java虚拟机栈非常相似,也是线程私有的,其主要异同点如下
- 虚拟机栈为虚拟机执行 Java 方法服务
- 本地方法栈为虚拟机执行 Native 方法服务
- 与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常
3)虚拟机栈
该区域也是线程私有的,它的生命周期也与线程相同。
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,栈它是用于支持续虚拟机进行方法调用和方法执行的数据结构。
对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。
- 每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息
- 每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程
- 局部变量表主要存放一些基本类型的变量和对象句柄,它们可以是方法参数,也可以是方法的局部变量
在编译程序代码时,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入了方法表的 Code 属性之中。因此,一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
栈帧结构图如下所示:
在 Java 虚拟机规范中,对这个区域规定了两种异常情况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
- 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这两种情况存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。在单线程的操作中,无论是由于栈帧太大,还是虚拟机栈空间太小,当栈空间无法分配时,虚拟机抛出的都是 StackOverflowError 异常,而不会得到 OutOfMemoryError 异常。而在多线程环境下,则会抛出 OutOfMemoryError 异常。
下面详细解释栈帧结构的信息
1、局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,其中存放的数据的类型是编译期可知的各种基本数据类型、对象引用(reference)和 returnAddress 类型(它指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,即在 Java 程序被编译成 Class 文件时,就确定了所需分配的最大局部变量表的容量。当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
局部变量表的容量以变量槽(Slot)为最小单位。在虚拟机规范中并没有明确指明一个 Slot 应占用的内存空间大小(允许其随着处理器、操作系统或虚拟机的不同而发生变化),一个 Slot 可以存放一个32位以内的数据类型:boolean、byte、char、short、int、float、reference 和 returnAddresss。reference 是对象的引用类型,returnAddress 是为字节指令服务的,它执行了一条字节码指令的地址。对于 64 位的数据类型(long和double),虚拟机会以高位在前的方式为其分配两个连续的 Slot 空间。
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从 0 开始到局部变量表最大的 Slot 数量,对于 32 位数据类型的变量,索引 n 代表第 n 个 Slot,对于 64 位的,索引 n 代表第 n 和第 n+1 两个 Slot。
在方法执行时,虚拟机是使用局部变量表来完成参数值到参数变量列表的传递过程的,如果是实例方法(非static),则局部变量表中的第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问这个隐含的参数。其余参数则按照参数表的顺序来排列,占用从1开始的局部变量 Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的 Slot。
局部变量表中的 Slot 是可重用的,方法体中定义的变量,作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超过了某个变量的作用域,那么这个变量对应的 Slot 就可以交给其他变量使用。这样的设计不仅仅是为了节省空间,在某些情况下 Slot 的复用会直接影响到系统的而垃圾收集行为。
2、操作数栈
操作数栈又常被称为操作栈,操作数栈的最大深度也是在编译的时候就确定了。32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2。当一个方法开始执行时,它的操作栈是空的,在方法的执行过程中,会有各种字节码指令(比如:加操作、赋值元算等)向操作栈中写入和提取内容,也就是入栈和出栈操作。
Java 虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。因此我们也称 Java 虚拟机是基于栈的,这点不同于 Android 虚拟机,Android 虚拟机是基于寄存器的。
基于栈的指令集最主要的优点是可移植性强,主要的缺点是执行速度相对会慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的优点是执行速度快,主要的缺点是可移植性差。
3、动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class 文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如 final、static 域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。
4、方法返回地址
当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异常,并且该异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的 PC 计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。
方法退出的过程实际上等同于把当前栈帧出站,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整 PC 计数器的值以指向方法调用指令后面的一条指令。
2. 线程共享内存区
1)Java堆
堆用于存放对象实例和数组,由于堆是用来存放对象实例,因此堆也是垃圾收集器管理的主要区域,故也称为 GC堆。
根据 Java 虚拟机规范的规定,Java 堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存可分配时,并且堆也无法扩展时,将会抛出 OutOfMemoryError 异常。
由于现在的垃圾收集器基本都采用分代收集算法,所以堆的内部结构只包含新生代和老年代
- 新生代:用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,对象就会被移入老年代
- 新生代:又可进一步细分为 eden、survivorSpace0 和 survivorSpace1,刚创建的对象都放入 eden,经过GC幸存下来的对象,就会被分配到survivorSpace0,依次类推直到被分配到老年代为止。在survivorSpace0 和 survivorSpace1 都至少经过一次GC并幸存,如果幸存对象经过一定时间仍存在,则进入老年代
2)方法区
方法区也是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
Java 虚拟机规范把方法区描述为 Java 堆的一个逻辑部分,而且它和 Java Heap 一样不需要连续的内存,可以选择固定大小或可扩展,另外,虚拟机规范允许该区域可以选择不实现垃圾回收。相对而言,垃圾收集行为在这个区域比较少出现。该区域的内存回收目标主要针是对废弃常量的和无用类的回收。
- 方法区通常和永久区(Perm)关联在一起,但永久代与方法区不是一个概念,只是有的虚拟机用永久代来实现方法区,这样就可以用永久代GC来管理方法区,省去专门内存管理的工作
- 根据Java虚拟机规范的规定,当方法区无法满足内存分配的需求时,将抛出 OutOfMemoryError 异常
1、常量池的回收
- 系统中任何对象没有引用常量池中的字面量、符号引用,当内存回收时,就会被清除
2、无用类的回收
虚拟机可以对满足下述3个条件的无用类进行回收
- 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
3. 对象实例化分析
对内存分配情况分析最常见的示例便是对象实例化:
Object obj = new Object();
这段代码的执行会涉及 Java 栈、Java 堆、方法区三个最重要的内存区域。假设该语句出现在方法体中,obj 会作为引用类型(reference)的数据保存在 Java 栈的本地变量表中,在 Java 堆中保存该引用的实例化对象。但是,Java 堆中还必须包含能查找到此对象类型数据的地址信息(如对象类型、父类、实现的接口、方法等),这些类型数据则保存在方法区中。
另外,由于 reference 类型在 Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到 Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄池和直接使用指针。
通过句柄池访问的方式如下:
通过直接指针访问的方式如下:
这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是 reference 中存放的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式的最大好处是速度快,它节省了一次指针定位的时间开销。目前 Java 默认使用的 HotSpot 虚拟机采用的便是是第二种方式进行对象访问的。
--------------------------- end ---------------------------
-----------------------------------------------------------------------------
更多推荐
所有评论(0)