JVM学习总结

基本概念

栈和堆

栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
在Java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。

对象的大小

基本类型的大小是固定的,而非基本数据类型就不是了,这里主要介绍非基本类型的java对象。java中,一个空的object对象大小为8byte,所谓空的就是没有任何属性。
当我们new一个object对象时,这个对象所占用的空间为4byte+8byte,4byte是栈中保存该对象的引用所需要的空间,8byte则是堆中此对象的大小。我们知道,所有的非基本类型的对象都继承object对象,所以可以理解java对象的大小都大于8byte。
另外,Java在对对象内存分配时都是以8的整数倍来分。如果一个对象的大小计算下来实际需要13byte,那么实际分配的大小是16byte。
这里需要注意一下基本类型的包装类型的大小。因为这种包装类型已经成为对象了,因此需要把他们作为对象来看待。包装类型的大小至少是12byte(声明一个空Object至少需要的空间),而且12byte没有包含任何有效信息,同时,因为Java对象大小是8的整数倍,因此一个基本类型包装类的大小至少是16byte。这个内存占用是很恐怖的,它是使用基本类型的N倍(N>2),有些类型的内存占用更是夸张(随便想下就知道了)。因此,可能的话应尽量少使用包装类。在JDK5.0以后,因为加入了自动类型装换,因此,Java虚拟机会在存储方面进行相应的优化。

垃圾收集器与内存分配策略

提到垃圾收集(Garbage Collection,GC),都会思考以下几个问题:

  • 哪些内存需要回收?
  • 如何回收?
  • 什么时候回收?
  • 。。。

1.哪些内存需要回收

我们知道Java内存中,程序计数器PC、虚拟机栈、本地方法栈这三个区域都是随线程生而生,随线程亡而亡。每一个帧栈需要分配额内存基本都是在类结构确定下来的时候就确定了,在方法结束或者线程结束的时候,内存就随之回收了。也就是说,这几个区域的内存的分配和回收都具备确定性,不需要额外的去思考回收策略。而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存也可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序运行期间才能确定需要创建哪些对象,因而这部分内存的分配和回收都是不可预知的,都是动态的。GC所针对的内存就是这部分的内存。
那堆中哪些对象需要被回收呢,换句话说,就是怎么判断对象的“存活”状态。即判断哪些对象还“活着”,哪些对象已“死亡”(即不会再被任何途径引用的对象)。

1.1引用计数法

很多官方给出的判断对象是否存活的方法是:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效的时候,计数器值就减1。当计数器值为0时,就表示这个对象不再被使用。但是这个算法不能解决循环处理的问题。

1.2可达性分析算法

基本思路就是通过一系列称为“GC Roots”的对象作为起始点,从这些节点往下搜索,经过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,就认为GC Roots到这个对象不可达,那么这个对象就被认为是不可用的。

GC Roots
Object 1
Object2
Object3
Obeject4
Object5
Object6
Object7

上图中,虽然对象object5、object6、object7存在关联关系,但是GC Roots到它们之间是不可达的,所以它们就被认为是可回收的对象。
那么在这种方式的实现中,GC从哪儿开始的呢?即,从哪儿开始查找哪些对象是正在被当前系统使用的呢?我们知道,栈是程序开始执行的地方,所以要获取哪些对象正在被使用,则需要从Java栈开始。同时,一个栈是与一个线程对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。
同时,除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。这样,以栈或寄存器中的引用为起点,我们可以找到堆中的对象,又从这些对象找到对堆中其他对象的引用,这种引用逐步扩展,最终以null引用或者基本类型结束,这样就形成了一颗以Java栈中引用所对应的对象为根节点的一颗对象树,如果栈中有多个引用,则最终会形成多颗对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收。而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。

因此,垃圾回收的起点是一些根对象(java栈, 静态变量, 寄存器…)。而最简单的Java栈就是Java程序执行的main函数。这种回收方式,也是上面提到的“标记-清除”的回收方式

2如何回收

这里介绍几种垃圾回收算法

2.1标记-清除算法

标记-清除算法(Mark-Sweep)分为两个阶段,标记和清除:
第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。
在这里插入图片描述
由上图可以很直观的发现GC之后,会产生很多内存碎片,也就是内存空间不连续,这可能会导致后续如果需要分配较大内存的对象时,无法提供足够大的内存而提前触发另一次GC动作。还有一个缺点就是此算法的效率问题,标记和清除这个两个过程的效率都不是很高,期间需要暂停整个应用。

2.2复制算法

