JVM1:体系结构概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,栈和堆

JVM体系结构概述

JVM位置

JVM是运行在操作系统之上的,它与硬件没有直接的交互

在这里插入图片描述

JVM体系结构

在这里插入图片描述

类装载器ClassLoader

负责加载class文件,class文件在文件开头有特定的文件标示,
并且ClassLoader只负责class文件的加载,
至于它是否可以运行,则由Execution Engine决定 
在这里需要区分一下class与Class。小写的class,
是指编译 Java 代码后所生成的以.class为后缀名的字节码文件。
而大写的Class,是 JDK 提供的java.lang.Class,
可以理解为封装类的模板。多用于反射场景,
例如 JDBC 中的加载驱动,Class.forName("com.mysql.jdbc.Driver");

接下来我们来观察下图,
Car.class字节码文件被ClassLoader类装载器加载并初始化,
在方法区中生成了一个Car Class的类模板,
而我们平时所用到的实例化,就是在这个类模板的基础上,
形成了一个个实例,即car1,car2。反过来讲,
我们可以对某个具体的实例进行getClass()操作,
就可以得到该实例的类模板,即Car Class。再接着,
我们对这个类模板进行getClassLoader()操作,
就可以得到这个类模板是由哪个类装载器进行加载的。

在这里插入图片描述

JVM并不仅仅只是通过检查文件后缀名是否是.class来判断是否加载,
最主要的是通过class文件中特定的文件标示,
即下图main.class文件中的cafe babe和后面的数字要符合java的规范

在这里插入图片描述

类装载器分类
虚拟机自带的加载器
1.启动类加载器(Bootstrap)C++,
	加载%JAVAHOME%/jre/lib/rt.jar。
2.扩展类加载器(Extension)Java
	加载%JAVAHOME%/jre/lib/ext/ *.jar,例如javax.swing包。
3.应用程序类加载器(App)Java
	也叫系统类加载器,加载当前应用的classpath的所有类

用户自定义加载器  Java.lang.ClassLoader的子类,
用户可以定制类的加载方式

在这里插入图片描述

package com.jane;

/**
 * @author jane
 * @create 2021-03-09 13:11
 */
public class MyObject
{
    public static void main(String[] args)
    {
        Object object = new Object();
        System.out.println(object.getClass().getClassLoader());     //null

        MyObject myObject = new MyObject();
        System.out.println(myObject.getClass().getClassLoader());   //sun.misc.Launcher$AppClassLoader@14dad5dc

        System.out.println(myObject.getClass().getClassLoader().getParent().getParent());   //null
        System.out.println(myObject.getClass().getClassLoader().getParent());               //sun.misc.Launcher$ExtClassLoader@1b6d3586
        System.out.println(myObject.getClass().getClassLoader());                           //sun.misc.Launcher$AppClassLoader@14dad5dc

    }
}

先看自定义的MyObject,
首先通过getClassLoader()获取到的是AppClassLoader,
然后getParent()得到ExtClassLoader,再getParent()竟然是null?
可能大家会有疑惑,不应该是Bootstrap加载器么?
这是因为,BootstrapClassLoader是使用C++语言编写的,
Java在加载的时候就成了null。

我们再来看Java自带的Object,
通过getClassLoader()获取到的加载器直接就是BootstrapClassLoader,
如果要想getParent()的话,因为是null值,所以就会报
java.lang.NullPointerException空指针异常。

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

采用双亲委派的一个好处是,比如加载位于rt.jar包中的类
java.lang.Object,不管是哪个加载器加载这个类,
最终都是委派给顶层的启动类加载器进行加载,
确保哪怕使用了不同的类加载器,最终得到的都是同样一个Object对象。
沙箱安全机制
是基于双亲委派机制上采取的一种JVM的自我保护机制,
假设你要写一个java.lang.String的类,由于双亲委派机制的原理,
此请求会先交给BootStrapClassLoader试图进行加载,
但是BootStrapClassLoader在加载类时首先通过包和类名查找rt.jar中有没有该类,
有则优先加载rt.jar包中的类,因此就保证了java的运行机制不会被破坏,
确保你的代码不会污染到Java的源码。
因为双亲委派,所以这里我们自己写的java.lang.String就加载不到,
加载的是Bootstrap启动类加载器的String
package java.lang;

