二、深入剖析JVM内存管理

2.1 Java代码到底是如何运行起来的?

1、Mall.java –>javac –> Mall.class –> java Mall (jvm进程,也就是一个jvm虚拟机)

2、Mall.java –>javac–>Mall.class –>Mall.jar –> java -jar Mall.jar

3、Mall.java –> javac –> Mall.class –>Mall.war –> Tomcat –> startup.sh –> org.apache.catalina.startup.Bootstrap (jvm进程,也就是一个jvm虚拟机)

其实运行起来一个Java程序,都是通过D:\dev\Java\jdk1.8.0_251\bin\java 启动一个JVM虚拟机,在虚拟机里面运行Mall.class字节码文件;

在这里插入图片描述

总结:

java源文件通过javac命令转成java字节码文件,再通过java命令运行起来,JVM是用于屏蔽掉底层操作系统之间的差异,这里不同操作系统所装载的jdk是不同的,jdk中包含jvm

这里面的其他语言,如Groovy、Scala、Kotlin也是编译后为字节码,再通过JVM虚拟机处理。

2.2画一下JVM整个运行原理图?☆

在这里插入图片描述

2.3 请介绍一下JVM的内存结构划分?

加载进来的.class字节码文件、代码执行创建的对象、代码执行调用方法,方法中有变量等数据需要一个地方存放,所以JVM划分出了几个区域,用于存放这些信息;hotspot

在这里插入图片描述
在JDK1.8之前,元空间就是原来的方法区(永久代);

比如new User(),那么User这个变量在虚拟机栈中存储为局部变量,但是其值是在堆中存储的

程序计数器:表明代码执行到哪一行

当类加载后,Class文件首先会存储在元空间中,然后运行时才会涉及其他几个空间部分

2.4 JVM哪些区域是线程私有的,哪些区域是线程共享的?☆

1、堆、元空间(方法区)是线程共享的;

2、其他区域是线程私有的;

在这里插入图片描述
线程私有的区域如虚拟机栈、本地方法栈、程序计数器,这些就不存在线程安全的问题,每个线程之间这些区域是隔离的,不共享变量,就不会导致冲突,没有安全问题

举例:两个线程对应就有两个虚拟机栈,一一对应。无论有几个线程,堆和元空间都是公用的一个

2.5 从JVM角度剖析如下程序代码如何执行?

在这里插入图片描述
在这里插入图片描述

Config类在new的时候,对象存在堆中,该类的信息是在元空间里的

2.6 JVM运行时数据区 程序计数器 的特点及作用?

1、程序计数器是一块较小的内存空间,几乎可以忽略;

2、是当前线程所执行的字节码的行号指示器;

3、Java多线程执行时,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响;

4、该区域是“线程私有”的内存,每个线程独立存储;

5、该区域不存在OutOfMemoryError;

6、无GC回收;

总结:程序计数器是在线程产生的时候存在,在线程结束(销毁时候)消失,不需要垃圾回收(GC)

2.7 JVM运行时数据区 虚拟机栈的特点及作用?

1、线程私有;

2、方法执行会创建栈帧,存储局部变量表等信息;

3、方法执行入虚拟机栈,方法执行完出虚拟机栈;(先进后出)

4、【深度过长】 栈深度大于虚拟机所允许StackOverflowError;

5、栈需扩展而无法申请空间OutOfMemoryError(比较少见);hotspot虚拟机没有;

6、栈里面运行方法,存放方法的局部变量名,变量名所指向的值(常量值、对象值等)都存放到堆上的;

7、栈一般都不设置大小,栈所占的空间其实很小,可以通过-Xss1M进行设置,如果不设置默认为1M;

8、随线程而生,随线程而灭;

9、该区域不会有GC回收;

解释:OutOfMemoryError一般少见,比如写个死循环不断的创建线程,当创建到一定程度,无法再继续给空间到新线程时,每个线程都会生成一个线程栈,每个线程栈会占用一定的内存区域,从而导致超出,会把会报此错误。

