JVM性能调优-JVM内存区域划分

JVM性能调优-JVM内存区域划分

1、程序计数器(线程私有)

结论: 程序计数器线程私有,并且分配的空间大小不会随程序执行改变,所以不存在内存溢出等异常情况

程序计数器(Program Counter Register),也有称作为PC寄存器。保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。也就是说是用来指示执行哪条指令的。

​ 由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的

​ 在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址如果线程执行的是native方法,则程序计数器中的值是undefined。

​ 由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的

2、java栈(线程私有)

Java栈也称作虚拟机栈(Java Vitual Machine Stack)

Java栈中存放的是一个个的栈帧每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表、操作数栈、指向当前方法所属的类的运行时常量池的引用、方法返回地址、额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。
在这里插入图片描述

1、局部变量表,用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。 存储内容:引用对象,returnAddress类型。Long和double类型占用2个局部变量空间,其余的数据类型占据一个。局部变量表空间在编译期间完成分配。

存储内容:引用对象,returnAddress类型。Long和double类型占用2个局部变量空间,其余的数据类型占据一个。局部变量表空间在编译期间完成分配。

2、**操作数栈,**栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。 指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。 方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方(参考汇编),因此在栈帧中必须保存一个方法返回地址。 由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java栈线

**异常情况: **

1.栈深度大于已有深度:StackOverflowError

2.可扩展深度大于能够申请的内存:OutOfMemoryErro

3、本地方法栈(线程私有)

​ 本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地**方法栈则是为执行本地方法(Native Method)服务的。**在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,****虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一

**异常情况: **

**1.栈深度大于已有深度:StackOverflowError **

2.可扩展深度大于能够申请的内存:OutOfMemoryError

4、堆(线程共享)

Java中的堆是用来存储对象本身的以及数组

堆是被所有线程共享的,在JVM中只有一个堆。所有对象实例以及数组都要在堆上分配内存,

垃圾收集器管理的主要区域,很多时候被称作GC堆。现在收集器基本采用分代收集算法

新生代 和老年代,再细致点Eden空间,From Survivor空间,ToSurvivor空间等

​ **异常情况: **

1.可以处于物理上不连续的内存空间,逻辑连续即可。既可实现固定大小,也可扩展。如果堆中没有内存完成实例分配,并且堆无法再扩展是,将会抛出OutOfMemoryError;

这里简单介绍下基本的垃圾回收算法:

按照基本回收策略分

1、引用计数废弃)

​ 古老的回收算法,原理是此对象有一个引用,既增加一个计数,删除一个引用则会减少一个计数。垃圾回收的时候,只收集计数为0的对象。其中存在很多弊端,无法处理循环引用的问题,一般java不会使用这种算法。

​ 优点:实时性,一旦没有引一用,内存就直接释放了。不⽤像其他机制等到特定 时机。实时性还带来个好处,处理回收内存的时间分摊到了平时。

弊端:1、维护引计数消耗资源 2、循环引用问题

 举例:list1 = []   
      list2 = []
      list1.append(list2)
      list2.append(list1)
 总结:list1与list2相互引用,如果不存在其他对象对它们的引用,list1与list2的引用  计数也仍然为1,所占用的内存永远无法被回收,这将是致命的。
2、可达行分析清理
1、标记-清除算法此阶段分为两个阶段。首先从引用根节点开始标记所有被引用的对象,其次遍历整个堆**,把未标记的对象清除。此算法需要暂定整个应用

“标记”和“清除”。标记阶段,确定所有要回收对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不用的对象清除掉。概算该是基础的回收算法,标记清除阶段的效率不高,并且产生不连续的内存区域块,即内存碎片,这样操作当程序需要分配大内存对象时候,可能无法找到足够的连续空间。
在这里插入图片描述

2、复制算法

​ 此算法把内存划分成两个相等区域,每次只想·使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另一个区域中。然后把这块内存全部整个清理掉。复制算法实行简单,效率比较高,但是每次运行的时候只能使用其中内存的一半,造成资源利用率不高**,现在jvm用复制算法收集新生代,同时复制过去以后还能进行相应的内存整理,不会出现"碎片"问题**,由于新生代中大部分对象(98%)都是朝夕想死的,所以两块内存比例不是按1:1比例(大概是8:1)。

缺点比较明显,就是需要两倍内存空间

在这里插入图片描述

3、标记-整理算法

此算法结合了 标记-清除和复制两个算法的优点,分为两个阶段。首先开始标记所有被引用对象,其次遍历整个堆,清除标记对象,并把存活对象压缩到堆的其中一块,按顺序排放,形成连续的内存空间,解决了标记-清除的内存碎片问题,同时也避免了复制算法的空间问题,提高了内存的利用率,并且它适合在收集存活时间较长的老年代。
在这里插入图片描述

按照分区对待的方式分

1、增量收集

实时垃圾回收算法。在应用进行的同时进行垃圾回收,(后不使用此算法,不做分析)

2、分代收集

基于对象生命周期(加载、验证、准备、解析、初始化、使用、卸载)

把对象分为年轻代、老年代、持久代,对不同生命周期的对象使用不同的算法进行回收。现在的垃圾回收器都是使用此算法。

此算法根据各个对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用复制的算法,老年代采用标记-整理算法。大量程序细节可百度搜索,不同的虚拟机平台实现的方法也各有不同。

按照系统线程分

1、串行收集

​ 串行收集使用单线程处理所有垃圾回收工作,因为无需多线程交互,实现容易,而且效率比较高。但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。

2、并行收集

​ 并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势

3、并发收集

​ 相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越长。

分代处理垃圾

在这里插入图片描述

分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收

​ 虚拟机中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

​ 年轻代:

​ 年老代: 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

​ 持久代:用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,持久代大小通过-XX:MaxPermSize=进行设置。

5、方法区(线程共享)

1、存储了每个类的信息(包括类的名称、方法信息、字段信息)

2、静态变量

3、常量

4、编译器编译后的代码等。

在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。

​ 方法区还有一块内存,运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法

​ 在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。

**异常情况: **

**1.方法区调用递归,内存会溢出,报OutOfMemoryError; **

2.当常量池无法再申请到内存时OutOfMemoryError;

6、直接内存(线程共享)

NIO,使用native函数库直接分配堆外内存,不经过JVM内存直接访问系统物理内存的类——DirectBuffer。 DirectBuffer类继承自ByteBuffer,但和普通的ByteBuffer不同,普通的ByteBuffer仍在JVM堆上分配内存,其最大内存受到最大堆内存的限制;而DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制。 堆内存比较:

  1. 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
  2. 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显
    异常情况: 1.DirectBuffer分配内存溢出
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值