/**
 * @author jane
 * @create 2021-03-09 13:41
 */
public class String
{
    public static void main(String[] args)
    {
        System.out.println("hello");
        /**
         * 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
         *    public static void main(String[] args)
         * 否则 JavaFX 应用程序类必须扩展javafx.application.Application
         */
    }
}

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

rt.jar是什么?做了哪些事?
为什么可以在idea这些开发工具中可以直接去使用String、ArrayList、甚至一些JDK提供的类和方法?
这些都在rt.jar中定义好了,且直接被启动类加载器进行加载了。

本地方法栈 Native Method Stack

Java语言本身不能对操作系统底层进行访问和操作,
但是可以通过JNI接口调用其他语言来实现对底层的访问。

本地方法接口(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();),
但只有声明却没有具体的实现!。
为什么?我们都知道Thread是Class关键字修饰的类(class Thread implements Runnable),
而不是接口。一般来说,类中的方法都要有定义和实现,
接口里面才有方法的定义声明。这就是native方法的独特之处,说白了,
被native关键字修饰的方法,基本上和我们,甚至和 Java 都没啥关系了,
因为它要去调用底层操作系统或者第三方语言的库函数,
所以我们不需要去考虑它具体是如何实现的。

程序计数器 Program Counter Register

程序计数器(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)。
Java7中,方法区 f =new 永久代();
Java8中,方法区 f =new 元空间();
实例变量存在堆内存中,和方法区无关。

栈和堆

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

对于栈来说,不存在垃圾回收的问题,只要线程一结束该栈就Over。
栈存储什么数据?
	栈主要存储8种基本类型的变量、对象的引用变量、以及实例方法。

这里引出一个名词,栈帧,什么是栈帧?
	栈帧对应Java里面的方法
	每个方法执行的同时都会创建一个栈帧,
	用于存储局部变量表、操作数栈、动态链接、方法出口等信息,
	每个方法从调用直至执行完毕的过程,
	就对应着一个栈帧在虚拟机中入栈到出栈的过程。
简单来说,栈帧对应一个方法的执行和结束,是方法执行过程的内存模型。

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

	本地变量(Local Variables):输入参数和输出参数,以及方法内的变量。
	栈操作(Operand Stack):记录出栈、入栈的操作。
	栈帧数据(Frame Data):包括类文件、方法等。


栈的大小是根据JVM有关,一般在256K~756K之间,约等于1Mb左右。
在图中一个栈中有两个栈帧,分别是Stack Frame1和Stack Frame2,
对应方法1和方法2。其中Stack Frame2是最先被调用的方法2,
所以它先入栈。然后方法2又调用了方法1,
所以Stack Frame1处于栈顶位置。执行完毕后,
依次弹出Stack Frame1和Stack Frame2,然后线程结束,栈释放。
所以,每执行一个方法都会产生一个栈帧,并保存到栈的顶部,
顶部的栈帧就是当前所执行的方法,该方法执行完毕后会自动出栈。

在这里插入图片描述

总结如下,栈中的数据都是以栈帧(Stack Frame)的格式存在,
栈帧是一个内存区块,是一个数据集,
是一个有关方法(Method)和运行期数据的数据集,
当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,
方法A中又调用了方法B,于是产生栈帧F2也被压入栈中,
方法B又调用方法C,于是产生栈帧F3也被压入栈中······执行完毕后
,遵循“先进后出,后进先出”的原则,先弹出F3栈帧,再弹出F2栈帧,
再弹出F1栈帧。
java.lang.StackOverflowError是错误,不是异常!证明如下 :

在这里插入图片描述

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

在这里插入图片描述

比如MyObject myObject = new MyObject();,
等号左边MyObject myObject的myObject就是引用,在Java栈里面。
等号右边的new MyObject()new出来的MyObject实例对象在堆里面。
简单来说,就是Java栈中的引用myObject指向了堆中的MyObject实例对象。

