网络编程6(JVM)
在学习Java的时候会接触到这样三个概念:
JDK(Java开发工具包,写/编译Java代码时需要的内容)
JRE(JAVA运行环境,运行Java代码需要的环境)
JVM(Java虚拟机,JRE的核心模块)
传统的虚拟机,其实是通过软件的来模拟出硬件,构造出新的电脑,甚至可以在上面安装新的系统。而Java中的虚拟机,其实不能模拟出电脑上的所有硬件设备(只有一部分),只能运行Java的代码,所以JVM其实是Java语言的“运行环境”,JVM的核心功能其实就是将Java语言翻译成CPU的指令。这样的做法相较于C/C++是低效的,后者写出的程序就是标准的CPU指令,但是带来的好处就是Java可以做到跨平台和操作系统进行运行,同一个程序,可以在不同的操作系统和不同架构的CPU上进行运行(不同架构的CPU支持的指令是不同的,要通过不同的编译器进行翻译,得到CPU可以执行的指令),这样的操作在C/C++上是十分麻烦的,要想在不同的操作系统进行运行就需要学习不同操作系统的API,而Java中的API就是提供了很多版本的API,这些API在Java代码上都是统一要求的,这样在不同操作系统下JVM就可以将Java语言翻译成不同的CPU指令,这样也就弥补了编译时的时间长,转而提高了开发的效率。
JAVA程序是怎么运行的
1.当我们在编译器上写了Java代码时,就会生成了.java文本文件。
2.编写之后就会通过javac这样的命令行工具,将.java这样的文件编译成.class文件,一个.java文件中有多个.class文件,我们在.java中写的所有class都对应着一个.class文件。
3.通过Java这样的命令行工具,就可以运行.class文件,java命令行工具对应到一个Java进程,这个进程就可以表示一个JVM进程,jvm进程就可以对class文件进行解释了。
JAVA中的抛出异常就是在过程3进行的。
JAVA的内存划分
JAVA程序运行起来得到的JAVA进程需要申请一块内存空间,会把这一块内存空间划分成多个区域,不同区域的执行功能不一样,简单的来说,JAVA中的内存划分主要是
程序计数器(PC):
非常小的一个内存空间,一般只有八字节,用来存储“地址”,描述当前Java程序要运行的下一个字节码位置,一个Java进程中有多个程序计数器,每一个线程都有一个程序计数器来记录。程序计数器就类似于书签。
程序计数器中的值Java代码控制不了。
元数据区(方法区):
元数据区里是存储方法相关的指令,更准确地说其实是类的指令。Java中创建对象要基于类来执行,对象是在内存中存储的,类当然也是,所以会在元数据区中存储有关类的信息,例如:类的名字,类中的方法和属性等,.class文件就通过二进制的形式表示上面的内容。
类在JVM中是单例的,在一个Java进程中一个类对象只能有一个,由JVM自身控制的。
元数据区中的值也是Java代码控制不了的。
栈
这个区域中保存着方法间的调用关系,把JVM中的栈称为“栈帧”,每一个栈帧就代表着一次方法的调用,栈帧中有方法的参数,方法中的局部变量,方法执行结束后跳转回的地址和返回值的结果。
每个线程都有一个栈,记录着当前线程中的方法的调用关系。
堆
堆中存放着new出来的对象,对象所在的空间就在堆中。使用图形理解更直观:
Tast中的方法不管new出多少对象,都只有一份,而像n这样的成员变量就是每new一次就会重新创建。
空间大小及数量分配:
所以局部变量存在栈中,成员变量存在堆中,静态成员变量存在元数据区。
类加载
类加载其实就是JVM从最开始读取.class文件,到最后的构造完整个类对象的整个过程。
类加载有五个步骤:
步骤一:编写
1)根据代码中的“全限定类名”(包名+类名)找到.class文件(寻找的过程就是双亲委派模型)。
2)打开文件,将文件中的数据加载到内存中。
3)将数据结构进行解析
.class文件是二进制的,要如何解析,数据的结构是怎么样的。这些问题在Java文档中都有解释
,.class文件中包含了类的成员,类的编号,类的父类等
步骤二:验证
校验上面.class文件读出来的内容是否是合法的,如果在校验过程中格式出现错误,就要及时报错。
步骤三:准备
给类对象分配空间,此处申请的内存空间是一个未初始化的内存空间,空间上的每一个字节都是全“0”,此时类对象中的静态成员也就是0。
步骤四:解析
针对常量进行解析,类对象中会涉及到一些常量,常量通常也是要放在内存中的。需要把class文件中的常量加载到内存中。
步骤五:初始化
这里就会针对用户写的代码进行初始化,类的静态成员就要真正初始化了,此时例如静态成员的赋值操作就要开始了,静态代码块,针对父类和实现接口的加载就要开始了
什么时候涉及到类加载
类加载是更倾向于“懒汉模式”,相较于JVM一次性全部将.class加载,“懒汉模式”其实更加科学。当我们用到一个类的时候,再去加载。那么什么是用到一个类呢
1)创建类的实例时。
2)调用类的静态方法/类的静态成员。
3)使用子类的时候,也会触发父类的加载。
双亲委派模型
双亲委派模型是类加载中的一个环节,也就是JVM在寻找.class文件的方法。JVM进行类加载操作,需要使用JVM中的一个模块(类加载器 class loader )
类加载器JVM自带三种
Bootstrap loader :
负责在JAVA标准库中的寻找。
Extension loader :
负责在扩展库中寻找。
Application loader :
负责在第三方库/当前项目中寻找。
这三个类加载器之间存在“父子”关系(不是继承关系),B加载器最大,E加载器第二,A加载器最小加载器之间的关系是固定的由源码决定。A加载器是类加载的入口,当有任务时会先会进入A加载器,但是A加载器不会自己进行查找,而是交给自己的上一级E加载器,E加载器也不会进行查找,而是再次交给自己的上一级B加载器。当B加载器拿到任务,此时B就会发现自己的上级是NULL,此时就这个B加载器就会自己开始查询,如果没有就会从小交给下面的加载器,直到找到,然后继续加载。如果没有找到就会抛出异常。
双亲委派模型,就是为了保证类加载的优先级,例如我们自己写的类和标准库中的类重名,此时就还是加载标准库中的类。
JAVA垃圾回收机制(GC)
在C/C++中,申请用完的内存空间是需要手动进行释放的,不然会导致内存泄漏这样的问题,而Java中引入了垃圾回收机制来处理内存释放,垃圾回收机制大大解放了程序员,提高了工作效率,使内存释放由手动变为自动,但是GC也会降低运行速度。
GC在执行时会触发STW(stop the world)问题,其他线程就无法继续工作了。
1.GC回收的区域是哪个部分?
主要是堆
2.GC回收是以字节为单位的吗
不是的,主要是以对象为单位。当内存中出现正在使用的对象,GC不会回收;出现没使用的对象,也不会回收;出现使用一部分的对象,此时也不会回收。按照对象维度进行回收更加方便,如果按照字节维度进行回收,此时就需要表示出每个对象哪些需要回收,哪些不需要,这样就更加麻烦。
3.如何回收
第一是找到垃圾,找到哪些对象是不继续使用了;第二是释放内存。那么接下来的问题就是如何找到垃圾了。
如何找到垃圾
在Java中使用对象,往往是通过“引用”来使用的,使用对象,无非就是通过实例化对象来访问对象中的成员和属性,通过 . 的方式,而 . 前面的就是指向对象的引用。如果没有任何一个引用指向这个对象,就可以认为这个引用是无用的。所以如何找的垃圾就转换成如何判断一个对象是否有引用指向。
在《在深入理解JVM》一书中就介绍了两种方法:引用计数和可达性分析(引用计数不是使用在JAVA而是在别的语言中使用)
如果是在JVM中进行垃圾的查找就只有可达性分析,如果是垃圾回收机制中就是有两种方式。
1.引用计数
引用计数是通过一个计数器来记录一个对象是被多少引用指向。例如当我们通过一个引用指向一个实例时,此时在堆中的对象中就有一个计数器会记录,当计数器为0时就代表这个对象是无用对象 。但是这样的方法有两个缺陷:内存消耗较大和循环引用问题。
内存消耗较大主要是当对象内存较小时,计数器额外的开销。循环引用举例就是两个引用内部的应用都指向对方的对象,此时两个对象中的计数器就会显示有两个引用指向,但是当将这两个引用都设为null时,此时两个计数器都只会减一,就会导致这两个对象无法释放,同时无法通过任何引用访问到这两个对象。
2.可达性分析
在Java中“可以访问的对象”一定是可以通过一系列的操作进行访问的。
例如创建遍历二叉树,就需要通过引用来进行访问,当两个节点之间有引用指向,此时就是可以通过引用指向来进行访问。JVM安排专门的线程,负责上述的“扫描”的过程会从一些特殊的引用开始扫描(GCroots)
1.栈上的局部变量(引用类型)
2.常量池里指向的对象(final修饰的,引用类型)
3.元数据区(静态成员,引用类型)
JVM会通过这些特殊的引用开始访问一些可能被访问的对象,但凡被访问到的对象都可标记为可达的,而JVM就会根据对象列表,对标记剩下的就是垃圾了,这样的操作不需要额外的内存空间,而是在执行过程中触发STW,使用时间换取了空间了。
如何释放内存
内存回收涉及到一些算法
1.标记,清除
标记就是可达性分析,清除就是将内存直接释放(通过free/delete的方式释放)。
但是这样的操作可能导致内存碎片化的情况(总的内存空间是足够的,但是不是连续的),下次申请连续内存的时候就可能会失败。
2.复制算法
这样的算法是为了解决内存碎片化问题,就是划分出两个内存,将不是垃圾的对象放到另一部分,然后将另一半全部清零。这样的方法缺点就是空间浪费严重,如果复制对象太大复制开销就太大。
3.标记,整理
将后面的对象放到前面,类似于顺序表的搬运。但是这个方法也不是健全健美。
4.分代回收(综合方案)
将对区域划分成两个区域,一个是新生代一个老生代。“年龄”就是垃圾回收机制可达性分析扫描的次数,如果每一个对象被扫描一次没被淘汰的,年龄就加一。在新生代区又被分为三个区域伊甸区,幸存区,刚创建的新对象经过一轮GC就进入伊甸区,如果又经过一轮没被淘汰,就进入幸存区(相当于复制算法),往后就一直在两个幸存区中经过一轮又一轮CG,当年龄到达老年的标准就进入老年代区,进入老年代进行CG轮次就会比较少,老年代淘汰的次数就大大减少。