JVM、GC详解

本文解析了Java类加载器的双亲委派和沙箱安全原则,探讨了程序计数器、方法区、栈和堆内存的工作原理,以及它们之间的交互。重点讲解了类加载顺序、本地方法栈、内存溢出及对象生命周期管理,包括新生代GC机制和元空间的变化。
摘要由CSDN通过智能技术生成

在这里插入图片描述

上面图中是亮色的地方有两个特点:1.所有线程共享(灰色是线程私有)
2.亮色地方存在垃圾回收
在这里插入图片描述
在这里插入图片描述

类加载器

在这里插入图片描述
参考:https://blog.csdn.net/q961250375/article/details/107499173

双亲委派和沙箱安全

(1)双亲委派,当一个类收到了类加载请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,因此所有的加载请求都应该传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

采用双亲委派的一个好处是,比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委派给顶层的启动类加载器进行加载,确保哪怕使用了不同的类加载器,最终得到的都是同样一个Object对象。

(2)沙箱安全机制,是基于双亲委派机制上采取的一种JVM的自我保护机制,假设你要写一个java.lang.String的类,由于双亲委派机制的原理,此请求会先交给BootStrapClassLoader试图进行加载,但是BootStrapClassLoader在加载类时首先通过包和类名查找rt.jar中有没有该类,有则优先加载rt.jar包中的类,因此就保证了java的运行机制不会被破坏,确保你的代码不会污染到Java的源码。
类加载器的加载顺序如下:

  1. 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  2. 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载。
  4. 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

本地方法栈 Native Method Stack

本地方法接口(Native Interface),其作用是融合不同的编程语言为 Java 所用,它的初衷是用来融合 C/C++ 程序的,Java 诞生的时候是 C/C++ 流行时期,要想立足,就得调用 C/C++ 程序,于是 Java
就在内存中专门开辟了一块区域处理标记为 native 的代码。
而本地方法栈(Native Method Stack),就是在一个 Stack 中登记这些 native 方法,然后在执行引擎Execution Engine执行时加载本地方法库native libraies。
接下来,我们通过下图的多线程部分源码来理解什么是native方法。首先我们观察start()的源码,发现它其实并没有做什么复杂的操作,只是单纯的调用了start0()这个方法,然后我们去观察start0()的源码,发现它只是一个使用了native关键字修饰的一个方法(private native void start0();),但只有声明却没有具体的实现!。

程序计数器 Program Counter Register

也叫PC寄存器。每个线程启动的时候,都会创建一个PC寄存器。PC寄存器里保存当前正在执行的JVM指令的地址。 每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。

简单来说,PC寄存器就是保存下一条将要执行的指令地址的寄存器,其内容总是指向下一条将被执行指令的地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎Execution Engine读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。

PC寄存器一般用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出(OutOfMemory,OOM)错误。

如果执行的是一个native方法,那这个计数器是空的。

方法区 Method Area

方法区(Method Area),是供各线程共享的运行时内存区域,它存储了每一个类的结构信息。例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。

上面说的是规范(定义的一种抽象概念),实际在不同虚拟机里实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Meta space)。

实例变量存在堆内存中,和方法区无关。

栈 Stack

栈管运行,堆管存储
栈(Stack),也叫栈内存,主管Java程序的运行,在线程创建时创建。其生命期是跟随线程的生命期,是线程私有的,线程结束栈内存也就是释放。

对于栈来说,不存在垃圾回收的问题,只要线程一结束该栈就Over。
在这里插入图片描述
简单来说,栈帧对应一个方法的执行和结束,是方法执行过程的内存模型。

其中,栈帧主要保持了3类数据:

本地变量(Local Variables):输入参数和输出参数,以及方法内的变量。
栈操作(Operand Stack):记录出栈、入栈的操作。
栈帧数据(Frame Data):包括类文件、方法等。
栈的大小是根据JVM有关,一般在256K~756K之间,约等于1Mb左右。
java.lang.StackOverflowError
栈是一个内存块,它是有大小长度的,而我们观察代码发现,只要代码一运行,test()递归方法就会一直进行入栈操作,而没有出栈操作,结果肯定会超出栈的大小,进而造成栈溢出错误