而方法区中的对象类型数据是Class,是MyObject的模板

堆 Heap

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

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

	Young Generation Space,新生区、新生代
	Tenure Generation Space,老年区、老年代
	Permanent Space,永久区、元空间
Java7之前,堆结构图如下,而Java8则只将永久区变成了元空间。

在这里插入图片描述

总结一下:
	堆内存在逻辑上分为新生+养老+元空间,而堆内存在物理上分为新生+养老。
对象在堆的生命周期
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,
最后被垃圾回收器收集,结束生命。
新生区又分为两部分: 
	伊甸区(Eden space)和幸存者区(Survivor pace) 

所有的类都是在伊甸区被new出来的。
幸存区有两个: 
	0区(Survivor 0 space)和1区(Survivor 1 space)。
	
当伊甸园的空间用完时,程序又需要创建对象,
JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),
将伊甸园区中的不再被其他对象所引用的对象进行销毁。
然后将伊甸园中的剩余对象移动到幸存0.若幸存0区也满了,
再对该区进行垃圾回收,然后移动到1区。
那如果1区也满了呢?再移动到养老区。若养老区也满了,
那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。
若养老区执行了Full GC之后发现依然无法进行对象的保存,
就会产生OOM异常“OutOfMemoryError”。

如果出现java.lang.OutOfMemoryError: Java heap space异常,
说明Java虚拟机的堆内存不够。原因有二:
	(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
	(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
然后理解点深层的东西

在这里插入图片描述

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有一个要是空的。
面试真题
package com.jane;

/**
 * @author jane
 * @create 2021-03-09 20:07
 */
public class PoolTest
{
    public static void main(String[] args)
    {
        PoolTest test = new PoolTest();
        String str="abc";
        test.Change(str);
        System.out.println(str);
        //结果是:abc
    }
    private void Change(String str)
    {
        str="def";
    }
}
是这样的,对于String类型的数据,有个字符串数据池
在main方法里面创建abc的str,首先去线程池里面看看有没有
值是abc的字符串,发现没有,就创建,然后在线程池保存起来了
然后调用Change()方法,传入的是str的地址,然后想修改,
然后看看线程池里面有没有值是def的字符串,发现没有又重新找个
地方创建一个值是def的字符串,然后将Change()方法里面的
str的指向地址改成def字符串的地址
HotSpot虚拟机的内存管理

在这里插入图片描述

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

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

在这里插入图片描述

如果没有明确指明,Java虚拟机的名字就叫做HotSpot。
整个堆分为新生区和养老区,新生区占整个堆的1/3,养老区占2/3。
新生区又分为3份:伊甸园区:幸存者0区(from区):幸存者1区(to区)=811
每次从伊甸园区经过GC幸存的对象,年龄(代数)会+1
永久区
永久区是一个常驻内存区域,
用于存放JDK自身所携带的Class,Interface的元数据(也就是上面文章提到的rt.jar等),
也就是说它存储的是运行环境必须的类信息,
被装载进此区域的数据是不会被垃圾回收器回收掉的,
关闭JVM才会释放此区域所占用的内存。

JDK1.7
在这里插入图片描述
JDK1.8
在这里插入图片描述

堆的参数主要是有两个: -Xms 和 -Xmx
	-Xms	堆的初始化的大小
	-Xmx	堆的最大的大小

Yong Gen(新生代)有个参数 -Xmn,这个参数可以调新生区和养老区的比例
		但是一般不会调

永久代的两个参数-XX:PermSize和-XX:MaxPermSize
	分别调永久待的初始值和最大值,
	jdk8之后就没有这两个参数了,Java8之后元空间不在虚拟机内,
	而是在物理内存中
在JDK1.8中,永久代已经被移除,被一个称为元空间的区域所取代。
元空间的本质和永久代类似。

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

因此,默认情况下,元空间的大小仅受本地内存限制。
类的元数据放入native memory,字符串池和类的静态变量放入Java堆中,
这样可以加载多少类的元数据就不再由MaxPermSize控制, 
而由系统的实际可用空间来控制。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ReflectMirroring

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值