如下,死循环中不断创建新的线程,而每个线程又不会结束,即不会销毁,从而导致大量内存占用。

在这里插入图片描述

解释:递归调用时,有时会出现StackOverflowError,如下案例:

在这里插入图片描述
输出结果:

解释栈大小设置:

一般1M足够大了,往往会将其设置小一点,在idea中添加JVM配置,默认是1M,也可以改为128k:

在这里插入图片描述
改了栈大小之后,原来的栈深度35710就会减小,再报Stack Overflow:

相当于把栈变小了,压的栈就会相应变少。

在这里插入图片描述

总结:实际项目中,一般也不会递归调用太多次,1M的话,调用个3w多次肯定是够用的!所以改小一点是有好处的,改小一点反而可以启动更多线程,比如原本10M的内存空间可以创建10个1M的线程,那么把栈大小改小后,线程所占用的内存空间就变小了,从而使得线程数量变多

2.8 JVM运行时数据区 本地方法栈的特点及作用?

1、与虚拟机栈基本类似;

2、区别在于本地方法栈为Native方法服务;

3、HotSpot虚拟机将虚拟机栈和本地方法栈合并;

4、有StackOverflowError和OutOfMemoryError(比较少见);

5、随线程而生,随线程而灭;

6、GC不会回收该区域;

程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;

解释:本地方法栈和虚拟机栈类似,只不过存储的是Native方法,也就是底层由C++写的方法;而HotSpot是把虚拟机栈和本地方法栈合并到一起了!

在这里插入图片描述

2.9 JVM运行时数据区 Java堆的特点及作用?☆

1、线程共享的一块区域;

2、虚拟机启动时创建;

3、虚拟机所管理的内存中最大的一块区域

4、存放所有实例对象或数组

5、GC垃圾收集器的主要管理区域;

6、可分为新生代、老年代;

7、新生代更细化可分为Eden、From Survivor、To Survivor,Eden:Survivor = 8:1:1

8、可通过-Xmx、-Xms调节堆大小;

9、无法再扩展java.lang.OutOfMemoryError: Java heap space

10、【TLAB】 如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率;

解释:堆的年代划分

在这里插入图片描述

解释:堆内存不足了,就会OOM,java.lang.OutOfMemoryError: Java heap space 堆溢出

在这里插入图片描述
输出结果:

在这里插入图片描述
如果改一下堆大小:
在这里插入图片描述
在这里插入图片描述

解释:TLAB:

因为堆是共享区域,所以当多个线程往堆里面放内容时,会产生一个竞争关系、冲突问题,那么最朴素的想法就是用锁去解决这个线程安全问题,但是用锁呢,又会导致效率比较低,所以干脆给每个线程一个默认区域,区域不大,但是供每个线程各自去放内容,也就是给每个线程分配了一个较小的缓冲区,当把缓冲区放满后,再去公共区放数据!

2.10 JVM中对象如何在堆内存分配?

1、指针碰撞(Bump The Pointer):内存规整的情况下;

2、空闲列表(Free List):内存不规整的情况下;

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定;

因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;

而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存;

3、本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):对象创建在虚拟机中频繁发生,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况;

那么解决方案有两种:

(1)同步锁定,JVM是采用CAS配上失败重试的方式保证更新操作的原子性;

(2)线程隔离,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定;

1
-XX:TLABSize=512k 设置大小;

解释:指针碰撞:

当内存排列规整的时候,指针不断向右遍历,依次排列对象

在这里插入图片描述

空闲列表:
和指针碰撞不同,该方式的内存排列不规整,有一个空闲列表去记录哪些区域是空闲的,那么当存储时去找对应的空闲内存进行存储

在这里插入图片描述

解释:具体用哪种方式,这个是根据垃圾回收器的空,间压缩整理能力来决定的,如果GC每次回收完后,将空内存排列的规整,那么就用指针碰撞

