JVM及GC知识整理

1.Java程序执行过程?
大致过程是,程序员编写的.java文件(源文件)通过编译器编译后变成.class文件(字节码),然后被JVM的类加载器加载到内存,通过字节码校验器去做一些校验,校验通过后交由解释器将字节码文件解释成计算机能够识别的机器指令。
步骤:编译–>装载字节码–>校验字节码–>解释字节码–>执行。
.java文件通过javac xxx.java编译成.class文件,其交给JVM执行(即使用java命令),执行java xxx(不带后缀)。java命令将会启动 JVM,并将后面的参数作为初始化类,通过 JVM 的类加载器将字节码文件装载到内存中。
类的加载是通过类加载器进行的,加载完后,先由字节码校验器负责检查那些无法执行的明显有破坏性的操作。除了系统类之外,其他类都要被校验。JVM 把每一条要执行的字节码交给解释器,翻译成对应的机器语言,最终由操作系统执行。
JVM解释执行字节码文件就是JVM操作Java解释器进行解释执行字节码文件的过程。
在这里插入图片描述

2.Java类加载机制?
①什么是类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
②类加载过程(生命周期)
JVM 类加载机制分为:加载Load,链接Link(验证,准备,解析),初始化Initialize。
1)加载:查找并加载类的二进制数据(查找和导入Class文件)
加载阶段JVM需要完成下面三件事情:
1、通过一个类的全限定名来获取其定义的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。
2)验证:确保被加载的类的正确性
验证为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
3)准备:为类的静态变量分配内存,并将其初始化为默认值
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
1.这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2.这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
假设类变量的定义为:public static int value = 3; 那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。
4)解析:把类中的符号引用转换为直接引用
解析阶段是JVM将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
5)初始化:对类的静态变量,静态代码块执行初始化操作
初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
1.声明类变量是指定初始值。
2.使用静态代码块为类变量指定初始值。

3.类什么时候初始化?
①创建类的实例,也就是new一个对象
②访问某个类或接口的静态变量,或者对该静态变量赋值
③调用类的静态方法
④反射(Class.forName(“com.lyj.load”))
⑤初始化一个类的子类(会首先初始化子类的父类)
⑥JVM启动时标明的启动类,即文件名和类名相同的那个类 只有这6中情况才会导致类的类的初始化。

4.JVM加载class文件的原理机制
1.装载:查找和导入class文件;
2.连接:
① 检查:检查载入的class文件数据的正确性;
② 准备:为类的静态变量分配存储空间;
③ 解析:将符号引用转换成直接引用(这一步是可选的)
3.初始化:静态变量,静态代码块。
这样的过程在程序调用类的静态成员的时候开始执行,所以静态方法main()才会成为一般程序的入口方法。类的构造器也会引发该动作。

5.JVM是什么?
虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

6.JVM内存结构
在这里插入图片描述
JVM运行时内存结构有五个:
方法区:存放JVM加载的类信息、类的静态变量、常量、运行时常量池等,方法区的大小是可以动态扩展的。方法区就是常说的永久代。
运行时常量池:属于方法区的一块区域,jvm虚拟机在完成类装载操作后,将class文 件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中 的运行时常量池。
堆:存放的是数组、类的实例对象、字符串常量池等。
程序计数器:是一个比较小的内存空间,用来记录当前线程正在执行的那一条字节码指令的地址。如果当前线程正在执行的是本地方法,那么此时程序计数器为空。
作用:
1.字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制,比如我们常见的顺序、循环、选择、异常处理等。
2.在多线程的情况下,程序计数器用来记录当前线程执行的位置,当线程切换回来的时候仍然可以知道该线程上次执行到了哪里。而且程序计数器是唯一一个不会出现OutOfMeroryError的内存区域。
虚拟机栈:描述JAVA方法运行过程的内存模型,Java虚拟机栈会为每一个即将执行的方法创建一个叫做“栈帧”的区域,该区域用来存储该方法运行时需要的一些信息,包括:局部变量表、操作数栈、动态链接、方法返回地址等;
StackOverFlowError异常:当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。
OutOfMemoryError异常:当线程申请栈时发现栈已经满了,而且内存也全都用光了。
本地方法栈:结构上和Java虚拟机栈一样,只不过Java虚拟机栈是运行Java方法的区域,而本地方法栈是运行本地方法的内存模型。
运行本地方法时也会创建栈帧,同样栈帧里也有局部变量表、操作数栈、动态链接和方法返回地址等,在本地方法执行结束后栈帧也会出栈并释放内存资源,也会发生OutOfMemoryError。

方法区和堆都是线程共享的,在JVM启动时创建,在JVM停止时销毁,而Java虚拟机栈、本地方法栈、程序计数器是线程私有的,随线程的创建而创建,随线程的结束而死亡。
除了以上介绍的JVM运行时内存外,还有一块内存区域可供使用,那就是直接内存。Java虚拟机规范并没有定义这块内存区域,所以它并不由JVM管理,是利用本地方法库直接在堆外申请的内存区域。

7.堆内存分配
在这里插入图片描述
新生代占1/3,老年代占2/3
-XX:NewRatio:老年代和年轻代内存大小的比例
新生代中按8:1:1进行分配,两个幸存区大小需要保持一致
-XX:SurvivorRatio: Eden和Survivor的比值,默认是8(8:1)

