类加载器(ClassLoader)用来加载 class字节码到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源文件在经过 Javac之后就被转换成 Java 字节码文件(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。每一个这样的实例用来表示一个 Java 类。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载。
Java运行数据区域也就是Java内存区,常常出现内存异常的地方(OOM等),也是我们最关心的一个地方。大家说到内存往往会想到“栈”和“堆”,但Java虚拟机内存并不是仅仅只有这两个区域。 下面依次介绍java虚拟机内存的各个区域,介绍这些区域的作用、服务对象以及其中可能产生的问题。
程序计数器Program Counter Register(PRC)如果大家了解过相对底层一点的东西如汇编,嵌入式等相关学习,对程序计数器PRC应该不会陌生,当然在Java里面程序计数器也有它的作用。一、在Java内存里面程序计数器只占一块很小的区域。二、它的作用可以看作是当前线程所执行的字节码的行号指示器,通过改变PRC的值进行字节码寻指。分支、循环、跳转、异常处理等都是依赖它完成。三、每一条线程都拥有一个自己私有的PRC,且每个CRC互不干扰。同时我们可以将CRC称为“线程私有”的内存。四、如果虚拟机正在执行Java方法,PRC记录的是正在执行的虚拟机字节码指令的地址。如果执行的是native方法,那么PRC为空。五、PRC是唯一一个在内存区域不会出现OutOfMemoryError的地方
虚拟栈主要描述的是Java方法执行的内存模型(你需要明白:每一个Java线程会有(私有)一个相对应的虚拟机栈区,栈的一段区域叫做栈帧):每个方法被执行一次,都会在栈区内创建一个栈帧(Stack Frame),用于存储局部变量表,操作栈、动态链接、方法出口等信息。每一个方法的执行到完成过程代表着一个栈帧的入栈和出栈过程。局部变量表存放着编译期可知的基本数据类型(boolean,byte、char、short、int、float、long、double)、对象引用和returnAddress(指向一条字节码指令地址)。其中long和double占两个局部变量空间(Slot),其余数据类型只占一个。局部变量表的内存空间大小在编译期间就已经分配,在方法执行的过程中不会发生变化。在Java虚拟机规范中,在这个区域规定了两种内存异常:StackOverflowError:栈的请求深度大于虚拟机的允许最大深度。OutOfMemoryError:(大部分Java虚拟机都允许在内存不够的情况下,动态扩展内存,同时也允许固定内存长度的虚拟机栈),当扩展时无法申请到足够的内存就会抛出此异常。
Java堆(heap) Java堆是Java虚拟机所管理内存的最大一块区域,在虚拟机启动时创建。唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此很多时候被称为GC堆(Garbage Collected heap,“垃圾堆”),他是一个物理上不连续的内存空间只是逻辑上连续。如果堆内存无法扩展将会抛出OOM异常。
方法区(Method Area)方法区和Java堆一样,是各个线程共享的一个内存区域。主要用来存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范中把方法区描述为堆的逻辑部分,但是它还有一个别名Non-Heap(非堆),目的就是为了将它与堆区分。垃圾收集行为在方法区是比较少见的,在这个区域里面内存回收的目标主要是针对常量池和类型(自定义的类加载器加载的类的类可以被卸载的)卸载的。运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译期产生生成的各种字面量和符号引用(加载后放入)。抛出OOM异常。
直接内存(Direct Memory)不属于运行时是数据区的一部分,也不是虚拟机规范的内存区域,但这一部分也常常被使用,而且抛出OOM。在JDK1.4中新加入了NIO(new input/output),引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用本地方法库直接分配内存,然后通过一个存储在java heap 里的DirectByteBuffer对象作为这快内存的引用进行操作。
上面已经将Java虚拟机运行时数据区的各区域的作用,存储内容,以及抛出的异常等做出了介绍(更具体的介绍,大家可以自己搜索下)。其中程序计数器,虚拟机栈,本地方法栈随线程生,随线程亡;栈中的栈帧随着方法的执行和退出而有条不絮的进行着出栈和进栈。每一个栈帧中分配多少内存,基本上在类的结构确定下来就已知了,因此这几个区域内存的分配和回收都具有确定性,因为在方法结束或者线程结束,内存自然而然的跟着回收了。Java堆和方法区则不一样,一个接口中的多个类的实现,一个方法多个分支,多可能造成内存不一样,不确定性,只有当程序处于运行时我们才知道,有多少个对象被创建,这部分的内存分配和回收都是动态的。因此接下来主要介绍这一部分的垃圾回收与内存分配
对象是否死亡?堆中几乎存放这Java世界里的所有实例对象,垃圾收集器在对堆进行回收前,第一件事就是确定那些对象还“存活”着。下面主要介绍:引用计数算法;根搜索算法。
引用计数算法(Reference Counting)很多教科书上判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方去引用它的时候,计数器加一,当引用失效时计数器减一;任何时刻计数器的值都为零的对象是不可能被引用的。引用计数算法的实现简单,效率也高,但是Java没有选用引用计数算法对内存进行管理,最主要的原因是因为它难以解决对象之间的相互循环引用问题。
在根搜索算法中,不可达的对象,也并非“非死不可”。如果对象在进行根搜索后,发现没有与GC Roots相连的引用链,那么此对象会进行一次标记并进行一次筛选,筛选的条件是该对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经虚拟机调用过,虚拟机将这两种情况。如果这个对象被判定为有必要执行finalize()方法,那么对象会放置在一个名为F-Queue的队列中,并稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行。第一次标记后,GC会对F-Queue队列中的对象进行第二次小规模标记,如果在第二次标记之前,对象重新与引用链上的任何对象进行了关联,那么在第二次标记时,他会被清除出队列,不进行回收。如果被进行第二次标记那么对象就会被回收。