JVM内存管理知识点

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程序,都是通过 jdk1.8.0_251\bin\java 启动一个JVM虚拟机,在虚拟机里面运行 Mall.class 字节码文件;

画一下JVM整个运行原理图

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

在JDK1.8之前,元空间就是原来的方法区(永久代)

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

  1. 堆、元空间(方法区)是线程共享的;
  2. 其他区域是线程私有的;

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

  1. 程序计数器是一块较小的内存空间,几乎可以忽略;
  2. 是当前线程所执行的字节码的行号指示器;
  3. Java多线程执行时,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响;
  4. 该区域是“线程私有”的内存,每个线程独立存储;
  5. 该区域不存在OutOfMemoryError;
  6. 无GC回收;

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

  1. 线程私有;
  2. 方法执行会创建栈帧,存储局部变量表等信息;
  3. 方法执行入虚拟机栈,方法执行完出虚拟机栈;(先进后出)
  4. 栈深度大于虚拟机所允许StackOverflowError;
  5. 栈需扩展而无法申请空间OutOfMemoryError(比较少见);hotspot虚拟机没有;
  6. 栈里面运行方法,存放方法的局部变量名,变量名所指向的值(常量值、对象值等)都存放到堆上的;
  7. 栈一般都不设置大小,栈所占的空间其实很小,可以通过 -Xss1M 进行设置,如果不设置默认为1M;
  8. 随线程而生,随线程而灭;
  9. 该区域不会有GC回收;

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

  1. 与虚拟机栈基本类似;
  2. 区别在于本地方法栈为Native方法服务;
  3. HotSpot虚拟机将虚拟机栈和本地方法栈合并;
  4. 有StackOverflowError和OutOfMemoryError(比较少见);
  5. 随线程而生,随线程而灭;
  6. GC不会回收该区域;

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

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

  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. 如果从分配内存的角度看,所有线程共享的 Java 堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率;

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

  1. 指针碰撞(Bump The Pointer)
    内存规整的情况下;
  2. 空闲列表(Free List)
    内存不规整的情况下;

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

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

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

  1. 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

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

那么解决方案有两种:

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

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

JVM堆内存中的对象布局?

在 HotSpot 虚拟机中,一个对象的存储结构分为3块区域:对象头(Header)实例数据(Instance Data)对齐填充(Padding)

对象头(Header)

第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit(8字节),称为 Mark Word

第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,另外,如果是Java数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以;

实例数据(Instance Data)

程序代码中所定义的各种成员变量类型的字段内容(包含父类继承下来的和子类中定义的)

对齐填充(Padding)

不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。HotSpot虚拟机,任何对象的大小都是8字节的整数倍。

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

Java堆中用于储存对象,只要不断地创建对象,并且保持GC Roots到对象之间有可达路径来避免垃圾回收机制清理这些对象,那么随着对象数量的增加,总容量达到最大堆的容量限制后就会产生内存溢出。

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

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

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

该算法的基本思路:通过一系列称为 GC Roots 的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连(也称为不可达),则证明此对象是不可能再被使用的对象,就可以被垃圾回收器回收。

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

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等所引用的对象;
  • 方法区/元空间中的类静态属性引用的对象;
  • 方法区/元空间中的常量引用的对象;
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象;
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如
    NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器;
  • 所有被同步锁(synchronized关键字)持有的对象;
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等;
  • 其他可能临时性加入的对象;

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

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

强引用:Object object = new Object()

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

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

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

JVM堆内存分代模型

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

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

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

JVM里垃圾回收针对的是 新生代,老年代,还有元空间/方法区(永久代),不会针对方法区的栈帧进行回收,方法一旦执行完毕,栈帧出栈,里面的局部变量直接就从内存里清理掉,也就是虚拟机栈不存在垃圾回收。

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

  1. 一种是短期存活的,分配在 Java 堆内存之后,迅速使用完就会被垃圾回收;
  2. 一种是长期存活的,需要一直生存在 Java 堆内存里,让程序后续不停的去使用;

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

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

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

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

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

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

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

新生代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 用于开启是否要进行空间担保;

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

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的收集器组合;

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

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

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

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

4、元空间采用的是本地内存,本地内存有多少剩余空间,它就能扩展到多大空间,也可以设置元空间大小;-XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m

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

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

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

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

-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垃圾回收器

查看默认值:

PS C:\Users\gang> java -XX:+PrintCommandLineFlags -version
-XX:G1ConcRefinementThreads=8 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=535843392 -XX:MaxHeapSize=8573494272 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
openjdk version "11.0.12" 2021-07-20 LTS
OpenJDK Runtime Environment Corretto-11.0.12.7.1 (build 11.0.12+7-LTS)
OpenJDK 64-Bit Server VM Corretto-11.0.12.7.1 (build 11.0.12+7-LTS, mixed mode)

   转载规则


《JVM内存管理知识点》 Harbor Zeng 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
JVM垃圾回收机制知识点 JVM垃圾回收机制知识点
堆为什么要分成年轻代和老年代? 因为年轻代和老年代不同的特点,需要采用不同的垃圾回收算法。 年轻代的对象,它的特点是创建之后很快就会被回收,所以需要用一种垃圾回收算法; 老年代的对象,它的特点是需要长期存活,所以需要另外一种垃圾回收算法; 所以需要分成两个区域来放不同的对象。 绝大多数对象都是朝生夕灭的; 如果一个区域中大多数对象都是朝生夕灭,那么把它们集中放在一起,每次回收时只关注如何保
2021-11-13
下一篇 
JVM类加载机制知识点 JVM类加载机制知识点
Java运行时一个类是什么时候被加载的? 一个类在什么时候开始被加载,《Java虚拟机规范》中并没有进行强制约束,交给了虚拟机自己去自由实现,HotSpot虚拟机是按需加载,在需要用到该类的时候加载这个类。 使用 -XX:+TraceClassLoading 开启追踪类加载的细节。 JVM一个类的加载过程? 一个类从加载到jvm内存,到从jvm内存卸载,它的整个生命周期会经历7个阶段: 加
2021-11-13
  目录