目录
一. Java语言的特点
二. 如何跨平台
三. JVM简介
四. 垃圾回收
JVM的内存结构
JVM内存模型的划分:堆(Heap)、栈(Stack)、程序计数器(PC)、方法区。
堆和栈的区别:
说说GC和分代回收策略(分代垃圾回收算法)
说一下JVM中的分代回收?
说一下JVM有哪些垃圾回收器?
Minor GC、Mixed GC、Full GC的区别是什么?
Mixed GC
对象的构成:
标记复制:
引用类型的应用
程序计数器是一块儿较小的内存
1. 百度
2. 思维导图
一次编写、到处运行(Write Once,Run Anywhere);真正意义上的实现了跨平台。
那再问一个问题,为什么Java可以跨平台?
大多数人都知道Java可以跨平台得益于 JVM(Java虚拟机)。
“一次编译,到处运行” 是Java的跨平台特性。像 C 、C++ 这样的编程语言没有它。
Java是一种可以跨平台的编程语言。首先,我们需要知道什么是平台。我们把CPU处理器与操作系统的整体叫平台。
CPU相当于计算机的大脑,指令集是CPU中用来计算和控制计算机系统的一套指令的集合。
指令集分为精简指令集(RISC)和复杂指令集(CISC)。每个CPU都有自己的特定指令集。
要开发一个程序,我们必须首先知道程序运行在什么CPU上,也就是说,我们必须知道CPU使用的指令集。
操作系统是用户与计算机之间的接口软件。不同的操作系统支持不同的CPU。严格来说,不同的操作系统支持不同的CPU指令集。
如果你想开发一个程序,首先应该确定:
CPU类型,即指令集类型;
操作系统;我们称之为软硬件平台的结合。也可以说“平台=CPU+OS”。而且由于主流操作系统支持主流CPU,有时操作系统也被称为平台。
- 通常,我们编写的Java源代码在编译后会生成一个Class文件,称为字节码文件。Java虚拟机负责将字节码文件翻译 / 解释成特定平台下的机器代码,然后运行。简言之,Java的跨平台就是因为不同版本的 JVM。换句话说,只要在不同的平台上安装相应的JVM,就可以运行字节码文件(.class)并运行我们编写的Java程序。
- 首先将Java代码编译成字节码文件,然后通过JVM将其翻译成机器语言,从而达到运行Java程序的目的。因此,运行Java程序必须有JVM的支持,因为编译的结果不是机器代码,必须在执行前由JVM再次翻译。即使您将Java程序打包成可执行文件(例如。Exe),仍然需要JVM的支持。
- 字节码是一种中间语言,它被JVM可以解释为适用于不同平台的机器码,这样就可以实现它的跨平台。
- 我们通过java命令就可以运行字节码文件,实际上当我们执行java命令的时候,它就会创建一个Java虚拟机。Java虚拟机有了之后,它做的一件非常重要的事情就是创建了一个名字为main的主线程,主线程就是用来执行我们程序的入口方法,也就是这个主方法的代码,主线程执行主方法。主线程也需要分配内存,由JVM Stacks --- Java虚拟机栈来为我们的主线程分配内存。不止是主线程,只要你是Java中的线程,那么它所使用的内存都是来自于Java虚拟机栈中 --- JVM Stacks。
注意:编译的结果不是生成机器代码,而是生成字节码。字节码不能直接运行,必须由JVM转换成机器码。编译生成的字节码在不同的平台上是相同的,但是JVM翻译的机器码是不同的。
1. 什么是JVM?JVM的作用是什么?
JVM是 Java Virtual Machine Java虚拟机的缩写,是Java程序的运行环境(Java二进制字节码的运行环境)。它的作用是将Java程序编译后的字节码转换为机器码并执行。JVM------Java Virtual Machine。Java虚拟机位于操作系统之上(如下图所示),将通过JAVAC命令编译后的字节码加载到其内存区域,通过解释器将字节码翻译成CPU能识别的机器码执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
JVM是运行在操作系统之上的,它与硬件没有直接交互。
JVM有什么好处?
- 一次编写,到处运行
- 自动的内存管理,垃圾回收机制(C语言需要程序员自己去管理内存,如果程序员编码不当,则很容易造成内存泄漏的问题)
JVM由哪些部分组成,运行流程是什么?
从图中可以看出 JVM 的主要组成部分:两个子系统:
- ClassLoader(类加载器)
- Execution Engine(执行引擎)
两个组件:
- Runtime Data Area(运行时数据区,内存分区)
- Native Method Interface(本地方法接口)
运行流程:(1)首先通过编译器(javac命令)把Java 代码转换为字节码文件。(2)类加载器( ClassLoader )再把字节码文件加载到内存中,将其放在运行时数据区 (Runtime Data Area )的方法区内,而字节码文件只是JVM的一套指令集规范,并不 能直接交给底层系统去执行,而是由特定的命令解析器执行引擎(Execution Engine)运 行(java命令),将字节码翻译成底层系统指令,再交由CPU去执行。此时需要调用其他 语言的本地库接口(Native Method Interface) 来实现整个程序的功能。用一句话来解释: 类的加载就是将类的字节码文件(.class文件)中的二进制数据读入到内存 中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class类对象,用来封装在方法区内的数据结构。
垃圾回收概念:
- 当堆内存中的类对象或数组对象,没有被任何变量引用时,就会被判定为内存中的垃圾,Java存在自动垃圾回收器,会定期进行清理。
- 垃圾回收机制是一种自动化的内存管理机制,它可以自动识别和回收不再使用的内存空间,避免内存泄漏和内存溢出问题。
- 垃圾回收机制通过跟踪对象的引用关系,确定哪些对象可以被回收,然后释放这些对象占用的内存空间。
先了解什么是引用?
A a = new A(); 变量a不就在引用对象A吗?
比如说我new了一个对象A,它依赖一个对象B,B里面就有A的依赖,A就依赖B,那么对象B就有一个引用在它身上,这个时候对象B是不能被回收的。
JVM中常见的两种标记垃圾的算法,什么是垃圾,怎么样才能够定义什么是垃圾(两种判断对象的存活算法),2种垃圾回收机制:
- 引用计数法、可达性分析算法。
1. 引用计数法:只要这个对象被其它人引用了,这个对象就不能被回收掉。
- 在对象中添加一个引用计数器,每当有一个地方引用它时,就在当前对象的对象头上递增依次引用次数,计数器就加 1,当引用失效时计数器减 1。当计数器为0的时候,也就表示这个对象的引用次数为0,表示当前对象可以被回收。
优点:这种方法的原理很简单,判断起来也很高效,可以立即回收垃圾对象
但是存在两个问题,缺点:
- 堆中对象每一次被引用和引用清除时,都需要进行计数器的加减法操作,会带来性能损耗。
- 缺点是无法处理循环引用的问题,会导致内存泄漏。
- 当两个对象相互引用时,计数器永远不会0。也就是说,即使这两个对象不再被程序使用,仍然没有办法被回收,通过下面的例子看一下循环引用时的计数问题:
这种情况也不是完全可用的,如果说A依赖B,B又依赖A,它产生了一个循环依赖,但这个时候A,B两个对象又没有被其它人引用了,那这个时候A、B按道理也要被回收,如果按照引用计数法,它们两个身上都有对方的引用,按这种算法它就回收不了,它就不能被判定为垃圾,这样就会导致内存泄漏。
循环引用,会引发内存泄漏。
引用计数的变化过程如下图所示:
- 可以看到,在方法执行完成后,栈中的引用被释放,但是留下了两个对象在堆内存中循环引用,导致了两个实例最后的引用计数都不为0,最终这两个对象的内存将一直得不到释放,也正是因为这一缺陷,使引用计数算法并没有被实际应用在gc过程中。
首先垃圾回收的是对象,new出来的才能叫对象。
这个概念就类似于我们正常生活中,相当于你每次在丢垃圾的时候,你都会先向一下你的这个东西需不需要用到,如果要用到,我肯定就不扔,用不用其实就是引用的概念。就比如你可能有一个电视遥控器和一个电视,电视遥控器要去操控电视,电视也需要又电视遥控器,它两互相引用,但是你整个电视你都不要了,那肯定这两个都要扔掉。
2. 可达性分析算法(GC ROOT):可达性分析算法是jvm默认使用的垃圾回收机制,现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾,其实就是通过GC ROOT去判断。什么叫GC ROOT,就是在某一时刻 / 当前一定不能够被回收的对象,因此GC ROOT对象所依赖的其它对象肯定也不能被回收。一个对象被 GC Root 直接 或 间接持有,那么该对象就不会被当作垃圾对象。
在Java中,可作为GC Root的对象有以下几种:
- 在虚拟机栈(栈帧的本地变量表)中引用的对象:方法的局部变量,指存储在Java虚拟机栈中的局部变量和操作数栈中的对象。
- 在方法区中静态属性引用的对象:静态变量/类变量
- 在方法区中常量引用的对象:常量,在Java中,常量可以通过类名直接访问
- 在本地方法栈中JNI --- Java Native Interface(
native
方法)引用的对象。JNI是Java Native Interface的缩写,它是Java虚拟机提供的一种机制,用于在Java程序中调用本地方法(Native Method)。本地方法是指使用其他编程语言(例如C、C++、Assembly等)编写的方法,它们可以直接访问底层系统资源和硬件设备。在Java程序中调用本地方法时,需要使用JNI来进行交互。JNI提供了一组标准的接口,用于在Java程序和本地方法之间进行数据传输和调用。- 被同步锁持有的对象
- 被synchronized锁住的对象是绝对不能回收的,因为GC如果回收了对象,锁不就失效了嘛
- jvm内部的引用,如基本数据类型对应的Class对象、一些常驻异常对象等,及系统类加载器
- 可达性分析 解决了 引用计数 循环依赖 / 相互引用的问题
举例:现在有个new一个对象B,你给它调用一个start()方法,这个方法在执行的时候,对象B能够被回收吗?当然不能!还有一种静态变量,比如说我有一个类,里面有一个static B b = new B();这个静态变量/类变量是伴随着这个类的生命周期一直存在的,只要这个类不被卸载,它就肯定不能被回收。
可达性分析算法寻找的是仍然存活的对象。至于这样设计的理由,是因为如果直接寻找没有被引用的垃圾对象,实现起来相对复杂、耗时也会比较长,反过来标记存活的对象会更加省时。
注意:千万不要把引用和对象两个概念混淆,对象是实实在在存在于内存中的,而引用只是一个变量/常量并持有对象在内存中的地址值。
- 在Java中一个对象被回收会调用其
finalize
方法- JVM中垃圾回收是在一个单独线程进行(GC线程)
=======================================================================
JVM垃圾回收算法 / GC算法:标记复制算法、标记清除算法、标记整理算法。
标记:就是找到那些还在使用不能被垃圾回收的对象,给它们加一个标记。将来垃圾回收发生了,标记的对象我就把它保留下来,没有标记的对象我就把它作为垃圾给它清理掉。
标记清除:垃圾回收分为2个阶段,分别是标记和清除。遍历整个堆中所有的 GCRoot 对象,如果可以被 GCRoot 就加个标记,沿着 GC Root 对象的引用链找,如果被GC ROOT对象直接或间接引用到的对象也加上标记,剩下所有未被标记的对象都将视为垃圾被清除。直接把所有未被标记的对象全部清除,把它从JVM引用里面给它去掉。这种是最简单的一个方式。
GC Root对象(根对象):即那些一定不会被回收的对象,如正执行方法内局部变量引用的对象、静态变量引用的对象(除非你的类卸载了,否则静态变量所引用的对象会一直存在)
- 优点:实现简单,不需要移动和复制对象
但是它也有缺点:容易没有连续的内存空间,容易生成空间碎片 / 内存碎片,简答点儿说空间碎片就是不连续的空间,可用内存分布比较分散,就放不了大对象了,影响堆内存的利用率。
标记清除算法的执行效率比较低。标记清除算法需要遍历整个堆,标记和清除操作都非常耗时,同时在很多大型应用程序中标记清除算法的执行效率会受到极大的影响。
由于清除操作会在堆中留下许多空洞,因此标记清除完需要进行压缩整理,以便讲堆内的存活对象尽可能地移到一端,形成一个连续、无碎片的存储空间,以便新地对象可以被顺序地分配和使用。
CMS垃圾回收器使用的是标记 - 清除算法。唯一一个使用标记-清除算法的垃圾回收器。
标记清除算法基本不用了,已经被淘汰了。
JVM内存模型/结构
- JVM内存模型也叫Java内存区域或者Java运行时数据区,它是Java虚拟机-JVM在运行时为Java进程对内存进行的逻辑划分。
JAVA源代码文件通过编译后变成虚拟机可以识别的字节码,JAVA程序在执行时,会通过类加载器把字节码加载到虚拟机的内存中(虚拟机的内存是一个逻辑概念,相当于是对主内存的一个抽象,实际上真实的数据还是存放在主存中),详见下图。
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分为若干个不同的数据区域。每个区域都有各自的作用。
分析 JVM 内存结构,主要就是分析JVM 运行时数据存储区域。JVM 的运行时数据区主要包括:Java堆、栈、方法区、程序计数器等。
在JVM中,程序计数器(PC)、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动内存清理,因此垃圾回收器主要集中于Java堆和方法区,程序运行期间,这部分内存的分配和使用都是动态的。
- 而 JVM 的优化问题主要在线程共享的数据区中:堆、方法区(因为你创建出来的这个对象,放在堆里了,那线程一线程二都可以访问到,包括方法区里这些类的信息也是多个线程大家都能访问到)。
- 栈(Java虚拟机栈 + 本地方法栈)和程序计数器是线程私有的。
JVM内存模型的划分:堆(Heap)、栈(Stack)、程序计数器(PC寄存器)、方法区。
JDK 1.8 之前: