一个Java进程占用的内存都哪些部分?
-
如下图所示
-
如上为JVM的运行时内存
-
堆内存
-
用于存放对象实例
-
-
栈内存:
-
Java虚拟机栈:
-
用于存储Java中的局部变量。
-
每次方法调用都会创建一个栈帧,该栈帧用于存储局部变量,操作数栈,动态链接,方法出口等信息。
-
当方法执行完毕之后,这个栈帧就会被弹出,变量作用域就会结束,数据就会从栈中消失。
-
-
本地方法栈:
-
本地方法栈中存放本地方法( Native Method )的参数和局部变量,以及其他一些附加信息。
-
这些本地方法一般是用C等本地语言实现的,虚拟机在执行这些方法时就会通过本地方法栈来调用这些本地方法。
-
-
-
堆外内存
-
堆外内存
-
元空间(Meta Space)
-
用来来存储类的元数据信息的。
-
-
压缩类空间(Compressed Class Space)
-
压缩类空间是元空间的一部分,使用较小的指针来引用类的元数据
-
-
代码缓冲区(Code Cache)
-
主要用于存储编译器编译后的本地机器代码。当Java方法被JVM的即时编译器(JIT编译器)编译成本地代码后,这些代码被存储在代码缓冲区中,以便后续直接执行,提高程序运行效率。
-
-
直接缓冲区(Direct Buffer)
-
用于在Java程序和操作系统之间高效地传递数据。
- 在物理内存中分配存储空间,从而减少了数据在Java堆内存和操作系统之间来回复制的需要,提高了数据处理的效率。
-
-
-
-
非JVM内存
-
本地运行库指的是操作系统中用本地编程语言(如C或C++)编写的库,这些库直接运行在操作系统上,而不是在Java虚拟机(JVM)内部执行。
-
JNI(Java Native Interface)是一个编程框架,允许Java代码与本地代码(如C和C++代码)进行交互。它是Java平台的一部分,为Java程序调用本地方法提供了一套标准的接口。
-
-
JVM的运行时内存区域是怎样的?
-
共享区域
-
java堆:
-
用于存放对象实例
-
由java虚拟机管理
-
垃圾回收区域(内存占用最大)
-
-
方法区:
-
用于存放被加载的类信息、常量、静态变量、即时编译后的代码等
-
实现方式如堆,永久代,元空间
-
方法区是线程共享的,每个虚拟机实例只有一个方法区。
-
-
运行时常量池 :
-
是方法区的一部分。
-
用于存储编译阶段生成的信息,主要有字面量和符号引用常量两类
-
其中符号引用常量包括了类的全限定名称、字段的名称和描述符、方法的名称和描述符
-
-
-
线程独享区域
-
程序计数器:
-
只读的存储器,用于记录Java虚拟机执行的字节码指令的地址
-
-
java虚拟机栈
-
存储java中的局部变量
-
每次方法调用都会创建一个栈帧,该栈帧用于存储局部变量,操作数栈,动态链接,方法出口等信息。
-
当方法执行完毕之后,这个栈帧就会被弹出,变量作用域就会结束,数据就会从栈中消失。
-
-
本地方法栈
-
存放本地方法的参数和局部变量和其他附加信息。
-
-
堆内存
-
堆内存的结构
-
不同的"代"设置不同的回收策略,有助于提升垃圾回收的效率。
-
很多对象都会出现在Eden区,当Eden区的内存容量用完的时候,GC会发起,非存活对象会被标记为死亡,存活的对象被移动到Survivor区。如果Survivor的内存容量也用完,那么存活对象会被移动到老年代
-
什么是young GC与Full GC
-
youngGC主要用于对新生代垃圾回收
-
年轻代中的eden区分配满 就会触发youngGC
-
-
FullGC除了会对新生代垃圾回收以外,还会对老年代或者永久代进行回收。相比较于youngGC,Full GC的收集频率更低,耗时更长
-
老年代空间不足,空间分配担保失败,永久代空间不足,代码中执行system.gc()会触发FullGC
-
-
-
一次GC流程是怎样的
-
基于jdk1.8来说,过程如下:
-
对象在创建时,会根据大小进入年轻代或者老年代。
-
在年轻代创建对象,会发生在Eden区,可能会因为Eden区内存不够,会尝试触发一次YoungGC。
-
年轻代采用的是标记复制算法,主要分为,标记、复制、清除三个步骤。
-
标记:进行存活对象的标记
-
复制:然后把Eden区和Survivor区复制到另外一个Survivor区
-
清除:然后再把Eden和From Survivor区的对象清理掉。
-
-
YoungGC这个过程,可能会发生两件事情
-
第一个就是Survivor有可能存不下这些存活的对象,这时候就会进行空间分配担保。如果担保成功了,那么就没什么事儿,正常进行Young GC就行了。但是如果担保失败了,说明老年代可能也不够了,这时候就会触发一次FullGC了。
-
还会发生第二件事情就是,在这个过程中,会进行对象的年龄判断,如果他经过一定次数的GC之后,还没有被回收,那么这个对象就会被放到老年代当中去。
-
如果老年代在做FullGC之后,如果空间还是不够,那就要触发OOM(内存溢出)了。
-
-
-
-
-
JVM如何判断对象是否存活?
-
JVM有两种算法来判断对象是否存活:
-
引用计数法
-
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
-
主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。循环引用会导致对象无法被回收,最终会导致内存泄漏及内存溢出
-
-
可达性分析算法
-
“GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
-
-
-
可达性分析算法如何判断对象存活
-
第一次标记:通过可达性分析算法进行第一次标记
-
第二次标记:如果对象的finalize()方法被重写并且没有执行过,则放在相关队列中,如果一段时间后该队列的finalize()方法被执行且和GC Roots关联,则移出“即将回收”集合。如果仍然没有关联,则进行第二次标记,才会对该对象进行回收。
-
不过现在都不提倡覆盖finalize方法,它的本意是像Cpp一样在对象销毁前执行,但是它影响了JAVA的安全和GC的性能,所以第二种判断会越来越少
-
-
-
-
可达性分析算法的不足?
-
STW时间长
-
可达性分析算法需要对程序进行全局分析,可能需要很长的时间,整个过程都是STW的
-
解决问题方法:
-
三色标记算法
-
-
-
内存消耗大
-
需要存储程序中对象和对象之间的引用关系,占用大量的内存空间。
-
-
-
什么是三色标记算法?
-
定义:
-
一种JVM中垃圾标记的算法,可以减少JVM在GC过程中的STW时长
-
CMS、G1等垃圾收集器中主要使用的标记算法
-
-
三色标记法的标记过程可以分为三个阶段:
-
初始标记(时间短,stw)
-
垃圾回收器遍历所有的根对象,将根对象和直接引用的对象标记为灰色。
-
在这个阶段中,垃圾回收器只会扫描被直接或者间接引用的对象,而不会扫描整个堆。因此,初始标记阶段的时间比较短。
-
-
并发标记(时间长,非stw)
-
垃圾回收器从灰色对象开始遍历整个对象图,将被引用的对象标记为灰色,并将已经遍历过的对象标记为黑色
-
并发标记过程中,应用程序线程可能会修改对象图,因此垃圾回收器需要使用写屏障(Write Barrier)技术来保证并发标记的正确性。
-
写屏障是内存操作的正确性的技术,通过控制内存访问的顺序和可见性,保障多线程环境下并发标记的正确执行
-
写屏障技术可能会带来多标和少标的问题
-
-
-
重新标记(stw)
-
重新标记的主要作用是标记在并发标记阶段中被修改的对象以及未被遍历到的对象。
-
垃圾回收器会从灰色对象重新开始遍历对象图,将被引用的对象标记为灰色,并将已经遍历过的对象标记为黑色。
-
-
-
三色标记法如何减少GC时间的
-
最耗时是并发标记的这个阶段,三色标记把这个阶段做到了和应用线程并发执行,大大降低了GC的停顿时长。
-
-
-
什么是空间担保
- 进行young GC时,老年代也是可能空间不足的 ,需要做一次空间分配担保
- 过程如下
- 虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,则认为担保成功。如果小于,则虚拟机会查看HandlePromotionFailure设置值,如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次young GC。如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC
-
Java 8/11的GC
- Java 8中默认的Parallel Scavenge GC+Parallel old GC的,分别用来做新生代和老年代的垃圾回收
- Java 11中默认采用的是G1进行整堆回收的,还新增了ZGC。