『一篇就够了系列』关于JVM,这里有你想知道的一切

本文详细介绍了JDK、JRE和JVM的概念、使用场景、逻辑与运行关系。重点阐述JVM架构,包括类加载子系统、运行时数据区等。还介绍了类加载过程,以及运行时数据区各部分特点。此外,讲解了堆垃圾回收的引用计数法和可达性分析算法。

使用场景:JRE一般是给想要运行Java程序的人员使用。

2. JDK


The Java Development Kit (JDK) is a software development environment used for developing Java applications and applets. It includes the Java Runtime Environment (JRE), an interpreter/loader (Java), a compiler (javac), an archiver (jar), a documentation generator (Javadoc) and other tools needed in Java development.

JDK是一个用来开发Java Application和Applets的开发环境。它包含了JRE、解释器、编译器、打包工具、文档生成器等。

使用场景:JDK一般是给开发人员使用。

3. JVM


  1. A specification where working of Java Virtual Machine is specified. But implementation provider is independent to choose the algorithm. Its implementation has been provided by Sun and other companies.

  2. An implementation is a computer program that meets the requirements of the JVM specification

  3. Runtime Instance Whenever you write java command on the command prompt to run the java class, an instance of JVM is created.

JVM,Java Virtual Machine的简称,简单来说就是用来执行字节码的,它并不认识Java代码,它将字节码翻译成对应平台的机器指令,再由对应的平台执行机器指令。这也就是我们所说的写一次Java代码,能够在各种平台上运行:WORA(Write Once Run Anywhere)。所以每个平台需要有一个对应的JVM。

另外JVM本身也是一种规范(specification),不同厂商可以有不同的实现。目前比较出名的JVM是Oracle的HotSpot,它就是JVM规范的一种实现。

最后说到编程语言,不管是用Java还是其它的什么语言,只要编译器生成的字节码能够符合JVM规范,JVM能够读懂就行。简单理解这样的编程就是面向JVM规范编程, JVM成为了一个能容纳其它语言运行的平台,如Scala、Kotlin、Groovy、Clojure等。

使用场景:不管是用JRE还是JDK,最后都要用到JVM,因为JVM会一行一行地解释代码(字节码),因此JVM也可以认为是一个解释器。

4. 三者逻辑关系


在这里插入图片描述

**JDK = JRE + Development/debugging tools

JRE = JVM + Java Packages Classes(like util, math, lang, awt,swing etc)+runtime libraries.

JVM = Class loader system + runtime data area + Execution Engine.**

JVM是JRE的子集,JRE是JDK的子集,JDK是JRE的超集。

再来个详细的图:

在这里插入图片描述

5. 三者运行关系


Runtime是指JRE执行环节;

Compile是指JDK中的编译环节;

在这里插入图片描述

运行Java应用时,三者的关系图:

在这里插入图片描述

上图同样展示了Java程序的运行流程有如下步骤:

  1. 可以使用你熟悉的IDE进行开发,保存为一个.java的源文件;

  2. 通过JDK中的编译器,将源文件编译成字节码格式的文件,后缀是.class;

  3. .class文件通过各种可能的渠道,交由这个平台对应的JVM处理;

  4. JVM将字节码翻译成机器码,然后由本机执行这个机器码;

二、JVM架构

=================================================================

要深入了解JVM,先从JVM的架构说起。

从网上找到一张图,清晰地展示了JVM的架构:

在这里插入图片描述

可以看到,JVM架构分五块,其中前三个较为重要:

**1. Class Loader SubSystem:类加载子系统;

2. Runtime Data Areas:运行时数据区域;

3. Execution Engine:执行引擎;**

4. Java Native Interface:JNI接口;

5. Native Method Libraries:本地方法库;

1. 类加载子系统


(1) 类加载定义

