JVM内存模型与垃圾收集

JVM内存模型与垃圾收集

  • JVM堆内存模型
    JVM的垃圾收集主要指的是堆内存空间,那么在每一次执行GC的时候需要区分出哪些堆内存空间需要被回收,哪些不需要被回收,所以为了整体的回收处理方便,JVM将堆内存分为如下的几个组成部分,而这几个组成部分还需要去考虑JDK版本,现在的JVM内存划分就必须考虑JDK1.8之前和JDK1.8之后的问题了

这里写图片描述

jdk1.8之前主要分为
新生代、Eden(伊甸园)、S0、S1(S0和S1合成为存活区)
老年代、
永久代
永久代:jdk1.8之后被干掉
如果简化点来理解的话:
1)新生代:那些刚刚创建的对象,刚刚创建的对象有可能会存在有许多的垃圾对象,这些对象应该是被优先回收的。
2)老年代:老不死的那类对象,经过了很多次的清理之后你会发现该对象依然有用。
3)永久代:intern()方法进行入池的对象实际上就在永久代之中,永久代不会被回收,除非JVM崩溃死机,因为其本身属于bug性质的存在,所以在jdk1.8之后将其更换为元空间(电脑的直接内存)
100g电脑内存80g给了java,剩余的20g称为元空间

这里写图片描述

每一个空间上都有一个伸缩区
在整个内存的组成过程之中每一代的内存空间都会有一个伸缩区,那么该区域就可以由JVM根据空间使用情况动态扩充。
如果伸缩区内存大小有限,每次扩充的时候会进行判断,这样会降低性能。
当我们适当合理的设置了伸缩区的内存大小之后,那么就可以得到良好的性能提升,也就是说最容易的性能提升就是改变伸缩区的内存大小设置。
JVM的垃圾回收实际上本质上就是如何让JVM在电脑上发挥出最大的性能优势

  • java对象的创建和垃圾回收的流程
    在java之中支持有GC的概念,那么GC有两种调用的形式:
    1.自动调用
    2.手动调用(Runtime.getRuntime().gc()):很少使用
    那么什么时候会进行自动的GC调用呢,这里面就牵扯到了两类的GC操作环境。

这里写图片描述

1.当现在程序之中需要产生新的实例化对象(关键字new 、对象克隆、反射实例化(包含classloader))的时候,那么就一定要进行内存空间的开辟,所以此时需要申请新的内存空间,
2.新对象要申请的对象空间默认都是在伊甸园区(新生代区)进行开辟,所以首先要判断伊甸园是否有空余的内存空间,如果有空余的内存空间,则直接在伊甸园区开辟新的堆内存空间,此时不会发生有GC处理;
3.如果新对象无法在伊甸园区申请出新的空间,那么就表示现在的伊甸园区的内存空间不足,不足就需要将那些无用的新对象进行回收(Minor GC);—JVM回收不活跃对象,当回收完成后要继续判断该空间是否还有剩余的空间可以容纳下新的对象,如果可以则开辟新空间,保存新对象;
4.如果此时伊甸园区即使执行了Minor GC之后发现仍然没有可以被回收的对象,(或者说伊甸园内存仍不足以创建新对象),那么这个时候将继续判断存活区是否有空间,(存活区一般与伊甸园区的比率:1:1:8)S0:S1:Eden ,如果存活区有空余空间,则将那些活跃的伊甸园区的部分对象直接保存给存活区,这样就相当于伊甸园区可以腾出部分的空间(这个空间非常小)来,供新对象使用。
5.如果此时存活区依然满了(空间不足),则继续向老年代进行内存空间的申请,首先会判断老年代的空间是否空余,如果有空余的空间,则将存活区中的活跃对象保存在老年代区,而后存活区得到了空间释放,伊甸园区也得到了空间释放,则对象空间申请成功。

