虚拟机类加载机制总结与整理

JVM基础知识点

1.内存模型以及分区,需要详细到每个区方放什么

JVM所管理的内存分为以下几个运行时数据区:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fLYHFJC8-1594727415062)(https://camo.githubusercontent.com/6d3533c08ec53b6b8e23c73d42793af33272f041/687474703a2f2f696d672e626c6f672e6373646e2e6e65742f3230313331323236313531373434323530)]

1.1.程序计数器

一块较小的内存空间,它是当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变程序计数器的值来选择下一条需要执行的字节码指令,分支跳转循环等基础功能都需要依赖它来实现,每条线程都有一个独立的程序计数器,各线程间的程序计数器互不影响,因此该区域是线程私有的。

当线程在执行一个Java方法时,该计数器记录的是正在执行的虚拟机字节码指令的地址,当线程在执行Native方法(调用本地操作系统方法)时,该计数器为空,另外该内存区域时唯一一个在Java虚拟机规范中没有规定OOM情况的区域。

1.2.Java虚拟机栈

栈是一种先进后出的抽象数据结构,可以用数组和链表实现。

该区域也是线程私有的,它的生命周期也是与线程相同的,虚拟机栈描述的是Java方法执行的模型:每个方法被执行的时候都会同时创建一个栈帧。栈它是用于支持虚拟机进行方法调用和方法执行的数据结构,对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。栈帧用于存储局部变量表、操作数栈,动态链接,方法返回地址和一些额外的附加信息,在编译程序代码时栈帧中需要多大的局部变量表和多深的操作数栈都已经全部确定了,并且写入方法表的Code属性之中。因此,一个栈帧需要分配多少内存,不会受程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

方法的执行与结束对应着当前栈帧的入栈与出栈操作。

  • 局部变量表 顾名思义就是用来存储局部变量的。它是一个 32 位长度的数据,用来存放我们Java中的8大基本数据类型,如果是引用数据类型的局部变量,则只需要存放它在堆内存中的地址。
  • 操作数栈 用来存放我们对方法内部数据操作的次数,所以当一个方法刚执行时,操作数栈是空的。
  • 动态链接
  • 返回地址 正常返回(调用程序计数器中的地址作为返回),异常的话(通过异常处理器表<非栈帧中的>来确定)

1.3.本地方法栈

该区域与虚拟机栈所发挥的作用非常的相似,只是虚拟机栈执行Java方法服务,而本地方法栈则为使用到的本地操作系统方法服务。

1.4.Java堆

Java 堆是Java虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域,几乎所有的对象实例和数组都在这里分配内存,Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称为GC堆。Java堆又被成为新生代。

根据Java虚拟机规范的规定,Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可,如果堆中没有内存可分配时,并且也无法扩展时,就会抛出OOM异常

1.5.方法区

方法区也是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量,即时编译器编译后的代码等数据,方法区域又被称为永久代,即垃圾回收进程很少会收集该区域的内存,因此应控制使用。

Java虚拟机规范把方法区描述为Java堆的一个逻辑部分,而且它和Java堆一样不需要连续的内存,固定大小可扩展,另外虚拟机规范允许该区域可以选择不实现垃圾回收,相对而言,垃圾收集行为在这个区域比较少出现,该区域的内存回收目标主要是针对废弃常量的和无用类的回收。运行时常量池是方法区的一部分,class文件中除了有类的版本字段方法接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后放到方法区的运行时常量中,运行时常量池相对于class文件常量池的另一个重要特征就是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置class文件中的常量池的内存才能进入方法区的运行常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用的比较多的是String类的intern()方法。

根据Java虚拟机规定,当方法区无法满足内存分配需求时,将抛出OOM异常。

2.内存泄漏和内存溢出的差别

内存泄漏是指分配出去的内存没有被回收回来,由于失去了对该内存区域的控制,因而造成资源的浪费,Java中一般不会产生内存泄漏,因为有垃圾回收器自动回收垃圾,但这不是绝对的,当我们new了一个对象,并保存了其引用,但是后面没有用它,而垃圾回收器又不会去回收它,这就造成了内存泄漏

内存溢出是指程序所需要的内存超出了所能分配的内存的上限。

3.类型擦除

Java语言在JDK1.5后引入的泛型实际上只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原声类型,并且在相应的地方插入强制转化代码,因此对运行期的Java语言来说,ArrayList和ArrayList就是同一个类,所以泛型技术实际上就是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型。

下面是一段简单的Java泛型代码:

Map<Integer,String> map = new HashMap<Integer,String>();  
map.put(1,"No.1");  
map.put(2,"No.2");  
System.out.println(map.get(1));  
System.out.println(map.get(2)); 

将这段代码编译成class文件,然后再用字节码反编译工具进行反编译之后,将会发现发型都变会了原生类型,如下变所示

Map map = new HashMap();  
map.put(1,"No.1");  
map.put(2,"No.2");  
System.out.println((String)map.get(1));  
System.out.println((String)map.get(2)); 

为了更详细的说明类型擦除,再看看如下代码:

import java.util.List;  
public class FanxingTest{  
	public void method(List<String> list){  
    	System.out.println("List String");  
	}  
	public void method(List<Integer> list){  
    	System.out.println("List Int");  
	}  
}

用Javac编译器进行编译的时候,会报错

FanxingTest.java:3: 名称冲突:method(java.util.List<java.lang.String>) 和 method
(java.util.List<java.lang.Integer>) 具有相同疑符
public void method(List<String> list){
^
FanxingTest.java:6: 名称冲突:method(java.util.List<java.lang.Integer>)
 和 method(java.util.List<java.lang.String>) 具有相同疑符
public void method(List<Integer> list){
^

这是因为泛型List和List编译后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样,在class文件结构一文中讲过,class文件不能存在特征签名相同的方法。

4.对象创建方法,对象的内存分配,对象的访问定位

对内存分配情况分析最常见的实例便是对象实例化 Object obj = new Object();

这段带密码的执行会涉及Java栈、Java堆、方法区三个重要的内存区域,假设该语句出现在方法体重,及时对JVM虚拟机不了接的Java使用者,应该也知道obj会作为引用类型的数据保存在Java的本地变量表中,而会在Java堆中保存该引用的实例化对象,但可能并不知道Java堆中还必须包含能查到此对象类型的地址信息,这些类型数据则保存在方法区中。

另外,由于reference类型在Java虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java堆中的对象的具体位置,因此不同虚拟机实现对象访问方式会有所不同,主流的访问方式有两种,使用句柄池和直接使用指针。

5.类加载的五个过程:加载 验证 准备 解析 初始化

类加载过程

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括加载、验证准备解析初始化使用卸载。

其中类加载过程包括了加载、验证、准备和初始化这四个阶段发生的顺序也是确定的。而解析阶段则不一定,它在某些情况下可以在初始化之后开始,这是为了支持Java语言的运行时绑定,另外注意这里的几个阶段是按顺序开始的,而不是按顺序完成的,因为这些阶段通常都是互相交叉混合进行的,通常在一个阶段执行的过程中调用或者激活另一个过程。

这里简要说明一下Java中的绑定:绑定指的是把一个方法的调用与方法所在的类(主体方法)关联起来,对Java来说,绑定可以分为动态绑定和静态绑定

  • 动态绑定:即前期绑定,在程序执行前方法已经被绑定,此时由编译器或其他连接程序实现,针对Java,简单的可以理解为程序编译器的绑定,Java当中方法只有final,static,private和构造方法是前期绑定的。
  • 静态绑定:即晚期绑定,也叫运行时绑定,在运行时根据具体对象的类型进程绑定,在Java中,几乎所有方法都是后期绑定的。

加载阶段是类加载过程的第一个阶段,在此阶段,虚拟机需要完成以下三件事:

  • 1.通过一个类的权限定名来获取定义此类的二进制字节流。
  • 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 3.在Java堆中生成一个代表这个类的Java.long.class对象,作为方法区这些数据的访问入口

总结:根据类的全路径名称获取此类的二进制字节流,将字节流放进方法区,在堆中创建指向该字节流的对象。

验证是连接阶段的第一步,这一阶段的目的是为了确保class文件的字节流中包含信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,

准备阶段是为类的静态变量分配内存并且将初始化为默认值,这些内存都将在方法区中进行分配,此阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程

类初始化是类加载过程的最后一步,前面的类加载过程,除了在加载用户应用程序可以通过自定义类的加载器参与之外,其余动作完全由虚拟机主导和控制,到了初始化阶段,才真正开始执行类中定义的Java程序代码。

6.双亲委派模式,Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。

  • 1.启动类加载器,负责将存放在<JAVA_HOME>\lib目录汇中的,或被-Xbootclasspath参数所指定的路经中,并且是虚拟机识别类库加载到虚拟机内存中,启动类加载器无法被Java程序直接引用
  • 2.扩展类加载器,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用该类加载器。
  • 3.应用程序类加载器,负责加载用户路径上所指定的类库,开发者可以直接使用该类加载器,也就是默认的加载器,三种加载器的关系;启动类加载器->扩展类加载器->应用程序类加载器->自定义类加载器。

这种关系即为类加载器的双亲委派模型,其要求除启动类加载器外,其余类加载器都应有自己的父类加载器,这里类加载器之间的父子关系一般不以继承关系实现,而是用组合的方式来复用父类的代码。

双亲委派模型的工作过程:如果一个类加载器接收到了类加载的请求。它首先把这个请求委托给他的父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求都应该传递到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试去自己加载。

好处:Java类随着它的加载器一起就被了一种带有优先级的层次关系,例如类Object,它存放在rt.jar中无论哪个类加载器要加载这个类,最终都会委派给启动类加载进行加载,因此Object类在程序的各种类加载环境中都是同一个类,相反如果自己写了一个Object类,并且放到程序的classpath总,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也无法保证,应用程序也会变得一片混乱。

实现:在java.long.classloader的loadclass方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadclass方法,若父加载器为空则默认使用启动类加载器作为父加载器,如果父加载是被则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

7.分派:静态分配和动态分派

静态分派和重载有关,虚拟机在重载时通过参数的静态类型,而不是运行时实际类似作为判定依据,静态类型在编译期是可知的,动态分派与重写有关,invokevirtual(调用实例方法),指令执行的第一步就是运行期确定接受者的实际类型,根据实际类型进行方法调用。

逃逸分析

栈上分配 虚拟机提供的优化技术,基本思想是:对于线程私有的对象,将它们分配在Java虚拟机栈上,而不是堆上,原因是: 虚拟机栈是线程私有的,对象可以随着方法调用自行销毁,不需要进行垃圾回收,提高了性能,因为GC进程运行时,所有的进程都会停止(Stop The World)。

栈上分配需要的技术基础: 逃逸分析。逃逸分析的目的是判断对象的作用域是否会逃逸出方法体。任何可以在线程间共享的对象,一定属于逃逸对象

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值