前言
通过学习深入理解java虚拟机的教程,以及自己在网上的查询的资料,做一个对jvm学习过程中的小总结。
本文章内容首先讲解java的内存分布区域,之后讲内存的分配原则以及内存的监控工具。再下来会着重讲解垃圾回收这一章节,该章节涉及了垃圾的标记算法以及各种垃圾回收算法,然后大概的介绍下市面上使用的垃圾收集器。之后就总结下上面的原理,讲解相关的jvm调优案例。然后会着重讲解类加载过程。最后一章讲字节码的部分,字节码相对来说是比较枯燥而且特别繁琐的内容,最好是自己动手配合着学习会好一点,或者观其大略,不求甚解也可。
java内存区域
先来一段简单的java创建对象的代码热一下身,如下,
//运行时, jvm把TestObjectCreate的信息都放入方法区
public class TestObjectCreate {
//main方法本身放入方法区
public static void main(String[] args) {
Sample test1 = new Sample("测试1");
//test1是引用,所以放到栈区里,Sample是自定义对象应该放到堆里面
Sample test2 = new Sample("测试2");
test1.printName();
test2.printName();
}
}
//运行时, jvm把Sample的信息都放入方法区
class Sample {
//new Sample实例后,name引用放入栈区里,name对象放入堆里
private String name;
public Sample(String name) {
this.name = name;
}
public void printName() {
System.out.println(name);
}
}
栈、堆、方法区的交互图如下,
进入正题,java的内存区域一般分成两大区域:线程独占区和线程共享区。
线程独占区:虚拟机栈、本地方法栈、程序计数器
线程共享区:堆、方法区
线程独占区
顾名思义,线程独占区就是这块内存区域在运行过程中,是每个线程独有的内存空间。在这块内存空间中,就有我们平常所认知的栈,专业的术语叫虚拟机栈,因为还有另外一个区域叫本地方法栈,然后除了前面说的这两个栈外,还有一个程序计数器。线程独占区大概分为这三块内容。
虚拟机栈
虚拟机栈就是我们日常开发中经常提到的栈了,这里其实有几个概念:栈帧、局部变量表、操作数栈、动态连接、返回地址。
栈帧
栈帧(stack frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。
我们平常所说的方法进栈出栈,就是指栈帧。**每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。 对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。 **
在Java程序被编译成class文件时,栈帧的大小就已经确定,在jvm运行过程时栈帧的大小是不会改变的。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
局部变量表
局部变量表是一块比较 重点的内容,因为他在平常的java开发的过程中,接触的还是比较直接的,所以对于局部变量表的内容需要记住与理解。局部变量表不止包含我们方法中的局部变量,还包含了参数列表也会保存进局部变量表中,而且非static方法的this指针也会store进局部变量表。
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
我们平常所说的new了一个对象,然后在堆中开辟了一段空间,然后在栈中添加了一个引用指向了堆的这段空间,而这里添加的一个引用就是在局部变量表。
系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段。
在Java程序被编译成class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。
局部变量表的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference和returnAddress八种)。
reference类型虚拟机规范没有明确说明它的长度,但一般来说,虚拟机实现至少都应当能从此引用中直接或者间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。
returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。
对于64位的数据类型,虚拟机会以高位在前的方式为其分配两个连续的Slot空间。Java语言中明确规定的64位的数据类型只有long和double数据类型分割存储的做法与"long和double的非原子性协定" 中把一次long 和double 数据类型读写分割为两次32位读写的做法类似,在阅读JAVA内存模型时对比下。不过,由于局部变量表建在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否是原子操作,都不会引起数据安全问题。
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的Slot数量。如果32位数据类型的变量,索引N就代表了使用第N个Slot,如果是64位数据类型(long、double)的变量,则说明要使用第N个和N+1两个Slot。
虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中通过this访问。
**为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。 **
就是说,局部变量表Slot槽位的是可以复用的,当一个变量已经超过其作用域,即后续代码已经不会再对其使用了,那么jvm会复用该变量的Slot槽位给后续代码的变量使用。那么这里就会带来另外一个问题,当变量超出了其作用范围,jvm还保留着其Slot槽位,在Slot槽位还没被复用的时候该变量实际上就还没被回收,如下代码
public class TestStackLocalVariableTable {
private static int _M = 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
byte[] array = new byte[60 * _M];
System.gc();
}
}
代码其实也很简单,就是new了一个60M的字节数组,然后就调用gc垃圾回收,运行结果如下所示,在Full GC中最终也没有去回收这个变量。这里没有回收array所占的内存能说得过去,因为在执行System.gc()时,变量array还处在作用域之内,虚拟机自然不敢回收这部分的内存。 那我们把代码修改一下 。
(ps1:控制台中的结果是怎么打印出来的呢?其实是在运行的时候在虚拟机上加入参数:-verbose:gc ,如下所示)
(ps2:控制台中的gc信息要怎么看?如 [GC (System.gc()) 64102K->62224K(125952K), 0.0170834 secs]这个结果,表示系统进行了gc操作,然后操作的结果是从原来堆内存的使用量64102K,gc之后变成了62224K,只回收了一点点,即其实实际上回收的是其他在运行过程的其他空间,而我们定义60M的字节数组并没有回收到,而上面的结果只是jvm进行Minor GC而已,Minor GC是只回收堆内存的Eden区域内存,后面讲堆内存的时候会详细讲,这里可以认为Minor GC是轻量级的GC回收,速度快,而在Minor GC没有回收成功之后,jvm进行了Full GC,Full GC是扫描整个堆内存的gcroot去回收垃圾,后面也会细讲,这里可以认为Full GC是重量级的GC回收,速度慢,回收的时长可能是Minor GC的几倍或10倍不止,返回正题,就是从运行结果[Full GC (System.gc()) 62224K->62107K(125952K), 0.0230050 secs]中看,发生了Full GC之后,也没有回收60M字节数组这部分内存)
我们修改下上面的这段代码,如下所示,其实就是将byte[] array = new byte[60 * _M]用代码块包起来而已
public class TestStackLocalVariableTable {
private static int _M = 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
{
byte[] array = new byte[60 * _M];
}
System.gc();
}
}
按照原来的理解,在超出代码块的之后,array对象已经不会再使用了,应该调用gc之后回收了才对,但是从下面的结果中可以发现,array对象还是没有被回收,因为array的Slot槽位会被复用,jvm还保留着array在堆中的引用,那么gc就不会回收这一部分的内存。
再将上面的代码再修改下,如下,
public class TestStackLocalVariableTable {
private static int _M = 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
{
byte[] array = new byte[60 * _M];
}
int b = 0;
System.gc();
}
}
实际上就是在第二段代码的基础上,加了 int b = 0而已,但是从结果可知,array对象被回收了,因为这个时候,操作了局部变量表,局部变量b复用了array的Slot槽位,Full GC时发现array对象已经没有了引用,就把array回收了。
虽然现在知道了原理,但是实际上问题还没解决,在实际开发中,朝生夕死的对象的内存肯定是要回收的,也不可能在业务代码写完之后,去写一个无用的变量去操作局部变量表,那该怎么办呢?
所以,当处理完业务流程之后,将需要回收的对象,最好手动赋值为null,这样有助于gc的回收。
public class TestStackLocalVariableTable {
private static int _M = 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
// int a = 0;
{
byte[] array = new byte[60 * _M];
array = null; //手动赋值对象为null,array对象没有了引用,GC会将这个对象回收
}
// a = 1; //读取了局部变量表,但是没有复用array的Slot槽位,jvm还保留着array的引用,此时GC不会回收array对象
// int b = 0; //操作了局部变量表,复用了array的Slot槽位,array对象没有了引用,GC会将array对象回收
System.gc();
}
}
操作数栈
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈(Expression Stack)。操作数栈和局部变量表在访问方式上存在着较大差异,操作数栈并非采用访问索引的方式来进行数据访问的,而是通过标准的入栈和出栈操作来完成一次数据访问。每一个操作数栈都会拥有一个明确的栈深度用于存储数值,一个32bit的数值可以用一个单位的栈深度来存储,而2个单位的栈深度则可以保存一个64bit的数值,当然操作数栈所需的容量大小在编译期就可以被完全确定下来,并保存在方法的Code属性中。
HotSpot中任何的操作都需要经过入栈和出栈来完成,那么由此可见,HotSpot的执行引擎架构必然就是基于栈式架构,而非传统的寄存器架构。简单来说,操作数栈就是JVM执行引擎的一个工作区,当一个方法被调用的时候,一个新的栈帧也会随之被创建出来,但这个时候栈帧中的操作数栈却是空的,只有方法在执行的过程中,才会有各种各样的字节码指令往操作数栈中执行入栈和出栈操作。比如在一个方法内部需要执行一个简单的加法运算时,首先需要从操作数栈中将需要执行运算的两个数值出栈,待运算执行完成后,再将运算结果入栈。
下面用一个简单的例子来说明,
public class TestOperandStack {
public static void main(String[] args) {
add(1, 2);
int d = 2 + 2;
}
public static long add(int a, int b) {
long c = a + b;
return c;
}
}
上面这段是java源码,下面这段代码是根据 javap -verbose指令打印的结果(javap是JDK自带的反汇编器,可以查看java编译器为我们生成的字节码。通过它,我们可以对照源代码和字节码,从而了解很多编译器内部的工作。在字节码一章会比较详细的解释,没有前置知识的可以先看字节码的章节)
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_1
1: iconst_2
2: invokestatic #2 // Method add:(II)J
5: pop2
6: iconst_4
7: istore_1
8: return
LineNumberTable:
line 6: 0
line 7: 6
line 8: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
8 1 1 d I
public static long add(int, int);
descriptor: (II)J
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=2
0: iload_0
1: iload_1
2: iadd
3: i2l
4: lstore_2
5: lload_2
6: lreturn
LineNumberTable:
line 11: 0
line 12: 5
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 a I
0 7 1 b I
5 2 2 c J
}
从上往下读javap的结果,从main的Code开始,iconst_1表示int类型常量1进操作数栈,iconst_2表示int类型常量2进操作数栈,然后invokestatic调用静态方法即调用add方法,main暂时停下,看add的Code方法,iload_0表示从局部变量表中取出int变量进操作数栈,尾数0表示第一个Slot槽位此时即为a,然后iload_1又是一个从局部变量表中取出int变量进操作数栈即为b,然后iadd指令表示将操作数栈顶的两个数取出并相加,即将a,b两数从操作数栈顶取出,进行相加,i2l表示是类型转换指令,将int类型转换为long类型(int to long)即表示long c = a + b;这句代码将a与b相加的结果int类型转换为long类型,然后lstore_2表示将long类型变量即c加到局部变量表,然后lload_2从局部变量表中取出long类型的变量即为c,lreturn表示返回long类型指令。这就是上面样例代码的操作数栈的大致流程。
这里补充一个知识点,返回main方法暂停读下去的地方,第5行,pop2表示将操作数栈顶的2个元素出栈,即源码中1跟2,接下的指令是iconst_4,对应源码我们知道,此时的代码是走到了int d = 2 + 2;,按照前面的解读,我们理解应该是两个iconst_2进栈,然后做iadd操作,但是此处的字节码指令确是iconst_4,说明是编译器做了优化,也就是编译器优化,将在编译期间可以计算的结果先计算出来,然后在运行期间直接取值即可,少了一步计算的操作,因为java的原则是一次编译,多次运行,所以编译期间能计算的结果,就在编译器处理了。
其实使用javap查看字节码信息还是一件挺有意思的事情,建议还没玩过的同学可以自己写些代码调试调试,看看结果,你可以发现以前看不见摸不着的原理性的东西,调试之后会有不一样的理解,像“==”,equals,++i,i++等等。
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。
返回地址
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址,以恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。
本地方法栈
本地方法,即为native方法,在看jdk源码的时候,可以经常看到,像Object的getClass方法、hashCode方法等都是native方法,这些方法不是用Java实现的,本地方法本质上是依赖于实现的,虚拟机实现的设计者们可以自由地决定使用怎样的机制来让Java程序调用本地方法。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,但不止如此,它还可以做任何它想做的事情。
任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。
如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。当C程序调用一个C函数时,其栈操作都是确定的。传递给该函数的参数以某个确定的顺序压入栈,它的返回值也以确定的方式传回调用者。同样,这就是虚拟机实现中本地方法栈的行为。
很可能本地方法接口需要回调Java虚拟机中的Java方法,在这种情况下,该线程会保存本地方法栈的状态并进入到另一个Java栈。
下图描绘了这样一个情景,就是当一个线程调用一个本地方法时,本地方法又回调虚拟机中的另一个Java方法。这幅图展示了Java虚拟机内部线程运行的全景图。一个线程可能在整个生命周期中都执行Java方法,操作它的Java栈;或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。
该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。假设这是一个C语言栈,其间有两个C函数,第一个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过本地方法接口回调了一个Java方法(第三个Java方法),最终这个Java方法又调用了一个Java方法(它成为图中的当前方法)。
本地方法栈与虚拟机栈其实很类似,只是虚拟机栈为虚拟机执行java方法服务,而本地方法栈为虚拟机执行本地方法服务。
程序计数器
程序计数器是一块较小的内存空间,他的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程独占区”的内存。(其实这点也很好理解,每个线程在执行字节码指令的时候,肯定是读取各自的指令的行号位置去执行,如果是线程共享,那指令不就全乱套了吗)
如果线程正在执行一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器为空(undefined)。
此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。 (因为这部分空间是完全jvm自己管理的,与程序员无关,而且所占内存也很小)
1. 通俗解释:
对于一个运行中的java程序而言,其中的每一个线程都有他自己的PC(程序计数器)寄存器,他是在该线程启动时创建的,PC寄存器的大小是一个字长,因此它既能够持有一个本地指针,也能持有一个returnAddress(returnAddress类型会被java虚拟机的jsr、ret和jsr_w指令所使用。returnAddress类型的值只想一条虚拟机指令的操作码。与前面介绍的那些数值类的原生类型不同,returnAddress类型在java语言之中并不存在相应的类型,也无法在程序运行期间更改returnAddress类型的值。)。当线程执行某个java方法时 ,PC寄存器的内容总是下一条被执行指令的”地址”,这里的”地址”可以是一个本地指针,也可以是在方法字节码中相对于该方法起始指令的偏移量。如果该线程正在执行一个本地方法,那么PC寄存器的值是”Undefined”。
2.本地方法和java方法:
java中有两种方法:java方法和本地方法。java方法是有java语言编写,编译成字节码,存储在class文件中的。本地方法是有其它语言(比如C,C++,或者是会变语言)编写的,编译成和处理器相关的机器代码。本地方法保存在动态连接库中,格式是各个平台专用的。java方法是与平台无关的,但是在本地方法却不是。运行中的java程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。
线程共享区
线程共享区即在运行过程中,是每个线程共享的内存空间。其中包含堆与方法区。
堆
java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着jit编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
java堆可以是物理上不连续的空间,只要逻辑上连续即可,主流的虚拟机都是按照可扩展的方式来实现的。如果当前对中没有内存完成对象实例的创建,并且不能在进行内存扩展,则会抛出OutOfMemory异常。
Java堆是垃圾收集器管理的主要区域,所以也称为“GC堆”。 如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。
在分代收集算法中,堆的设计如上图所示,分成新生代与老年代,而在新生代中再细分出一个大概占80%空间的Eden区域,与两块各占百分10%空间的Survivor区域(空间比例可以配置),为什么这么分,在这章就不说了,因为内容太多,而且涉及到了垃圾回收的知识,所以这块内容就放到垃圾回收的章节去讲。
写段代码,验证下堆内存的分代,如下,
public class TestHeap {
private static int _M = 1024 * 1024;
@Test
public void testParallel() {
byte[] array = new byte[2*_M];
}
}
代码也很简单,只是创建一个2M大小的数组而已,但是要跟踪堆内存跟gc的话,还需要在运行时加入如下几项jvm参数
-verbose:gc //设置跟踪gc
-XX:+PrintGCDetails //打印详细的gc内容
-Xms20M //设置堆内存最小内存为20M
-Xmx20M //设置堆内存最大内存为20M
-Xmn10M //设置新生代内存为10M
运行结果如下,可以看出我本机的jvm默认的垃圾收集器(parallel)采用的分代收集算法,堆内存进行了分代管理
从上面的结果图中,不知道有没有人发现一个问题,就是我明明设置了新生代是10M,老年代是10M(堆内存20M-新生代10M),但是上面的结果,新生代可用空间是9216K,老年代可用空间是10240K,老年代是10M没错,但是新生代怎么是9M,少了1M?这个问题跟复制算法有关,在复制算法中,有一块内存区域是用来做复制迁移对象用的,不算入实际可用的空间。
另外,最低边界与最高边界,指的这片内存空间的起始位置与终止位置,如新生代的边界:(0x0000000100000000-0x00000000ff600000)/1024/1024=10M,然后,当前边界是什么呢?就是当前可用空间的内存边界,这样干说的话不好理解,再做个实验看下结果就能明白了。
调整下jvm的参数,将上面的 -Xmx20M,改为-Xmx40M即可,其他参数不变,再重新运行上面的程序,结果如下
刚刚的程序,我们设置的jvm的最小堆大小为20M,而最大堆大小为40M,但是我们发现,新生代的空间已经固定为10M,而老年代的可用空间total实际上还是10M,这是因为jvm在内存够用的情况下,会去维持一个最小的堆空间,所以,此时程序所占空间就eden区域用了6M多一点,远不超过20M,所以此时可用空间就还是维持在20M,我们可以拿老年代的当前边界去减最低边界,去计算出来,(0x00000000fe200000-0x00000000fd800000)/1024/1024=10M,而用最高边界减最低边界,(0x00000000ff600000-0x00000000fd800000)/1024/1024=30M,即,堆空间确实设置了最大内存为40M,但是此时维护在最小堆内存20M。
另外再讲几个跟堆相关的配置参数,如下
-XX:+printGC //打印GC的简要信息
-XX:+PrintGCTimeStamps //打印CG发生的时间戳
-Xloggc:log/gc.log //指定GC log的位置,以文件输出,可以帮助开发人员分析问题
-XX:+PrintHeapAtGC //每次一次GC后,都打印堆信息
-Xmn //设置新生代大小
-XX:NewRatio //新生代(eden+2*s)和老年代(不包含永久区)的比值,如-XX:NewRatio=4表示新生代:老年代=1:4,即年轻代占堆的1/5
-XX:SurvivorRatio //设置两个Survivor区和eden的比,如-XX:SurvivorRatio=8表示两个Survivor:eden=2:8,即一个Survivor占年轻代的1/10
-XX:+HeapDumpOnOutOfMemoryError //OOM时导出堆到文件
-XX:+HeapDumpPath //导出OOM的路径
-XX:OnOutOfMemoryError //在OOM时,执行一个脚本,如"-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p"
方法区
在java虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但事实上,方法区并不是堆(Non-Heap);另外,不少人的博客中,将java GC的分代收集机制分为3个代:青年代,老年代,永久代,这些作者将方法区定义为“永久代”,这是因为,对于之前的HotSpot Java虚拟机的实现方式中,将分代收集的思想扩展到了方法区,并将方法区设计成了永久代。不过,除HotSpot之外的多数虚拟机,并不将方法区当做永久代,HotSpot本身,也计划取消永久代。
方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。
方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。一般的,方法区上执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(HotSpot),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。
在方法区上进行垃圾收集,条件苛刻而且相当困难,效果也不令人满意,所以一般不做太多考虑,可以留作以后进一步深入研究时使用。
在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。在jdk1.4中加入了NIO类,引入了一种基于通道(Channel)于缓冲区(Buffer)的I/O方式,他可以使用native函数库直接分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在java堆中和native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括ram及swap区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。