JVM内存结构和垃圾回收
Posted 七月的小尾巴
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM内存结构和垃圾回收相关的知识,希望对你有一定的参考价值。
前言
在做性能测试之前,首先我们需要了解JVM的内存结构和垃圾回收机制。只有了解了JVM的底层原理,这样才能帮助我们后期更好的进行性能测试和调优。
Java内存管理机制
- Java 采用了自动管理内存的方式
这一点有区别于我们的传统一些编写语言如c、c++,他们都是通过手动管理的方式来运行的。如果我们写代码的过程中我们申请内存,就需要手动释放内存。否则可能会内存泄露。
那么Java的采用的是自动管理内存,就不需要关注内存释放,只需要关注业务代码实现,这样就大大的降低了我们编程的难度。 - java程序是是运行在JVM之中的
JVM可以理解成是一个JAVA的虚拟机。一旦我们启动一个java程序,就会去启动一个JVM,可以理解成每一个java进程,都会启动一个jvm - Java的跨平台的基于jvm的跨平台特性
- 内存的分配和对象的创建是在JVM中
- 用户可以通过一系列参数来配置JVM
JAVA运行时区域
重点学习,可能再面试的过程中,尤其是性能测试,会被经常面到将JVM运行时的区域图手动绘画出来
。
下面我们来看一下JVM运行时区域主要分为:方法区、堆、虚拟机栈、本地方法栈、程序计数器五个区域。
上方有两个区域我们一般不太会关注,这个是偏向底层的,一个是程序计数器,一个是本地方法栈。
- 程序计数器:这个JDK中提供的一个小组件,主要是记录我们程序执行的位置,因为cpu执行时需要不断的去切换线程的执行,如果我们线程当前丧失了cup的执行权他就会暂停,如果获取到了执行权就会继续。那么他是如何知道上一次是执行到哪一步呢?程序计数器主要就是做这件事情。他会把当前的线程记录再内存中。
- 本地方法栈:这个指的是调用一些操作系统提供的函数,这些方法我们叫做本地方法。
这两个主要都偏向底层,平时性能测试中都接触不到。那么我们平时工作中接触最多的主要是方法区、堆、虚拟机栈。我们将这个区域又细分了一些,整理了这张图。如下:
我们将堆用到的内存叫做堆内存,虚拟机栈用到的内存叫做栈内存,方法区用的的叫做永久代。我们再上方图中可以看到,每一个区域都会有相关的参数来进行控制。
比如我们的栈内存,他用到的是 -Xss,堆内存用到的是 -Xms, -Xmx,永久代用到的是 -XX:PermiSize、-XX:MaxPermSize。
从上方图中我们可以看到堆内存所占用的内存是最多的,再堆内存中又细分了两个区域,一个是New generation、Old generation。我们叫做新生代和老年代。
我们对上方图中有了一个初步的了解后,下面我们来整体的看一下这些区域他们主要是做什么的,到底存了哪些数据呢?
栈内存
栈内存的特点:
- 线程私有
- 栈内存生命周期和线程相同
这里指的程序每创建一个线程,就会自动分配一个独立的栈内存,内存之间不能共享,互不干扰,当我们线程运行结束之后,栈内存就会被回收
- 栈内存主要存放内容:
基本数据类型(int、char、float、double、bool、long、short、bit),八大类型为栈内存,其余的都为堆内存。
对象的引用,指定了对象在堆内存中的起始位置
通过 -Xss参数配置
了解了上方栈内存的特点后,我们就知道栈内存的配置不能配太大,因为每个线程他的内存都是独立的,如果说内存分配太大,那么他所能运行的线程就会变少,当然我们也不能配置太少,如果内存太小的话,会导致内存不够用,从而导致程序报错,通常我们配置的都是1024,一兆。
堆内存
我们来看上方的堆内存图,我们来了解一下,一个对象它在创建的时候,在堆内存中究竟是如何扭转的呢?
1、首先我们代码中创建对象,对象会在新生代中,会进入到Eden(生成区)。那么我们程序中会创建非常多的对象,当Eden空间不足时,新生代会进行一次扫描,将Eden和S1区域中有用的对象(一开始S1区域没有数据,也会进行扫描),全部放入S0中,然后将Eden 和 S1中没用的对象全部清空,现在S0中全部放的都是有用的对象
2、假设过一段事情,Eden区域又满了,因为之前S1被清空了,那么此时会扫描Eden和S0区域,然后将Eden和S0区域中有用的对象全部都放到S1中,同样将没用的对象都清空。
3、内存满了又会重复上面的步骤,循环进行,S0或者S1会有一方被清空之后放有用的内存。
那么内存中是如何判断将内存在新生代变更成老年代呢?主要有如下几点:
- 当新生代内存不足时,会将新生代的内存放到老年代中
- 当一个内存,在新生代中被清除了15次,改内存仍然在新生代中,就会将其内存放到老年代
- 内存自身特别大,会默认不适合放在新生代中,如我们设置的新生代内存为1M,但是该内存大小为3M,那么系统会将其放到老年代。
堆内存-heap
- 堆内存是JVM空间中最大的区域
- 所有线程共享堆
- 所有数组以及内存对象的实例都在此区域分配
- 堆内存的大小通过参数进行配置
- Xmx:最大堆内存
- Xms:最小堆内存
老年代内存通常都不会出现内存满的情况,如果说老年代内存满了,JVM会进行一次新生代+老年代整体的扫描,叫做垃圾回收。
堆内存的构成
- 新生代:包含三块区域,eden、from Space(s0) 、to Spece(s1)
- 老年代:old gen
object test = New Objetc()
其中,test存放在堆内存中, New Objetc() 存放在堆内存中,变量test是 Objetc() 对象的引用,test上存放了 Objetc()对象占用内存的起始地址。
永久代
- 永久代也叫做(Method Area)
- 各线程共享,主方法区要存放类信息,常量、静态变量,如
public static int a = 10
,设置静态变量之后该内存则放在永久代中 - 垃圾回收行为比较少见,一般来说如果出现内存空间不足的情况,通常都是JVM配置的空间太少了,自身不够用导致的。
JVM结构总结
- 年轻代 = eden + survivor
- survivor = from Space(s0) + to Spece(s1)
- 年轻代 = eden + from Space(s0) + to Spece(s1)
- 堆内存 = 年轻代 + 老年代
- 堆内存 = eden + from Space(s0) + to Spece(s1)+ 老年代
Java 8 新变化
java8从JVM移除了PermGen(永久代),使用Metaspace(元空间)来替代永久代
Metaspace不存在JVM中,而是存在本地内存中
配置元空间初始值和最大值参数:
-XX:MetaspaceSize = 64m
-XX: MAXMetaspace = 64m
垃圾回收
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
首先我们在垃圾回收中,有分为两种,一种是YoungGC,一种是FullGC。新生代引发的GC叫做YoungGC,老年代引发的GC,叫做FullGC。FullGC会引起整个JVM用户线程暂停,待垃圾回收完毕之后,才进行运行。
对象存活状态
首先,我们在JVM内存满GC的时候,出现GC的时候就会进行一次扫描,也称垃圾回收,那么我们如何找到垃圾回收呢?我们所谓的垃圾回收就是找到存活的对象和死去的对象,那么我们如何分别对象存活的状态呢?下面我们来看一下。
-
确定对象“死去”还是“存活”
在早期的时候,JVM会使用一种算法,叫做引用计数算法
。假设我们有一个对象a被引用一次,那么会被计数1次,被两个对象引用,则计数2次,但是这个算法有个缺点就是他没有办法处理相互引用的对象。假设我们有两个对象A和B,他们之前相互引用对方,没有被其他对象引用,那么他们两个就是没用的对象,除了他们自己根本就没有被引用。但是按照引用计数算法,他们的计数是1,无法被垃圾回收。而 引用计数算法无法处理该问题,因此后面又出了一个根搜索算法(GC Roots)
根搜索算法
根搜索算法,我们之前有说过栈内存中存放的都是一些基础数据,还存了一些对象的引用。那么跟搜索算法它会在扫描期间如果发现有对象栈中的某一个变量所指向的,那么他就会定义成根对象。
永久代的垃圾回收
- 永久代回收“性价比”比较低
- 主要回收
1、废弃的常量
2、无用的类
1)类的所有实例都已被回收
2)加载该类的ClassLoader已被回收
3)该类的Class对象没有在任何地方被引用
堆垃圾回收算法
- 标记-清除算法(Mark-Swap)
特点:
1、分为“标记”和“清楚”两个阶段
2、标记完成之后,统一进行回收
缺点:
1、效率,标记和清楚过程效率都不高
2、空间,标记清楚后会产生大量不连续的内存碎片
- 复制算法
所谓的复制算法,即将内存平均分成A区、B区两块,系统会将A区的存活内存复制到B区中,然后将原先的A区内存清空,算法图解如下:
特点:
1、内存分为相同的两块
2、当一块内存用完,将存活对象复制到另一块中,原内存一次性清理掉
3、复制时按照顺序分配内存,无内存碎片问题
4、新生代使用此算法
缺点:
1、将内存分为两块,利用率较低
- 标记-压缩算法
前面我们说了标记清楚算法,它最大的问题就是会产生内存碎片,那么后面就有人根据标记清楚算法进行优化,新增了一个压缩功能,所以叫做标记-压缩算法,它最大的区别就在于可以做压缩。
特点:
1、先对存活的对象进行标记
2、让所有存活的对象向一边移动
3、清理掉存活对象边界外的所有内存
注:老年代使用此算法
- 分代收集算法
1、当代的商业虚拟机都采用“分代收集”
2、根据对象的存活周期的不同将内存划分成几块,一般JAVA堆分为新生代和老年代
3、新生代采用复制算法
4、老年代采用标记-压缩算法
垃圾收集器
上面我们主要讲了一些算法,但是我们光有算法是不行的,是不是要把它实现起来呢?那么就有了垃圾收集器。
- 垃圾收集器是内存回收算法的具体实现
- 没有完美的收集器
- JVM不同的区域可以采用不用的垃圾收集器组合,主要有:
1、serial 收集器(串行)
2、 ParNew(并行)
3、CMS收集器(并发)
4、G1(时间优先)
Serial收集器
- 单线程收集器
- 用户线程全部停止(stop the world)
假设我们现在有很多用户再使用登录接口,那么这个时候GC了,在垃圾回收的过程中,所有的登录操作都会被终止,需要等垃圾回收完成之后,才能正常运行。这也是为什么早期的安卓设备会经常出现卡顿的现象,安卓的底层是JAVA,他们也会有虚拟机JVM,他们也需要做GC。 - client模式下,新生代默认收集器
JVM会根据当前你的电脑环境,然后来判断启动的是client模式,还是servier模式。主要根据CPU、内存、操作系统等多个条件综合判断。 - 优点:简单、高效
ParNew收集器 - 并行收集器,Serial收集器的多线程版本
- Server模式下JVM默认的新生代收集器
- 默认开启垃圾回收线程与cpu核数一致
CMS收集器
- 并发收集器(ConcurrentMarkSweep)
- 采用了标记-清楚、标记-压缩算法
- 并发收集、低停顿
缺点:
1、消耗cpu
2、会产生内存碎片
3、浮动垃圾(Concurrent Mode Failure)
以上是关于JVM内存结构和垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章