8.堆和栈的区别
物理地址
堆:物理地址分配的对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
内存分别
堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
存放的内容
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
程序的可见度
堆对于整个应用程序都是共享、可见的。
栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。

9.JAVA内存模型
Java内存模型(JMM)一套多线程读写共享数据时,对数据的可见性,有序性和原子性的规则。
Java的多线程之间是通过共享内存进行通信的,而由于采用共享内存进行通信,在通信过程中会存在一系列如可见性、原子性、有序性等问题,而JMM就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。
在这里插入图片描述
10.JAVA内存模型的实现
原子性:
一个操作或者多个操作要么全部执行要么全部不执行;
在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。
可见性:
当多个线程同时访问一个共享变量时,如果其中某个线程更改了该共享变量,其他线程应该可以立刻看到这个改变;
Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。
Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。Java中的synchronized和final两个关键字也可以实现可见性
有序性:
程序的执行要按照代码的先后顺序执行;
在Java中,可以使用synchronized和volatile来保证多线程之间操作的有序性。实现方式有所区别:
volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。

可以看出好像synchronized关键字是万能的,可以同时满足以上三种特性,这其实也是很多人滥用synchronized的原因。但是synchronized是比较影响性能的,虽然编译器提供了很多锁优化技术,但是也不建议过度使用。

11.内存泄露和内存溢出
内存泄露:申请了内存用完了不释放,比如一共有 1024M 的内存,分配了 521M 的内存一直不回收,那么可以用的内存只有 521M 了,仿佛泄露掉了一部分;
内存溢出:申请内存时,没有足够的内存可以使用;
关系:内存泄露的增多,最终会导致内存溢出。
通俗一点儿讲,一个厕所就三个坑,有两个站着茅坑不走的(内存泄漏),剩下最后一个坑,厕所表示接待压力很大,这时候一下子来了两个人,坑位(内存)就不够了,内存泄漏变成内存溢出了。
导致内存泄露的原因:
①循环过多或死循环,产生大量对象;
②静态集合类引起内存泄漏,因为静态集合的生命周期和 JVM 一致,所以静态集合引用的对象不能被释放;
③单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性;
④数据库连接、IO、Socket连接等等,它们必须显示释放(用代码 close 掉),否则不会被 GC 回收;
⑤内部类的对象被长期持有,那么内部类对象所属的外部类对象也不会被回收;
⑥内存中加载数据量过大。

12.垃圾判别方法
①引用计数法
判断对象的引用数量来决定对象是否可以被回收,每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1。
优点:执行效率高,程序执行受影响小
缺点:无法检测出循环引用的情况,导致内存泄露
②可达性分析
沿着GC Root对象为起点的引用链找到该对象,找不到则可以回收。

13.GC Root对象有哪些?
①虚拟机(JVM)栈中引用对象
②方法区中的类静态属性引用对象
③方法区中常量引用的对象(final 的常量值)
④本地方法栈JNI的引用对象。

14.垃圾回收算法
①标记清除法
标记没有被GC Root引用的对象,清除被标记位置的内存。
优点:处理速度快
缺点:空间不连续,产生内存碎片。
在这里插入图片描述
②标记整理法
标记没有被GC Root引用的对象,整理被引用的对象。
优点:空间连续,没有内存碎片
缺点:整理导致效率较低。
在这里插入图片描述
③复制算法
分配同等大小的内存空间,标记为GC Root引用的对象,将引用的对象连续的复制到新的内存空间,清除原来的内存空间,交换From空间和To空间。
优点:空间连续,没有内存碎片
缺点:占用双倍的内存空间在这里插入图片描述在这里插入图片描述在这里插入图片描述
15.分代垃圾回收机制
在这里插入图片描述
①对象首先分配在eden区域;
②新生代空间不足时,触发Minor GC(新生代GC),eden和from存活的对象使用复制算法复制到to中,存活的对象年龄加一,并且交换from区和to区;
③Minor GC会引发Stop the world(STW)现象,暂停其他用户的线程,不过这个过程非常短暂。垃圾回收结束后,用户线程才恢复运行;
④当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4位二进制);
⑤当老年代空间不足,会先尝试触发Major GC,如果之后空间仍不足,会触发Full GC
(STW时间更长,老年代可能使用标签清除或标记整理算法);
⑥当存放大对象新生代放不下而老年代可以放下,大文件会直接晋升到老年代;
⑦当存放大对象新生代和老年代都放不下时,抛出OOM异常。

16.四种引用
在这里插入图片描述
①强引用
最常见的对象:通过new关键字创建,通过GC Root能找到的对象。
当所有的GC Root都不通过【强引用】引用该对象时,对象才能被垃圾回收
②软引用
创建:SoftReference ref = new SoftReference<>(new Object());
仅有【软引用】引用该对象时,在垃圾回收后,内存仍不足时会再次发起垃圾回收,回收软引用对象,可以配合引用队列来释放软引用自身。
软引用被回收后,仍然还保留一个null,如将软引用加入集合,回收后遍历集合仍然还存在一个null。

17.深拷贝和浅拷贝
浅拷贝(shallowCopy):只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy):是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值