类加载的过程要做的事情,简单来说就是将.class中的字节码文件读入内存, 并将这个字节流所代表的静态存储结构转化为Method Area的运行时数据结构,最后还会在Heap中生成一个Class类对象( java.lang包内的Class,区别于类的对象),作为方法区数据的访问入口。

注:

查看Class类源码可以看到,Class类的构造函数是私有的,也就是不能直接通过new创建,而是只有JVM才能创建Class类对象。

在这里插入图片描述

通常可以通过class<?> clazz = Class.forName("com.java.Test");进行类加载。

(2) 类加载时机

只有在 第一次 主动调用 这个类的时候才会对这个类进行加载。

也就是说类只有一次加载机会,一旦加载了后续就不会重新加载。

关于主动调用,有以下六种情况,其它情况都属于被动调用:

  1. 一个类的实例被创建(new操作、反射、cloning,反序列化)

  2. 调用类的static方法

  3. 使用或对类/接口的static属性进行赋值时(这不包括final的与在编译期确定的常量表达式)

  4. 当调用 API 中的某些反射方法时

  5. 子类被初始化

  6. 被设定为 JVM 启动时的启动类(具有main方法的类)

(3) 类加载过程

JVM将字节码转换成运行时对象分为三个流程:Loading、Linking、Initialization。

1) Loading(加载)

Loading即类加载,JVM提供了三种类加载器(Class Loader):Bootstrap、Extension、Application。

先找到所加载类的依赖,首先从Bootstrap类加载器开始,它会在$JAVA_HOME/jre/lib目录下的rt.jar包中查找;如果没有找到,再通过Extension类加载器在$JAVA_HOME/jre/lib/ext目录下的类文件中查找;如果还没找到,再通过Application类加载器查找CLASSPATH环境变量下的所有类文件和jar包文件。如果通过以上步骤仍然未找到.class文件,那么直接抛出异常ClassNotFoundException

需要注意的是,Java中类加载器的加载方式采用了双亲委托机制(Delegation-Hierarchy principle)

在这里插入图片描述

流程就是我们上面提到的从Bootstrap到Extension再到Application,依次递归。

双亲委托的好处:

  1. 强制从最高一级开始加载,保证核心类能够加载完毕;

  2. 避免重复加载已有的类;

  3. 从安全角度考虑,避免不安全的类被加载进来(比如已经由最高一级加载进来了String类,其它第三方的String类就无法被加载);

注意:

  1. 不同的classloader加载的同一个class文件,也是不同的类;

  2. 实现自己的类加载器,只需要继承ClassLoader,并覆盖findClass方法。

2) Linking(链接:Verify、Prepare、Resolve)

Linking即链接,分为三个阶段:Verify、Prepare、Resolve。

  1. Verify:校验

校验.class文件的正确性,比如.class文件是否适当地格式化了,又或者.class文件是不是由有效的编译器编译出来的。如果校验不通过,则会抛出异常java.lang.VerifyError

注:此步骤不是必须,可通过配置跳过。

  1. Preparation:准备

为类中的静态变量(static variables)分配内存空间, 不进行任何初始化动作,也就是0或null;

注:类中的静态变量可以理解为是类成员变量,而不是实例成员变量。

  1. Resolve

将符号引用替换成直接引用。

3) Initialization(初始化)

类加载最后一个流程,初始化所有静态变量,并执行静态代码块。

加载的顺序是在这个类中从上到下,从这个类的父类再到这个类本身。

这个阶段会去真正执行代码,具体包括:静态变量、静态代码块、构造函数、变量显式赋值。

这些代码执行的顺序遵循以下两个原则:

  1. 有static先初始化static,然后是非static的

  2. 显式初始化,构造块初始化,最后调用构造函数进行初始化

2. 运行时数据区


运行时数据区,即Runtime Data Areas,也叫也叫JVM Memory(JVM内存区)。它由五大块组成:

  1. Method Area:方法区;

  2. Heap Area:堆区;

  3. Stack Area:栈区;

  4. PC Registers:程序计数器;

  5. Native Method Stack:本地方法栈;