针对标记-清除算法的效率和空间问题,提出了复制算法。它将可用内存按容量分为两个大小相同的模块,分配内存时只使用其中一块A,当A中内存快用完的时候,就会将存活的对象全部复制到另一块B中,然后再将A中已使用的内存空间全部清理掉,这样也就不会出现大量内存碎片的情况。GC后分配时内存只需要按顺序就可以,实现简单,运行高效。
在这里插入图片描述
但这个算法也存在一个缺陷,可用内存缩小为原先的一半。
现代的商业虚拟机很多都采用这种算法来回收新生代。新生代的特点就是“朝生夕死”,所以并不需要按照1:1来划分内存空间。而是将内存分为一块较大的Eden空间和两块较小的Survivor空间。每次使用Eden和其中一个Survivor空间,GC时,将所有存活的对象全部复制到另一块Survivor中,然后再将使用的内存空间全部清理掉。一般情况而言,Eden和Survivor的比例是8:1,也就是只有10%的空间被浪费掉。
但是我们不能保证每次只有不超过10%的对象存活下来,如果超过10%,survivor空间不够怎么办,这就需要其他内存(这里指老年代)来分担。

2.3标记-整理算法

在对象存活率较高的时候,如果使用复制算法,会导致效率较低,且需要占用其他空间来分配内存,就有人提出了标记-整理算法(Mark-Compact)。标记阶段同前面讲的标记-清除算法一致。不同的就是标记完之后是将所有存活的对象都向一端移动,然后统一清理掉端边界以外的所有对象。
在这里插入图片描述
这样即避免了标记-清除算法的内存碎片问题,也避免了复制算法的空间利用率问题。

2.4分代收集算法

当前的商业虚拟机一般都采用分代收集算法(Generational Collection)。顾名思义,就是根据对象的存活周期将内存划分为不同的部分:老年代和新生代。然后根据不同的特点采用不同的收集算法。
比如,由于新生代的朝生夕死的特点,每次GC只有很少的对象存活下来,就可以采用复制算法。而老年代的生存率很高,采用复制算法代价比较高,需要额外的空间来进行分配担保,所以一般采用标记-清除或者标记-整理算法实现。

2.4.1为什么要进行分代

在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收,以便提高回收效率。

2.4.2 如何进行分代

虚拟机中的共划分为三个代:新生代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与GC要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
新生代:
所有新生成的对象首先都是放在年轻代的。新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。在前面介绍复制算法的时候知道,新生代分三个区,一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在新生代中的存在时间,减少被放到年老代的可能。
年老代:
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
持久代:
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

3.什么时候回收

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Minor GC和Full GC。
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。以下几个原因可能会导致Full GC:

  • 老年代空间没有剩余
  • 持久代空间没有剩余
  • System.gc()被显性调用
  • 上一次GC之后Heap的各域分配策略动态变化

4.垃圾收集器

不同的厂商,不同的虚拟机提供的垃圾收集器都可能会有很大的差别,并且一般都会提供参数供用户根据应用需求组合各个年代所需的收集器。
在这里插入图片描述

  • Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。适用于Client模式。
  • ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。多运行在Server模式。
  • Parallel Scavenge收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间和CPU总消耗的时间的比值。
  • Serial Old收集器,Serial Old是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法。适用于Client模式。
  • Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
  • CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法,垃圾收集线程和用户线程几乎同时工作。
  • G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征

并行(Parallel):多条垃圾收集器线程并行工作,但此时用户线程仍处于等待状态。
并发(Concurrent):用户线程与垃圾收集器线程同时执行(但不一定是并行,可能是交替执行),用户线程在继续执行,而垃圾收集程序运行于另一个CPU上

总结:

  • 串行处理器:
    适用情况:数据量比较小(100M左右);单处理器下并且对响应时间无要求的应用。
    缺点:只能用于小型应用
  • 并行处理器:
    适用情况:“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。
    缺点:垃圾收集过程中应用响应时间可能加长
  • 并发处理器:
    适用情况:“对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境。

GC算法和垃圾回收器算法图解以及更详细内容参考:http://www.cnblogs.com/ityouknow/p/5614961.html

5.参数配置

JVM参数含义参考:
https://blog.csdn.net/wang379275614/article/details/78471604
常见配置汇总:

  • 堆设置
    -Xms:初始堆大小
    -Xmx:最大堆大小
    -XX:NewSize=n:设置年轻代大小
    -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
    -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
    -XX:MaxPermSize=n:设置持久代大小
  • 收集器设置
    -XX:+UseSerialGC:设置串行收集器
    -XX:+UseParallelGC:设置并行收集器
    -XX:+UseParalledlOldGC:设置并行年老代收集器
    -XX:+UseConcMarkSweepGC:设置并发收集器
  • 垃圾回收统计信息
    -XX:+PrintGC
    -XX:+PrintGCDetails
    -XX:+PrintGCTimeStamps
    -Xloggc:filename
  • 并行收集器设置
    -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
    -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
    -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
  • 并发收集器设置
    -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
    -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值