6.如果老年代内存空间也是满的,那么这个时候会执行FULL GC(完全GC、major GC)进行老年代的内存释放,如果可以释放成功,则可以进行对象的保存,如果释放不成功,则表示已经没有无用的内存空间了,那么就会抛出OOM(OutOfMemoryError)

这些是GC自动回收触发条件。

面试题:请问自动的GC什么时候会触发:
最笨的回答:内存不够了,触发GC
GC的触发有两类:Minor GC 和FULL GC
Minor GC发生在新生代内存空间不足的情况下,当新生代内存空间不足时,会进行触发,释放新生代中不活跃的对象
FULL GC发生在老年代内存空间不足的情况下,当老年代的内存空间不足时会自动触发FULL GC,如果触发FULL GC之后内存空间仍然不足,则会产生OutOfMemoryError错误提示信息,表示已无内存可以被创建。

  • Java堆内存调整参数
    本质:GC发生的越频繁,对系统性能越大
    GC虽然可以进行内存空间的释放,但同时频繁的GC一定会影响系统性能,如何才可以不频繁的发生GC呢?(GC发生频率低一点):目标:伊甸园区、存活区、老年代空间足够大,GC发生频率会低。
    内存越大,GC发生的频率就越低,系统的性能就越高。
    代码中减少不必要的对象的产生。

    Java内存调整策略(优化策略)

    范例:首先来取得当前可用的内存空间量。

 Runtime run = Runtime.getRuntime();//此处是单例模式
 System.out.println("MAX_MEMORY = " + run.maxMemory());
 System.out.println("TOTAL_MEMORY = " + run.totalMemory());
 System.out.println("FREE_MEMORY = " + run.freeMemory());

这里写图片描述

结果返回的是字节:

 Runtime run = Runtime.getRuntime();//此处是单例模式
 //默认情况下分配给JVM的最大内存空间:大约总内存的四分之一(电脑为64g内存,此结果大约是14g)
 System.out.println("MAX_MEMORY = " + run.maxMemory() +"("+((double)run.maxMemory() / 1024 / 1024)+"M)");
 //默认情况下除了伸缩区之外的可用内存空间
 System.out.println("TOTAL_MEMORY = " + run.totalMemory() +"("+((double)run.totalMemory() / 1024 / 1024)+"M)");
 System.out.println("FREE_MEMORY = " + run.freeMemory() +"("+((double)run.freeMemory() / 1024 / 1024)+"M)");

根据结果可知:
//默认情况下分配给JVM的最大内存空间MAX_MEMORY:大约是总内存的四分之一

在整个分配给JVM使用的对内存之中返现,TOTAL表示用户的最大可用,而MAX - TOTAL才表示伸缩区,所以每一块的内存之中都存在伸缩区的概念。

如果说现在给用户的使用内存空间留有伸缩区,那么会造成以下的一种情况:
如果可用内存不足,会判断是否伸缩区有空间,而后开辟伸缩区的空间。那么这种伸缩区的开辟与回收操作部分就有可能产生性能下降。

参数:

这里写图片描述

如果现在取消掉伸缩区的概念,让初始的内存就是最大的可用内存空间,这样就可以实现JVM的性能调整,避免了重复的内存控制操作,可以让整个代码的执行速度上升。

-Xms :设置初始分配大小,默认为物理内存的1/64(比如内存64G,实际初始大小约为1G)
-Xmx:最大分配内存,默认为物理内存的1/4

double x = (double)64 *1024*1024*1024
System.out.println(x/run.totalMemory());

大约为66

如果要想修改内存的大小,可以使用的单位:K、M、G;但是如果要改则考虑两类情况,一个是Eclipse修改、另一个是Java运行修改

1.Eclipse修改:
右键-run configurations

这里写图片描述

设置完成后,可以发现Runtime类能够取得到的数据就是修改后的结果
如果是一台java专用服务器,则把所有内存都加给jvm使用,比如拥有64g,则全部分配给JVM使用
-Xmx=64g -Xms=64g
这是一个很有用的性能调优,避免伸缩区不断扩展和回收带来的性能问题

