Java内存

試著忘記壹切 提交于 2020-02-06 16:06:36

Java 虚拟机在执行 Java 程序的过程中会把他所管理的内存划分为若干个不同的数据区域。Java 虚拟机规范将 JVM 所管理的内存分为以下几个运行时数据区:程序计数器、Java 虚拟机栈、本地方法栈、Java 堆、方法区。

一,内存区域划分

  1.线程共享区域:

    (1)Java堆(对象实例),GC的主要区域,会出现OutOfMemoryError

    (2)方法区(加载的类信息,常量,静态变量,即时编译器编译后的代码)会出现OutOfMemoryError

  2.线程私有区域:

    (1)虚拟机栈(操作数栈,动态链接,方法返回地址,局部变量) 

      用于支持虚拟机进行方法方法调用和方法执行的数据结构。生命周期与线程相同,每个方法执行时会创建一个栈帧并入栈,对于执行引擎,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,所关联的方法称为当前方法。

      如果申请的栈深度大于虚拟机允许的栈深度则抛出StackOutflowError

      如果在动态扩展时,无法申请到足够的内存则抛出OutOfMemoryError

      单线程:无论是栈空间太大,还是虚拟机内存太小,抛出的都是StackOutflowError

      多线程:抛出OutOfMemoryError

        局部变量表:是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。最小单位为变量槽(Slot)大小为32字节,对于超过32字节的变量,使用连续的槽进行存储

        操作数栈:又被称为操作栈,最大深度在编译时确定,32位数据类型占用容量为1,64为数据类型占用容量为2。在方法执行过程中,根据字节码指令,进行入栈出栈操作。

        动态连接:栈帧中存在一个指向运行时常量池的引用,对常量池中符号的引用如果在类加载阶段转换为直接引用则称为静态解析。在每一次运行期间转换为直接引用,称为动态连接。

        方法返回地址:方法正常退出时,调用者的计数器指则为返回地址。方法异常退出时,返回地址是通过异常处理器来确定的。

    (2)本地方法栈(和虚拟机栈概念几乎相同,不同的是服务于本地操作系统(Native)方法)

    (3)程序计数器:一块内存较小的内存空间,是当前字节码执行的行号指示器,根据该指示器来确定下一条需要执行的指令。

  <注>Object obj = new Object();这段代码的执行会涉及到Java栈,Java堆,方法区三个重要的内存区域。假设该语句出现在方法体中,obj会作为引用类型存储到Java虚拟机栈的局部变量表中,实例对象则保存在java堆中。而该对象的地址信息(如对象类型,父类,实现的接口等)则保存在方法区中。

 

