JVM (一) 运行时数据区

JVM 运行时数据区

1 类装载器ClassLoader

负责加载class文件,class文件在文件开头有特定的文件标识,将class文件字节码内同加载到内存中,并将这些内容转化为方法区中的运行时数据结构,并且classLoader 只负责class文件的加载,至于它是否可以运行,则由Execution engine决定。
在这里插入图片描述

1.1 Bootstrap ClassLoader 启动类加载器

在虚拟机启动的时候,加载一些运行时基本的类,如 /jre/lib/rt.jar 包下的类,在虚拟机启动的时候就被加载进虚拟机。该类加载器由 c++ 开发

1.2 Extend ClassLoader 拓展类加载器

随着Java的发展,启动类rt.jar 等基本类库已经不满足使用情况,开始出现各种拓展包,此时拓展类加载器Extension ClassLoader 就诞生了。jre/lib/ext/*.jar 包下的class 由拓展类加载器进行加载。

1.3 AppClassLoader 应用程序类加载器

加载当前classpath 的所有类。用户自定义的类编译生成的class就由此加载器进行加载

1.4 自定义类加载器

实现Java.lang.ClassLoader 实现自定义类加载器

类加载器层级结构,代码展示

       // java 自带的基础类
        Object o = new Object();
        // bootstrap classloader 打印结果为null
        System.out.println(o.getClass().getClassLoader());

        System.out.println("==================");
        // 自定义类,使用拓展类加载器加载
        Person person = new Person();
        System.out.println(person.getClass().getClassLoader().getParent().getParent());
        System.out.println(person.getClass().getClassLoader().getParent());
        System.out.println(person.getClass().getClassLoader());

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

1.5 双亲委派机制

口诀: 我爸是李刚,有事找我爹!!
  加载一个类的时候,类加载器依次从 Bootstrap ClassLoader ->Extension ClassLoader->System ClassLoader->自定义类加载器。
当一个类收到了类加载请求,它首先不会舱室自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所哟与的加载请求都会传送到 Bootstrap ClassLoader, 只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是比如加载位于rt.jar包中的java.long.Object, 不管是哪个加载器家在这类,最终都是委托给顶层的启动类加载器进行加载。这样就保证了使用不同的类加载器最终得到的都是同样的一个Object对象。

1.6 沙箱安全机制

沙箱机制: 为了避免用户的代码,污染jre的代码,比如自定义一个String 类,在jvm 运行的时候,始终是顶层的Bootstrap ClassLoader加载的String 始终是 rt.jar 包下的String 类,来实现沙箱安全机制。

2 JVM运行时数据区

在这里插入图片描述

2.1 本地方法接口 native interface

本地接口的作用是融合不同的编程语言为Java 所用,他的初衷是融合C/C++程序,Java 诞生的时候是C/C++横行的时候,要想立足,必须调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,他的具体做法是Native Method Stack 中登记native方法,在Execution Engine 执行时加载 native libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域件的通信很发达,比如可以使用Socket通信,也可以使用Web Service 等等
异构领域: 不同的系统之间,通过标准的接口或者方式进行交互。

2.2 native Method Stack

本地方法栈,中登记了,本地方法。 方法的具体实现是在Execution Engine 执行时加载本地的方法库中。

2.3 程序计数器

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法去中方法字节码(用来存储指向下一条指令的地址,即即将执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
找个内存区域很小,他是当前线程所执行的字节码的行号治时期,字节码解释器通过改变找个计数器的值来选取下一条需要执行的字节码指令。 如果执行的是一个native方法,那么这个计数器是空的,用已完成分支,循环,跳转,异常处理 ,线程恢复等基础功能。不会发生内存溢出(OOM)错误。
程序计数器记录了方法之间的调用和执行情况,类似于排班值日表,用来存储指向下一条执行的指令的地址。

2.4 方法区

方法区是一个线程共享的运行时内存区域,它存储了每一个类的结构信息,例如运行时常量池,字段和方法数据,构造函数和普通方法的字节码内容,上面讲的是规范,在不同的虚拟机里头实现是不一样的,最典型的就是永久代(jdk1.7)和元空间(jdk1.8)
在这里插入图片描述
其中Car Class 即为类的模板信息,存储于方法区, 类的实例存储于堆中,与方法区无关。

2.5 栈

栈也叫栈内存,主管java程序的运行,是在线程创建时创建,线程结束后释放,线程私有,不存在垃圾回收。
8中基本类型+对象的引用变量+和实例方法都是存储再函数的栈内存中。
栈运行原理: 栈中的数据都是以栈帧StackFream 的格式存在的,栈帧是一个内存区块,是一个数据集,是一个有方法和运行期数据的数据集,当一个方法被调用时就产生了一个栈帧F1,并被压倒栈中,A方法又调用B方法,于是产生栈帧F2也被压入栈…依此类推。
执行完成之后先弹出F2栈帧,再弹出F1栈帧。遵循先进后出,后进先出的原则,
每个方法执行的同时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态连接,方法出口等信息,每一个方法从调用直至执行完毕的过程九对应着一个栈帧再虚拟机中从入栈到出栈的过程。栈的大小和具体和JVM的实现有馆,通常再256k~756K之间,约等于1M左右。
在这里插入图片描述
在这里插入图片描述

stackOverFlow

栈溢出,无限递归会导致该错误。错误的原因是栈内存被耗尽。

2.6 堆

一个JVM 实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件后,需要把类方法,常量放到堆内存中,保存所有引用类型的真实信息。以方便执行器执行。堆内存逻辑上分为3个部分,分别如下。堆的结构如下

在这里插入图片描述

新生代
  • 伊甸园区 Eden Space
  • 幸存者0区 Survivor 0 Space, 别名 from 区, S0区
  • 幸存者1区 Survivor 1 Space,别名 to 区, S1区
    新生区是类的诞生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收期手机,借宿声明。 新生区又分为两部分。伊甸区,和西村这区。所有的对象都是在伊甸区被new出来,幸存区有两个,0 区和 1区,当伊甸园的孔家用完时,程序又要创建对象,JVM 的垃圾回收器,将对伊甸园区进行垃圾回收,将伊甸园区中不被其他对象所引用的对象进行销毁,然后将伊甸园区中的剩余对象异动到幸存0区,如果幸存0区也满了,再对该区进行垃圾回收,然后将幸存的移动到幸存1区。
    S0区和S1区名称并不是固定的,每次GC 之后,会进行交换,谁空谁就是to区,即S1区。
    新生代占,总堆存储空间1/3, 新生代内部, 伊甸区:S0:S1 = 8:1:1
MinorGC 过程

在这里插入图片描述

  • 复制->清空->互换
  • 1 eden,SurvivorFrom 复制到SurvivorTo,年龄+1 ,首先, 当Eden区满的时候,会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区,和From区域,对着两个区域进行垃圾回收。经过这次回收之后。将这两个区域的幸存者直接复制到To区域(如果有对象的年龄已经达到了,老年的标准,则复制到老年区),同时将这些对象的年龄+1。
  • 2 清空Eden,SurivorFrom 中的对象,业绩复制之后有交换,谁空谁是TO
  • 3 SurvivorTo 和 SurivorFrom 互换
    最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo 成为下一次GC的SurvivorFrom区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终,如果还是存活,就存入到老年代。

口诀: GC之后有交换,谁空谁是TO

老年代 Tenure generation space

如果幸存区也满了,再将幸存区的对象异动到养老区。 如果养老区内存也满了,那么这个时候将会产生MajorGC(FullGC)。 进行养老区的内存清理。若养老区执行Full GC之后发现依然无法进行对象的保存,就会产生OOM异常"OutOfMemoryError"。
如果出现OOM异常,说明java虚拟机的堆内存不足,原因有如下两种

  • java虚拟机的对内存设置不够,可以用过参数-Xms, -Xmx 来调整
  • 代码中创建了大量大对象,并且长时间不能被垃圾收集器手机(存在被引用)
    老年代空间大小占堆存储 2/3
永久代(java7) / 元空间(java8) Permanent Space

永久存储区,是一个内存常驻其余,用于存放JDK自身携带的Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不回被垃圾回收器回收掉的,关闭JVM才会释放此区域所占的内存。永久代几乎没有垃圾回收

3 HotSpot 虚拟机

实际而言,方法区和堆一样,是哥哥线程共享的内存区域,它用于存储虚拟机加载的:类信息+ 普通变量+静态常量+编译器编译后的代码等等,虽然JVM 规范将方法去描述为堆的一个逻辑v部分,但是她却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
对于HotSpot虚拟机,很多开发者习惯将方法去称之为永久代,但是严格本质上说两者不同。 胡总和说使用永久代来实现方法区而已。 永久代是方法去(相当于是一个接口的interface)的一个实现,jdk1.7的版本中,已经将原本放在永久太的字符串常量池移走。

在这里插入图片描述

3.1 堆参数调优(java8)

在这里插入图片描述
在java8 中,永久代已经被移除, 被一个称为元空间的区域所取代。元空间的本质和永久代类似。
元空间与永久代的最大区别在于:
永久代使用的JVM堆内存,但是java8以后的元空间并不在虚拟机内存中,而是使用本机物理内存。
因此,默认情况下,元空间的大小仅受本地内存限制,类的元数据放入native memory, 字符串和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。

  • -Xms 设置初始堆大小,默认为物理内存的1/64
  • -Xmx 设置堆内存最大分配,默认为物理内存的1/4
  • -XX:+PrintGCDetails 输出详细的GC日志
        // 获取核数
        System.out.println(Runtime.getRuntime().availableProcessors());
        // 获取java 虚拟机视图使用的最大内存
        System.out.println(Runtime.getRuntime().maxMemory() );
        // 获取java 虚拟机的内存总量
        System.out.println(Runtime.getRuntime().freeMemory());

通常,将最大内存和初始内存设置为相同,将两个值设置为相同的值。避免GC 和程序争夺内存,导致内存占用忽高忽低。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值