其中Method Area、Heap Area都只有一个,跟随JVM创建而创建,而Stack Area、PC Registers、Native Method Stack是随线程创建而创建,随线程销毁而销毁的,所以可能会有多个。

在这里插入图片描述

(1) Method Area(线程共享)

  1. 永久代(Permanent Generation), 用于存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。需要注意的是,在HotSpot中,HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。

  2. 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池 (Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。 Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。

(2) Heap Area(线程共享)

  1. 创建的对象和数组都保存在 Java 堆内存中,

  2. 也是垃圾收集器进行垃圾收集的最重要的内存区域。

  3. 由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden区、From Survivor区和To Survivor区)和老年代。

(3) Stack Area(线程私有)

也叫虚拟机栈,它有如下特点:

  1. Stack Area是描述 java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。由此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部,以及使用递归方法的时候容易导致栈内存溢出的现象。

  2. 栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

  3. 栈上分配:对于小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上(直接分配在栈上,可以自动回收,减轻GC压力);大对象或者逃逸对象无法栈上分配。

(4) PC Registers(线程私有)

PC Registers也叫程序计数器,由于是线程私有的,所以它存储的是这个线程当前执行指令的物理地址。唯一一块不会出现OutOfMemoryError的区域。

(5) Native Method Stack(线程私有)

虚拟机栈为执行 Java 方法服务, 而本地方法栈则为本地方法服务。什么是本地方法?简单的说Native Method就是Java能调用非Java方法的一个接口。

3. 执行引擎


执行引擎就是用来运行.class文件的,它一行一行地读取运行数据区中的字节码。执行引擎分为三个部分:Interpreter、JIT、Garbage Collector。

  1. 解释器(Interpreter):

JVM解释器根据预先定义好的JVM指令到机器指令的映射,将每一条字节码指令都转换成对应的本地指令(native instruction),并且在没有对代码进行任何优化的情况下,直接执行。

  1. JIT 编译器(JIT compiler):

为了提高性能,JIT 编译器会将合适的字节码序列(如上面提到的重复方法调用代码,以及其他的重复代码)编译成本地机器码,这些本地机器码可以被重复使用,以此来提高系统的性能。

JIT编译器由四个部分组成:

  • 中间代码生成器(Intermediate Code Generator): 用于生成中间代码;

  • 代码优化器(Code Optimizer): 用于优化上面生成的中间代码;

  • 目标代码生成器(Target Code Generator): 生成本地机器码;

  • 配置器(Profiler): 一个特殊的部分,用于查找hotspot中的方法是否多次执行。

[补充:]

解释执行:

将高级语言编写的代码一条一条读取,解释成等价的低级语言代码并在对应的低级虚拟机上执行,在读取解释下一条代码,直到全部代码解释执行完毕。

编译执行:

将所有的由高级语言编写的程序进行编译(转换成能够实现等价功能的低级语言程序),并在低级虚拟机上执行。

(JIT, Just-in-time)及时编译:

结合解释执行和编译执行的特点,它编译一部分代码,执行,再继续编译执行(不是一次性编译)。

计算机体系结构中将计算机系统按功能划分层次结构:

第6级(虚拟机) -> 应用语言机器

第5级(虚拟机) -> 高级语言机器

第4级(虚拟机) -> 汇编语言机器

第3级(虚拟机) -> 操作系统机器

第2级(物理机) -> 传统机器语言机器

第1级(物理机) -> 微程序机器

  1. 翻译(Translation): 先用转换程序把高一级机器上的程序转换为低一级机器上的等效程序,然后再在该低级机器上运行,实现程序的功能。
  1. 解释(Interpretation): 是对于高一级机器上的程序的每一条语句或指令,都转成去执行低一级机器的一段等效程序.
