WEB入门浅谈22

JVM

JVM全称 Java Virtual Machine。就是java的虚拟机,虚拟机就是软件模拟出来的 “计算机” ,是一个运行平台。虚拟机诞生的意义就是为了跨平台。
最初,操作系统种类很多,CPU种类也很多,C/C++就遇到一个致命难题,写的代码换平台后就很难运行通过。于是后面的语言如 java python go等都带有虚拟机(但是现在基本都不用考虑跨平台的问题)
而虚拟机的作用就是把字节码转化成 操作系统/CPU 可以识别的二进制指令
JVM要做的事情:
类加载
执行引擎(解释执行字节码)
动态内存管理

JVM的内存区域划分

在这里插入图片描述
堆:new的对象,都是在 堆 这个区域,堆也是占据JVM空间最大的区域
方法区:我们写代码时是在.java文件中,而编译后生成的.class二进制字节码文件在程序运行的时候,就会把这个字节码文件加载到内存(方法区)里。(也就是存放类对象)
栈:JVM栈和本地方法栈( java代码的调用栈 和 JVM内部C++代码的调用栈 )(现在已经不区分了)
程序计数器:放入一个内存地址(CPU在运行时就会读取这个地址,执行这个地址的指令)

可以通过 -xss 配置栈的大小,超出栈的最大大小,就会StackOverflowExecption
可以通过 -xmx -xms 来配置堆的大小,超出堆的大小,就会OutOfMemoryExecption
如果出现这栈溢出,就排查是否代码中出现无限递归的情况
如果出现堆溢出,就排查是否内存空间占用合理,以及是否存在内存泄露

垃圾回收

垃圾回收机制(GC):为了能够 自动判断某个对象是否应该回收,如果可以回收了,就对其进行回收
垃圾回收主要做两件事:明确 谁是垃圾,明确 如何回收垃圾
垃圾:这个对象以后没有被使用了,于是这个对象就叫垃圾
涉及到内存管理,就需要考虑:
什么时候申请内存[时机明确],new对象
什么时候是释放内存[时机隐晦],仔细分析代码
内存泄露:表示用完的内存没有及时回收,导致内存越来越小的行为
如果是一个客户端程序,存在内存泄漏,那么问题不大
如果是一个服务器程序,存在内存泄漏,那么问题就比较大了

如果是人工保证 释放内存 ,是比较容易出现内存泄露问题的
如果是引入了垃圾回收机制,就可以很大程度上避免内存泄漏(并非完全避免)

垃圾回收具体回收:
堆(主要):主要回收一些不再使用的内存
方法区(偶尔回收)
栈和程序计数器不需要回收,栈上的内存会自动回收,方法的结束就表示栈上的内存被回收
垃圾回收就是以 对象 为单位的,而不是以 字节 为单位

垃圾回收的缺点:
内存回收没有那么及时
消耗一些额外的资源
STW问题(stop the world) 影响程序的性能

识别垃圾的手段

1、引用计数(java中没用到)
2、可达性分析(java中采取的手段)
引用计数
创建该对象时,给该对象搭配一个计数器,每有一个引用指向该对象时,计数器+1,每有一个引用被销毁时,计数器-1。当计数器为0时,这个对象就没人引用,也就属于垃圾了
引用计数虽然是一个鉴别垃圾比较简单的方法,但是也有缺陷,要求引用计数的自增和自减为线程安全的。(计数器不是线程安全,容易计数错误)
引用计数可能出现 循环引用 的问题(两个对象的引用分别指向对方,此时两个对象应该都为垃圾,但是由于互相引用,计数器无法归0,就无法被回收)

