对于JVM的重新认知

写了不短时间的代码了,一直对于JVM(Java Virtual Machine)没有一个整体的认识,最近利用闲余的一些时间,重新回忆了一下JVM相关的一些东西,应该也不够很深入,但对于当前的自己来说需要一次整理来让自己对JVM有一个整体的认知。
JVM架构图

这张关于JVM运行时的架构图相信在很多地方都能看得到,它其实就是Java程序通过编译生成class文件,然后生成的class文件通过JVM来运行,而JVM在执行Java程序的过程中把它所管理的内存就划分为上图中的几个运行时的数据区域。

程序计数器(Program Counter Register)
学过《计算机组成原理》的人对于这个名词都不会感到陌生,PC指向当前执行的指令地址。在JVM中,也可以这么理解,根据PC值选取将要执行的指令。JVM的多线程是通过线程轮流切换并根据CPU时间分片的方式来实现,任何时刻,一个处理器只会执行一条线程中的指令。每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,这类内存区域为“线程私有”的内存。Java中有两种方法:Java方法和本地方法。如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的指令地址;如果正在执行的是Natvie 方法,这个计数器值为空(Undefined)。

本地方法栈(Native Method Stack)
Java本地方法(Native Method)是由其它语言编写的,编译成和处理器相关的机器代码,保存在动态链接库中,即.dll(windows系统)文件中,格式是各个平台专有的。虽说Java方法是与平台无关的,但是本地方法不是。程序运行中Java方法调用本地方法时,JVM装载包含这个本地方法的动态库的,并调用这个方法。通过本地方法,Java程序可以直接访问底层操作系统的资源,如果你这样用,你的程序就变成平台相关了,因为本地方法的动态库是与平台相关的,但是使用本地方法还可能把程序变得和特定的Java平台实现相关。

方法区(Method Area)
方法区是各个内存所共享的内存空间, 方法区中主要存放被JVM加载的类信息、常量、静态变量、即时编译后的代码等数据。常把方法区成为永久代(听说不严谨),方法区的大小用户可以更改,如果发生溢出会出现java.lang.OutOfMemoryError:PermGen space信息。

运行时常量
运行时常量是方法区的一部分。Class文件有一个常量池用来存放编译器生成的各种字面量和符号引用,这部分内容在类加载后存放到方法区的运行时常量池中。
运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只能在编译期产生,也就是并非预置入Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。

虚拟机栈(VM Stack)
从图中可以看出,JVM的栈中存放的数据主要有(关于虚拟机栈可以参考
java内存区域-虚拟机栈
):
1.局部变量表(基本数据类型:boolean、byte、char 、short、int、long、float、double );
2.执行环境上下文;
3.操作指令区(存放操作指令)。
JVM的栈也属于线程私有的内存,用户可以设置大小,后面会讲到。对于异常,这块区域有两种情况:如果线程请求的栈深度大于JVM所允许的深度,将抛出StackOverflowError异常;如果JVM的栈可以动态扩展,但是在尝试扩展时无法申请到足够的内存则抛出OutOfMemoryError异常。
堆(Heap)
Java 堆是JVM所管理的内存中最大的一块并且是所有线程共享的内存区域,在JVM启动时创建。堆存放的就是存放对象实例和数组,几乎所有的对象实例都在这里分配内存。Java堆是垃圾回收器管理的主要区域,后面将详细介绍。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现。
为了提高IO速度,在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

堆的分代以及垃圾回收
现在的垃圾回收器都采用分代收集算法,堆分代的唯一理由就是优化GC性能,如果没有分代,那么所有的对象都在一块,GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
堆可以细分为年轻代与年老代,并且年轻代还分为了三部分:1个Eden区和2个Survivor区(分别叫From和To),默认比例为8:1。一般情况下,新创建的对象都会被分配到Eden区(大对象直接分配到年老代),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每经过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
IBM公司的专门研究表明,年轻代中98%的对象是朝生夕死的,因此在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都对整个半区进行内存回收,分配时不用考虑碎片的情况。下图表示“复制算法”:
复制算法
在GC开始的时候,存在对象于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。Eden区和Survivor区并不是按照1:1的比例来划分内存空间的,当Survivor空间不够用时,需要依赖老年代的空间来进行分配。如下图:
这里写图片描述
年轻代采用复制算法,这种算法在对象存活率较高时就要进行较多的复制操作,效率将变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配,以应对一些极端情况,所以这种算法对于年老代不适用。对于年老代,采用“标记-清除”或者“标记-整理”算法进行回收。“标记-清除”算法分为两个过程:标记的过程其实就是,遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象;清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。下图表示“标记-清除”算法:
这里写图片描述
“标记-整理”算法前期跟“标记-清除”算法一样,但后续的整理步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。下图表示“标记-整理”算法:
这里写图片描述
JVM堆栈大小以及一些参数设置
JVM 中设置堆大小有三方面限制:操作系统位数(32位、64位)限制;可用虚拟内存限制;可用物理内存限制。32位系统下,一般限制在1.5G~2G;64位操作系统无限制。下面举例说明:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM初始内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=3
-XX:NewRatio=2:设置年老代与年轻代(包括Eden和两个Survivor区)的比值(除去持久代)。设置为2,则年轻代与年老代所占比值为1:2,年轻代占整个堆栈的1/3。Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
-XX:SurvivorRatio=8:设置年轻代中Eden区与Survivor区的大小比值。设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。
-XX:MaxPermSize=16m:设置持久代最大值为16m。
-XX:MaxTenuringThreshold=3:设置垃圾最大年龄为3,即对象在Survivor区存在的年龄为3(复制一次年龄+1)。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。 对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率该参数只有在串行GC时才有效。
关于JVM参数的含义,可以查看Oracle官方文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值