jvm内存模型及内存溢出简介

一、运行时数据区域

Java虚拟机在运行Java程序时会将他管理的内存分为若干个不同的数据区域,有的在Java虚拟机进程中一直存在有的依赖线程的启动和介绍而建立和销毁。包括:程序计数器,Java虚拟机栈,本地方法栈,Java堆,方法区,运行时常量池,直接内存


1.1程序计数器

可以看做是当前线程所执行的字节码的行号的指示器;

每条线程都有自己独立的程序计数器,用来记录当前线程正在执行的虚拟机字节码指令地址(当前操作的是Java方法,如果是本地方法计数器的值为空)

原因:多线程的情况下,存在频繁的上下文切换(一个处理器同一时刻只能处理一个线程),所以需要记录当前线程执行的位置

各线程之间的计数器互不影响,存在于线程的“私有内存”中

1.2Java虚拟机栈

1.线程私有

2.虚拟机栈描述的是Java执行过程中线程的内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储 局部变量,操作数栈,动态连接,方法出口等信息。每个方法被调用直至执行完毕的过程,就对应一个栈帧在虚拟机栈中从入栈到出栈(数据结构 因为方法是层级调用)的过程。

2.存放基本数据类型和对象引用,对象实例存放在堆中

如何理解栈和栈帧的区别?

Java虚拟机启动的时候会指定栈和堆的大小 好比栈的大小是1024k

每个线程都能1024K大小的栈空间

线程每进一个方法会创建一个栈帧,好比进入了很多方法创建的栈帧超过了栈的大小就会栈溢出

在可以接受的范围内,栈的大小设置的越小方法返回的速度越快

如何确定java方法栈(栈帧)的大小?

其他的暂时还不知道,先说一下局部变量

局部变量表存放编译期可知的基本数据类型,引用数据类型,returnAddress(指向了一条字节码指令的地址)

局部变量表所需的空间在编译期就完成了分配,运行时当进入一个方法时需要在栈帧中分配多大的局部变量空间是确定的

HotSpot 虚拟机栈帧 的大小是不能动态扩展的

猜想创建栈帧大小时应该是有个读取的依据

设置堆栈大小      栈溢出这个栈的大小是什么时候规定的

对于Java方法栈这块内存区域可能出现的异常:

StackOverflowError栈溢出,如果线程请求的栈的深度大于虚拟机允许的深度就会抛出这个异常

OutOfMemoryError堆溢出,如果虚拟机栈大小可以动态扩展(有的虚拟机可以像classic),当无法申请到足够的内存就会抛这个异常

可能出现栈溢出的场景:方法的循环调用(循环中一直在调用就一直创建栈帧,循环中所有的方法一直没有释放就一直不会出栈,所以很容易就栈溢出了)

1.3本地方法栈

和Java方法栈和相似,只不过一个执行的是Java方法服务一个是本地方法服务

和虚拟机栈一样,本地方法栈也会在深度溢出和扩展失败是抛出StackOverflowError和OutOfMemoryError

hotspot直接将这两个方法栈合二为一了

1.4java堆

1存储对象

几乎所有的对象实例都在堆内存当中,堆内存区域所有线程共享

堆中具体存放那些对象?

全局普通变量是否存在堆中

2细化

垃圾回收的对象也是堆

现代垃圾回收器大部分是基于分代收集理论设计,所以堆又被划分为新生代、老年代、永久代

这种区域划分并非堆的固有设计而是垃圾回收器的特性

也有不采用分代回收的垃圾回收器

3其他特性

可以在物理上处于不连续的内存空间

堆的大小上可以设置成固定的也可以设置成可扩展的 -Xmx -Xms (可扩展的当无法扩展时就会内存溢出)

1.5方法区(别名 非堆)

1.线程共享

2.存放已被虚拟机加载的类信息(类名,访问修饰符、字段描述、方法描述)、常量、静态变量、即时编译期编译后的代码缓存等数据

3.方法区的垃圾回收

常量池的回收和类的卸载

Hotspot中方法区和“永久代”的关系及演变

最早是使用永久代来实现方法区,相当于永久代就是方法区

方法区物理上存在堆里,而且是在堆的持久代里面;但在逻辑上,方法区和堆是独立的。

一般说堆的持久代就是说方法区,因为一旦JVM把方法区(类信息,常量,静态静态变量)加载进内存以后,这些内存一般是不会被回收的了。