可达性分析
java里的对象都是通过引用来获取到的,一个引用可以指向一个对象,一个对象还可能包含若干个引用,这中引用关系,就形成了一个图状结构,在图状结构里的,就是可达的,不在图状结构里的,就是不可达的,一旦不可达,就会被回收(从GCRoot出发,能访问到就是可达的,访问不到就是不可达的)(引用是单向的 ,A引用B,A就印象B,B不一定影响A)
JVM会有一个专门的线程定期去扫描对象之间的引用关系,识别哪个对象已经引用不到(是垃圾),就对其进行回收(从三个地方开始遍历,分别是:栈上局部变量表中的引用,常量池中的引用指向对象,方法区中的静态引用类型的属性)

垃圾回收算法

垃圾确定后具体如何回收,在垃圾回收机制里有很多的算法(解决问题的思路,大的方向并不是具体的实现)来处理垃圾

标记清除

先标记出垃圾,再进行清除[简单粗暴]
标记的方式就是可达性分析,可达的对象之外都标记为垃圾。
清除:释放对象对应的内存空间
缺点:可能会产生很多内存碎片(空闲空间与已使用的空间穿插开来,而申请空间时,一般都申请一片连续的空间)(日常开发不会考虑这个问题,JVM和操作系统会帮我们把内存碎片妥善处理)

复制算法

可以解决内存碎片的问题
把内存分为两份,用其中的一份,其余一份备用,垃圾回收后,把剩余的对象拷贝到没用过的内存里,这样一来,就不存在内存碎片的问题了(把不是垃圾的对象拷贝到另一半内存中,然后整体回收这一半的内存)
缺点:有局限性,如果剩余的对象太多,就会变得很低效
内存利用率不高,始终要留一半的内存空间

标记整理

采取类似顺序表删除元素的手段(挪元素)
缺点:内存搬运操作比较频繁,效率不高

分代回收

把垃圾回收的过程分成了几个场景,不同的场景运用不同的方式进行回收
如(实际可能更多的区域):把内存分为两个区域,一个放新的,一个放旧的(基于一种规律:一个对象存货的越久,那么就认为这个对象可以一直存活下去),然后每个区域进行标记整理
确定一个对象的存活时间:根据这个对象躲过GC的轮次(也就是看周期)
核心思路:根据对象的年龄来预测生命周期。认为年龄越大,生命周期就越长
新区域扫描概率高
老区域扫描概率低
如果有一个占用内存很大的对象,就不适合在幸存区复制,就直接来到老的区域


以上的都是一些抽象的算法,实际实现的是基于这些基本的算法,但是可能会在细节上进行一些调整

垃圾回收器

垃圾回收器有很多种,可以通过 JVM 的一些参数来配置使用哪种垃圾回收器,但是大部分情况都不用我们手动配置 (CMS回收器、G1收集器…)
判断一个垃圾回收器:
回收的空间效率 一遍回收,是否可以回收干净
回收速度 一遍回收需要的时间
是否允许和应用程序并行 回收是否会影响其它的工作
回收器是否多线程
回收的时间是否可预测

类加载

类加载就是JVM把.class文件的内容加载到内存中,只有把程序加载到内存了,才可以正常运行。
C++没有类加载,但是有类似类加载的模块加载
类加载的步骤:
加载:找到.class文件,解析文件格式,读取到内存中
连接:类和类之间需要配合(如类A会用到类B),就需要把依赖的类也加载
初始化:对类对象进行初始化(初始化静态成员,静态代码块)
类加载最终的结果就是得到了类对象,在代码中可以通过 类名.class 这样的方式获取到类对象

初始化流程
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 Demo05 extends B{
    public static void main(String[] args) {
        System.out.println("开始");
        new B();
        new B();
        System.out.println("结束");
    }
}

运行结果:
在这里插入图片描述
要想执行main方法,就要先加载Demo05,而Demo05又继承自B,B继承自A,所以最先加载A,然后加载B,最后加载Demo05类。
然后开始执行main方法,打印开始后,开始创建B实例,执行B的构造方法前,由于B是A的子类,就需要先执行A的构造方法,在A执行构造方法之前,就会先执行A的代码块,然后执行A的构造方法,之后执行B的代码块,B的构造方法。
所以代码的执行顺序应该为:A的静态代码块,B的静态代码块,开始,A的代码块,A的构造方法,B的代码块,B的构造方法,A的代码块,A的构造方法,B的代码块,B的构造方法,结束。