二.垃圾回收(GC)

  垃圾回收(GC)指回收掉没用的内存数据,将空间释放出来以便存储新的数据。垃圾回收可以有效的防止内存泄漏。

  1.GC类型

    根据不同区域发生的GC,可主要将GC分为YGC和FullGC

      YGC:当新生代满了之后,会触发minor collection(YGC)。

      FullGC:当整个堆内存达到阈值时,会触发major collection(Full GC),导致整个heap的回收。(应尽量避免出现FullGC,因为该GC收集时间较长,频繁的FullGC会导致应用性能受到严重影响)

  2.Java堆内存

    新生代

      Eden区:新生区,新创建的对象都存储在该区域

      Survior区:幸存区,主要存储既未到达进入老年代的条件又位于To Survior的对象 Survior区由两部分组成

        From区:新生代发生minorGC时,该区域内存会根据年龄决定去To区还是老年代

        To区:新生代发生minorGC时,该区域内存数据不会被清除

    老年代:年龄达到老年的对象数据会从From Survior区进入该区域

    永久代(java8之后叫做元空间,不属于堆):主要存储的是JVM运行时需要的类和方法,元空间的类的对象是在进行FullGC时才进行垃圾收集

  3.内存申请过程

    JVM会试图为java对象在Eden中初始化一块内存区域

    当Eden空间足够时,内存申请结束,否则进入下一步

    JVM试图释放在Eden中所有的不活跃对象(YGC),释放后若仍不足以放入新对象,则试图将Eden中部分活跃对象放入Survivor的To区,

    若JVM的From Survivor区内存不足,则JVM会判断对象的年龄,然后选择性的将对象移动到To Survivor区或老年代。

    如果老年代的内存不足,JVM会在老年代进行major collection(FULL GC)

    完成垃圾回收后,如果Survivor和老年代仍然无法存放eden区中的数据,导致JVM无法在Eden区为新建对象分配内存区域,则出现OutOfMemeryError

  4.内存的衰老过程

    新建对象的内存都分配自eden区,MinorCollection(YGC)的过程就是将eden中的对象移动到空闲的To Survivor区中,将From survivor区中经历过一定次数YGC(次数可以i通过参数配置)的对象移动到老年代

  5.常用的垃圾回收方法

    是否进行垃圾回收,需要知道一个对象是否可用。

    (1)引用计数算法:每当又一个地方引用一个对象时,计数器就加1,当引用失效时,计数器减一,任何时刻计数器为0的对象就是不再被使用的对象(不能解决循环引用的问题)

    (2)可达性分析算法:用于判断对象是否存活,基本思想是通过GC Roots对象作为根节点,从这些节点向下搜索,搜索的路径称为引用链,当一个对象到GC Roots没有任何引用链时就认为对象属于不可达,证明此对象时不可用的。

    可作为GC Roots的对象:

      虚拟机栈中引用的对象

      方法区中类静态属性引用的对象或常量引用的对象。

      本地方法栈中JNI(Native方法)引用对象

    (3)无论是通过引用计数法,还是可达性分析算法,都是为了判断对象是否存在引用。而引用又分为强引用和弱引用,软引用,虚引用

        强引用:代码中普遍存在的,类似Object obj = new Object(),垃圾回收器永远不会回收这部分对象

        软引用:用来描述一些有用但是非必需的对象,在内存即将发生内存溢出时,会回收这部分对象

        弱引用:也是用来描述非必需的对象,但是比软引用更弱一些,被弱引用关联的对象只能生存道下一次垃圾回收发生之前

        虚引用:也叫幽灵引用或幻影引用,是最弱的一种引用关系,不会影响到垃圾回收

    (4)垃圾算法

        标记-清除法:最基础的收集算法,分为标记和清除两个阶段,首先标出所有需要回收的对象,在标记完成之后统一回收标记对象。这种算法会产生大量不连续的内存碎片,堆内存使用率低

        标记-整理法:首先标出所有需要回收的对象,然后将这部分对象清除,再将所有存活的对象向一端移动,这种算法不会出现内存碎片

        分代收集算法:将内存划分为多块,然后再根据不同区域的特点选择使用不同的垃圾回收算法例如新生代每次垃圾回收后只会有少量对象存活,则使用复制算法,而老年代的存活率高,则可以使用标记清理或者标记整理法

  6.常用垃圾收集器

    (1)串行收集器(Seiral Collector):最简单的垃圾收集器,基本上都是涉及单核环境下工作,几乎不会使用该收集器,因为它在收集时会暂停整个应用的运行。

      使用方法:-XX:+UseSerialGC

    (2)并行/吞吐优先收集器(Parallel/Throughput Collector):这是JVM默认的收集器,跟它的名字显示的一样,它最大的优点就是使用多个线程来扫描和压缩堆,缺点是在minorGC和fullGC时会暂停应用的运行,并行收集器适合可以容忍程序停滞的环境使用,它占用较低的CPU因而可以提高应用的吞吐

      使用方法:-XX:UseParallelGC

    (3)CMS收集器(CMS Collector):CMS使用的是并发的标记与清除,这个算法使用多个线程并发的扫描堆,标记不使用的对象,然后清除它们回收内存。

      使用方法:-XX:UseConcMarkSweepGC,此时可同时使用-XX:UseParNewGC将并行收集作用于年轻代,新的JVM自动打开这个配置

      原理:并发标记清除算法

      流程:初始化标记->并发标记->并发预清理->重新标记->并发清理->并发重置

      优点:减少了回收的停顿时间,回收效率高

      缺点:产生的空间碎片多,需要更多的CPU资源,需要更大的堆空间,降低了堆空间的利用率

      使用场景:程序对停顿比较敏感,并且应用程序运行时可以提供更大的内存和更多的CPU

    (4)G1收集器(Garbage First Collector):G1收集器适用于堆内存大于4G的JVM,它会将堆分成多个区域,大小从1MB-32MB,并使用多个后台线程来扫描这些区域,优先会扫描最多垃圾的区域

      使用方法:-XX:+UseG1GC

          Java8和G1收集器:G1收集器在Java8上最好的优化是String去重,String对象和它内部使用的char[]数组会占用比较多的内存,因为优化过的G1收集器会把重复的String对象指向同一个char[]数组,避免多个副本存在堆里

      使用方法:-XX:UseStringDeduplication

      原理:标记整理算法

      流程:初始标记->并发标记->最终标记->筛选回收

      优点:并行于并发,分带收集,空间整合,可预测停顿

      缺点:需要更大的内存和更多的CPU

      使用场景:对堆内存的使用率要求较高,对停顿时间容忍性较低,并且堆内存足够大

    (5)G1和CMS的区别

      CMS是以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现,比较占用CPU资源,容易产生内存碎片

      G1是面向服务器端的垃圾收集区,是JDK9的默认收集器,基于标记-整理算法实现,可利用多核,多CPU,保留分带,实现可预测可控停顿

  7.一次完整的GC

    当新生代发生minorGC时,Eden区中存活的对象都被复制到To Survivor区,然后From Survivor区中的对象根据年龄决定区老年代还是To survivor区,然后清空Eden区和From Survivor区,最后再交换From区和To区。

    当老年代中如果对象到达了回收的年龄,则会根据对象的垃圾回收器使用对应的算法进行内存Full GC。

    永久代(元空间)在进行FullGC时进行垃圾回收

三.JVM内存模型

  1.内存屏障:为了保障顺序和可见性的一条CPU指令

  2.重排序:为了提高性能,编译器和处理器会对执行语句进行重排序

  3.happen-before:操作间执行的顺序关系,有些操作优先于有些操作发生

  4.主内存:共享变量存储的区域

  5.工作内存:每个线程copy的本地内存,存储了该线程已读/已写的共享变量的副本

 

四.JVM常用参数

  -server -Xms512m -Xmx512m -Xss1024k

  -XX:PermSize=256m -XX:MaxPermSize=512m -XX:MaxTenuringThreshold=20

  -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly

  server模式启动

  -Xms512m:最小堆内存512M

  -Xmx512m:最大堆内存512M

  -Xss1024k:每个线程栈空间1M

  -XX:PermSize=256M:永久代256M

  -XX:MaxPermSize=512m:永久代最大512M

  -XX:MaxTenuringThreshold=20:最大转为老年代检查次数20

  XX:CMSInitiatingOccupancyFraction:CMS回收开启时间,内存占用80%

 

五.JVM常用工具

  https://www.cnblogs.com/ymqj520/p/11378498.html

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!