一些JVM可以配置的参数:
在这里插入图片描述

2.11 JVM堆内存中的对象布局?

在 HotSpot 虚拟机中,一个对象的存储结构分为3块区域:

对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding);

  • 对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit,官方称为 ‘Mark Word’;
    在这里插入图片描述
    第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,另外,如果是Java数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以;
  • 实例数据(Instance Data):程序代码中所定义的各种成员变量类型的字段内容(包含父类继承下来的和子类中定义的);
  • 对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍,HotSpot虚拟机,任何对象的大小都是8字节的整数倍;

2.12 JVM什么情况下会发生堆内存溢出?☆

Java堆中用于储存对象,只要不断地创建对象,并且保持GC Roots到对象之间有可达路径

来避免垃圾回收机制清理这些对象,那么随着对象数量的增加,总容量达到最大堆的容量限制后就会产生内存溢出;

MAT工具分析xxx.hprof文件(相当于一个内存log),排查溢出的原因;

添加JVM参数 输出hprof文件:

1
2
3
-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=d:/dev/heapdump.hprof

在这里插入图片描述
在这里插入图片描述
利用Eclipse的MAT工具来分析文件:

官网即可下载,open file,生成怀疑报告Leak suspects

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-Xms3072M

-Xmx3072M

-Xmn1536M

-Xss1M

-XX:-UseCompressedClassPointers

-XX:MetaspaceSize=256M

-XX:MaxMetaspaceSize=256M

-XX:SurvivorRatio=8

-XX:MaxTenuringThreshold=5

-XX:PretenureSizeThreshold=1M

-XX:+PrintGCDetails

-XX:+PrintGCDateStamps

-Xloggc:d:/dev/gc.log

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=d:/dev/heapdump.hprof

在这里插入图片描述

解释:Java堆中用于储存对象,只要不断地创建对象,并且保持GC Roots到对象之间有可达路径

比如这里的orderList是一个GC root根,然后引用了new ArrayList()这个List对象地址,然后这个List对象又在循环中,不断的去引用到了order对象,由于对象一直在创建,所以就能保持GC Roots到对象之间有可达路径!

如果这个List对象引用断了,那么就会将剩余的进行垃圾回收,从而不会导致堆内存溢出,也就是说,该List对象不再被引用了,就可以销毁了(被GC回收),从而就不会占用堆内存了

在这里插入图片描述

验证猜想:

如果将order的引用注释掉:

这样一来,虽然orderList还是指向了堆内存中的List对象,但是List对象没有去引用新生成的order对象,导致order对象由于没有被引用,就会被GC回收,这样就不会导致堆内存溢出了.
在这里插入图片描述
可以通过visualVM软件来可视化堆内存中对象的变化过程:

可以看出黄色的是每一个对象新生和销毁的过程,顶峰代表出生,下降到0代表销毁,所以就可以一直跑,不会堆溢出.
在这里插入图片描述
对比溢出的情况:
在这里插入图片描述
New区域不断的增加,到顶后,Old再累积

2.13 JVM如何判断对象可以被回收?

在JVM堆里面存放着所有的Java对象,垃圾收集器在对堆进行回收前,首先要确定这些对象之中哪些还“存活”着,哪些已经“死去”;

Java通过 可达性分析(Reachability Analysis) 算法 来判定对象是否存活的;

该算法的基本思路:通过一系列称为“GC Roots”的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连(也称为不可达),则证明此对象是不可能再被使用的对象,就可以被垃圾回收器回收;
在这里插入图片描述
对象object 5、object 6、object 7虽然有关联,但它们到GC Roots是不可达的,所以它们将会被判定为可回收的对象;

哪些对象可以作为GC Roots呢?

1、在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等所引用的对象;

2、方法区/元空间中的类静态属性引用的对象;

3、方法区/元空间中的常量引用的对象;