双亲委派模型

负责类加载工作的模块叫做 类加载器 ,一个JVM在工作过程中会有很多类加载器一起工作,这些类加载器,默认下都有父子的关系,一个类加载器只有一个父亲。(虽然叫双亲,但实际只有一个父亲,不存在两个父亲的情况)
JVM里默认的类加载器主要有三个:
BootstrapClassLoader 用来加载Java标准库中的类。
ExtensionClassLoader 用来加载JVM扩展出来的类。
ApplicationClassLoader 用来加载用户自定义类。
这三个类加载器的关系为:BootStrapClassLoader 最大,ExtensionClassLoader 其次,ApplicationClassLoader 最小。
类加载器:负责类加载,根据类名找到对应的类的.class文件。双亲委派模型就是在 找文件的过程中起到的作用。
如:找一个java.lang.String的类,那么就先由 ApplicationClassLoader 进行查找,并不会立刻就去寻找,它先会询问父亲 ExtensionClassLoader 能不能找到,而ExtensionClassLoader 也不会立刻寻找,而是询问BootstrapClassLoader ,BootstrapClassLoader 没有父亲了,就会自己去找,如果可以找到,然后就对其进行加载,如果找不到,就告诉儿子,让儿子去找,如果到最后的儿子还是没有找到,就会抛出 ClassNotFind 异常
这种做法就是为了防止自己写的类与标准库里的类重名的情况

补充

(加载在方法区里的二进制字节码文件)类对象就描述了每个类的具体:
类有哪些属性、属性名字、属性类型、属性的访问限定修饰符等
类有哪些方法、方法名字、方法返回值、方法参数、方法的访问限定修饰符等
如果要new这个类的实例时,就会根据类对象来决定给这个实例分配多少内存
调用某个方法,就是从方法区里读取指令并执行的


CPU执行指令时,就会根据程序计数器中的地址来从内存中读取对应的指令到CPU中


内存不释放是否可行(站在进程的角度来看,如果进程结束,那么这些内存也就随之释放)
原则上,来说是不行的,但是实际上,这种操作还是比较常见的。
如果不及时释放内存,就会出现内存泄露,内存一直被使用,随着时间的推移,剩下的空闲内存就会越来越小,如果再想申请内存,就会出现申请不到的情况,如果一旦申请失败,程序可能就直接奔溃了。(内存不够与卡不卡没有太大的关系)(一个比较复杂的项目很难完全没有 内存泄露 的问题,所以一般都会采取 例行重启 来解决。通过Linux/Windows的定时任务)


java中的四种引用:
强引用(平时用的引用)既可以找到对象,也能决定对象的生死
软引用 能找到对象,只在一定程度上决定对象的生死
弱引用 能找到对象,不能决定对象的生死
虚引用 不能找到对象,也不能决定对象的生死


分代回收:
新的区域 = 新生代 = 伊甸区+幸存区(不绝对),老的区域 = 老年代
新生对象于伊甸区
有一个经验表明:绝大多数的对象都活不过一轮GC
熬过一轮GC就进入了幸存区,幸存区撑过一轮GC就可以通过复制算法来到另一个幸存区,如此反复进行这个过程。
年龄到一定成都后,才会进入老的区域,就认为这个对象可以一直存活
因此,老的这个区域被回收的概率就大大降低了


相关术语
Particial GC 进行部分内存区域的垃圾回收
Full GC 针对全部内存进行回收
Minor GC 进行部分内存区域的垃圾回收(一般指新生代)
Major GC 进行大部分内存区域的回收(一般针对新生代+老年代)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值