java学习之---java虚拟机浅谈

java虚拟机是java实现跨平台的基础,其内部实现有很多值得学习的地方。如果想进一步学习,可以研读《深入java虚拟机》一书。这里谈了java虚拟机在应用开发中的部分参数的配置,然后谈了java虚拟机的层次结构,最后谈了java虚拟机GC的回收算法和已经实现的模型。

java虚拟机的参数配置

参数:注意:-表示不采用,+表示采用。例如-Xdiag表示不显示附加诊断消息,反之是显示。
Xms:设置初始java堆的大小 Xmx:设置最大堆的大小。
Xss:设置java线程堆的大小
Xprof:输出cpu配置文件的数据
Xrs:减少java/VM对操作系统信号的使用。
Xshare:on/off/auto :要求/不尝试/在可能的情况下使用共享数据。
XshowSettings:(all/vm) 现实所有的设置(VM的设置)并继续
非堆内存分配
JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。

java虚拟机的层次
类加载器、堆、栈、方法区、执行器、PC寄存器、本地方法栈、本地接口栈、执行引擎、运行时向量池。

1、类加载器主要完成对本地或者网络中获得的.class文件加载到JVM中。
2、PC寄存器(program counter register)程序计数寄存器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码指令的行号指示器。对于多线程的程序,每个线程都有自己的独立的程序计数器,各个线程之间的计数器互不影响,独立存储,这类内存区域为线程私有的内存。但是如果执行native方法计数器为undefined的。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
3、栈
是线程所私有的,它的生命周期和线程一样,它描述的是方法执行的内存模型:每个方法被执行时都会创建一个栈帧(Statck Frame)用于存放局部变量表、操作栈、动态链接等信息,每个方法被调用直到被执行完都对应着一个栈帧入栈到出栈的过程。局部变量的内存空间在编译期间完成分配,运行期间不再改变。两个运行时异常:栈溢出错误(线程请求的栈深度大于虚拟机所允许的深度)和内存溢出错误(虚拟机栈可以动态拓展的的大小大于虚拟机所允许的固定长度)。
本地方法栈:主要是为native方法服务的。虚拟机规范并没有对其实现有限制,各个版本都有差别。但是,其异常和栈相同。
4、堆:
以回收角度来看堆分为新生区(eden区、form区、to区)和老年区。以内存分配角度看,线程共享的java堆可以划分成多个线程私有的分配缓冲区(TLAB)。堆可以处于物理上不连续的内存中,但是逻辑地址必然联系。也就是采用了虚拟内存技术,程序员关注的是逻辑地址,至于物理内存的具体地址由操作系统分配。
新生区中:eden区是存放刚创建的对象的区域,此区地特点是经常被GC光顾,也就是这里的对象经常被用完后丢弃。from和to区的大小相等,每次执行GC时,会将其中一个区中的有引用的对象复制到另一个区中,然后将被复制区的对象全部回收。
老年区中的对象一般为经过多次回收后仍然没被回收的对象。实现是利用一个计数器,每次执行回收时未被回收的对象计数器会加1.当然可以利用参数的配置实现进入到老年区地对象的条件,比如设置进入对象的大小。一般设置参数时,新生区和老年区地比例在四分之一到三分之一。一般而言堆内存中老年区越大GC的次数就越少,系统用于GC的开销就越少。当然,如果系统有一次创建但基本一直使用的对象可以在创建后就把它放到老年区,从而减少创建对象的系统开销。
另外,在新生区和老年区之外还有一个叫栈的结构,对于比较小的对象,不会装入到新生区内,会装入到它里面。
一般为了减少系统再次分配堆内存的开销,将初始堆内存和最大堆内存的大小设置成一样的。
5、方法区
方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。即使是HotSpot 虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory 来实现方法区的规划了。Java 虚拟机规范对这个区域的限制非常宽松,除了和Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun 公司的BUG 列表中,曾出现过的若干个严重的BUG 就是由于低版本的HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。根据Java 虚拟机规范的规定, 当方法区无法满足内存分配需求时, 将抛出OutOfMemoryError 异常。
6、运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool
Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。Java 虚拟机对Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只能在编译期产生,也就是并非预置入Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String 类的intern() 方法。既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError 异常。
7、直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导OutOfMemoryError 异常出现,所以我们放到这里一起讲解。在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。逻辑内存模型我们已经看到了,那当我们建立一个对象的时候是怎么进行访问的呢?在Java 语言中,对象访问是如何进行的?对象访问在Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及Java 栈、Java 堆、方法区这三个最重要内存区域之间的关联关系,如这句代码:Object obj = new Object();假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java 栈的本地变量表中,作为一个reference 类型数据出现。而“new Object()”这部分的语义将会反映到Java 堆中,形成一块存储了Object 类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java 堆中
还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。
GC的回收机制:
1、垃圾回收算法
Mark-Sweep(标记-清除)算法
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
Copying(复制)算法
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
Mark-Compact(标记-整理)算法
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
Generational Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
2、垃圾回收已实现的模型
垃圾收集算法是 内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。下面介绍一下HotSpot(JDK 7)虚拟机提供的几种垃圾收集器,用户可以根据自己的需求组合出各个区域使用的收集器。
串行的回收机制:单线程进行垃圾回收
它是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。
并行的回收机制:多线程进行垃圾回收
它分为parallelNew、Parallel Scavenge和Parallel Old三种回收机制。它们的侧重点不同。
parallelNew是Serial收集器的多线程版本,使用多个线程进行垃圾收集。Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。
CMS:主要实现最小的等待时间
它的全称是:Current Mark Sweep,是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。这是目前普遍采用的回收模型。
G1收集器
G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。但是目前还没有广泛应用。

你可能感兴趣的:(java学习之---java虚拟机浅谈)