2.命令行方式:在执行java程序的前面追加相应的配置信息:
这里写图片描述

所有的java程序运行的参数要写在执行类的前面。而且每个执行类都要分开配置。
一般一台计算机上只跑一个JVM

在整体参数之中提供有一个GC处理详情的问题:
这里写图片描述

通过此操作可以详细得取得GC处理中的执行问题的分析过程。

范例:编写一个程序

这里写图片描述

在eclipse中运行的时候添加参数:

这里写图片描述

入池:一定会有新生代保存到永久代内存区(活跃对象)
这里写图片描述

包含了GC和FULL GC的操作
以上就将整个程序运行中出现的GC过程进行了记录。
以两条记录为例:
1.新生代的GC处理(Minor GC处理)
分配失败,意味着新生代没有可用内存空间了,
会执行好几次

这里写图片描述

2.老年代的GC处理:(FULL GC)
此时老年代内存分配失败,引发FULL GC,是一个完整的GC,
这里写图片描述
eden:伊甸园处理
PSYoungGen:新生代
ParOldGen : 老年代
Metaspace :元空间

3.会详细显示出每一块内存空间的内存使用情况:
但是需要注意的是:此时的内存状态实际上取得了只是供开发者观察的。
但如果是项目的运维人员,它是看不见这些内容的,就必须使用一些监控程序,
java有两类监控程序:
jdk安装目录下bin里边:jmap.exe(cmd下监控)、jvisualvm.exe( 图形化监控)两个工具

可视化监控:

这里写图片描述

这里写图片描述

cmd下输入:
jmap pid(进程号)

这里写图片描述

这里写图片描述

可以参考:
jmap -heap pid
比如:jmap -heap 3248
可以通过这个实现内存监控处理

面试题:你们的项目之中是如何对java进行调优?
1.在堆内存中会存在一个伸缩区的概念,默认情况下最大可用内存为整体内存的四分之一,默认使用的内存为整体内存的六十四分之一,这个时候只需要避免伸缩区的频繁变更就可以提升程序的系能。
2.可以在程序执行的时候使用 -Xmx设置最大可用内存,-Xms设置初始化内存空间大小,将这两个内容设置为一样的空间大小就可以提升JVM的运行性能。
3.以上调优都是相对而言,性能的提升必须建立在合理的代码基础之上。程序合理完善。

  • *年轻代(新生代)

年轻代调整参数:
年轻代主要分为两个区域:伊甸园区 、存活区(分为两个);
这里写图片描述
所有新创建的对象都会存活在伊甸园区,但是伊甸园区的保存的空间一定是最大的,毕竟产生新对象的几率是很高的,在伊甸园区里面由于里面的对象经常只是临时创建的,所以其拥有一个Minor GC的操作处理,而经过多次的MinorGC处理后依然被保留下来的对象就认为该对象不应该被回收,则将此对象保存到存活区之中。
存活区一共分为两类:
存活0区(S0区、From Space):
存活1区(S1区、To Space):
这两个存活区主要负责对象的晋级,向老年代的晋级。并且这两个存活区有一块是专门负责对象回收的,所以有一块内存空间总是空的。所以有一块内存空间总是空的(一个负责晋升,一个负责回收)

由于伊甸园区保存的数据对象一般较多,所以默认的比率是8:1:1,两个存活区的大小是相同的。s1和s0名称是可以互换的。由系统完成。

这里写图片描述

这个算法不是固定的,取决于使用电脑的硬件环境,单CPU和多CPU是不一样的。

存活区 :动态对象数组
再进行GC之前一定要先发生一次全对象的扫描处理操作,通过扫描才可以知道哪些对象是垃圾空间。
年轻代中
这里写图片描述

这里写图片描述

BTP技术:查找剩余空间:根据栈顶元素

在java的内存之中,所有的堆内存是线程共享的,这样根据栈顶元素判断剩余空间就变得不够合理。

