一、Heap 堆
1、定义
1:此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例在这里分配内存
2:一个JVM实例只存在一个堆内存
3:Java堆区在JVM启动的时候即被创建,其空间大小也就确定了,是JVM管理的最大一块内存空间
4:《Java虚拟机规范》规定堆可以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的
2、特点
- 是线程共享的,堆中对象都需要考虑线程安全问题,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)
- 有垃圾回收机制
3、堆内存溢出
Java.lang.OutOfMemoryError: Java heap space
对象不再被使用才能被回收,如果对象一直被使用,并且数量越来越多,那么可能就导致溢出,可以用 -Xmx size控制堆空间大小
4、堆内存诊断
- jps工具:查看当前系统中有哪些Java进程
- jmap工具:查看某一时刻堆内存使用情况
- jconsole:图形界面,多功能的监测工具,可以连续监控
新创建的对象会使用Eden Space
案例
- 立即回收之后,内存占用仍然很高
使用 jvisualvm ,然后点击堆 dump,将那一时刻堆的内存信息截取下来
再查找保留大小最大的对象
5、堆内存细分
1:Java 7及之前堆内存逻辑上分为三部分:新生代+养老代+永久代
新生代又被划分为Eden区和Survivor区
2:Java8及之后堆内存逻分为三部分:新生代+养老代+元空间
新生代又被划分为Eden区和Survivor0空间和Survivor1空间(有时也叫做from区和to区)
3:约定:新生区<=>新生代<=>年轻代
养老区<=>老年代<=>老年区
永久代<=>永久区
5、堆空间大小的设置
-Xms用于表示堆区的起始内存,等价于-XX:InitialHeapSize
-X是jvm的运行参数
ms 是memory start缩写
-Xmx用于表示堆区的最大内存,等价于-XX:MaxHeapSize
一旦堆区中的内存大小超过超过-Xmx所指定的最大内存时,将会抛出OOM
默认堆空间大小
初始内存大小,物理电脑内存大小 / 64
最大内存大小,物理电脑内存大小 / 4
开发中建议将初始堆内存和最大的堆内存设置成相同的值
查看设置的参数:方式一:jps jstat -gc 进程id
方式二:-XX:+PrintGCDetails
6、年轻代与老年代
存储在JVM中的对象可以被划分为两类:
一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
另外一类对象的生命周期却非常长,在某些极端情况下还能够与JVM的生命周期保持一致
配置新生代与老年代在堆结构的占比
默认-XX:NewRatio = 2,表示新生代占1,老年代占2,新生代占整个堆的1/3,NewRatio表示老年代比新生代
在hotspot中Eden空间和另外两个Survivor空间空间缺省所占比例是8:1:1,可以使用-XX:SurvivorRatio=8来调节该比例
查看新生代和老年代比例:
jps jinfo -flag NewRatio 进程id
几乎所有的Java对象都是在Eden区被new出来的
可以使用-Xmn设置新生代最大内存大小(一般使用默认值)
7、对象分配过程
1.new出来的对象先放Eden区
2.当Eden填满之后,程序又需要创建对象,JVM的垃圾收集器将对Eden区进行垃圾回收(Minor GC或者YGC),将剩余对象移动到Survivor0区,然后再加载新的对象到Eden区
3.如果又触发垃圾回收,Eden区没有被回收的进入Survivor1区,Survivor0区没被回收的也放进Survivor1区
4.如果再次经历垃圾回收,此时会,此时会重新放回Survivor0区,接着再区Survivor1区
5.默认15次之后放到养老区,可以用参数-XX:MaxTenuringThreshold=N进行设置
6.当养老区内存不足时候,再次触发Major GC,进行养老区的内存清理
7.若养老区执行了Major GC之后发现依然无法进行对象的保存,就会OOM
注意,survivor区满了,不会触发YGC;
如果进行YGC之后,Eden区放不了,会看Old区是否能放下,如果可以就放下,如果不可以,先进行FGC,再看Old区是否可以,如果不可以就OOM;
Eden区对象进入Survivor区时候,如果Survivor满了,就会直接进入老年代
8、Minor GC,Major GC与Full GC的比较
针对HotSpot VM的实现,它里面的GC按照回收区域分为两大类型,部分收集(Partial GC),一种是整堆收集(Full GC)
部分收集分为:
1新生代收集:只是新生代的垃圾收集
2老年代收集:只是老年代的垃圾收集,目前,只有cms gc会有单独收集老年代的行为
3混合收集:收集整个新生代以及部分老年代的垃圾收集,目前只有G1 GC会有这种行为
整堆收集:收集整个Java堆和方法区的垃圾收集
9、堆空间分代思想
为什么需要把Java堆分代?不分代就不能正常工作了么?
经研究,不同对象的生命周期不同,70%-99%的对象都是临时对象
10、什么是TLAB
1:从内存模型而不是垃圾收集的角度,对Eden区继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内
2:默认情况下,TLAB空间的内存非常小,仅仅占有整个Eden空间的1%,可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小
二、方法区
1、组成
实现:1.6版本用PermGen 永久代,1.8版本用Metaspace 元空间实现
2、方法区内存溢出
- 1.8以前会导致永久代内存溢出:Java.lang.OutOfMemoryError: PermGen space,可以用 -XX:MaxPermSize=size调节
- 1.8开始会导致元空间内存溢出:Java.lang.OutOfMemoryError: Metaspace,可以用 -XX:MetaspaceSize=size调节
OTS:
jvisualvm工具的使用:可以查看堆空间设置情况等
3、方法区在哪里
<Java虚拟机规范>中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩”,对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开
4、方法区的基本理解
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutofMemoryError: Metaspace,比如当加载大量的第三方jar包时
- 关闭jvm就会释放这个区域的内存
5、设置方法区内存的大小
元数据区大小可以使用参数-XX:MetaspaceSize=**m和-XX:MaxMetaspaceSize指定,默认初始值依赖与平台,windows下,初始值是21M,最大值是-1,表示没有限制
jinfo -flag MetaspaceSize 进程id,可以查看默认的元空间大小
对于一个64位的服务器端JVM来说,其默认的-XX:MetaspaceSize值为21MB,这就是初始的高水位线,一旦触及这个水位线,FullGC就会触发并且卸载没用的类,然后这个高水位线将会被重置,新的高水位线取决于GC后释放了多少元空间,如果释放的空间不足,那么在不超过最大值时,适当提高该值,如果释放空间过多,则适当降低该值
如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次,FullGC会多次调用,为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值
6、方法区存储什么
存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存
类型信息:
1:这个类型的完整有效名称( 全名=包名.类名)
2:这个类型直接父类的完整有效名
3:这个类型的修饰符(public,abstract)
4:这个类型直接接口的一个有序列表,有多个接口会有序存放
域(Field)信息
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
域的相关信息包括:域名称、域类型、域修饰符(public private protected…)
方法信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序
1:方法名称
2:方法的返回类型(或void)
3:方法参数的数量和类型(按顺序)
4:方法的修饰符(public private static。。。)
5:方法的字节码、操作数栈、局部变量表及大小(抽象方法和本地方法除外)
6:异常表(抽象方法和本地方法除外)
7:每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
non-final的类变量
静态变量和类关联在一起,随着类的加载而加载
类变量被类的所有实例共享,即时没有类实例你也可以访问它
运行时常量池
常量池包括各种字面量和对类型、域和方法的符号引用
1:数量值
2:字符串值
3:类引用
4:字段引用
5:方法引用
为什么需要常量池?
Java中的字节码需要数据支持,通常这种数据很大以至于不能直接存到字节码中,可以存储到常量池,这个字节码包含了指向常量池的引用
常量池可以看作一张表,虚拟机指令根据这张表找到要执行的类名,方法名,参数类型,字面量等类型
JVM为每个加载的类型(类或接口)都维护一个常量池,池中的数据项像数组一样,是通过索引访问的
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析才能获得的方法或者字段引用,此时不再是常量池中的符号地址,这里转换为真实地址
7、方法区的演进细节
- 首先明确:只有HotSpot才有永久代,BEA JRockit、IBM J9等来说是不存在永久代的概念的
- HotSpot中方法区的变化:
jdk1.6之前:有永久代,静态变量存放在永久代上
jdk1.7:有永久代,但已经逐步 “去永久代”,字符串常量池、静态变量移除,保存在堆中
jdk1.8及之后:无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆
8、永久代为什么要被元空间替换?
- 随着Java8的到来,类的元数据信息被移到一个与堆不相连的本地内存区域,这个区域叫做元空间,由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间
1:永久代设置空间大小是很难确定的
在某些场景下,如果动态加载类过多,容易产生Perm区的OOM
而元空间和永久代使用本地内存,默认情况下,他的大小仅受本地内存限制
2:对永久代调优比较困难
9、StringTable为什么要调整
因为开发中会大量创建字符串,但是永久代的回收效率很低,在full gc的时候才会触发,会导致永久代内存不足,放到堆里,能及时回收内存
10、方法区的垃圾回收
方法区的垃圾回收主要分为两部分:
常量池中废弃的常量和不再使用的类型
方法区内常量池之中主要存放的两大类常量:字面量和符号引用,字面量如文本字符串,被声明为final的常量值等,而符号引用则属于编译原理方面的概念:包括:
1:类和接口的全限定名
2:字段的名称和描述符
3:方法的名称和描述符
HotSpot虚拟机对于常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收
回收废弃常量与回收Java堆中的对象非常相似
判定一个类型是否属于“不再被使用的类“的条件就比较苛刻了,需要同时满足下面三个条件:
1:该类所有的实例都已经被回收,也就是堆中不存在该类及其任何派生子类的实例
2:加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi,JSP的重加载等,否则通常很难达成
3:该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是”被允许“,而不是和对象一样,没有引用就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息
在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力