zoukankan      html  css  js  c++  java
  • JVM问题

    JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)
    堆区: 存储的单位、堆中存的是对象
    提供所有类实例和数组对象存储区域
    jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
    栈区: 运行时的单位、栈中存的是基本数据类型和堆中对象的引用
    每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
    每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
    程序运行时永远都是在栈中进行的,参数传递时,只存在传递基本类型和对象引用的问题,不会直接传递对象本身。
    方法区:
    又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
    方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
    运行时常量池都分配在 Java 虚拟机的方法区之中
    垃圾回收的起点是一些根对象(java栈, 静态变量, 寄存器...)。而最简单的Java栈就是Java程序执行的main函数。这种回收方式,也是上面提到的“标记-清除”的回收方式

    堆heap : 新生代、老年代、元空间(永久代 jdk8以后被取消)

    PSYoungGen 新生代(年轻代):所有新生的对象首先放在新生代,目标是尽可能快速的收集掉那些生命周期较短的对象。新生代范围三个区:一个Eden区(伊甸园区),两个Survivor区(from和on(幸存区)总有一个是空的,纪录GC操作之后存活下来的对象,将form区的对象存到空的on区,(反之见on区的对象存到空的form区)达到一定次数(默认15次)之后还存活的,进入老年代)
    老年代:在新生代记录了N次垃圾回收后仍然存活的对象,就会被放到老年代中,因此老年代中存放的是一下生命周期比较长的对象。
    永久代 PermGen(方法区):永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现。方法区也是所有线程共享。主要用于存储类的信息、常量池、静态文件、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”
    元空间MetaSpace :


    为什么要用Metaspace替代方法区
    随着动态类加载的情况越来越多,这块内存变得不太可控,如果设置小了,系统运行过程中就容易出现内存溢出,设置大了又浪费内存。
    1、字符串存在永久代中,容易出现性能问题和内存溢出。
    2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
    3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
    4、Oracle 可能会将HotSpot 与 JRockit 合二为一。

    对于Java8, HotSpots取消了永久代,那么是不是也就没有方法区了呢?
    当然不是,方法区是一个规范,规范没变,它就一直在。那么取代永久代的就是元空间。它可永久代有什么不同的?存储位置不同,永久代物理是是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存;存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。


    对象引用类型分为强引用、软引用、弱引用和虚引用。
    1、强引用:就是我们一般声明对象是时虚拟机生成的引用,强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,则不会被垃圾回收
    2、软引用:软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生OutOfMemory时,肯定是没有软引用存在的。
    3、弱引用:弱引用与软引用类似,都是作为缓存来使用。但与软引用不同,弱引用在进行垃圾回收时,是一定会被回收掉的,因此其生命周期只存在于一个垃圾回收周期内。
    4、虚引用(PhantomReference):此引用的对象,在触发GC时直接被GC(用的最少,类似没有引用,主要用于记录对象的销毁)
    强引用不用说,我们系统一般在使用时都是用的强引用。而“软引用”和“弱引用”比较少见。他们一般被作为缓存使用,而且一般是在内存大小比较受限的情况下做为缓存。因为如果内存足够大的话,可以直接使用强引用作为缓存即可,同时可控性更高。因而,他们常见的是被使用在桌面应用系统的缓存。

    思考:Java中的对象什么情况下会被回收(GC)?被谁回收?如何确定这个对象是否要被回收了。
    Scavenge GC:一般情况下,新对象生成,并且在Eden申请空间失败时,会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
    Full GC:对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。以下原因可能导致Full GC:
    1、老年代被写满
    2、永久代被写满
    3、System.gc()被显示调用
    4、上一次GC之后heap的各域分配策略动态变化

    利用GC的可达性分析算法,定义一些对象为GC Roots,从GC Roots出发的引用链向下寻找,当某个对象不存在着引用时,那么虚拟机就认为该对象可以被回收了;【可以作为GC Roots的对象有:虚拟机栈(栈桢中的本地变量表)中的引用的对象;方法区中的类静态属性引用的对象;方法区中的常量引用的对象;方法区中的类静态属性引用的对象;方法区中的常量引用的对象;本地方法栈中JNI(Native方法)的引用的对象】 即没有任何引用指向的时候,会被当做垃圾对象,被GC
    被java虚拟机JVM的GC系统回收
    当对象被可达性分析算法判断为“垃圾”的时候,还不会被立刻回收,会被第一次标记并进行一次筛选,判断该对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。(即意味着直接回收)。

    触发GC操作(GC系统触发时会对内存中的对象进行可达性分析,就是检测是否还可以访问到此对象,假如不能通过任何引用访问此对象,这个对象就会被标识为垃圾)
    1)手动GC,手动设置对象值为null
    System.gc();
    2)自动GC(满足GC条件时或者说内存使用达到一定的GC启动标准)
    当小对象分配在栈内存上时,不需要GC也会被回收处理
    通过jvm参数检测是否触发GC :-XX:+PrintGCDetails
    finalize方法会在对象GC回收之前执行,可以对对象的回收进行监控

    新生代一般用复制算法,老年代一般用标记清除或标记整理算法

    判断对象可以GC:
    1、可达性分析:定义一些对象为GC Roots,从GC Roots出发的引用链向下寻找,当某个对象不存在着引用时,那么虚拟机就认为该对象可以被回收了;【可以作为GC Roots的对象有:虚拟机栈(栈桢中的本地变量表)中的引用的对象;方法区中的类静态属性引用的对象;方法区中的常量引用的对象;方法区中的类静态属性引用的对象;方法区中的常量引用的对象;本地方法栈中JNI(Native方法)的引用的对象】
    2、引用计数:对象有一个引用,就增加一个计数;删除一个引用就减少一个计数,垃圾回收时,只回收计数为0的对象。无法处理循环引用的问题。

    垃圾回收算法:
    1、标记清除:算法执行分两阶段,第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
    2、复制:此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
    3、标记整理:此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
    4、G1算法:回收步骤:
    4.1、初始标记
    4.2、并发标记
    4.3、最终标记暂停
    4.4、存活对象计算及清除

    常见配置汇总
    堆设置
    -Xms:初始堆大小
    -Xmx:最大堆大小
    -XX:NewSize=n:设置年轻代大小
    -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
    -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
    -XX:MaxPermSize=n:设置持久代大小

    收集器设置
    -XX:+UseSerialGC:设置串行收集器
    -XX:+UseParallelGC:设置并行收集器
    -XX:+UseParalledlOldGC:设置并行年老代收集器
    -XX:+UseConcMarkSweepGC:设置并发收集器

    垃圾回收统计信息
    -XX:+PrintGC
    -XX:+PrintGCDetails
    -XX:+PrintGCTimeStamps
    -Xloggc:filename

    并行收集器设置
    -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
    -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
    -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

    并发收集器设置
    -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
    -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。


    调优总结
    1、年轻代大小选择
    响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
    吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
    2、年老代大小选择
    响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
    2.1、并发垃圾收集信息
    2.2、持久代并发收集次数
    2.3、传统GC信息
    2.4、花在年轻代和年老代回收上的时间比例,减少年轻代和年老代花费的时间,一般会提高应用的效率
    3、吞吐量优先的应用
    一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
    4、较小堆引起的碎片问题
    因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

    1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
    2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

    JVM调优工具:
    Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。
    JProfiler:商业软件,需要付费。功能强大。
    VisualVM:JDK自带,功能强大,与JProfiler类似。推荐。

    如何调优?
    观察内存释放情况、集合类检查、观察树
    堆信息查看:
    可查看堆空间大小分配(年轻代、年老代、持久代分配)
    提供即时的垃圾回收功能
    垃圾监控(长时间监控回收情况)

    查看堆内类、对象信息查看:数量、类型等:

    对象引用情况查看:

    有了堆信息查看方面的功能,我们一般可以顺利解决以下问题:

    年老代年轻代大小划分是否合理
    内存泄漏
    垃圾回收算法设置是否合理

    内存泄漏检查
    内存泄漏是比较常见的问题,而且解决方法也比较通用,这里可以重点说一下,而线程、热点方面的问题则是具体问题具体分析了。
    内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。
    内存泄漏对系统危害比较大,因为他可以直接导致系统的崩溃。
    需要区别一下,内存泄漏和系统超负荷两者是有区别的,虽然可能导致的最终结果是一样的。内存泄漏是用完的资源没有回收引起错误,而系统超负荷则是系统确实没有那么多资源可以分配了(其他的资源都在使用)。
    年老代堆空间被占满
    异常: java.lang.OutOfMemoryError: Java heap space
    说明:

    这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。
    如上图所示,这是非常典型的内存泄漏的垃圾回收情况图。所有峰值部分都是一次垃圾回收点,所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有谷底的点,可以发现一条由底到高的线,这说明,随时间的推移,系统的堆空间被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄漏。(上面的图仅供示例,在实际情况下收集数据的时间需要更长,比如几个小时或者几天)
    解决:
    这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。
    持久代被占满
    异常:java.lang.OutOfMemoryError: PermGen space
    说明:
    Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。
    更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有N个classLoader那么他将会被加载N次。因此,某些情况下,这个问题基本视为无解。当然,存在大量classLoader和大量反射类的情况其实也不多。
    解决:
    1、-XX:MaxPermSize=16m
    2、换用JDK。比如JRocket。
    堆栈溢出
    异常:java.lang.StackOverflowError
    说明:这个就不多说了,一般就是递归没返回,或者循环调用造成
    线程堆栈满
    异常:Fatal: Stack size too small
    说明:java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。
    解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。
    系统内存被占满
    异常:java.lang.OutOfMemoryError: unable to create new native thread
    说明:
    这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在Java堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。
    分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。
    解决:
    1、重新设计系统减少线程数量。
    2、线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。


    新生代一般采用复制算法
    复制算法:将内存按容量划分为两块,每次只使用其中一块,当这一块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉,这样使得每次都是对半内存区回收,也不用考虑内存碎片问题。缺点:需要两倍的内存空间。

    老年代采用标记清除算法、标记整理算法
    标记清除算法:GC分为两个阶段,首先标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。同时产生不连续的内存碎片。碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得已再次出发GC。
    标记整理算法:也分为两个阶段,首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。

  • 相关阅读:
    ASP.NET CORE 使用Consul实现服务治理与健康检查(2)——源码篇
    ASP.NET CORE 使用Consul实现服务治理与健康检查(1)——概念篇
    Asp.Net Core 单元测试正确姿势
    如何通过 Docker 部署 Logstash 同步 Mysql 数据库数据到 ElasticSearch
    Asp.Net Core2.2 源码阅读系列——控制台日志源码解析
    使用VS Code 开发.NET CORE 程序指南
    .NetCore下ES查询驱动 PlainElastic .Net 升级官方驱动 Elasticsearch .Net
    重新认识 async/await 语法糖
    EF添加
    EF修改部分字段
  • 原文地址:https://www.cnblogs.com/liangxr/p/13869880.html
Copyright © 2011-2022 走看看