JVM内存与内存溢出

什么是Java内存管理

对于从事C语言,C++语言的程序员来说,在内存管理这个方面,那是他们的看家本领,也是让他们成为最基础劳动人员的主要原因。

拥有每一个对象的"所有权",又担着每一个对象从出生到终结的管控。

对于我们Java程序员来说,我们不需要去在New操作的时候再去free/释放内存空间的操作,JVM已经为我们解决了这些头大的问题。并且JVM处理内存问题都处理的非常美好。正因为如此,我们Java程序员才要了解JVM内存的管理,虚拟机是怎么使用内存的,这样一来,排查错误也就成为了一项轻而易举的事情了。


接下来我们区了解一下Java内存模型是个啥样吧,废话不多说,上图!
图片来自于知乎creep博主
在这里插入图片描述
简单介绍一下JVM内存模型


类加载器(class loader)

  • 主要负责把编译好的java文件(class文件)加载到内存中

本地方法栈

《Java虚拟机规范》对本地方法的使用语言、使用方式与数据结构并没有任何强制的规定

  • 为虚拟机使用的本地方法服务

虚拟机栈

  • 从上图可以看出,虚拟机栈是线程私有的,他的生命周期是和单个线程相同的。每一个方法执行的时候,虚拟机栈会更新一次栈帧到局部变量表里面,每个方法的初始到return就代表了一个栈帧在虚拟机栈里面的入栈出栈操作。
  • 局部变量表里面存放了各种Java的基本数据类型和引用类型,不过这里请注意,这里的引用类型并不是对象本身,而只是一个引用。就好比是C语言的指针一样。

《Java虚拟机规范》对这个内存有着两种异常情况,如果线程请求的栈深度大于虚拟机允许的最大深度,也就是GC Roots过长了,就是抛出StacKOverFlowError;如果栈容量不可以动态扩展,当内存不够时则会OutOfMemoryError(oom)异常。

方法区

  • 方法区和堆一样,都是线程共享的区域,方法区拿来存放已经被虚拟机加载的类,常量,静态变量信息。Java8以前也叫方法区叫做永久代,但是他们本质上是不同的,只是因为HotSpot的设计团队把粉黛收集器扩展到了方法去才有这个叫法。

  • 这个是Java内存管理的大头,Java所有的对象实例都存放在Java堆当中。可以说Java世界里的所有对象分类都是在这里进行。但是随着Java语言的发展,这话也不能太绝对了。
  • Java堆内存管理也叫做"GC堆",现在主流的JDK版本是8,默认的垃圾收集器是parallel Scavenge 和 Parallel old (ps,po),不过ParNew 和 CMS现在企业比较欢迎

上述的Java堆是两种回收算法是基于分代理论。

Java为什么会内存溢出


Java为什么会内存溢出??

因为存满了呗

好了,不开玩笑。既然说到内存溢出,那我们就来说一下内存泄露这个话题。

程序执行需要引入数据,装载数据,加载数据等等这些都是需要消耗内存的。 正常的一个Java程序都是开始执行,然后结束之后释放内存才能保证程序的基本健壮性。这样内存才能够反复利用。回过头来说,因为一些内存的释放不了的问题,就会出现我们现在所说的内存溢出问题。

当内存被占用的越来越多,但是却(FULL GC)释放不了太多,知道最后,内存所剩下来的空间不足接下来的程序所需要开辟的空间,就会出现内存溢出。

如何判断对象是不是垃圾,一共有两种方法

一、计数器法

  • 引用计数算法就是在对象中添加了一个引用计数器,当有地方引用这个对象时,引用计数器的值就加1,当引用失效的时候,引用计数器的值就减1。当引用计数器的值为0时,jvm就开始回收这个对象。简单的来说,在JVM中的栈中,如果栈帧中指向了一个对象,那么堆中的引用计数器的值就会加1,当栈帧这个指向null时,对象的引用计数器就减1。这种方法虽然很简单、高效,但是JVM一般不会选择这个方法,因为这个方法会出现一个问题:当对象之间相互指向时,两个对象的引用计数器的值都会加1,而由于两个对象时相互指向,所以引用不会失效,这样JVM就无法回收。

二、可达性分析法(重点)

  • 针对引用计数算法的BUG,JVM采用了另一种方法:定义一个名为"GC Roots"的对象作为起始点,这个"GC Roots"可以有多个,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,即可以进行垃圾回收。简单来说,只要通过GCRoot根节点找不到的对象就是垃圾,马上将会进行回收。
    -在这里插入图片描述
    在上图中可以看到,如果时选用引用计数算法,object5, object6, object7之间互相引用,所以无法被回收。但是如果选用了可达性分析算法,虽然他们之间时相互引用,但是他们没有任何引用链和GC Roots连接,所以是可回收对象。说白了就是GCROOT找不到obj5,导致和他相关的难兄难弟一起被当作是垃圾被回收。