坏处:这种方式更容易内存溢出,永久代有内存大小限制,其他虚拟机方法区可以使用进程最大内存

Hotspot中的演变:jdk7 将永久代中的字符串常量池,静态变量移出到堆中

jdk8废弃了永久代的概念,将剩下的类信息等全部放到元空间

1.6运行时常量池

1.是方法区的一部分,存放class文件的常量池表里的数据和运行时生成的常量数据

1.7直接内存

不是虚拟机运行时内存区域,其内存大小的设置会影响到jvm内存

如果 其他内存 + jvm内存 > 总内存  容易触发jvm内存溢出

直接内存也可以通过参数设置,如果不设置默认和堆大小一致

1.8普通Java对象的创建过程

1.8.1创建过程

1.根据指令参数在常量池中看能否找到对应的类的符号引用

2.根据符号引用看这个类是否已经被加载、解析、初始化了

3.没有,则执行类的初始化过程

4.在堆中为新对象分配内存空间(“指针碰撞”、”空闲列表“)(对象的内存大小在类加载完就确定了)5.设置对象头信息(偏向锁信息,对象是哪个类的实例,GC分代年龄)

6.执行Class的方法,对对象进行初始化

7.对象引用入栈


1.8.2对象的内存布局

对象头:

对象运行时数据: 哈希吗、GC年龄分代、线程持有的锁、偏向锁的线程ID、偏向锁时间戳

类型指针:指向类的元数据的指针,元数据在方法区中(Hotspot里的在元空间)

如果对象是数组还有块空间记录数组大小

实例数据:

记录父类、子类中各种定义的类型的字段内容

对齐填充:

虚拟机要求对象的大小必须是8字节的整数倍,如果对象不是需要对齐填充来补充,没有实际意义

为什么要是8字节的倍数?

栈中存放的对象引用是什么?

对象在堆中的地址值

or对象在句柄池中的地址,句柄存放的是对象实例数据指针和对象元数据指针

二、内存溢出

除了程序计数器之外其他的内存区域都有可能内存溢出

内存泄漏和内存溢出的区别

2.1堆溢出

2.1.1堆溢出的原因

堆溢出是由于没被引用的对象(垃圾)过多造成JVM没有及时回收,造成堆的溢出。如果出现这种现象可行代码排查:

一)是否App中的类中和引用变量过多使用了Static修饰 如public staitc Student s;在类中的属性中使用 static修饰的最好只用基本类型或字符串。如public static int i = 0; //public static String str;

二)是否App中使用了大量的递归或无限递归(递归中用到了大量的建新的对象)

三)是否App中使用了大量循环或死循环(循环中用到了大量的新建的对象)

四)检查App中是否使用了向数据库查询所有记录的方法。即一次性全部查询的方法,如果数据量超过10万多条了,就可能会造成内存溢出。所以在查询时应采用“分页查询”。

五)检查是否有数组,List,Map中存放的是对象的引用而不是对象,因为这些引用会让对应的对象不能被释放。会大量存储在内存中。

六)检查是否使用了“非字面量字符串进行+”的操作。因为String类的内容是不可变的,每次运行"+"就会产生新的对象,如果过多会造成新String对象过多,从而导致JVM没有及时回收而出现内存溢出。

2.2.2堆溢出的排查方式

1.设置参数-XX:+HeapDumpOnOutOfMemoryError 堆溢出前将堆转储快照

2.根据快照文件排查是内存泄漏还是内存溢出

3.如果是内存泄漏找到内存泄漏的代码

4.如果是内存溢出,根据快照文件找到内存溢出代码是否有生命周期过长,持有时间过长,存储结构不合理等情况,同时优化堆的配置

2.2栈溢出

2.2.1栈溢出原因

1.栈的内存过小,虚拟机请求的栈的深度超过了所允许的最大深度

2.命名了大量对象,超过了本地变量表长度

2.3方法区溢出

2.3.1方法区溢出原因

1.程序中有生成大量动态类的场景

2.有大量jsp或动态生成jsp的应用(jsp第一次运行需要编译为Java类)

3.存在OSGI的应用(不同的方式加载生成不同的类)

总之,运行时生成了大量类的信息,导致溢出

2.4本机直接内存溢出

本机直接内存如不设置默认和堆大小一致,NIO的操作容易导致直接内存溢出

你可能感兴趣的:(jvm内存模型及内存溢出简介)