3万字的Java虚拟机学习笔记,首次公开分享,还贴心准备了PDF!

java虚拟机一直属于比较难的一个知识点,很多初学者会不知如何下手,很多文章写的晦涩难懂,只因JVM本身比较难理解,想学习的可以先看看我在学习JVM的时候记的笔记,大概三万多字,绝对让你有收获,底部有获取PDF的方法哦!

重点先理解这些

随着自己的不断学习,对之前所做的笔记会有不同的认识和理解,所以在不断学习的过程中,也要经常回顾自己的笔记

当我们完成一个java文件的编写,然后经过javac命令的编译成了class文件,这个class文件除了有类的版本,方法,字段和接口等信息以外,还有一项重要的信息就是常量池,这个叫做class文件常量池,主要就是用来存放

1.编译期生成的各种字面值

  1. i.文本字符串
  2. ii.八种基本数据类型的值
  3. iii.被声明为final的常量等

2.符号引用

i.类和方法的全限定名
ii.字段的名称和描述符
iii.方法的名称和描述符

当类从java文件编译成class文件,这个时候就有了class文件常量池,当被加载到内存中的时候class文件常量池也被加载进去了,这个时候class文件常量池就变成了运行时常量池,此时可以动态的添加字面量,符号引用也可以被解析为直接引用。

当一个线程开始的时候就产生了一个java虚拟机栈,当线程中的一个方法被调用的时候就会产生一个栈帧,这个栈帧就开始入栈(java虚拟机栈),这个栈帧中有一个局部变量表,用来存放基本数据类型和对象引用,基本数据类型的值存放在操作数栈中,而实例对象存放在堆中,但是对象引用在局部变量表中,此对象引用指向堆中的具体对象,基本数据类型指向操作数栈中的具体的值。

字符串常量池在jdk1.7之前字符常量池是存放在方法区中的,但是在jdk1.7及之后就从方法区中移除了字符串常量池,放在了堆中。

符号引用:强调的是编译成class文件之后,这个时候并不能确定一个类的引用到底指向谁,因此只能使用特定的符号代替,这就叫做符号引用,比如在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。

直接引用:在类加载阶段,经过解析将符号引用解析成直接引用,也就成了指向一个具体目标的内存地址。

对象引用:我们能看到的,在java文件中的实例对象的引用

1、什么是JVM(Java虚拟机)

维基百科的解释:

Java虚拟机(英语:Java Virtual Machine,缩写为JVM),一种能够运行Java
bytecode的虚拟机,以堆栈结构机器来进行实做。最早由太阳微系统所研发并实现第一个实现版本,是Java平台的一部分,能够运行以Java语言写作的软件程序。

Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。通过对中央处理器(CPU)所执行的软件实现,实现能执行编译过的Java程序码(Applet与应用程序)。

作为一种编程语言的虚拟机,实际上不只是专用于Java语言,只要生成的编译文件匹配JVM对加载编译文件格式要求,任何语言都可以由JVM编译运行。此外,除了甲骨文,也有其他开源或闭源的实现。

这里要注意的是Java虚拟机并不是专门针对Java语言的,只要可以编译生成字节码文件就可以被jvm执行,也就是说Java虚拟机所针对的对象是字节码文件,而不是一个特定的编程语言。

从操作系统层面理解虚拟机

Java虚拟机是运行在操作系统中的,也就是说Java虚拟机是以一个进程的方式运行在操作系统中的,因为进程是操作系统的执行单位。

当Java虚拟机在运行的时候就是操作系统中的一个进程实例,没有运行的话就是一个程序。

对于我们熟知的命令行而言,一个命令就对应一个进程,也就是说当你输入一个命令并且回车的时候就创建了一个进程实例。创建完一个进程之后,就会加载相对应的可执行文件到进程的地址空间中,然后执行其中的命令。

比如我们熟知的,使用javac可以将一个Java源文件翻译成Java字节码指令,然后使用java命令去执行这个字节码文件,当我们输入java然后回车的时候其实就是创建了一个Java虚拟机的进程实例,接着就会将这个字节码文件加载进Java虚拟机中(此进程),在Java虚拟机中是有相应的空间的。

拿javac和java这两个命令来详细的说一下:

在这个过程中javac就是将我们写的Java源文件翻译成Java虚拟机可以执行的字节码文件,也就是class文件(也叫做本地文件),重点在这个java指令上,当我们输入java这个指令,就会启动一个Java程序,这个Java程序运行起来就是一个Java虚拟机进程,这个进程启动之后就会把相应的类加载进内存中,加载进内存之后就会对这个类进行初始化和动态链接,接下来就是从这个类的main方法开始执行了。

字节码文件被加载进内存之后不是直接在cpu上执行,而是被Java虚拟机进程托管着,需要由Java虚拟机对这个字节码文件进行一系列的操作。字节码文件是被Java虚拟机中的类加载器加载的,然后由这个虚拟机进程去解释这个class文件中的字节码指令。然后再把这个字节码指令翻译成本机cpu能够识别的指令,然后再在cpu上运行。

也就是说我们在执行一个Java程序的时候,实际上是执行一个叫做Java虚拟机的进程,这个Java虚拟机进程会执行一些底层的操作,比如内存的分配和释放,我们写的Java源文件被翻译后的字节码文件只不过是虚拟机进程的一个“原料”而已。

之前说过字节码文件也就是class文件,是由Java虚拟机中的类加载器来加载的,但是这个加载是按照需要来加载的,也就是只有当一个类需要的时候才去加载这个类,并不是一开始就会加载所有的类。

当一个类被加载进Java虚拟机内部之后,Java虚拟机会去读取这个字节码文件中存在的字节码指令,在Java虚拟机中去读取这个字节码指令的部分叫做执行引擎,执行引擎负责字节码指令的执行。
Java一个非常大的特点就是内存的自动管理,不需要我们去写代码来进行内存的释放,它会自动的去进行内存的分配和释放,而这部分就是由垃圾收集这个子系统来执行的。

所以有三个子系统在Java虚拟机中是非常重要的。

1、类加载器子系统
2、执行引擎子系统
3、垃圾收集子系统

这里总结一句话,就是虚拟机的执行,必须加载字节码文件,然后执行字节码文件中的字节码指令。

那么Java虚拟机就需要有自己的空间来存放相应的数据了,比如加载的字节码需要一个单独的空间来存放,一个线程的执行也需要内存空间来维护方法的调用关系,存放方法中的数据和中间计算结果,还有创建的对象也需要一定的空间,这就牵涉到Java虚拟机的内存空间的知识了,也就是Java虚拟机的内存结构。

2、Java的运行条件

这个我们就要说说想要运行一个Java源程序,我们该有哪些东西或者做些什么,这个我们刚开始学Java或许都会傻傻分不清楚以下几个名词

  1. jdk
  2. jre
  3. Jvm

想必刚开始大家学习Java一定深受环境配置之苦吧,为啥我照着书上讲的视频里面说的配置的,可是为啥就是人家的可以我的就不行呢?

这是个神奇的问题,试问学编程的谁还没有遇到点诡异的事情,不过实际情况可能是你其中的某个步骤是真的错了,为啥,因为这些jdk啊,jre啊那么高级的词汇怎么可能让你那么轻松就能弄明白的

那这些高级词汇都是啥勒?

首先是这个jdk,对对对,我得先告诉你它的全名叫做

Java Development Kit

翻译过来也就是Java开发工具包,从这名字我们大概知道,有了它我们就能进行Java开发了,不过要怎么才能使用这个Java开发工具包呢?那就是你们遇到的诡异的环境变量配置了

其实这个吧网上教程多的很,跟着多操作几遍,自己多思考思考也没什么难的,咱们的重点不再这,所以不去说如何进行环境变量配置的问题

当我们把jdk弄好了,也就是可以使用这个Java开发工具包了,那我们怎么使用呢?就拿我们之前写的那个Test.java来说吧,我们可以在这个源文件的当前路径下打开终端,然后输入

javac Test.java

然后回车,然后你就会发现多出了一个叫做Test.class的文件,然后我们可以继续使用jdk中提供的命令

java Test

顺利的话你就会看到输出内容了,可是你真觉得你会顺利吗?那很有可能不啊,因为你少了一样东西啊?那就是jre

那什么是jre呢?根据jdk我们可以猜得出来这应该也是缩写了,赶紧查查,哦,是这个

Java Runtime Environment

翻译过来就是Java运行环境,哦,从字面意思理解好像有了它才能运行Java程序,那这个怎么弄呢?jdk我们可以下载下来,配置到环境变量,那么这个jre呢?其实也是一样的,而且在jdk中也包含jre。

总之吧,你想成功运行一个Java程序,那就得有jdk和jre,其实还应该有个jvm,不过这个就是底层的东西了,可是我们还是要说说这个jvm的

同理,这个jvm的全称是

Java Virtual Machine

也就是Java虚拟机,这个相当重要,为啥?如果你学习Java的话你就一定知道Java的跨平台,也就是一句非常经典的话

“write once,run everywhere”

要知道Java实现跨平台可全是jvm的功劳啊

3、Java的跨平台

下面我们就来好好说说这个跨平台,也就是来聊聊jvm,说到这个我们需要把c和c++也拿过来一起做个比较。

我们或许知道c语言是一个偏底层的语言,是面向过程编程的语言,对于面向过程语言,它专注的是数据之间的流向,重点在过程两个字,而c++和Java语言都属于面向对象编程语言,他们关注的则是不同对象之间的一个关系,重点在对象两个字。

