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。
标记整理算法:也分为两个阶段,首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。

posted on 2020-10-24 16:25  liangxiaolong  阅读(74)  评论(0编辑  收藏  举报