执行完之后,再去高一级机器取下一条语句或指令,再进行解释执行,如此反复,知道解释执行完整个程序。  
在6级层次中,一般下面三层是用解释实现的,而上面三层是经常使用翻译的方式。
  1. 垃圾收集器(Garbage Collector): 收集和清除那些不可达对象(unreferenced objects or dead objects)。

4. Java Native Interface


Java Native Interface,简称JNI,它是和Native Method Libraries交互的一个接口,它使得执行本地库(一般是C或C++写的)成为可能。

5. Native Method Libraries


本地方法库,一般给执行引擎使用。

三、对象创建

================================================================

1. 创建对象的六步


在这里插入图片描述

  1. 类加载检查

JVM遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。

如果没有,那必须先执行相应的类的加载过程。

  1. 对象分配内存

对象所需内存的大小在类加载完成后便完全确定(对象内存布局),为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

根据Java堆中是否规整有两种内存的分配方式:(Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定)。

  • 指针碰撞(Bump the pointer)

Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。例如:Serial、ParNew等收集器。

  • 空闲列表(Free List)

Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。例如:CMS这种基于Mark-Sweep算法的收集器。

  1. 并发处理

对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:

  • 同步

虚拟机采用CAS配上失败重试的方式保证更新操作的原子性

  • 本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)

把内存分配的动作按照线程划分为在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。

虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

  1. 内存空间初始化

虚拟机将分配到的内存空间都初始化为零值(不包括对象头),如果使用了TLAB,这一工作过程也可以提前至TLAB分配时进行。

内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

注意:类的成员变量可以不显示地初始化(Java虚拟机都会先自动给它初始化为默认值)。方法中的局部变量如果只负责接收一个表达式的值,可以不初始化,但是参与运算和直接输出等其它情况的局部变量需要初始化。

  1. 对象设置

虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

  1. 执行init()

在上面的工作都完成之后,从虚拟机的角度看,一个新的对象已经产生了。但是从Java程序的角度看,对象的创建才刚刚开始init()方法还没有执行,所有的字段都还是零。

所以,一般来说(由字节码中是否跟随invokespecial指令所决定),执行new指令之后会接着执行init()方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算产生出来。

2. 创建对象的四种方式


  1. new

  2. 反射

  3. Class类的newInstance方法

  4. Constructor类的newInstance方法

  5. clone

  6. 反序列化

四、『虚拟机栈』栈帧

====================================================================

JVM虚拟机栈中存放的是Stack Frame:栈帧。

一个线程对应一个虚拟机栈,虚拟机栈中存放着一组栈帧。

线程每调用一个方法就对应着 JVM Stack 中 Stack Frame 的入栈,方法执行完毕或者异常终止对应着出栈(销毁)。

由于先进后出的特点,位于栈顶的栈帧才是当前栈。

我们看一下爱栈帧所包含的内容:

在这里插入图片描述

1. 局部变量表(Local Variable Table)


在编译程序代码的时候就可以确定栈帧中需要多大的局部变量表,具体大小可在编译后的 Class 文件中看到。

局部变量表的容量以 Variable Slot(变量槽)为最小单位,每个变量槽都可以存储 32 位长度的内存空间。

在方法执行时,虚拟机使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那局部变量表中第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用(在方法中可以通过关键字 this 来访问到这个隐含的参数)。

其余参数则按照参数表顺序排列,占用从 1 开始的局部变量 Slot。

基本类型数据以及引用和 returnAddress(返回地址)占用一个变量槽,long 和 double 需要两个。

2. 操作数栈(Operand Stack)


同样也可以在编译期确定大小。

Frame 被创建时,操作栈是空的。操作栈的每个项可以存放 JVM 的各种类型数据,其中 long 和 double 类型(64位数据)占用两个栈深。

方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作(与 Java 栈中栈帧操作类似)。

操作栈调用其它有返回结果的方法时,会把结果 push 到栈上(通过操作数栈来进行参数传递)。

3. 动态链接(Dynamic Linking)


每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。

在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在运行时转化为直接引用,这部分称为动态链接。

4. 返回地址(Return Address)