所以才有了TLAB技术:解决了多线程,但是可能会存在碎片问题。
这里写图片描述

年轻代中内存还是可以通过参数来进行控制的。

这里写图片描述

-Xss:一般不做设置,一个对象可能会很大,线程栈本身太小可能会发生溢出问题。

这里写图片描述

年轻代的整体的空间控制的修改意义不大,其实最有用的部分就是在于线程的大小分配,但是又不能够轻易的修改,同时由于年轻d代的算法问题,所以理论上你的电脑程序(服务器)是不可能接受无限多的线程(用户),要想接受更多,必须具有更强的CPU和更大的内存。

  • 老年代调整
    一个对象要想真正的活到老年代,实在是太难了,因为现在的程序都属于多线程访问,所有的业务操作的对象都是针对线程操作的,那么所有的线程在整体的操作过程之中时间都是非常短的。(线程操作的生命周期长,占用资源多,反之则短)
    那么这些对象往往都在年轻代中开辟,很少能跑到老年代之中。

这里写图片描述

FULL GC = Major GC

老年代算法:
Mark-Sweep
这里写图片描述
碎片整理会发生性能问题的瓶颈

Mark-Compact
这里写图片描述

这些算法是根据硬件环境动态选择,当然也可以自己配置。

这里写图片描述

默认情况下,所有对象都会通过年轻代进行创建,而后经过不断的Minor GC之后要将其保存在老年代之中,但是如果现在有些对象的内容特别庞大,(我们在做数据库查询的时候,用多少数据取多少数据,否则会导致很大的对象,来回晋升可能会影响性能),那么就不建议经过年轻代而将其直接保存到老年代之中,就可以设置以上参数:
超过512k直接进入老年代。
这里写图片描述

一般一个良好的程序开发,是不可能出现这样的问题的。
数据库分页:
如果通过数据库分页,取得数据很有限,
但如果把所有数据拿到程序中在做算法分页的话,这样会内存分配会出现问题。
full gc发生的几率很低,尽可能少发生,full gc 操作处理会很耗时间,会影响程序性能。

  • 永久代
    jdk1.8之后被废除
    永久代是在jdk1.8之前的一个bug性的存在,其核心的本质在于:
    该区域中的对象不会被回收。所有的GC操作对老年代是无效的。
    或者简单理解来说就是 方法区就是永久代。
    HotSpot虚拟机规范中是存在有永久代概念的,但是BEA和IBM的虚拟机规范之中是不包含永久代概念的。
    Oracle现在将HotSpot中的永久代取消了,那么就意味着它现在希望HotSpot与JRockit两个虚拟机的规范进行合并。

这里写图片描述

这里写图片描述

这里写图片描述

所以现在永久代已经不做考虑了。

  • 元空间
    元空间是永久代的替代品。
    从jdk1.8之后正式取消了永久代之后,取而代之的是元空间(MetaSpace),所谓的元空间的本质指的就是本机的物理内存,起作用和永久代相同,但是元空间和永久代最大的区别:
    元空间用的是物理内存(收到本机的物理内存的限制),而永久代是JVM的内存空间(本身受到JVM的限制)。
    本质上两者都是保存那些基本上不会被清空的操作的。

调整参数:
这里写图片描述

设置元空间的初始大小等。基本上不需要调整。
范例:设置元空间的操作
这里写图片描述
会报错!
这里写图片描述

对于OOM的错误信息,可能是java的堆内存溢出(java heap space),元空间溢出(MetaSpace)、永久代溢出(PermGen Space)。
面试:请问是否知道什么叫OOM,怎么会出现?
OutOfMemoryError:指的是内存溢出问题,内存的溢出需要考虑以下情况:
java堆内存溢出java heap space 往往出现在FULL GC失败之后;
永久代溢出PermGen Space 一个方法中出现的内存溢出。
元空间(MetaSpace):分配的物理内存不足,或者数据量高于物理内存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值