栈、堆、方法区的交互关系

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

一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件之后,需要把类、方法、常量变量放到堆内存中,保持所以引用类型的真实信息,方便执行器执行。

其中,堆内存分为3个部分:

Young Generation Space,新生区、新生代
Tenure Generation Space,老年区、老年代
Permanent Space,永久区、元空间
在这里插入图片描述
Java8则只将永久区变成了元空间。堆内存在逻辑上分为新生+养老+元空间,而堆内存在物理上分为新生+养老。
对象在堆中的生命周期:
(1)首先,新生区是类的诞生、成长、消亡的区域。一个类在这里被创建并使用,最后被垃圾回收器收集,结束生命。

(2)其次,所有的类都是在Eden Space被new出来的。而当Eden Space的空间用完时,程序又需要创建对象,JVM的垃圾回收器则会将Eden Space中不再被其他对象所引用的对象进行销毁,也就是垃圾回收(Minor GC)。此时的GC可以认为是轻量级GC。

(3)然后将Eden Space中剩余的未被回收的对象,移动到Survivor 0 Space,以此往复,直到Survivor 0 Space也满了的时候,再对Survivor 0 Space进行垃圾回收,剩余的未被回收的对象,则再移动到Survivor 1 Space。Survivor 1 Space也满了的话,再移动至Tenure Generation Space。

(4)最后,如果Tenure Generation Space也满了的话,那么这个时候就会被垃圾回收(Major GC or Full GC)并将该区的内存清理。此时的GC可以认为是重量级GC。如果Tenure Generation Space被GC垃圾回收之后,依旧处于占满状态的话,就会产生我们场景的OOM异常,即OutOfMemoryError。
Minor GC的过程
Survivor 0 Space,幸存者0区,也叫from区;
Survivor 1 Space,幸存者1区,也叫to区。
from区和to区的区分不是固定的,是互相交换的,意思是说,在每次GC之后,两者会进行交换,谁空谁就是to区。
在这里插入图片描述
1)Eden Space、from复制到to,年龄+1。
首先,当Eden Space满时,会触发第一次GC,把还活着的对象拷贝到from区。而当Eden Space再次触发GC时,会扫描Eden Space和from,对这两个区进行垃圾回收,经过此次回收后依旧存活的对象,则直接复制到to区(如果对象的年龄已经达到老年的标准,则移动至老年代区),同时把这些对象的年龄+1。

(2)清空Eden Space、from
然后,清空Eden Space和from中的对象,此时的from是空的。

(3)from和to互换
最后,from和to进行互换,原from成为下一次GC时的to,原to成为下一次GC时的from。部分对象会在from和to中来回进行交换复制,如果交换15次(由JVM参数MaxTenuringThreshold决定,默认15),最终依旧存活的对象就会移动至老年代。

总结一句话,GC之后有交换,谁空谁是to。

这样也是为了保证内存中没有碎片,所以Survivor 0 Space和Survivor 1 Space有一个要是空的。

HotSpot虚拟机的内存管理

在这里插入图片描述
不同对象的生命周期不同,其中98%的对象都是临时对象,即这些对象的生命周期大多只存在于Eden区。
实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等。虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆内存),目的就是要和堆区分开。

对于HotSpot虚拟机而言,很多开发者习惯将方法区称为 “永久代(Permanent Gen)” 。但严格来说两者是不同的,或者说只是使用永久代来实现方法区而已,永久代是方法区(可以理解为一个接口interface)的一个实现,JDK1.7的版本中,已经将原本放在永久代的字符串常量池移走。(字符串常量池,JDK1.6在方法区,JDK1.7在堆,JDK1.8在元空间。)
在这里插入图片描述

永久区

永久区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据(也就是上面文章提到的rt.jar等),也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
jdk 1.8
在这里插入图片描述
jdk1.7
在这里插入图片描述
在JDK1.8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。

元空间与永久代之间最大的区别在于: 永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。

因此,默认情况下,元空间的大小仅受本地内存限制。
类的元数据放入native memory,字符串池和类的静态变量放入Java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值