4、在本地方法栈中JNI(即通常所说的Native方法)引用的对象;

5、Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如

NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器;

6、所有被同步锁(synchronized关键字)持有的对象;

7、反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等;

8、其他可能临时性加入的对象;

总结:一般我们最常见的就是虚拟机栈中的局部变量引用的对象、临时变量,作为GC Root。

2.14 谈谈Java中不同的引用类型?

Java里有不同的引用类型,分别是强引用、软引用、弱引用 和 虚引用;

强引用:Object object = new Object();

软引用:SoftReference 内存充足时不回收,内存不足时则回收;

弱引用:WeakReference 不管内存是否充足,只要GC一运行就会回收该引用对象;

虚引用:PhantomReference这个其实暂时忽略也行,因为很少用,它形同虚设,就像没有引用一样,其作用就是该引用对象被GC回收时候触发一个系统通知,或者触发进一步的处理;

解释:

强引用【大多用】,因为有变量指向对象,只要引用没有释放,即便是内存不足、溢出了,也不能回收这个引用

软引用【缓存用】,要看内存足不足,弱引用,一般用于缓存领域(内存充足就缓存起来,内存不足就清空缓存,比如mybatis中就用到过)

弱引用【很少用】:比如类库ThreadLocal用到过

虚引用【很少用】

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;

public class References {

public static void main(String[] args) {
//强引用
Object object = new Object();

//软引用
SoftReference softReference = new SoftReference(object);

//弱引用
WeakReference weakReference = new WeakReference(object);
}
}

2.15 JVM堆内存分代模型?

JVM堆内存的分代模型:年轻代、老年代;

大部分对象朝生夕死,少数对象长期存活;

在这里插入图片描述
From Survivor区也叫S0区,To Survivor 也叫S1区

大小也可以通过参数去调整

2.16 JVM堆中新生代的垃圾回收过程?☆☆☆☆

在这里插入图片描述
JVM里垃圾回收针对的是 新生代,老年代,还有元空间/方法区(永久代)

不会针对方法的栈帧进行回收,方法一旦执行完毕,栈帧出栈,里面的局部变量直接就从内存里清理掉,也就是虚拟机栈不存在垃圾回收

代码里创建出来的对象,一般就是两种:

1、一种是短期存活的,分配在Java堆内存之后,迅速使用完就会被垃圾回收;

2、一种是长期存活的,需要一直生存在Java堆内存里,让程序后续不停的去使用;

第一种短期存活的对象,是在Java堆内存的新生代里分配;

第二种长期存活的对象,通过在新生代S0区和S1区来回被垃圾回收15次后,进入Java堆内存的老年代中,这里的15次,我们也称为对象的年龄,即对象的年龄为15岁;

java -XX:+PrintFlagsFinal 打印jvm默认参数值;

总结:垃圾回收针对的是堆和元空间

过程详解:

新创建的对象先进Eden空间,满了后,再就触发Minor GC就会去回收Eden区域里面的对象,这里面有些需要存活、有些需要回收,先把要存活的移到From Survivor区域,把要回收的对象清除掉。
在这里插入图片描述
第二轮:当Eden区域第二次再满的时候,这时又要出发Minor GC,这时候处理回收的就是Eden和S0两个区域的对象了

先将不能回收的对象移到S1区域,把要回收的对象清空掉,此时Eden区域空闲了。
在这里插入图片描述
然后再处理S0区域中的对象,把要回收的清除,要存活的移到S1区域。
在这里插入图片描述
在这里插入图片描述
第三轮:当Eden区域再次满载状态时,再次触发Minor GC,这时处理的就是已满的Eden以及S1区域,先处理Eden:将回收的清除,需要存活的移至S0,针对S1区域做同理操作。
在这里插入图片描述
在这里插入图片描述
然后再回收S1区域的对象,其中不能回收的移到S0区域,要回收的清除掉。
在这里插入图片描述如此,循环往复,已存在于S0、S1的对象经过Minor GC后,要继续存活的就移到另一个区域(S0 or S1),要回收的清除即可,要回收的对象就会在S0、S1两个区域中间来回移动,当移动的次数大于15次后,那之后就放进老年代,表示这个对象是需要长期存活的对象。