c语言应该是每个学计算机的都会学的吧,或多或少都会知道一个很厉害的名字叫做“指针”,是可以直接去操作内存的,这带来的问题就是我们需要自己手动的去释放内存,而Java我们应该也知道,有了jvm来管理内存,我们不需要自己手动释放内存

也就是说,Java把c++和c语言中那个神奇的指针给去掉了,为啥,因为指针是可以直接操作内存的,因此肯定会带来很多的编程错误,这里给大家看一张图来理解吧

在这里插入图片描述

c和c++因为有了指针的存在,所以是可以直接与操作系统进行交互的,从而可以去操作内存,但是谁能保证你写的没啥问题,直接操作内存难免会出现问题,而且你还需要自己手动的去释放内存。

而Java就不同,他去掉了指针,不与操作系统直接交互,而是在中间多了一层jvm,也就是说你写的程序会先交由jvm去处理,然后才会与操作系统进行打交道,如此一来,你就可以随便造,反正有jvm在后面帮你把关,而且内存也会交由jvm来进行自动回收,着实很酷。

我们再来说说这个跨平台,从图中我们可以分析得到,无论是c还是c++他们都是严重依赖操作系统的,也就是说你在windows上写的c程序在其他平台是无法运行的,是无法做到跨平台的,为了更好的理解这块,我们需要补充下知识

编译和解释

说到编译和解释,可能会遇到编译器和解释器的概念,这个不难理解,编译器的作用就是编译,而解释器的作用就是解释,而他们操作的对象都是程序。

根据编译和解释其实可以将程序语言分为两大类,分别是编译型语言和解释型语言,说到这里我们还要明白一个问题,那就是为什么会有编译和解释,要知道,计算机是只认识机器码的,也就是0和1,所以我们通常情况下写的代码,计算机是不认识的,因此需要把我们写的程序翻译成计算机能够识别的机器码,那么翻译的方式就有两种

一是编译
二是解释

而他们最大的区别就是翻译的时间不同,对于编译而言,它是一次性的把源程序翻译成目标代码,然后计算机读取的时候就可以直接以机器码进行执行,这样的话效率就会很高,但是对于解释则不同,解释的特点是只有在执行的时候才去翻译,也就是边翻译边执行。

我记得之前看过很形象的一个回答,就是什么是编译和解释呢?

编译就是相当于提前做好了一桌子菜等着你吃
而解释就好比是吃火锅,边吃边下

个人觉得很形象,对理解什么是编译和解释很有帮助。

知道了什么是编译和解释我们就要说道说道什么是编译型语言和解释型语言了。

我们熟知的c和c++就是典型的编译型语言,对于编译型语言,有一个专门的编译过程,是直接把源程序翻译成目标代码,而且是一次性的翻译完成,因此执行效率很高。

而像python和js就是解释型语言,可能有人会说到脚本语言,脚本语言也是一种解释型语言,对于解释型语言,会有一个专门的解释器在执行的时候进行解释,也就是执行一句解释一句。

那么对于Java属与哪一种呢?

其实Java属于两者的结合,也就是Java即是编译型也是解释型,不过说到底,Java应该是解释型语言,为什么呢?可能有点绕,但是也好理解

我们知道Java是一种跨平台的高级语言,而实现跨平台的则是jvm,那这个跨平台到底是怎么回事呢?

再来看看这段熟悉的代码

class Test{
public static void main(String[]ithuangqing){
System.out.println("微信搜:编码之外");
}
}

那么这段代码是如何做到一次编译,到处执行的呢?注意我这里说的是编译,那么在Java程序的执行中肯定存在一个编译的过程,那么这个Java应该属于编译型语言啊,为什么是解释型语言呢?

不着急,慢慢来,我们来看一张图

在这里插入图片描述

这段代码我们把它命名叫做Test.java,它是一个Java源程序文件,接着我们可以使用javac命令生成一个Test.class文件,这个叫做字节码文件。

这里我们一定要理解的是,javac就是在执行一个编译的过程,通过javac把Java源程序编译成目标代码,只不过与传统的编译不同的是这里的编译并不是将Java源程序直接翻译成机器代码,而是翻译成来一个中间代码class文件,叫做字节码文件。

很重要的一点,这个字节码文件是与平台无关的,这是实现跨平台非常重要的前提,也就是生成的字节码文件是与平台无关的,那么接下来如何在不同的平台上进行执行的呢?

这就要靠jvm了,字节码虽然与平台无关,但是可以由jvm进行解释执行,因此只要在不同的平台上加装相对应的jvm那就可以实现跨平台执行来

这里需要理解的关键点就是不同的平台需要有不同的jvm,也就是在每一个平台上安装相对应的jvm,那么就都可以解释执行字节码文件了,字节码是与平台无关的,但是这个jvm却是与平台相关的,也就是说不同的平台上的jvm是不同的。

因此,jvm对于Java的跨平台来讲就是一个桥梁,Java源程序首先编译生成字节码文件,这个字节码文件是不能直接运行的,需要由不同平台上的jvm把这个字节码翻译成对应平台上的机器语言,这里的翻译其实就是解释,在这个过程中,字节码始终都是一样的,但是由各个平台上的jvm翻译之后的机器码却是不同的。

Java正是通过这种机制实现的跨平台,总结下就是Java是跨平台的,真正跨平台的是Java程序,而jvm是c和c++编写的软件,是编译后的机器码,不同的平台上jvm的版本是不同的。

经过编译之后的字节码文件是存放在我们电脑中的磁盘中的,当对字节码文件进行解释的时候,这个字节码文件就会通过一个类加载器的东西把字节码文件加载进电脑的内存中,当然这个加载过程是有特定的步骤的,主要就是检查这个字节码文件是否符合jvm规范等等,加载成功就会在电脑中的内存中开辟一块空间,这块空间其实就是jvm,然后再由内存输出内容。

到这里我们就可以发现,一个Java程序的运行必须有以下三个前提条件,那就是

  1. jdk
  2. jre
  3. Jvm

写到这里,我突然想了想我这篇文章的价值在哪里,如果你看完这篇文章能跟别人说保证Java程序运行的三个条件是什么以及为什么,那么就能证明我这篇文章对你还是有价值的。

4、Java中的引用

Java中的引用有如下几种:
1、强引用
2、弱引用
3、软引用
4、虚引用

强引用是最常见,最普通的引用了,我们来举一个例子,我们看下面这行代码

Object o=new Object();

以上代码就是一个强引用,也就是说我们创建了一个Object对象,并把它赋值给了o,那么,现在这个o其实就是代表着我们创建的这个Object对象,所以说,o其实就是一个引用,代指这个Object对象。

Object o;o=new Object();

这里的o肯定是同一个,那么这个o是个对象吗?我们知道对象的创建是通过new的方式,如果这个o是一个对象的话,那么为什么还需要再次通过new来创建呢?也即是这个o并不是一个对象。

那么,这个o到底是什么呢?对,这个o其实就是一个引用,也就是说,我们创建了一个Object对象的引用,然后通过new的方式创建了一个Object对象,然后用这个o指向我们创建的这个Object对象。这个o其实就是一个对象引用。

明白了对象的引用,那什么是对象呢?这个更简单,对象其实本质上就是对象的实例。

java语言抛弃了C和C++中的指针,但是,java中的引用其实和指针是很像的,可以说是一种变型!

Object o;o=new Object();o=new Object1();

我们看这段代码,首先,o指向了Object,然后又指向了Object1,所以说,引用可以指向任何实例对象,但是不同同时指向多个对象,由此,我们想一下,多个引用可不可以指向同一个对象呢?

我们再看这个代码

Object o=new Object();Object o1=o;

通过以上代码我们就可以看出,多个引用是可以指向同一个对象的。

其实强引用是最常见的引用了,也是最普遍的,剩下的三种引用都是引用关系逐渐减弱的,所以我们可以得知,对于强引用而言是不会被垃圾回收器给回收的,也就是说只要强引用关系还存在,这个对象就不会被回收。

软引用和弱引用其实可以理解成那种非必须的对象引用,这些也是会被垃圾回收器优先回收的对象,那虚引用是最弱的一种引用关系,这些暂时还不用深入了解,就先不深入的去说了。

5、Java内存结构

java内存结构也就是jvm内存结构,我们经常说的是jvm内存结构,包含了堆内存,栈和方法区等内容,是学习jvm必备的知识,所以jvm内存结构这块知识的学习是很重要的!

首先要知道的就是java内存结构等同于jvm内存结构!下面是jvm的内存结构图
在这里插入图片描述

然后jvm内存结构包含以下内容:

  1. 程序计数器
  2. java虚拟机栈
  3. 本地方法栈
  4. java堆
  5. 方法区运行时常量池

因此,学习jvm的内存结构也就是要弄懂上面几个东西!

程序计数器

程序计数器,有的地方也叫作pc计数器,都是它,是在jvm内存中属于较小的额一个内存空间,但是十分重要,我们知道在多线程中,是靠CPU来切换线程的执行顺序的方式实现的,也就是说,从线程A切换到线程B,然后再切换到线程A的时候,你有没有想过cpu是怎么知道应该执行线程A中的哪一步,也就是说,之前在线程A中执行到哪了,这就要靠程序计数器去记录了。

这就是程序计数器了,另外要知道的就是程序计数器是线程私有的,互相独立,如果被问到什么是程序计数器,我觉得可以这样回答:

当前线程所执行的的字节码的行号指示器!

java虚拟机栈

我们之前应该经常会说或者经常听到堆内存和栈内存,堆内存想必大家都很熟悉了,这个我们随堆内存经常说的栈内存准确的来说应该是java虚拟机栈中的局部变量表,在此之前我们应该也知道一个常识就是基本数据类型是存放在栈内存中的,对象是存放在堆内存的,现在准确的去说,基本数据类型是存放在java虚拟机栈中的局部变量表中的。

java虚拟机栈也是线程私有的,生命周期跟随线程,很重要的一个点就是要明白java虚拟机栈是与java方法相关的,什么意思呢?

当线程开始,也就产生了这个java虚拟机栈,当一个java方法会调用的时候就会产生一个栈帧,这个栈帧是用来干嘛的呢?

这个时候产生的一个栈帧就是用来存放局部变量表,操作数栈,动态链接和方法出口信息等,注意了,这里的局部变量表是在栈帧中保存。另外,一个方法从调用到执行结束的整个过程就是一个栈帧在java虚拟机栈中从入栈到出栈,这就把栈帧和java虚拟机栈联系起来了。

而这个局部变量表是干嘛的呢?就是用来存放各种基本数据类型,对象的引用得,想必这个大家都熟悉,就是大家常说的栈内存所做的事情啊。可能这里还要注意的就是这里说的基本数据类型都是在编译期就已经确定下来的,而且局部变量的的空间在编译期就是确定的,运行时期是不会再改变的。

本地方法栈

想必这个一定会让大家想到java虚拟机栈,那么两者有什么区别呢?其实极为相似,不同的是服务的对象不同,java虚拟机栈是为执行java方法服务,而本地方法栈是为使用到的Native方法服务,那么重点来了,什么是Native方法呢?

关于native方法我这里简单说下我的理解,java号称吸收了c加加和c语言的优点,剔除了较难的指针,不过,我是学java的,我就认为java好,可是嘞,不得不承认,java语言要比c++的运行慢很多,另外也是因为没有指针吧,所以java并不能去直接操作底层,为了弥补这个缺点,也就有了native方法,来建立这么一种联系。

关于native方法,就说这么多,大家可以自行搜索学习,等我研究的差不多了再来分享!

堆内存

堆内存是我们要经常与之打交道的一块内存地址,所有的实例对象和数组都在这里存储,也是垃圾回收器主要工作的地方,所以堆内存也叫作gc堆,也就是垃圾堆,哈哈。

当然可能大家也知道,在堆内存中其实也是有划分的,比如分有新生代和老年代,再细致一点的话有Eden空间,from和to空间等,我们这里要把握的是无论怎么划分,存放的就是对象实例就ok了。

对java堆中我们后面会单独拿出来说的,因为很重要!还需要记住的是堆内存是所有线程共享的。

直接内存(堆外内存)

堆外内存就是在Java堆之外的内存,也叫做直接内存,并不是jvm规范之内的内存区域。使用不多。

直接内存存在一个IO操作方面的优势,比如:
举一个例子:在通信中,将存在于堆内存中的数据 flush 到远程时,需要首先将堆内存中的数据拷贝到堆外内存中,然后再写入 Socket 中;如果直接将数据存到堆外内存中就可以避免上述拷贝操作,提升性能。类似的例子还有读写文件。

直接内存由DirectByteBuffer这个类来分配内存空间,这个类对象位于Java堆中,它链接着堆外一大块的内存块,这个类对象被回收的话,直接内存也就没有了。

DirectByteBuffer 中用于分配堆外内存的方法 unsafe.allocateMemory(size) 是个一个 native 方法,本质上是用 C 的 malloc 来进行分配的。分配的内存是系统本地的内存,并不在 Java 的内存中,也不属于 JVM 管控范围,所以在 DirectByteBuffer 一定会存在某种特别的方式来操纵堆外内存。

堆外内存主动回收原理

第一种就是基于Jvm gc机制,目的就是回收掉DirectByteBuffer,而它一般都是存在于老年代中,也就是只有在发生Full GC的时候直接内存才会被回收掉,但是这样的话会出现的情况就是,堆内存没有满而直接内存已经满了。

第二种就是使用Cleaner对象
从 DirectByteBuffer 里取出那个 sun.misc.Cleaner,然后调用它的 clean() 就行,而 clean() 执行时实际调用的是被绑定的 Deallocator 类的 run 方法,其中再调用 freeMemory 释放内存。

方法区和运行时常量池

这块知识我们需要掌握的一个重点就是在jdk1.7之前字符常量池是存放在方法区中的,但是在jdk1.7及之后就从方法区中移除了字符串常量池,放在了堆中。

方法区也是多个线程共享的,存放已经被虚拟机加载的类信息,常量和静态变量等数据信息,在方法区中还存在一个运行时常量池,这个是值得好好研究的。

首先是Class文件中除了有类的字段,方法和接口等描述信息之外还有一个常量池,这些内容会在类加载后进入方法区的运行时常量池。

而这个常量池是用于存放编译阶段生成的各种字面量和符号引用

6、Java虚拟机栈

java虚拟机栈是jvm内存结构中的一员,也就是我们平常所说的栈内存,它是线程私有的,每个线程都有属于自己的一个java虚拟机栈,java虚拟机栈的生命周期和线程相同,也就是说当一个线程开始了,也就产生了一个java虚拟机栈。

既然是栈,肯定有个什么玩意入栈和出栈,java虚拟机栈主要是用来存放线程运行方法时所需的数据,指令和方法返回地址等,那么靠什么存储?这就需要栈帧,

在这里插入图片描述

栈帧的产生必须是一个方法被调用了,也就是说,线程开始,有了一个java虚拟机栈,当一个方法被调用,就产生一个栈帧用来存放运行这个方法所需的一些数据,一个方法从被调用到结束就对应一个栈帧从入栈到出栈的过程,可以得知,栈帧是和方法息息相关的。这个栈帧包含这么些东西。

  1. 局部变量表
  2. 操作数栈
  3. 动态链接
  4. 方法返回地址

java虚拟机栈线程私有,随线程开始而产生。
java虚拟机栈主要靠方法被调用的时候产生的栈帧来存放数据。
栈帧随一个方法被调用而产生
一个方法从被调用到结束就对应栈帧在java虚拟机栈中入栈和出栈的过程。

我们之前常说,基本数据类型是保存在栈内存中的,现在要知道的是这是根据时期而定的,因为如果你单单理解基本数据类型是存放在栈内存的时候,当你遇到运行时常量池的时候你一定会迷,为啥,运行时常量池也是存放基本数据类型啊,那到底谁存放呢?

这就要根据时期来说了。当你编写一个java文件,被编译成class文件之后,这个class文件中就产生了一个class文件常量池,当被加载到内存中的时候,这个class文件常量池就成了运行时常量池,当然,运行时常量池包含的东西要多点,这时候基本数据类型也是存放在这个运行时常量池的。

但是,你要注意了,这个时候并没有什么线程开始和方法调用,所以也就没有什么栈帧来存放数据,只有当你的方法被调用的时候,才会产生一个栈帧来存放数据,这时候就会存放基本数据类型的数据,而我猜想这些数据也是从运行时常量池拿来的。那么栈帧中是如何操作的呢?其实栈帧也分为这么几个部分

  1. 局部变量表
  2. 操作数栈
  3. 动态链接
  4. 方法返回地址

而我们说的基本类型存放在栈内存,更加准确的说就是存放在栈帧中的局部变量表。

关于类变量和局部变量有这么一个区别,对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。

这个栈帧中除了局部变量表之外还有操作数栈,动态链接和方法返回地址。那什么是操作数栈呢?在《深入理解java虚拟机》中有这么一段话“整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。”也就是说操作数栈也是存储数据的区域。

每一个栈帧中都会有这么一个引用,这个引用存放在运行时常量池中,这个引用指向该栈帧,这个引用的目的是为了支持方法调用过程中的动态链接。符号引用在类加载阶段或者第一次使用阶段会直接转换为直接引用,这个叫做静态解析,还有的是在每一次运行期间转化为直接引用,这部分就称为动态链接。

方法的退出也就意味着栈帧从java虚拟机栈中出栈,方法的退出一般有两种,一种是正常退出,一种是异常退出,但是,无论是以哪种方式退出,最终都要返回到方法调用的地方,如果是正常退出的话,那么这个返回地址就是调用者的程序计数器的值,如果是异常退出的话,返回地址是由异常处理器表决定的。

7、对象的创建

对象有这么几种创建方式:

  1. new关键字

  2. 运用反射手段,调用java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法

  3. 调用对象的clone()方法

  4. 运用序列化手段,调用java.io.ObjectInputStream对象的readObject()方法.

在jvm层面对象的创建是这样滴

在这里插入图片描述

对象的创建?首先,什么是对象?面向对象编程?万物皆对象?算了,比如下面一段代码

Student s=new Student();
以上代码就创建了一个Student对象,这个s就叫做对象引用,后面的new Student()就在堆内存中开辟一块新的内存空间用来存放这个Student对象的实例,而这个内存空间有一个内存地址就存放在java虚拟机栈中的栈帧中的局部变量表。

也就是说在这个局部变量表中开辟一个内存空间存放这个堆中存放这个实例对象的内存空间的内存地址,而在这个局部变量表中新开辟的空间,我们就叫它“s”吧!

以上就是我们最为熟知的创建对象的一种方式,就是通过new这个关键字,不过创建对象的方式可不止这一种,还有这么几种

new关键字

运用反射手段,调用java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法

调用对象的clone()方法

运用序列化手段,调用java.io.ObjectInputStream对象的readObject()方法.

那么,这几种都是怎么实现对象的创建呢?你知道new是如何创建对象的吗?知道的话,那就可以了,剩下的不急着去研究他们,我们继续往下说对象的创建,也就是说,你要熟知使用new关键字创建对象的方式,然后还知道有其他创建对象的方式就可以了。

接着,我们继续!

我们在之前知道了jvm的内存结构,知道了当你编写一个java源文件之后可以使用javac命令将其编译成class文件,当class文件被加载进内存中,说的粗暴一点,也就是这个class文件会被弄得稀巴烂,然后存放在jvm内存结构中几个不同的区域之中。

对了,你还记得class文件常量池中都是存放些什么玩意吗?答案是字面量和符号引用。

那接下来我们就深入jvm层面去看看这个对象到底是怎么创建的,我们就以这个new关键字创建对象来说。

还拿这段代码来说

Student s=new Student();
下面开始分析啦,注意了

当你写了这么一段代码,在jvm中是如何执行的呢?首先当jvm发现这个new指令的时候就会先对符号引用进行分析,为什么要对符号引用进行分析呢?你可知道在运行时常量池阶段,符号引用会被解析成直接引用,也就是指向对象的那个地址,在此之前也就是这个符号引用可并不是这个地址,而是一个特定的符号,这个符号引用代表着你这个类被加载了,所以如果在class文件常量池中如果没有发现这个符号引用的话,说明了什么呢?

当然是你这个Student类还没有被加载呢?所以就需要进行和这个Student类的加载了,关于类加载,我们这里先不谈。

假设现在类加载完成了,找到了这个符号引用,那么在类的解析阶段就会把这个符号引用解析成直接引用,你想啊,直接引用都出来了,是不是独享就被创建成功了,对象创建在哪呢?

当然是堆啦,所以jvm会在堆中给这个Student对象分配一块内存来存放这几个对象,而这个内存是有一个地址的,就是直接引用啦。

jvm为对象分配完内存之后可没有闲着,紧接着会对分配的内存进行初始化为零值,这里不包括对象头。

等等,什么是对象头,我们常说的这个虚拟机啊一般指的就是HotSpot虚拟机,它实现的对象有这么三个部分组成。

  1. 对象头
  2. 实例字段
  3. 对齐填充字段

那么,如果你要再问这三个是什么玩意,我吧,就不知道怎么回答了,也就说,你就记得在HotSpot中实现的对象包含这三个玩意就行了,不用再往深处去研究了,至少现在不用,ok?

最后,对象的创建还差这么一步就是jvm会调用对象的构造函数,据听说,这个调用会一直追溯到Object类。

至此,对象算是创建成功了,当然,这里面还有很多细节,但是,这些细节你需要都把他们搞明白吗?这个还真的不需要,在学习中要有一个大的前进方向,不要被一些旁枝末节所阻挡,注意,我可没有说这些旁枝末节不重要。

8、堆内存

因为我们的对象是放在堆内存的,而我们的gc就是回收垃圾对象的,也就是说我们的gc是在跟堆内存不停的打交道

也许在你学习jvm之前你只知道堆就是一个内存空间吗,但是在学完jvm或者说学完gc之后你会发现在堆中其实也是分好几块的,我们看下这个图

这就是堆的内部结构了,在堆中分出了这几块内容

新生代老年代

其中新生代又被分成了三块,分别是eden,s0和s1,也叫作from区和to区。我们接下来分别说一下。

在这里插入图片描述

这就是堆的内部结构了,在堆中分出了这几块内容

新生代老年代

其中新生代又被分成了三块,分别是eden,s0和s1,也叫作from区和to区。我们接下来分别说一下。

新生代

只有你接触gc可能才会接触到新生代和老年代的概念,那这是什么意思呢?首先我们要知道,堆中无非就是存放对象的地方,新生代和老年代都是存在堆中,所以也就是都是存放对象的地方,可能存放的对象有所不同而已。

总的来说,记住一点就是,一些新创建的对象都会被放在新生代中,如果这个对象使用频率比较高就会被放在老年代中。

一般,一个刚刚创建的对象会被存放在eden区中,随着使用的频率会被转移在from或者to中,这两个区域其实差别不大。

老年代

如果一个对象经常被使用,也就是使用频率很高,就会被存放在老年代中,老年代中存放的对象都是经常使用的对象。

Gc会经常性的来新生代和老年代中逛一逛,当然,我们可以知道,gc经常关顾的应该是新生代,因为老年代中存放的都是经常使用的对象,所以被回收的几率较小,而新生代中的对象被回收的几率则较大,所以gc就会不断的根新生代和老年代打交道,从而进行相应的垃圾回收。

我们知道gc会经常去新生代或者老年代中看看有没有需要回收的对象,这期间gc需要对新生代和老年代中的对象进行算法分析,查找垃圾对象,从而回收,根据我们队垃圾回收算法的了解和堆新生代老年代中存放对象的了解我们可以得知

复制算法比较适合在新生代中,因为老年代中的有用对象较多,所以会执行较多的复制操作,这样的话效率就降低了,因此,老年代一般会采用其他的算法,比如标记—整理算法!

9、常量池和引用

在学习java的时候,我们经常会遇到一些很相似的概念,这个简单来说就是名字很相似,比如我们之前提到的对象和对象引用,还由今天我们要说到的

符号引用
直接引用
class文件常量池
运行时常量池
字符串常量池

有的人可能会觉得干嘛花费时间精力在这块,感觉有点抠字眼了,我想说的是,这绝对不是抠字眼,弄清楚这些概念,对以后的学习很重要,而且我们这个专题准备好好的说一说这个java虚拟机,这些概念,对于虚拟机的学习

很重要!!!

首先,我们来看下面一段叙述:

当我们完成一个java文件的编写,然后经过javac命令的编译成了class文件,这个class文件除了有类的版本,方法,字段和接口等信息以外,还有一项重要的信息就是常量池,这个叫做class文件常量池,主要就是用来存放

编译期生成的各种字面值:

文本字符串
八种基本数据类型的值
被声明为final的常量等

符号引用:

类和方法的全限定名
字段的名称和描述符
方法的名称和描述符

当类从java文件编译成class文件,这个时候就有了class文件常量池,当被加载到内存中的时候class文件常量池也被加载进去了,这个时候class文件常量池就变成了运行时常量池,此时可以动态的添加字面量,符号引用也可以被解析为直接引用。

当一个线程开始的时候就产生了一个java虚拟机栈,当线程中的一个方法被调用的时候就会产生一个栈帧,这个栈帧就开始入栈(java虚拟机栈),这个栈帧中有一个局部变量表,用来存放基本数据类型和对象引用,实例对象存放在堆中,但是对象引用在局部变量表中,此对象引用指向堆中的具体对象。

(如果上面有说得不对的地方,烦请指出!谢过!)

在上面这段描述中出现了这么几个概念

Class文件常量池
运行时常量池
符号引用
直接引用

我们这里再加上一个字符串常量池,也就是这次我们一定要弄清楚这几个概念

常量池:

Class文件常量池
运行时常量池
字符串常量池
引用:

符号引用
直接引用

首先,我们来说说这个Class文件常量池,我们编写的java文件会被编译为class文件,这个class文件除了有类的版本,方法,字段和接口等信息以外,还有一项重要的信息就是常量池,这个叫做class文件常量池,主要就是用来存放

编译期生成的各种字面值:

文本字符串
八种基本数据类型的值
被声明为final的常量等

符号引用:

类和方法的全限定名
字段的名称和描述符

方法的名称和描述符

也就是说,我们的java源文件生成的class文件中包含一个常量池,叫做class文件常量池,这里注意一点的就是这个时候只是从java源文件编译成class文件,然后其中产生一个class文件常量池,注意还没有加载到内存。
那什么是运行时常量池呢?

经过上一步骤,java源文件被编译成class文件,其中有一个class文件常量池,然后这些会别加载到内存中,也就是jvm的运行时数据区,也就是我们之前说的饿那几个内存区域,这块可以看看之前说的jvm内存结构,当被加载到内存中的时候,这个时候会有一个运行时常量池,那么这个运行时常量池是怎么来的呢?其实它就是之前的class文件常量池演变过来的,当然这个运行时常量池还包含一些其他内容。

可以这么说,这个运行时常量池是在被加载到内存之后,而class文件常量池并未涉及内存,还在内存之外!而此时的运行时常量池可以动态的添加字面量,符号引用也可以被解析为直接引用。

至于字符串常量池,应该是大家最为熟悉的一个了,我们要记住的一个知识点就是字符串常量池的位置,在jdk1.7以前是存放在方法区中的,但是在jdk1.7及之后就被放在了堆中。

下面我们再来说说引用,

可能我们之前一直在说引用引用,并没有细分到符号引用和对象引用,那么现在我们就来学习这两个概念,让我们对引用有个新的认识。

那什么是符号引用呢?

要想知道什么是符号引用,你必须知道的一个前提就是这里的符号引用强调的是在java源文件编译成class文件之后,这个时候你要知道其实一个类的引用并不能确定到底指向的是谁,因此只能使用特定的符号代替,这就叫做符号引用,比如在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。

我们在上面说过在运行时常量池那个阶段就可以将符号引用解析成直接引用,所以,所谓的直接引用是在类加载阶段,也就是在内存中了,经过解析会从符号引用解析成直接引用,也就成了一个指向具体目标的内存地址!

10、类加载

类的生命周期:

在这里插入图片描述

我们需要熟悉类加载过程:

加载(将类的二进制数据加载到内存)
验证(确保加载类的正确性)
准备(为类的静态变量分配内存,设置默认值)
解析(符号引用转换为直接引用)
初始化(设置类的正确初始化值,jvm初始化类)

准备阶段还真关系到我们日常编码的一个注意点呢!是个面试题也不为过!

在这里插入图片描述

对于什么是类我们比较清楚,那什么是类的加载呢?java程序是运行在内存中的,而类的加载就是将类的.class文件中的二进制数据存放在内存中,这个内存指的是jvm内存。方法区中有一个运行时常量池就是生成的class文件中的class文件常量池进入内存之后的版本。

类加载的最终结果是在堆中创建一个java.lang.Class文件。这个加载进内存的.class文件就是我们将java源文件动态编译得到的,也就是javac命令。

首先来看类的生命周期

类的生命周期一共七个步骤,其中加载,验证,准备,解析和初始化时类加载过程,验证和准备还有解析三个阶段也被叫做连接阶段。

这里有一个需要注意的就是加载,验证,准备和初始化这四个阶段的顺序是确定的,但是解析这一阶段就不一定了,它也有可能在初始化之后才开始,这是为了支持java的运行时绑定,另外以上这几个阶段是按顺序开始,但是可没有说按顺序结束,也就是他们一般情况下都是混杂着进行的。

加载阶段

加载阶段是类的生命周期最开始的步骤,这一阶段的目的主要在于将类的二进制数据加载到内存,一般都是通过类的全限定名称来获取二进制字节流,然后将这个字节流代表的静态存储机构转换为方法区中的运行时数据结构,我们知道类加载的最终结果是在java堆中产生一个Class对象,那么这个对象是用来干嘛的呢?它就是用于后续对方法区这些数据进行访问的一个结构,也就是我们可以通过这个类来访问到方法区中的这些数据。

这个加载过程一般有这么两种方式

使用系统已经为我们提供好的类加载器来进行加载
使用自定义的类加载器来完成加载
连接阶段

这个连接阶段包括验证,准备和解析

验证验证这一步骤的主要目的就是为了确保加载的类的正确性,以防加载对虚拟机有危害的类。这样的话对安全的类就有一个评判标准,一般有如下验证步骤

• 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

• 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

• 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

• 符号引用验证:确保解析动作能正确执行。

另外一点需要注意的是,验证这一阶段是非常重要的,是用来保证虚拟机的安全,但是这一阶段却不是必须的,什么意思呢?当你确定你这个类是安全的,也就是经过反复验证符合虚拟机规范的话,就可以考虑采用Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备准备阶段的主要目的是为类的静态变量分配内存,并将其设置默认值。

这一阶段会在方法区为类变量进行默认值赋值,但是这个类变量只是静态变量,而不是实例变量,实例变量会在对象实例化的时候跟随对象一块分配在java堆中。

这里指的默认值通常情况下基本数据类型就是默认的零值,像0,null和false等。

好好说说这个准备阶段

这个准备阶段是可以好好研究下的,在说这个准备阶段的时候,我们先来看两个关键字,那就是static和final,这个一个是代表静态,一个是代表常量,上面说过了,在准备阶段的主要目的就是给这些静态变量分配内存,设置初始默认值,这个是什么意思呢?我们拿代码来举例子看下面的代码

public static int age=2
以上代码就是简单定义了一个int变量,并且赋值成2,这个是在我们写代码的层面来说的,但是深入jvm,也就是类加载的是时候又是怎么回事呢?这个主要集中在类加载过程中的准备阶段,在准备阶段的时候,这个age其实不等于2,而是等于0,因为在准备阶段会为静态变量赋初始值,那什么时候才是等于2呢?这个是在后面的初始化阶段。

这里就又值得说道说道了,要知道,只有静态变量在这个准备阶段才会被赋初始值,其他的都靠边站了,所以这里就有个知识点了,就是局部变量和全局变量以及静态变量也就是static修饰的变量,记住了

如果是基本数据类型,在准备阶段会为静态变量和全局变量赋初始值,也就是说如果你没有给他们显示的赋值就直接使用的话,系统会为他们赋初始值,也就是默认值,这个是在准备阶段完成的,但是对于局部变量就不一样了,如果你要使用局部变量的话,那必须在使用之前就给它赋值,否则编译你都通过不了,还是举个例子吧

在这里插入图片描述

看这个例子,a是一个静态变量,b是一个全局变量,这些都可以实现不为其赋值,但是人家可以直接使用,因为在准备阶段它们会被设置默认值0,但是这个局部变量c就不一样了,如果你也不赋值,那结果是你编译都过不了。

解析阶段在解析阶段最主要的目的就是把类中的符号引用转换为直接引用。

另外要知道的是这个符号引用是怎么回事,当然还有和这个直接引用,知道了什么是符号引用和直接引用,那么这个解析阶段也就ok了

符号引用:强调的是编译成class文件之后,这个时候并不能确定一个类的引用到底指向谁,因此只能使用特定的符号代替,这就叫做符号引用,比如在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。

直接引用:在类加载阶段,经过解析将符号引用解析成直接引用,也就成了指向一个具体目标的内存地址。

初始化阶段

在这个初始化阶段就是将类的静态变量赋予正确的值了,也就是你想要它表示的值,也就是这个

public static int age=2
你这个在准备阶段给我搞个默认值0,但是我想让他等于2啊,所以在这个初始化阶段就给它设置成2了,不过在这个初始化阶段可不单单是给类的静态变量初始化正确的值,在这个阶段jvm还会对类进行初始化。

对类进行初始化?是的,这个主要就是对类的变量进行初始化,注意这里可不只是静态变量,另外还有一点就是类在什么时候才会被初始化呢?这个就是在类被主动使用的时候才会导致类的初始化,以下几种情况都会导致类的主动使用。

  1. 使用new来创建一个实例对象
  2. 访问了类或者接口的静态变量,
  3. 或者你对静态变量赋值
  4. 使用类的静态方法
  5. 你使用了反射 等等

11、java内存结构,java内存模型,java对象模型和jvm内存结构

JVM这块知识绝对是学习java过程中的重点和难点,我习惯把这块的知识叫做javaSE高级基础,在学习jvm这块的知识,你一定会遇到几个概念,那就是java内存结构,java内存模型,java对象模型和jvm内存结构!而这几个概念是很多人搞不清楚的,了解了这几个概念,将对你学习jvm很有帮助!

我们将要了解以下几个概念:

java内存结构
java内存模型
java对象模型
jvm内存结构

Java内存结构

什么是JVM内存结构:下面这张图就是jvm的内存结构
在这里插入图片描述
可以看到就是我们平常说的堆栈什么的!然后下面还有一个更加详细的图:
在这里插入图片描述
这就是jvm内存结构了,那什么是java内存结构呢?

记住了:jvm内存结构=java内存结构

记住java内存模型是与多线程相关,也叫作共享内存模型,如果被问什么是java内存模型可以这样回答:

Java内存模型简称jmm,它定义了一个线程对另一个线程是可见的,另外就是共享变量的概念,因为Java内存模型又叫做共享内存模型,也就是多个线程会同时访问一个变量,这个变量又叫做共享变量,共享变量是存放在主内存中的,而且每一个线程都有自己的本地私有内存,如果有多个线程同时去访问一个变量的时候,可能出现的情况就是一个线程的本地内存中的数据没有及时刷新到主内存中,从而出现线程的安全问题。

插播重点

我之前一直不理解的就是这个java内存结构和jvm内存结构到底什么关系,直到有一天我在一个博客中看到这么一句话。
在这里插入图片描述
也就是说java内存结构和jvm内存结构是一样的!
在这里插入图片描述
所以我们就拿jvm内存结构来说,这个是jvm内存结构图

从这个图中来看,这个java内存结构和jvm内存结构也就是我们平常经常说的堆内存,栈啊,方法区什么的,对,就是这个,以后再说起这个我们就知道是在说java内存结构(jvm内存结构了),那么我们要了解的也就是什么是堆内存啊,什么栈内存啊,什么又是方法区啊,这个我们今天就不详细说了,我们今天只要明白什么是java内存结构也即jvm内存结构是什么就行了,关于其本身的一些知识,由我们经常听说这几个名词就可知他们是非常重要的知识点,所以这个会单独拿出来讲的!

Java内存模型

下面咱们来说说什么是java的内存模型,这个和java内存结构从字面意思上看真的很相似,但是实际上,这两者相差不小,要谈java的内存模型,那么这张图就是必不可少的。

在这里插入图片描述
这就是java内存模型结构图了,我们从图中就可以直观的看到java内存模型是与多线程相关的,其中也提到了共享变量。

Java内存模型简称JMM,它定义了一个线程对另一个线程是可见的,另外就是共享变量的概念,因为Java内存模型又叫做共享内存模型,也就是多个线程会同时访问一个变量,这个变量又叫做共享变量,共享变量是存放在主内存中的,而且每一个线程都有自己的本地私有内存,如果有多个线程同时去访问一个变量的时候,可能出现的情况就是一个线程的本地内存中的数据没有及时刷新到主内存中,从而出现线程的安全问题。在Java当中,共享变量是存放在堆内存中的,而对于局部变量等是不会在线程之间共享的,他们不会有内存可见性问题,当然就不会受到内存模型的影响。

那么如果我们被别人问到什么是java内存模型的时候我们该怎么回答呢?这个你最好把这个图简单的画一下,最不济也要说下这个java内存模型抽象示意图,然后就想上面提到的你可以这么回答:

Java内存模型简称jmm,它定义了一个线程对另一个线程是可见的,另外就是共享变量的概念,因为Java内存模型又叫做共享内存模型,也就是多个线程会同时访问一个变量,这个变量又叫做共享变量,共享变量是存放在主内存中的,而且每一个线程都有自己的本地私有内存,如果有多个线程同时去访问一个变量的时候,可能出现的情况就是一个线程的本地内存中的数据没有及时刷新到主内存中,从而出现线程的安全问题。

接下来我们再来简单的看下这个java内存模型示意图:从这张图我们可以看出,线程之间的通信是受jmm控制的,我们就这张图来说,线程A和线程B如何才能进行通信,假如线程A先执行,它会拿到共享变量,然后进行操作产生共享变量的副本,紧接着将本地内存中的共享变量副本刷新到主内存中,这时共享变量也就得到的更新,同理线程B再去操作共享变量就达到了线程A和线程B之间的通信了。

基本上到这里你就知道了什么是java内存模型了,那其实关于到具体的应用当中,java内存模型还有很多内容,比如重排序,volatile关键字和锁等,这其实也牵涉到多线程了,因为本身java内存模型就是多线程相关的,所以在学习java多线程这快知识的时候,很多地方都是要借助这个java内存模型的!

在java中,我个人认为jvm,多线程以及并发编程,这三者是紧密相连的!我们以后慢慢来说。

在这几个易混淆的概念中,我觉得最不好理解的一个就是java对象模型,说实话这个java对象模型我问过一些人,基本上都不知道,我个人现在对它理解的也不是很透彻,为了避免误导大家,我在网上选取一篇大神的文章供你们参考学习,你们可以看看,这java对象模型是否不容易理解!(以下是个链接)

深入理解多线程(二)——Java的对象模型

12、Java中的类加载器

上一次我们简单说了下java中的类加载,那个知识点我们要记住的就是类的加载过程以及那几个阶段主要是干啥的,不过在谈及类加载的时候一定有一个知识点那就是类加载器。

什么?你不知道类加载器?那你一定知道ClassLoader或者双亲委派机制吧!

我记得我最先知道双亲委派的时候好像是从面试题中看到的,当时就觉得,什么玩意,还双亲委派,有点高大上,不知道是什么,那个时候我更不知道双亲委派是关于类加载的,这些知识点在当时都是知识盲区。

不过随着学习,积累的知识点不断变多,也就知道了什么是双亲委派机制。

双亲委派简单点来说是类加载器之间的一种模式,或者可以说是规则,也就是他们会按照这个模式去加载类,起了个名字叫做双亲委派,要知道什么是双亲委派,还要知道什么是类加载器。

那什么是类加载器呢?一个类如果被使用的话是会被加载进内存的,但是他们是如何加载进内存的呢?这就需要一个媒介,这个媒介就是类加载器,其实从这个类加载器的名字就显而易见就是用来加载类的。

简单知道了类加载器,接下来你还要知道,其实类加载器不止一个,有好几个,谈及类加载器,一定会有这么一个图

在这里插入图片描述
对,就是这个图,图上有三个重要的类加载器,可以这么说,类加载器也就是这几个了,当然我们还可以自定义类加载器,不过这个都是后话。

我在图上也标出来了,对于第一个启动类加载器,它是使用C加加实现的,而且是属于虚拟机自身的一部分,但是下面的两个就不同了,扩展类加载器和应用类加载器都是使用java语言实现的,而且是虚拟机之外,他们俩都是要靠启动类加载器来加载他们的,用C++实现的就是牛啊。

这里你还要知道的一个知识点就是关于子类和父类这个概念,这里可不是继承的关系,而是组合的关系,另外有这么一个例子,一起来看一下

public class ClassLoaderTest{
public static void main(String[]args){
ClassLoader loader=Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}

这个例子的意思就是看看应用类加载器的父类是谁,再看看扩展类加载器以及启动类的父类,结果是这样的
在这里插入图片描述
你会发现,应用类的父类是扩展类,但是扩展类的却是null,这是为啥呢?也很简单啊,因为启动类加载器是C加加实现的,扩展类是java嘛,压根就不是一个品种啊。

到这里,我们似乎熟悉了类加载器的一些知识,接下来我们继续。

你还要了解的就是这三个类加载器都是用来加载哪些类的,这个你网上一搜一大把,都是很直白的话,你看看,就是这些

启动类加载器:这个加载器可以说是顶层的,古老的,厉害的,它主要是用来加载放在JDK\jre\lib下或者被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

然后我就去看看了这个目录下都是些什么玩意

在这里插入图片描述
哦,知道了就是用来加载这些类库的。再来看这个扩展类加载器

扩展类加载器:这个加载器是由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。

嗯,说的也比较清晰,这个目录就不去看了

最后的这个应用类加载器可以说就是我们平常使用的默认加载器,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

到这里,我们也简单的了解了这三个类加载器,其实在实际的加载中是他们三个互相协作进行,这才有了接下来我们要说的双亲委派模式。

双亲委派机制

那么,这就来说说这个双亲委派吧!

什么是双亲委派机制呢?其实也蛮好理解的,这个类加载器是用来加载类的,而这三个类加载器都可以用来加载类啊,那么如果要加载一个类的话,谁先来加载呢,通常情况下是这样的,要加载的这个类首先到达这个类的默认类加载器,也就是应用类加载器,但是呢这个应用类加载器并不会去加载它,而是把这个类扔给他的父类也就是扩展类加载器,而这个扩展类加载器也会把这个类再次丢给启动类加载器来加载。

现在这个类到达了启动类加载器,没办法,上面没人了,只能自己加载了,如果启动类可以加载那就成功返回,实在加不了的话就把这个类原路返回。所以也有可能最后还是得这个应用类加载器来加载。

在这个过程中,我就感觉这个类像是个没人要的孤儿,而这个什么双亲委派不就是坑爹吗?

那么,这个双亲委派有啥用呢?为什么要这样搞?你还别说,用处蛮大,首先一点就是这样可以避免重复加载,也就是如果父类加载器已经加载过这个类的话,子类加载器就不需要再次加载了,另外还有非常重要的一点就是安全性,要知道,java的核心api类库都是被启动类加载器加载的,如果外部突然要加载一个,一个比如java.lang.xxx的话,这个会被传到启动类加载器,启动类加载器发现这个已经加载过啦,所以不管你,直接返回已经加载的,这样就有效的防止核心api被篡改。

13、Java内存模型详细

Java内存模型和Java内存结构的区别

首先要知道Java内存模型是多线程相关的。简称JMM,也即是共享内存模型,决定了一个线程对共享变量的写入时,能对另一个线程可见。

很多线程会使用同一个变量,称为共享全局变量,存放在主内存中,

在这里插入图片描述
从这张图我们可以看出,线程之间的通信是受jmm控制的,我们就这张图来说,线程A和线程B如何才能进行通信,假如线程A先执行,它会拿到共享变量,然后进行操作产生共享变量的副本,紧接着将本地内存中的共享变量副本刷新到主内存中,这时共享变量也就得到的更新,同理线程B再去操作共享变量就达到了线程A和线程B之间的通信了。

总结:什么是内存模型?

Java内存模型简称jmm,它定义了一个线程对另一个线程是可见的,另外就是共享变量的概念,因为Java内存模型又叫做共享内存模型,也就是多个线程会同时访问一个变量,这个变量又叫做共享变量,共享变量是存放在主内存中的,而且每一个线程都有自己的本地私有内存,如果有多个线程同时去访问一个变量的时候,可能出现的情况就是一个线程的本地内存中的数据没有及时刷新到主内存中,从而出现线程的安全问题。

在Java当中,共享变量是存放在堆内存中的,而对于局部变量等是不会在线程之间共享的,他们不会有内存可见性问题,当然就不会受到内存模型的影响。

Java内存模型–volatile关键字

首先我们要知道的就是volatile关键字的作用是什么?volatile的作用是使得变量在多个线程之间可见。
示例讲解
我们先创建一个线程

在这里插入图片描述
我们通过一个while循环来代表线程一直执行,然后通过isRun方法来控制线程的结束,接下来我们在主线程中这样操作
在这里插入图片描述
大家可以想一下,线程会停止吗?

实际运行的结果是不会,为什么呢?我们结合这张图来说明一下

在这里插入图片描述
这里的tag就是一个共享变量,首先子线程读取到的是在子线程中的本地内存中的共享变量副本,我们虽然在主线程中通过isRun方法将tag变成false,但是子线程中读取到的依然存放在本地内存中的副本

依然是ture,也就是说通过isRun已经将主内存中的共享变量tag刷新成false,但是子线程并没有在主内存中读取这个刷新后的值,所以线程不会停止,那么如何解决这个问题呢?

我们可以这样做

在这里插入图片描述
我们对tag使用volatile关键字,这样的话再次执行这个程序就会发现线程立马就结束了,这是因为一旦tag加上vola关键字,就强制要求每次使用tag都必须从主内存中取值,因此子线程可以拿到主内存中最新更新的tag也就是false,线程就自然而然的停止了。

原因
线程之间是不可见的,读取的是副本,没有即使读取到主内存结果,解决办法是使用volatile关键字解决线程之间的可见性,强制线程每次读取该值的时候都去主内存中读取。

Java内存模型–重排序

认识重排序
首先我们需要对重排序有一个简单的认识,那么什么是重排序呢?

我们从字面意思理解重排序肯定是与顺序有关,这里指的就是程序的执行顺序了,我们知道一段代码的执行会有先后顺序,但是也有这种情况就是代码执行的时候不是按照我们既定的顺序进行执行,而是发生了变化。

编译器和处理器就可能会对程序的执行进行重新排序,这就是重排序了。

我们这里举一个例子,我们首先要知道,堆内存是共享内存,可以被多个线程同时访问,假如现在有一个线程,线程中要执行两个操作,一个是写入a的值,另一个是写入b的值(注意a和b都是共享全局变量,存放在主内存中),而且b的值不依赖a的值,我们在线程中书写的代码可能是先写入a然后再写入b,但是在实际的运行中,处理器就能够自由的调整他们的顺序,而且b有可能会比a更加快的刷新到主内存中

数据依赖性

以上说的是在一个线程中操作两个变量,如果是两个操作同时访问一个变量,其中一个操作为写操作,那么这两个操作之间就形成了数据依赖,也就是具有数据依赖性。这里要明确一点,数据依赖性是形容操作之间的。

我们下面以一个示例来说明一下数据依赖性,首先定义一个user类,其中定义一个变量,如下

在这里插入图片描述
接下来我们的重点就在这个age上了,我们创建一个线程A

在这里插入图片描述
这里我们看操作1和操作2,可以看出操作2是依赖于操作1的,因为tag的值是受操作1影响的,我们就说操作1和操作2之间存在数据依赖性。

要知道我们这里主要说的是重排序,为什么要说数据依赖性呢?因为编译器和处理器不会对存在数据依赖关系的操作进行重排序,为什么?因为如果对存在数据依赖性的操作进行重排序的话,程序的执行结果就变了,我们来看下实际操作产生的结果。

我们看线程A应该是程序最开始的样子,也是我们要的结果,输出结果tag应该是25,但是如果发生重排序也就是操作2先于操作1执行,那么会发生什么情况呢?

在这里插入图片描述

很显然,因为重排序的原因,tag就成了0.

as-if-serial语义
其实无论怎么重排序,有一个规则是必须遵守的,那就是单线程程序执行的结果是不能被改变的,这个规则的官方叫法就是as-if-serial语义,编译器和处理器都是不能违背这个规则的,因此我们要记住这个结论:
编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变程序的执行结果。
重排序对多线程的影响
其实可以明显的知道重排序对多线程一定会有影响的,我们通过一个简单的代码来简单的说一下。

在这里插入图片描述
就比如这段代码,如果线程A执行writer方法,因为操作1和操作2之间并没有存在数据依赖关系,所以这里有可能发生重排序,可以想到,如果线程B执行reader方法,则一定会受到操作1和操作2重排序的影响。

那么,如何解决重排序问题就是个重要的话题了!

Java内存模型–锁(synchronized)

synchronized同步方法
首先看一个线程安全问题(实际代码模拟一个线程安全问题)

首先我们创建一个实体类

在这里插入图片描述
接下来在主线程中开启两个线程对实体类中的age进行数值修改
在这里插入图片描述
这个时候运行我们的代码可能会出现这样的问题
在这里插入图片描述
难道b线程中age不应该等于88吗?怎么都是66呢?这就出现了线程安全问题。

还可能会出现这样的情况

在这里插入图片描述
这种情况就说明线程的实际执行顺序并不一定按照代码书写的顺序。

而且还会出现这样的问题

在这里插入图片描述
这里也是发生了线程安全问题,那么我们该如何解决这个线程安全问题呢?要解决这个问题我们还要明白两个概念,那就是同步和异步,那什么是同步什么又是异步呢?

我们先来简单分析一下上述代码为什么会出现线程安全问题,其实很简单,对于age是共享内存,两个线程可以同时对它进行访问,当线程a访问它将它的数值修改成66的时候可能会出现的一种情况就是,线程a刚把age修改成66,线程b又把它修改成88了,导致读取到的都是88,也就是说线程a修改完成age之后被线程b打断了一下,没有及时的去读取到自己修改的值,而是读取到了被线程b修改的值。

再想一下为什么会出现这种情况呢?其实就是在线程a调用setAge之后,线程b又调用了这个setAge,因此发生线程安全问题,此时这个方法就是异步的,也就是可以被两个线程同时操作,如果这个setAge被线程a调用期间线程b不能调用,只有等线程a调用并且完成相关操作,线程b才能够调用,此时这个setAge就是同步的,而且也不会发生线程安全问题了

那么怎么实现我们上述所说的呢?

我们可以这样解决

在这里插入图片描述
也就是使用synchronized来修饰我们的setAge,这样的话当线程a调用setAge的时候就会把这个方法加锁,此时setAge是被锁住的,线程b是无法调用的,只有当线程a把setAge操作执行完成之后,锁才会被打开。

这就是我们要说的使用synchronized来同步方法

synchronized同步代码块
你觉得以上的方法就是最优的了吗?当然不是,你想一下我们使用synchronized来同步方法也就相当于给这个方法加上一个锁,不能同时被多个线程访问,但是如果这个方法中含有耗时操作而这个耗时操作又是不涉及线程安全的,那么我们使用synchronized来同步方法显然降低了性能,那该怎么解决这个问题呢?

解决的一个思路就是只对引起线程安全的代码进行synchronized同步,可以这样做

在这里插入图片描述
这就是使用synchronized来同步我们发生线程安全的代码块,这里要注意synchronized需要传入一个对象,这个对象可以是任意对象,但是要保证这个对象是被多个线程共享的,如果你把这个对象定义在了方法里,那么每个线程调用方法都会创建一个新的对象,如此一来,多个线程访问的就不是同一个对象,因此,依然发生线程安全问题,如下图代码操作就是错误的。
在这里插入图片描述

Java内存模型–final

写final域的重排序规则
在学习Java内存模型—final的时候是让我感觉最难的一个,不知道为什么,对这一块就是有点搞不懂,其实对于final它也是禁止指令重排序的,在学这个的时候上网搜索相关文章,好像只有一篇一位叫做程晓明的前辈写的《深入理解Java内存模型—final》,写的是比较详细的,也很感谢这位前辈的分享,在学习的时候,我一直纠结这样的一句话,就是对于final域,编译器和处理器要遵守的一个规则。

在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

不知道大家对这句话是否理解,我当初是十分不理解,首先你要知道,什么是重排序,重排序简单的说就是在程序执行的时候,编译器和处理器对程序的执行可能不会按照我们既定的顺序去执行,这里举一个非常简单的例子就是在一个主线程中,你写了这样的代码

int a=66;
int b=88;

这段代码从表面上看应该是线对a进行赋值操作,然后对b在进行赋值操作,但是实际的情况,可能是先对b进行赋值操作,这样的情况就是发生了重排序,而在某些情况,如果发生了重排序是会出现一些问题的。

我们继续来看这个规则

在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

为了叙述方便,我们继续使用程晓明前辈文章中的代码,后面回帖出原文地址,首先我们看这样一段代码
在这里插入图片描述
按照规则所说,意思是不是在写线程A中的创建FinalExample的时候会出现这样一种情况,那就是这个对象已经创建完成,可是在构造函数中对i和j的写操作还没有执行,这样的话在线程B执行的读取操作,获取到的i和j就都是初始化的值而并不是1和2了,而按照规则说的,对于普通变量也就是int i=1可能会发生这种情况,而对于final变量也就是final int b=2则不会发生这种情况

然后我们再想一下重排序,要知道重排序是指操作与操作之间发生了重排序,那么这里为什么会出现这样的重排序呢?是哪些操作发生了重排序呢?

那么重点就在obj=new FinalExample();这段代码上了

我们分析下这段代码,其实它包含两个操作,如下

  1. 构造一个FinalExample类型的对象;
  2. 把这个对象的引用赋值给引用变量obj。

但是,我就是理解不了,这两个操作会发生重排序?不大可能,那么这个重排序到底在哪,既然不在这,那就去其他地方找问题,经过分析,我觉得可能在new FinalExample();上,也就是创建FinalExample对象的时候,这个操作其实也有两个操作

  1. 创建FinalExample对象
  2. 对i和j赋值

想了又想,觉得能够发生重排序而且比较合理的只有这两个操作了,然后我们结合下面一张图来看会更加容易理解。

在这里插入图片描述
也就是说在执行构造函数,也就是创建FinalExample对象的时候,这一步创建对象的操作和构造函数中的给变量赋值的操作可能发生重排序,但是对于final的变量则不会发生重排序,也就是构造函数完成,创建对象成功的同时,final变量的值也成功写入,但是对于普通变量就有可能出现的情况就是,我构造函数已经执行完成,对象也创建成功了,但是还没有给普通变量赋值呢,等创建完对象之后在构造函数之外才开始对普通变量进行赋值,也就是对普通变量的赋值重排序到了构造函数之外

以上被称为写final域的重排序,下面还有一个读final域的重排序,这个我还是有疑问的,下面和大家一起看一下

读final域的重排序规则
首先对于读线程B执行的reader方法,就是这些代码

在这里插入图片描述
很显然,这里有三个操作

  1. 初次读引用变量obj;
  2. 初次读引用变量obj指向对象的普通域j。
  3. 初次读引用变量obj指向对象的final域i。

对了,我们需要知道读final域的重排序规则

在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。

以上就是读final域的重排序规则,什么意思呢?

我们说过重排序是针对操作之间的,那么这里就很明确,发生重排序一定是以下操作

  1. 初次读引用变量obj;
  2. 初次读引用变量obj指向对象的普通域i。
  3. 初次读引用变量obj指向对象的final域j。
    我们知道这里正确的执行顺序应该是先执行1也就是初次读取引用变量obj,不然的话你接下来的读值都是错误的啊,根据读final域重排序规则,3的执行必定在1之后,那也就是说对于普通变量,可能出现的情况就是,还没有读取obj呢,你就开始读i了,这肯定是错误的,也就是发生这样的重排序必然出错。

对于我不理解的地方就是,这两者怎么会发生重排序,我们知道在重排序里面有一个数据依赖性规则,也就是对于存在数据依赖关系的操作,不会发生重排序,那什么是数据依赖性呢?

数据依赖性就是对于两个操作,如果一个操作依赖于另一个操作,并i企鹅要提个操作为写操作,那么这两个操作就存在数据依赖性,这个时候我们在看这两个操作

  1. 初次读引用变量obj;
  2. 初次读引用变量obj指向对象的普通域i。

根据读final域重排序规则,这两个操作是会发生重排序,但是你仔细观察这两个操作的执行代码

在这里插入图片描述
如果你觉得是在使用obj和i,那这都是读操作,但是对于FinalExample object=obj;来说,object被赋值不就是写入操作吗?而且这两个操作都跟object有关系,这两个操作之间难道不存在数据依赖性吗?

14、Java的垃圾回收

什么是垃圾回收机制

什么是java垃圾回收机制啊,这个是不是就是跟java虚拟机有关啊?

Java的垃圾回收机制应该是java虚拟机中很重要的知识点,那么要学习这块我们首先要搞清楚的就是,什么是垃圾回收机制,那么,什么是垃圾回收机制呢?

垃圾回收机制我们主要需要理解的就是这个垃圾回收,那么你就要理解两点,第一点什么是垃圾,如何定义这个垃圾,另外一点就是什么是回收,这个回收是什么含义。

我们先从第一点说起,那就是垃圾,首先从字面意思理解那就是无用的东西,在java中指的就是那些无任何引用的对象,我们知道一个对象一旦被创建出来就会在内存中分配对应的内存,但是,内存空间是有限的,在一个程序中,我们可能需要创建很多的对象,但是,如果创建的对象很多的情况下就必定会出现一种情况,那就是内存空间不够用了,怎么办,像C语言或者C++中都是需要我们手动的去释放一些内存的,也就是说,这个对象如果没有用了,就需要将它销毁,如此一来它所占用的内存也就得到了释放,但是在java中,我们是不需要自己手动的去释放内存的,垃圾回收机制会主动自动的帮我们去做这件事,那么,到这里你就要明白,所谓的垃圾,就是一些用不到的对象,但是还占着内存空间,这就是垃圾。

那么,我们再来说回收,要注意,这里的回收不是回收对象本身,而是对象所占用的内存,这样说,你可能还是不太理解,简单来说就是,你这个对象不用了,但是还占用着内存,那么,我就把你销毁掉,然后你所占用的内存也就是释放了出来,这就是回收。

到这里,我们就可以理解垃圾回收机制就是帮我们自动销毁无用对象,释放对象所占用内存的一种机制,这是java中比较重要的一个特性。

哦哦!你这么一说,我就明白了,java的垃圾回收机制真好,不用像C或C++那样需要手动释放内存了!对了,庆哥,我经常听到jvm和gc,这是什么啊?

这个其实很简单,jvm指的就是java虚拟机,而gc指的就是我们的垃圾回收啦,为什么,你看这个

垃圾回收(Garbage Collection)Java虚拟机(JVM)

看到没,其实就是英文的首字母缩写!

如何判断垃圾对象

庆哥:终于问到点子上了,那么,你觉得gc是如何判断哪些对象有用,哪些对象是没用的呢?

小白:我觉得,这应该是某种神奇的算法起的作用,哈哈!

庆哥:可以啊,还能想到算法,其实gc就是通过一些垃圾回收算法来判断哪些是垃圾对象的!

小白:那是什么算法啊,赶快说说。

庆哥:其实java语言规范并没有规定要使用哪一种算法来进行垃圾的回收,而是在不断的演变过程中,另外这一块需要分清两点

判断垃圾对象的算法回收垃圾对象的算法

以上可以说是垃圾回收算法,我们可以想一下,如果让你来设置一个算法进行垃圾回收,那么你该怎样设计呢?首先你肯定要考虑该怎么将这些垃圾对象给查找出来,然后就是如何对这些垃圾对象进行回收了。

那么,我们首先来看两种经典的判断垃圾对象的算法

引用计数算法

首先就是jvm早期使用的引用计数算法了,这种算法是如何查找出哪些对象是垃圾对象呢?

在引用计数算法当中,每一个对象都有一个计数器,每当这个对象的引用被使用一次,这个计数器就会自动加1,当然,如果这个对象的引用被重新赋值等操作,这个对象被使用的次数也就减少,说以这个计数器也就减一,当一个对象的计数器为0的时候也就是说这个对象没有任何地方被引用,可以判断为垃圾对象。

你觉得这种算法好吗?

小白:这个挺好理解的,而且感觉这个算法很不错啊,应该没有什么不好吧!

庆哥:其实任何一个算法都是好坏并存的,又优点肯定也会有缺点,对于这种引用计数算法的有点就是执行起来非常简单,而且效率还不低,但是缺点就是对循环引用的检测不是很好,而且增加了程序执行的开销。

所以在早期的jvm当中是采用这种算法,但是现在更多的是采用根搜索算法。

根搜索算法

小白:根搜索算法?这个是什么,感觉不那么好懂啊!

庆哥:在这种算法当中会以一些列的gc跟对象作为起始点,然后去搜索先关的引用节点,然后通过这些引用节点再去搜索其下面的引用节点,然后这样重复,最后搜索的路径被称为引用链,如果到最后一个对象的引用并没有跟任何的一个引用链相连接的话,就可以判定这个对象是无用对象,也就是垃圾,我在网上找了一张图,你可以看看。

在这里插入图片描述

图中的GC ROOTS就代表着gc根对象,蓝色的点就代表有用的对象,而灰色的就是垃圾对象了。

小白:有点晕啊,那什么是gc根对象啊?

庆哥:算法本身就是较为抽象的东西,所以理解起来有点困难,这个gc根对象很重要,对理解根搜索算法很重要,那什么是gc根对象呢?

gc根对象包括以下内容(1)虚拟机栈中引用的对象(栈帧中的本地变量表);(2)方法区中的常量引用的对象;(3)方法区中的类静态属性引用的对象;(4)本地方法栈中JNI(Native方法)的引用对象。(5)活跃线程。

小白:啊,感觉好难理解啊!脑细胞要死光光了!

庆哥:哈哈,正常,慢慢来,多理解理解,我们继续讲哈!

小白:嗯嗯,继续吧,让暴风雨来的更猛烈些吧!

庆哥:那就满足你,我们以上说了判断垃圾对象的两种经典算法,其实最主要的就是根搜索算法,这个跟下面我们要说的回收垃圾算法是有关系的,接下来我们就来说一下这个回收垃圾的几种经典算法

如何回收垃圾对象

标记—清除算法

这种算法我们从字面意思上就能猜出它是分为两个阶段的,第一个阶段就是标记,什么意思呢?在标记阶段其实就是查找垃圾对象的,而这个标记过程其实就是前面我们说到的根搜索算法,第二个阶段就是清除了,当第一阶段标记完成,会将所有的垃圾对象统一收集起来,然后清除掉。

那么这种对象的优缺点呢?

标记—清除算法只标记垃圾对象,所以有用对象较多的情况下极为高效,但是这种算法在执行的过程中标记和清除效率都不是很高,而且会产生大量的内存碎片。

标记—整理算法

这种算法和标记—清除算法中的标记阶段是一样的,但是在此算法中并不是将所有的垃圾对象收集在一起同意的清除掉,而是将所有的垃圾对象移动到一端,然后直接清除掉端边界以外的内存。

这种算法的有点在于新对象的分配简单而且不会产生碎片的问题,但是缺点就在于gc暂停的时间会增长。

复制算法

这种算法将内存容量分成大小相等的两块,当这一块内存使用完毕之后,就把还有用的对象统一复制到另外一块内存上,然后将这块内存统一清除掉。

这种算法的运行是很高效的,而且实现简单也不会有碎片问题,但是缺点也很明显,因为内存被划分为两半,所以一次性可分配的内存就减少了一半!

jvm gc机制
要分清垃圾回收和垃圾回收机制,垃圾回收机制也就是GC机制,这个主要讲的就是如何对堆内存中的对象进行内存回收的,是一个方式或者方法。

也就是先从哪里回收,什么时候回收或者是满了该怎么办。

一个jvm实例只会存在一个堆内存,而且大小可以调节。

GC机制分为两类:Minor GC和Full GC

注意,这两个可以理解为是一个动作,动作执行之后的结果就是释放相应的内存空间(将对象转移,空出该对象占用的空间以便新对象使用,删除垃圾对象,释放空间)

具体的情况是这样的:
在这里插入图片描述

感谢阅读,PDF获取

谢谢大家的阅读,我估计你们没有看完吧,哈哈,3万多字呢?我这里准备了PDF,你们可以去我的公众号获取,微信搜索“编码之外”,关注后回复“虚拟机”即可获得PDF,啥?你不知道怎么关注公众号,那好吧,加我微信H653836923,我亲自给你发,另外大家有啥问题可以在这里留言讨论,大家一起学习哈!

在这里插入图片描述

ithuangqing CSDN认证博客专家 终身学习者 自学Java 原创Java教程
一个自学Java的程序员,通俗易懂的聊聊技术与生活!原创20万字的Java零基础自学教程,适合各种新手小白,欢迎下载学习,微信搜“编码之外”,关注后回复“PDF”即可获取下载链接!
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 终极编程指南 设计师:CSDN官方博客 返回首页
实付 9.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值