方法开始执行后,只有 2 种方式可以退出 :方法返回指令,异常退出。

5. 帧数据区(Stack Data)


帧数据区的大小依赖于 JVM 的具体实现。

关于栈帧的具体分析,我们可以自己编写一个java文件,然后编译再反编译,查看代码的内容。

/**

  • 编译:javac com\jvm\StackFrame.java

  • 反编译:javap -p -v com\jvm\StackFrame.class

*/

在这里插入图片描述

在这里插入图片描述

五、『堆』内存布局

===================================================================

一个Java对象在内存中包括对象头、实例数据和补齐填充3个部分:

在这里插入图片描述

1. 对象头


  • Mark Word:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等。在32位系统占4字节,在64位系统中占8字节;

  • Class Pointer:用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在32位系统占4字节,在64位系统中占8字节;

  • Length:如果是数组对象,还有一个保存数组长度的空间,占4个字节;

2. 对象实际数据


对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定,比如:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,reference是4个字节(64位系统中是8个字节)。

在这里插入图片描述

对于reference类型来说,在32位系统上占用4bytes, 在64位系统上占用8bytes。

3. 对齐填充


Java对象占用空间是8字节对齐的,即所有Java对象占用bytes数必须是8的倍数。例如,一个包含两个属性的对象:int和byte,这个对象需要占用8+4+1=13个字节,这时就需要加上大小为3字节的padding进行8字节对齐,最终占用大小为16个字节。

注意:以上对64位操作系统的描述是未开启指针压缩的情况,关于指针压缩会在下文中介绍。

4. 对象头占用空间大小


这里说明一下32位系统和64位系统中对象所占用内存空间的大小:

  • 在32位系统下,存放Class Pointer的空间大小是4字节,MarkWord是4字节,对象头为8字节;

  • 在64位系统下,存放Class Pointer的空间大小是8字节,MarkWord是8字节,对象头为16字节;

  • 64位开启指针压缩的情况下,存放Class Pointer的空间大小是4字节,MarkWord是8字节,对象头为12字节;

  • 如果是数组对象,对象头的大小为:数组对象头8字节+数组长度4字节+对齐4字节=16字节。其中对象引用占4字节(未开启指针压缩的64位为8字节),数组MarkWord为4字节(64位未开启指针压缩的为8字节);

  • 静态属性不算在对象大小内。

5. 指针压缩


从上文的分析中可以看到,64位JVM消耗的内存会比32位的要多大约1.5倍,这是因为对象指针在64位JVM下有更宽的寻址。对于那些将要从32位平台移植到64位的应用来说,平白无辜多了1/2的内存占用,这是开发者不愿意看到的。

从JDK 1.6 update14开始,64位的JVM正式支持了 -XX:+UseCompressedOops 这个可以压缩指针,起到节约内存占用的新参数。

6. 什么是OOP?


OOP的全称为:Ordinary Object Pointer,就是普通对象指针。启用CompressOops后,会压缩的对象:

  • 每个Class的属性指针(静态成员变量);

  • 每个对象的属性指针;

  • 普通对象数组的每个元素指针。

当然,压缩也不是所有的指针都会压缩,对一些特殊类型的指针,JVM是不会优化的,例如指向PermGen的Class对象指针、本地变量、堆栈元素、入参、返回值和NULL指针不会被压缩。

7. 实例计算对象占用内存


static class B {

int a;

int b;

}

static class C {

int ba;

B[] as = new B[3];

C() {

for (int i = 0; i < as.length; i++) {

as[i] = new B();

}

}

}

  1. 对象本身大小

直接计算当前对象占用空间大小,包括当前类及超类的基本类型实例字段大小、引用类型实例字段引用大小、实例基本类型数组总占用空间、实例引用类型数组引用本身占用空间大小; 但是不包括超类继承下来的和当前类声明的实例引用字段的对象本身的大小、实例引用数组引用的对象本身的大小。