在中间每次清理S0、S1后,每次处理后都会有一个是空闲状态的,下一次把需要存活的移到空闲的区域

年龄阈值

关于年龄阈值,如果是并行GC,默认值是15,如果是CMS垃圾处理器的,就是6,详细可以看官方文档:

一般不指定垃圾处理器,那就是并行的,所以默认15次

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BGBCIEFC

在这里插入图片描述
可以执行命令来看JVM的默认参数:

1
java -XX:+PrintFlagsFinal 打印jvm默认参数值;

2.17 JVM对象动态年龄判断是怎么回事?☆☆☆

虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold=15才能晋升老年代;

结论
动态年龄判断:Survivor区的对象年龄从小到大进行累加,当累加到X年龄(某个年龄)时占用空间的总和大于50%(可以使用-XX:TargetSurvivorRatio=? 来设置保留多少空闲空间,默认值是50),那么比X年龄大的对象都会晋升到老年代;

1、Survivor区分布如下图:

这里是三轮后的的一个分布结果
在这里插入图片描述

解释:也就是还有45%的对象还在存活,只要大于50%,才会触发动态年龄判断

2、此时新生代GC后,有6%的对象进入Survivor区,Survivor区分布如下图:
在这里插入图片描述
这时从1岁加到4岁时,总和51% 大于50%,但此时没有大于四岁的对象,即没有对象晋升

解释:动态年龄判断是在大于50%后触发,然后针对的是50%以后的大年龄对象,也就是4岁以后的,5岁 6岁…此处没有

3、又经过一次新生代GC后,有40%的对象进入Survivor区,Survivor区分布如下图:
在这里插入图片描述
Survivor区的对象年龄从小到大进行累加,当累加到 3 年龄时的总和大于50%,那么比3大的都会晋升到老年代,即4岁的20%、5岁的20%晋升到老年代

总结:并不是严格的到了15岁,就会到达老年代,也有动态年龄判断机制!

2.18 什么是老年代空间分配担保机制

在这里插入图片描述

Eden:800m –>300m

S0:100m

S1:100m

老年代:1000m,剩350m、200m

新生代Minor GC后剩余存活对象太多,无法放入Survivor区中,此时就必须将这些存活对象直接转移到老年代去,如果此时老年代空间也不够怎么办?

1、执行任何一次Minor GC之前,JVM会先检查一下老年代可用内存空间,是否大于新生代所有对象的总大小,因为在极端情况下,可能新生代Minor GC之后,新生代所有对象都需要存活,那就会造成新生代所有对象全部要进入老年代;

2、如果老年代的可用内存大于新生代所有对象总大小,此时就可以放心大胆的对新生代发起一次Minor GC,因为Minor GC之后即使所有对象都存活,Survivor区放不下了,也可以转移到老年代去;

3、如果执行Minor GC之前,检测发现老年代的可用空间已经小于新生代的全部对象总大小,那么就会进行下一个判断,判断老年代的可用空间大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小,如果判断发现老年代的内存大小,大于之前每一次Minor GC后进入老年代的对象的平均大小,那么就是说可以冒险尝试一下Minor GC,但是此时真的可能有风险,那就是Minor GC过后,剩余的存活对象的大小,大于Survivor空间的大小,也大于老年代可用空间的大小,老年代都放不下这些存活对象了,此时就会触发一次“Full GC”;

所以老年代空间分配担保机制的目的?也是为了避免频繁进行Full GC

4、如果Full GC之后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致“OOM”内存溢出 ;

在JDK6的时候有一个参数-XX:+HandlePromotionFailure用于开启是否要进行空间担保;

解释:

简单来说,就是往老年代放对象的时候,这个空间够不够,有一个担保机制。

当Eden新生代区域装满对象后,触发Minor GC,这里的核心在于做一个判断:

判断老年代的可用空间能不能大于新生代的对象大小总和,(这里相当于假设新生代对象全部都不可回收 并且S0 S1也不够,那么就得往老年代里面放,如果老年代可以全覆盖了,说明可以直接Minor GC,是没有OOM风险的);如果老年代空间不足以覆盖全部新生代对象,那么就再作第二个判断:看历史往老年代放对象的平均大小和老年代的可用空间做比较(这里相当于是个评估,用历史平均去预算一下,但是也可能这一次比历史平均的都大,那么最终还得Full GC,Full GC指的是对老年代做回收,如果仍然不够则OOM),这样的目的还是为了避免Full GC

在这里插入图片描述
核心关键:
避免频繁的Full GC,所以每次判断都是先尽量走Minor GC,实在不行再Full GC。

2.19 什么情况下对象会进入老年代?

1、躲过15次GC之后进入老年代,可通过JVM参数“-XX:MaxTenuringThreshold”来设置年龄,默认为15岁;

2、动态对象年龄判断;

3、老年代空间担保机制;

4、大对象直接进入老年代

大对象是指需要大量连续内存空间的Java对象,比如很长的字符串或者是很大的数组或者List集合,大对象在分配空间时,容易导致内存明明还有不少空间时就提前触发垃圾回收以获得足够的连续空间来存放它们,而当复制对象时,大对象又会引起高额的内存复制开销,为了避免新生代里出现那些大对象,然后屡次躲过GC而进行来回复制,此时JVM就直接把该大对象放入老年代,而不会经过新生代;

我们可以通过JVM参数“-XX:PretenureSizeThreshold”设置多大的对象直接进入老年代,该值为字节数,比如“1048576”字节就是1MB,该参数表示如果创建一个大于这个大小的对象,比如一个超大的数组或者List集合,此时就直接把该大对象放入老年代,而不会经过新生代;

-XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,其他新生代垃圾收集器不支持该参数,如果必须使用此参数进行调优,可考虑 ParNew+CMS的收集器组合;

解释:对于大对象来说,一般大概率是不会被回收的,那么如果在S0和S1区域来回的移动,这样的内存开销是很大的,为了避免这一个巨大开销,就直接将过大的对象直接放入到老年代中存储

2.20 JVM运行时数据区 元空间的特点及作用?

1、在JDK1.8开始才出现元空间的概念,之前叫方法区/永久代;

2、元空间与Java堆类似,是线程共享的内存区域;

3、存储被加载的类信息、常量、静态变量、常量池、即时编译后的代码等数据

4、元空间采用的是本地内存,本地内存有多少剩余空间,它就能扩展到多大空间,也可以设置元空间大小;

-XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m

5、元空间很少有GC垃圾收集,一般该区域回收条件苛刻,能回收的信息比较少,所以GC很少来回收;

6、元空间内存不足时,将抛出OutOfMemoryError;

解释:

jdk1.8前都叫方法区,目前都叫元空间,这个区域和堆是并列的,也属于线程共享的内存区域,其存储的主要是类的信息,静态变量、常量等等一系列静态的量,比如下图,类加载后,类的信息就会存储在元空间中;

元空间的内存是不需要垃圾回收的,能回收的信息比较少

当元空间内存不足时候也会报OOM

元空间的大小占用的是本地内存,所以本地内存还剩多少,就可以给它扩展到多少,一般来说都是绝对足够的

在这里插入图片描述
测试元空间溢出

这里采用了动态代理方式,来不断的创建类到元空间中

死循环,这里当超出了内存大小后就会溢出,如果不设置参数的话,就是根据机器剩余的内存来扩展,这时候需要等待很久才会溢出,如下图修改元空间大小参数,大小设置为20m,会马上内存溢出:

在这里插入图片描述
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
* 元空间溢出测试
*
*/
public class MetaSpace {

public static void main(String[] args) throws InterruptedException {
long counter = 0;

while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setUseCache(false);//缓存
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, objects);
}
});

//创建动态代理类class
UserService userService = (UserService)enhancer.create();
userService.find();

Thread.sleep(100);

System.out.println("创建了" + (++counter) + "个动态代理对象");
}
}
}
1
2
3
4
5
6
7
8
9
10
public class UserService {

static {
System.out.println("UserService类加载......");
}

public void find() {
System.out.println("find......");
}
}

2.21 JVM本机直接内存的特点及作用?

1、直接内存(Direct Memory)不属于JVM运行时数据区,是本机直接物理内存;

2、像在JDK 1.4中新加入了NIO(New Input/Output)类,一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的 DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据;

3、可能导致OutOfMemoryError异常出现; netty

2.22 JVM本机直接内存溢出问题?

直接内存(Direct Memory) 的容量大小可通过-XX:MaxDirectMemorySize参数来指定,该参数表示设置新I / O(java.nio程序包)直接缓冲区分配的最大总大小(以字节为单位);默认情况下,大小设置为0,这意味着JVM自动为NIO直接缓冲区分配选择大小;

由直接内存导致的内存溢出,无法生成Heap Dump文件,如果程序中直接或间接使用了NIO技术,那就可以重点考虑检查一下直接内存方面的原因;

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/dev/heapdump.hprof

解释

  1. 如果想输出原因文件,则要输入JVM参数XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/dev/heapdump.hprof

  2. 测试NIO的直接内存溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

public class DirectBufferOOM {

public static void main(String[] args) {
final int _1M = 1024 * 1024;
List<ByteBuffer> buffers = new ArrayList<>();
int count = 1;
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);
buffers.add(byteBuffer);
System.out.println(count++);
}
}
}

报直接内存溢出(物理内存)

在这里插入图片描述
修改直接内存参数
在这里插入图片描述
在这里插入图片描述

如果想输出原因文件,则要输入JVM参数XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/dev/heapdump.hprof

  1. 先配置
  2. 运行程序,报Direct buffer memory错误
  3. 发现在D:/dev/下没有生成heapdump.hprof文件

这个步骤可以用来排查出是直接内存溢出,再进一步考虑是否直接或者间接的使用到了NIO技术

2.23 几个与JVM内存相关的核心参数?

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BGBCIEFC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-Xms Java堆内存的大小;
-Xmx Java堆内存的最大大小;

-Xmn Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小;

-XX:MetaspaceSize 元空间大小;

-XX:MaxMetaspaceSize 元空间最大大小;

-Xss 每个线程的栈内存大小;

-XX:SurvivorRatio=8 设置eden区 和survivor 区大小的比例,默认是8:1:1;

-XX:MaxTenuringThreshold=5 年龄阈值;

-XX:+UseConcMarkSweepGC 指定CMS垃圾收集器;

-XX:+UseG1GC 指定使用G1垃圾回收器

–查看默认的堆大小及默认的垃圾收集器

java -XX:+PrintCommandLineFlags -version

2.24 查看一个对象的大小

导入Lucene库

1
2
3
4
5
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>4.0.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import lombok.Data;

import java.math.BigDecimal;

@Data
public class Order {

private int id;

private String name;

private BigDecimal money;

private byte[] bytes = new byte[1024 * 1024]; //1024kb = 1m

public static void main(String[] args) throws IOException{
Order order = new Order();
//计算指定对象及其引用树上所有对象的综合大小,单位字节
long size = RamUsageEstimator.sizeOf(order);
//计算指定对象及其引用树上所有对象的综合大小,返回可读的结果,如:2KB
String humanSize = RamUsageEstimator.humanSizeOf(order);

System.out.println(size);
System.out.println(humansize);
}

}

在这里插入图片描述