本篇文章给大家带来了关于java的相关知识,其中主要整理了JVM的相关问题,包括了JVM内存区域划分、JVM类加载机制、VM的垃圾回收等等内容,下面一起来看一下,希望对大家有帮助。
推荐学习:《java视频教程》
一.JVM内存区域划分
JVM为什么要划分出这些区域呢?JVM内存是从操作系统里面申请过来的,而JVM就根据功能需求将这些划分成了一些小的模块,这样一块大的场地就可以划分成一些小的模块,然后每个模块就负责自己的功能就可以了,那接下来看看这些区域的功能到底是什么呢!
1.程序计数器
程序计数器是内存中最小的区域,这里面主要保存了下一条要执行的指令的地址在哪里(指令就是字节码,一般程序要运行,JVM就需要把字节码加载出来放到内存中,然后程序再把一条一条的指令从内存中取出来放到CPU上去执行,所以必须要记住当前执行到哪一条指令,以及下一条在哪里,因为CPU不是只给一个进程提供服务的,是给所有的进程都提供服务,是并发式的执行程序的,又因为操作系统是以线程为单位进行调度执行的,所以每个线程都要有自己的执行位置,也就是每一个线程都需要有一个程序计数器来记录位置!)
2.栈
栈里面存放的主要是局部变量和方法调用信息,只要涉及到新方法的调用,就会有"入栈"的操作,每执行完成一个方法,就会有"出栈"的操作,而且栈也是每个线程都有一份的
因此对于递归来说,一定要控制好递归条件,否则很有可能会出现栈溢出(StackOverflowException)异常的!
3.堆
堆是内存中空间最大的区域,而且堆是每个进程只有一份的,进程中的多个线程公用一个堆,里面主要存放着new出来的对象以及对象的成员变量,例如String s = new String()如果在方法里面这里的s就是局部变量是在栈上的,如果这个s是成员变量,就是在堆上的,而后面new String()是对象的本体,对象是在堆上的,这是容易混淆的地方,另外堆还有一个重要的点就是关于垃圾回收问题,这个后面再详细介绍!
4.方法区
方法区中存放的是"类对象",平常所写的.java代码经过编译器翻译过后就会变成.class(二进制字节码),然后.class就会被加载到内存中,也就被JVM构造成了类对象(加载的过程就是称为"类加载"),而这些类对象就会存放到方法区中,这里面就具体描述了类长啥样(类的名字,类的成员及其成员名成员类型,类的方法及其方法名方法类型,以及一些指令…另外类对象里面还存放了一个很重要的东西,就是静态成员,一般被static修饰的成员就成为了类属性,而普通的方法被称为实例属性,这是有很大差别的)!
上面所介绍的是JVM中比较常见的区域,而一些JVM的内存区域划分不一定是符合实际情况的,JVM在实现的过程中区域的划分是不尽相同的,不同的厂商不同版本的JVM都是有可能存在差异的,不过对于我们普通的程序员而讲,只要不是去实现JVM,那么就不需要了解那么深刻,讲上面的几个常见的区域加以了解就可以了!
二.JVM类加载机制
类加载其实是设计一个运行时环境的一个重要的功核心功能,这是非常重量级的,因此我这里也就简单介绍一下!
上述就是类加载的具体过程,最后面的Using和Unloading就是使用的过程就不介绍了,就介绍一下前面的三个大的步骤:
1.Loading(加载)
在loading阶段就会先找到对应的.class文件,然后打开并读取(根据字节流).class文件,同时初步生成一个类对象,这个和完成的类加载(class Loading)是不相同的,不要弄混淆了!
class文件的具体格式(如果要实现一个Java编译器就得按照这样的格式来构造,实现JVM就得按照这个格式来进行加载!):
观察这个格式就可以看到.class文件就把.java文件中的核心信息都表述进去了,只不过组织格式上发生了转变,所以loading环节就会把读取到的信息,初步填写到类对象中
2.Linking(连接)
连接一般就是建立好多个实体之间的联系
2.1.Verification(验证)
Verification就是一个校验的过程,主要就是验证读到的内容是不是和规范中规定的格式完全匹配,如果发现读到的数据格式不符合规范,就会类加载失败,并且抛出异常!
2.2.Preparation(准备)
Preparation阶段是正式为定义的变量(静态变量,就是static修饰的变量)分配内存并设置类变量初始值的阶段,就会给每个静态变量分配内存,并且设置为0值!
2.3.Resolution(解析)
Resolution阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程,.class文件中常量是集中放置的,每个常量会有一个编号,而在.class文件中的结构体里初始情况就只是记录的编号,然后就可以根据这个编号找到对应的内容,再填充到类对象中!
3.Initialization(初始化)
Initialization阶段就是真正的对类对象进行初始化(根据写的代码),尤其是针对静态成员
4.典型的面试题
class A { public A(){ System.out.println("A的构造方法"); } { System.out.println("A的构造代码块"); } static { System.out.println("A的静态代码块"); }}class B extends A{ public B(){ System.out.println("B的构造方法"); } { System.out.println("B的构造代码块"); } static { System.out.println("B的静态代码块"); }}public class Test extends B{ public static void main(String[] args) { new Test(); new Test(); }}
可以自己先尝试写一下输出的结果
做这样的题就需要把握几个大的原则:
-
类加载阶段就会进行静态代码块的执行,要想创建实例,势必要先进行类加载
-
静态代码块只是类加载阶段执行一次,其他阶段都不会再执行
-
构造方法和构造代码块每次实例化都会执行,而且构造代码块会在构造方法前面执行~~
-
父类执行在前,子类执行在后!
-
程序是从main开始执行的,main的Test的方法,因此要执行main就需要先加载Test类
-
只有涉及到这个类了,类里面的东西才会被加载
输出结果: A的静态代码块 B的静态代码块 A的构造代码块 A的构造方法 B的构造代码块 B的构造方法 A的构造代码块 A的构造方法 B的构造代码块 B的构造方法
5.双亲委派模型
这个东西是类加载中的一个环节,处于Loading阶段(比较靠前的部分),双亲委派模型描述的就是JVM中的类加载器,如何根据类的全限定名(java.lang.String)找到.class文件的过程。这里的类加载器是JVM专门提供的对象,主要负责进行类加载,所以找文件的过程也是由类加载器来负责的,.class文件可能放置的位置有很多,有的要放到JDK目录里面,有的放到项目目录里面,还有的在其他特定的位置里面,因此JVM提供了多个类加载器,每个类加载器负责一个片区,而默认的类加载器主要有3个:
-
BootStrapClassLoader:负责加载标准库中的类(String,ArrayList,Random,Scanner…)
-
ExtensionClassLoader:负责加载JDK扩展的类(现在很少用到)
-
ApplicationClassLoader:负责加载当前项目目录中的类
-
另外程序员还可以自定义类加载器,来加载其他目录中的类,Tomcat就自定义了类加载器,用来专门加载webapps里面的.class
双亲委派模型就描述了这个找目录的过程,也就是上述类加载器是如何配合的
考虑找一下java.lang.String:
-
程序启动,就会先进入ApplicationClassLoader类加载器
-
ApplicationClassLoader类加载器就会检查下,它的父加载器是否已经加载过了,如果没有,就调用父 类加载器ExtensionClassLoader
-
ExtensionClassLoader类加载器就会检查下,它的父加载器是否已经加载过了,如果没有,就调用父 类加载器BootStrapClassLoader
-
BootStrapClassLoader类加载器也会检查下,它的父加载器是否已经加载过了,然后发现没有父亲,于是就扫描自己负责的目录
-
然后java.lang.String这个类就在标准库中能找到,然后后续就由BootStrapClassLoader加载器负责后续的加载过程,查找环节就结束了!
考虑找一下自己写的Test类:
-
程序启动,就会先进入ApplicationClassLoader类加载器
-
ApplicationClassLoader类加载器就会检查下,它的父加载器是否已经加载过了,如果没有,就调用父 类加载器ExtensionClassLoader
-
ExtensionClassLoader类加载器就会检查下,它的父加载器是否已经加载过了,如果没有,就调用父 类加载器BootStrapClassLoader
-
BootStrapClassLoader类加载器也会检查下,它的父加载器是否已经加载过了,然后发现没有父亲,于是就扫描自己负责的目录,没扫描到,就会回到子加载器中继续扫描
-
ExtensionClassLoader扫描自己负责的目录,也没有扫描到,再回到子加载器中继续扫描
-
ApplicationClassLoader也扫描自己负责的目录,自己写的类就在自己的项目目录下,因此就能找到,然后后续的类加载就由ApplicationClassLoad完成,此时查找目录的环节就结束了~~(另外如果ApplicationClassLoader也没有找到们就会抛出ClassNotFoundException异常)
这一套查找规则就称为双亲委派模型,那为啥JVM要这样设计呢,理由就是一旦程序员自己写的类和全限定类名重复了,也能够成功加载标准库中的类,而不是自己写的类!!!
另外如果是自定义的类加载器,要不要遵守这个双亲委派模型呢?
答案是可以遵守也可以不遵守,主要看需求,例如Tomcat加载webapp中的类,就没有遵守,因为遵守了上面的类加载器也是不可能找到的!
三.JVM的垃圾回收
JVM中的垃圾回收机制(GC),一般在写代码的时候,经常就会涉及到申请内存,例如创建一个变量,new一个对象,调用一个方法,加载类…而申请内存的时机一般是明确的(需要保存某个或某些数据就需要申请内存),但是释放内存的时机,却是不那么清楚的,释放的早了也不行(如果还是要使用的,结果已经被释放了这就让其无内存可用了,就让这些数据"无处可去"),释放的晚了也不行(释放晚了,大量的囤积很有可能让可用内存逐渐变少,很有可能会出现内存泄漏问题,就是无内存可以使用),因此内存的释放要恰到好处才好!
而垃圾回收的本职是靠运行时环境额外做了很多的工作来完成释放内存操作的,这让程序员的心智负担大大降低了,但是垃圾回收也是有劣势的:①消耗额外的开销(消耗资源耕