现在我们来看一下C对象本身自己所占用的内存空间:

未开启压缩:

16(对象头)+4(ba)+8(as引用的大小)+padding/4=32

开启压缩:

12+4+4+padding/4=24

  1. 当前对象占用的空间总大小

递归计算当前对象占用空间总大小,包括当前类和超类的实例字段大小以及实例字段引用对象大小。

递归计算复合对象占用的内存的时候需要注意的是:对齐填充是以每个对象为单位进行的,看下面这个图就很容易明白。

现在我们来手动计算下C对象占用的全部内存是多少,主要是三部分构成:C对象本身的大小+数组对象的大小+B对象的大小。

未开启压缩:

(16 + 4 + 8+4(padding)) + (24+ 8*3) +(16+8)*3 = 152bytes

开启压缩:

(12 + 4 + 4 +4(padding)) + (16 + 4*3 +4(数组对象padding)) + (12+8+4(B对象padding))*3= 128bytes

六、『堆』垃圾回收(GC)

=======================================================================

垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

1. 垃圾判断算法


(1) 引用计数法

给每个对象添加一个计数器,当有地方引用该对象时计数器加1,当引用失效时计数器减1。用对象计数器是否为0来判断对象是否可被回收。缺点:无法解决循环引用的问题。

先创建一个字符串,String m = new String(“jack”);,这时候 “jack” 有一个引用,就是m。然后将m设置为null,这时候 “jack” 的引用次数就等于 0 了,在引用计数算法中,意味着这块内容就需要被回收了。

引用计数算法是将垃圾回收分摊到整个应用程序的运行当中了,而不是在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。因此,采用引用计数的垃圾收集不属于严格意义上的Stop-The-World的垃圾收集机制。

看似很美好,但我们知道JVM的垃圾回收就是Stop-The-World的,那是什么原因导致我们最终放弃了引用计数算法呢?看下面的例子。

public class ReferenceCountingGC {

public Object instance;

public ReferenceCountingGC(String name) {

}

public static void testGC(){

ReferenceCountingGC a = new ReferenceCountingGC(“objA”);

ReferenceCountingGC b = new ReferenceCountingGC(“objB”);

a.instance = b;

b.instance = a;

a = null;

b = null;

}

}

我们可以看到,最后这2个对象已经不可能再被访问了,但由于他们相互引用着对方,导致它们的引用计数永远都不会为0,通过引用计数算法,也就永远无法通知GC收集器回收它们。

(2) 可达性分析算法

通过GC ROOT的对象作为搜索起始点,通过引用向下搜索,所走过的路径称为引用链。通过对象是否有到达引用链的路径来判断对象是否可被回收(可作为GC ROOT的对象:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象)

在这里插入图片描述

通过可达性算法,成功解决了引用计数所无法解决的循环依赖问题,只要你无法与GC Root建立直接或间接的连接,系统就会判定你为可回收对象。那这样就引申出了另一个问题,哪些属于GC Root。

Java内存区域中可以作为GC ROOT的对象:

  • 虚拟机栈中引用的对象

public class StackLocalParameter {

public StackLocalParameter(String name) {}

public static void testGC() {

StackLocalParameter s = new StackLocalParameter(“localParameter”);

s = null;

}

}

此时的s,即为GC Root,当s置空时,localParameter对象也断掉了与GC Root的引用链,将被回收。

  • 方法区中类静态属性引用的对象

public class MethodAreaStaicProperties {

public static MethodAreaStaicProperties m;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

如何做好面试突击,规划学习方向?

面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。

学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。

同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节

image

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

image

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

g-aIcBKRwd-1712825041365)]

[外链图片转存中…(img-UwCyzPHU-1712825041365)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

如何做好面试突击,规划学习方向?

面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。

学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。

同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节

[外链图片转存中…(img-aaoJwvsU-1712825041365)]

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

[外链图片转存中…(img-6iwDJOx1-1712825041366)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值