当前位置: 首页 > news >正文

网络编程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轮次就会比较少,老年代淘汰的次数就大大减少。

http://www.xdnf.cn/news/1346455.html

相关文章:

  • 保护 PDF 格式:禁止转换为其他格式文件
  • html基本元素
  • C#_接口设计:角色与契约的分离
  • HTML5详篇
  • 自定义单线通信协议解析
  • Yapi中通过MongoDB修改管理员密码与新增管理员
  • 【Java后端】 Spring Boot 集成 Redis 全攻略
  • 软件设计师——计算机网络学习笔记
  • 华为网路设备学习-29(BGP协议 四)路由策略-实验
  • 分段渲染加载页面
  • 【LeetCode 热题 100】139. 单词拆分——(解法一)记忆化搜索
  • 浏览器开发CEFSharp+X86+win7(十三)之Vue架构自动化——仙盟创梦IDE
  • STM32F1 EXTI介绍及应用
  • 光耦合器:电子世界的 “光桥梁“
  • ZYNQ启动流程——ZYNQ学习笔记11
  • X00238-非GNSS无人机RGB图像卫星图像视觉定位python
  • 25年8月通信基础知识补充1:中断概率与遍历容量、Sionna通信系统开源库、各种时延区分
  • Android 16环境开发的一些记录
  • Prometheus+Grafana监控redis
  • 制造企业用档案宝,档案清晰可查
  • 81 柔性数组造成的一些奇怪情况
  • 农业-学习记录
  • 关于 WebDriver Manager (自动管理浏览器驱动)
  • 当下一次攻击发生前:微隔离如何守护高敏数据,防范勒索攻击下的数据泄露风险!
  • 一、Python IDLE安装(python官网下的环境安装)
  • 腾讯云EdgeOne安全防护:快速上手,全面抵御Web攻击
  • Datawhale AI夏令营---coze空间共学
  • 【图像算法 - 21】慧眼识虫:基于深度学习与OpenCV的农田害虫智能识别系统
  • 关于日本服务器的三种线路讲解
  • 在自动驾驶中ESKF实现GINS时,是否将重力g作为变量考虑进去的目的是什么?