GC Roots对象一般包括有:1.虚拟机栈(栈帧中本地变量表)中引用的对象;2.方法区中类静态属性引用的对象;3.方法区中常量引用的对象;4.本地方法栈中JNI(Native方法)引用的对象。

判断是否是垃圾的算法是三色标记法,具体点击三色标记法

常见的两种内存溢出类型

一、栈溢出(StackOverFlowError)

  • stack是Java内存中的栈空间,主要用来存放方法中的变量,参数等临时性的数据的,发生溢出一般是因为分配空间太小,或是执行的方法递归层数太多创建了占用了太多栈帧导致溢出。(GC Roots)引用过长。这是一种Java堆的判断对象是不是垃圾的一种方法,我们在下文将会提及。

针对这个问题,除了修改配置参数-Xss参数增加线程栈大小之外,优化程序是尤其重要。

二、堆溢出(OutOfMemoryError)

  • Java堆是Java内存中主要存放对象的一块空间,分为新生代和老年代。新生代中又分为Eden和s0,s1两个幸存区。每次新生代Eden进行GC的时候不是垃圾的对象会进入幸存区,进入一次默认加一。当达到15(默认)的时候对象将会被标记成老年对象进入老年代。当老年代内存不够用的时候,老年代和新生代会进行新生代。文字很抽象,我们上图片!
    图片转自与沈洲行
    在这里插入图片描述
    当什么时候会出现Java堆溢出呢?废话不多说,我们看代码
import java.util.ArrayList;
import java.util.List;

/**
 * ClassName: HeapOutOfMemoryError
 * Description:
 * date: 2022/2/21 22:59
 *
 * @version JDK 1.8
 * @author: lisu
 */
public class HeapOutOfMemoryError {

    // 用于存放GC root 根节点测试的类
    static class OOMDemoTest{}

    public static void main(String[] args) {

        //  无止境的向GCRoot根节点添加对象,导致GC无法回收
        List<OOMDemoTest> list = new ArrayList<>();

        for (;;) {
            list.add(new OOMDemoTest());
        }
    }
}

接下来让我们打开jdk自带的调优工具visualvm等待两分钟,当然我更推荐阿里巴巴开发的ather工具。它更方便。
在这里插入图片描述
可以看出没有进行垃圾回收,五分钟后……
程序最终抛出

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory

堆内存的对象布局

前面在堆溢出的时候我们简单看了一下堆内存结构,也了解到这块区域是GC的主要对象,接下来我们更进一步。
在这里插入图片描述
一、新生代

  • 新生代分为三个区,eden,s0,s1。它们的比例分别是8:1:1。当然这个也是可以自己设置的。不同的区域,GC的算法就不一样。新的对象实例创建,会放入到Eden,随着存储对象实例增多,消耗内存接近Eden最大值,则会触发Minor GC,Minor GC之后,则会将活下来的对象实例放入生存区域,生存区域也会被定期扫描,经过多次扫描之后,还存活下来的,则放入老年代,如果老年代内存快消耗完,就触发major GC,也就是full GC动作,将会对整个堆内存进行回收动作。
    java中对堆内存设置参数说明:

-Xms:设置堆的最小空间大小。
-Xmx:设置堆的最大空间大小
-Xmn:设置年轻代大小
-XX:NewSize 设置新生代最小空间大小
-XX:MaxNewSize设置新生代最大空间大小方法区
方法区主要存储虚拟机加载类信息、常量、静态变量。方法区也称“永久代”,是所有线程共享的资源。当永久代区域内存消耗解决上限,就会触发FullGC。

-XX:PermSize设置永久代最小空间大小
-XX:MaxPermSize设置永久代最大空间大小

二、老年代(什么样子的对象能通过YoungGc到达OldArea)

  • 大对象:所谓的大对象是指需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组,大对象对虚拟机的内存分配就是坏消息,尤其是一些朝生夕灭的短命大对象,写程序时应避免。
  • 长期存活的对象:虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,。对象在Survivor区中每熬过一次Minor GC,年龄就增加1,当他的年龄增加到一定程度(默认是15岁), 就将会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
  • 动态对象年龄判定:为了能更好地适应不同程度的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

文章转自与
在这里插入图片描述

添加链接描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值