Java基础知识(八) Java平台与内存管理

上一篇:Java基础知识(七) 输入输出流

1. 为什么说Java是平台独立性语言

平台独立性是指可以在一个平台上编写和编译程序,而在其他平台上运行。保证 Java 具
有平台独立性的机制为“中间码”和“Java 虚拟机(Java Vintual Machine,JVM)”。Java 程序
被编译后不是生成能在硬件平台上可执行的代码,而是生成了一个“中间码”。不同的硬件平
台上会安装有不同的 JVM,由 JVM 来负责把“中间码”翻译成硬件平台能执行的代码。由此
可以看出 JVM 不具有平台独立性,而是与硬件平台相关的。

解释执行过程分三步进行:代码的装人、代码的校验和代码的执行。装入代码的工作由
“类装载器”完成。被装入的代码由字节码校验器进行检查。

Java 字节码的执行也分为两种方式:即时编译方式与解释执行方式,即时编译方式指的是
解释器先将字节码编译成机器码,然后再执行该机器码。解释执行方式指的是解释器通过每次
解释并执行—小段代码来完成 Java 字节码程序的所有操作。通常采用的是解释执行方式。
而在 C/C ++语言中,编译后的代码只能在特定的硬件上执行,换个硬件平台这些代码就
无法执行了,从而也导致了 C/C++没有跨平台的特性。但 C/C ++有更高的执行效率。

2. Java 平台与此他语言平台有哪些区别

Java 平台是一个纯软件的平台,这个平台可以运行在一些基于硬件的平台(例如 Linux,Windows 等)之上。Java 平台主要包含两个模块:JVM 与 Java API(Application Program Interface,应用程序接口)。

JVM 是一个虚构出来的计算机,用来把 Java 编译生成的中间代码转换为机器可以识别的编码并运行。它有自己完善的硬件架构,例如处理器、堆栈、寄存器等,还具有相应的指令系统,它屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(即字节码),就可以在多种平台上不加修改地顺利运行。每当一个 Java 程序运行时,都会有一个对应的 JVM 实例,只有当程序运行结束后,这个 JVM 才会退出。JVM 实例通过调用类的 main()方法来启动一个 Java 程序,而这个 main()方法必须是公有的、静态的且返回值为void 的方法,该方法接受一个字符串数组的参数,只有同时满足这些条件才可以作为程序的入口方法。

Java API 是 Java 为了方便开发人员进行开发而设计的,它提供了许多非常有用的接口,这
些接口也是用 Java 语言编写的,并且运行在JVM上。

3. JVM 加载 class 文件的原理机制是什么

Java 语言是一种具有动态性的解释型语言,类(class)只有被加载到 JVM 中后才能运行。
当运行指定程序时,JVM 会将编译生成的.class 文件按照需求和一定的规则加载到内存中,并
组织成为一个完整的 Java 应用程序。这个加载过程是由类加载器来完成的,具体来说,就是
由 ClassLosder和它的子类来实现的。类加载器本身也是一个类,其实质是把类文件从硬盘读
取到内存中。

类的加载方式分为隐式加载与显式加载两种。隐式加载指的是程序在使用 new 等方式创建
对象时,会隐式地调用类的加载器把对应的类加载到 JVM 中。显式加载指的是通过直接调用
class. forName()方法来把所需的类加载到 JVM 中。

任何一个工程项目都是由许多个类组成的,当程序启动时,只把需要的类加载到 JVM 中,
其他类只有被使用到的时候才会被加载,采用这种方法,一方面可以加快加载速度,另外一方
面可以节约程序运行过程中对内存的开销。此外,在 Java 语言中,每个类或接口都对应一个
. class 文件,这些文件可以被看成一个个可以被动态加载的单元,因此当只有部分类被修改
时,只需要重新编译变化的类即可,而不需要重新编译所有文件,因此加快了编译速度。

在Java 语言中,可以把类分为3 类:系统类、扩展类和自定义类。Java 针对这3 种不同的类提供了 3 种类型的加载器,这3 种加载器的关系如下:Bootstrap Loader - 负责加载系统类(jre/lib/rt.jar的类) --ExtClassLoader - 负责加载扩展类(jar/lib/ext/*.jar 的类) – AppClassLoader - 负责加载应用类
(classpath 指定的目录或 jar 中的类)

以上这3 个类是如何协调工作来完成类的加载呢?其实,它们是通过委托的方式实现的。
具体而言,就是当有类需要被加载时,类加载器会请求父类来完成这个载人工作,父类会使用
其自己的搜索路径来搜索需要被载人的类,如果搜索不到,才会由子类按照其搜索路径来搜索
待加载的类。

下例可以充分说明类加载器的工作原理:

/**
 * 描述:测试类加载原理
 *
 * @author Ye
 * @version 1.0
 * @date 2021/8/10 20:12
 */
public class TestLoader {
    public static void main(String[] args) {
        //调用class加载器
        ClassLoader clApp = TestLoader.class.getClassLoader();
        System.out.println(clApp);
        //调用上一层class加载器
        ClassLoader clExt = clApp.getParent();
        System.out.println(clExt);
        //调用根部class加载器
        ClassLoader clBoot = clExt.getParent();
        System.out.println(clBoot);
    }
}

运行截图:
在这里插入图片描述
类加载的主要步骤分为以下 3 步:

  1. 装载。根据查找路径找到相对应的 class 文件,然后导人。
  2. 链接。链接又可以分为 3 个小的步骤,具体如下。
    ① 检查。检查待加载的 class 文件的正确性。
    ②准备。给类中的静态变量分配存储空间。
    ③解析。将符号引用转换成直接引用(这一步是可选的)。
  3. 初始化。对静态变量和静态代码块执行初始化工作。

4. 什么是GC

在Java 语言中,垃圾回收(Garbage Collection,GC)是一个非常重要的概念,它的主要
作用是回收程序中不再使用的内存。在使用 C/C ++语言进行程序开发时,开发人员必须非常
仔细地管理好内存的分配与释放,如果忘记或者错误地释放内存往往会导致程序运行不正常甚
至是程序崩溃。为了减轻开发人员的工作,同时增加系统的安全性与稳定性,Java 语言提供了
垃圾回收器来自动检测对象的作用域,可自动地把不再被使用的存储空间释放掉。具体而言,
垃圾回收器要负责完成 3 项任务:分配内存、确保被引用对象的内存不被错误地回收以及回收不再被引用的对象的内存空间。

垃圾回收器的存在一方面把开发人员从释放内存的复杂工作中解脱出来,提高了开发人员
的生产效率;另一方面,对开发人员屏蔽了释放内存的方法,可以避免因开发人员错误地操作
内存而导致应用程序的崩溃,保证了程序的稳定性。但是,垃圾回收也带来了问题,为了实现
垃圾回收,垃圾回收器必须跟踪内存的使用情况,释放没用的对象,在完成内存的释放后还需
要处理堆中的碎片,这些操作必定会增加 JVM 的负担,从而降低程序的执行效率。

对对象而言,如果没有任何变量去引用它,那么该对象将不可能被程序访问,因此可以认
为它是垃圾信息,可以被回收。只要有一个以上的变量引用该对象,该对象就不会被垃圾
回收。

对于垃圾回收器来说,它使用有向图来记录和管理堆内存中的所有对象,通过这个有向图
就可以识别哪些对象是“可达的”(有引用变量引用它就是“可达的”),哪些对象是“不可
达的”(没有引用变量引用它就是不可达的),所有“不可达”对象都是可被垃圾回收的。

/**
 * 描述:垃圾回收测试
 *
 * @author Ye
 * @version 1.0
 * @date 2021/8/11 8:56
 */
public class TestGC {
    public static void main(String[] args) {
        Object obj1 = new Object();
        Object obj2 = new Object();
        obj1 = obj1;
        // 省略代码
    }
}

在这里插入图片描述
如上所示,obj2的内存是不可达的,所以它就会被回收。

垃圾回收都是依据一定的算法进行的,下面介绍其中几种常用的垃圾回收算法。

  1. 引用计数算法(Reference Counting Collector)
    引用计数作为一种简单但是效率较低的方法,其主要原理如下:在堆中对每个对象都有一个引用计数器;当对象被引用时,引用计数器加 1;当引用被置为空或离开作用域的时,引用计数减1,由于这种方法无法解决相互引用的问题,因此 JVM 没有采用这个算法。

  2. 追踪回收算法(Tracing Collector)
    追踪回收算法利用JVM 维护的对象引用图,从根结点开始遍历对象的应用图,同时标记
    遍历到的对象。当遍历结束后,未被标记的对象就是目前已不被使用的对象,可以被回收了。

  3. 压缩回收算法(Compacting Collector)
    压缩回收算法的主要思路如下:把堆中活动的对象移动到堆中一端,这样就会在堆中另外
    一端留出很大的一块空闲区域,相当于对堆中的碎片进行了处理。虽然这种方法可以大大简化
    消除堆碎片的工作,但是每次处理都会带来性能的损失。

  4. 复制回收算法(Coping Collector)
    复制回收算法的主要思路如下:把堆分成两个大小相同的区域,在任何时刻,只有其中的一个区域被使用,直到这个区域的被消耗完为止,此时垃圾回收器会中断程序的执行,通过遍历的方式把所有活动的对象复制到另外一个区域中,在复制的过程中它们是紧挨着布置的,从而可以消除内存碎片。当复制过程结束后程序会接着运行,直到这块区域被使用完,然后再采用上面的方法继续进行垃圾回收。
    这个算法的优点是在进行垃圾回收的同时对对象的布置也进行了安排,从而消除了内存碎
    片。但是这也付出了很高的代价:对于指定大小的堆来说,需要两倍大小的内存空间;同时由
    于在内存调整的过程中要中断当前执行的程序,从而降低了程序的执行效率。

  5. 按代回收算法(Generational Collector)
    复制回收算法主要的缺点如下:每次算法执行时,所有处于活动状态的对象都要被复制,
    这样效率很低。由于程序有“程序创建的大部分对象的生命周期都很短,只有一部分对象有
    较长的生命周期”的特点,因此可以根据这个特点对算法进行优化。按代回收算法的主要思
    路如下:把堆分成两个或者多个子堆,每一个子堆被视为一代。算法在运行的过程中优先收集
    那些“年幼”的对象,如果一个对象经过多次收集仍然“存活”,那么就可以把这个对象转移
    到高一级的堆里,减少对其的扫描次数。

5. Java 是否存在内存泄露问题

内存泄露是指一个不再被程序使用的对象或变量还在内存中占有存储空间。在 C/C++语言中,内存的分配与释放是由开发人员来负责的,如果开发人员忘记释放已分配的内存就会造成内存泄露。而在 Java 语言中引进了垃圾回收机制,由垃圾回收器负责回收不再使用的对象,既然有垃圾回收器来负责回收垃圾,那么是否还会存在内存泄露的问题呢?

其实,在 Java 语言中,判断一个内存空间是否符合垃圾回收的标准有两个:第一,给对象赋予了空值null,以后再没有被使用过;第二,给对象赋予了新值,重新分配了内存空间。一般来讲,内存泄露主要有两种情况:一是在堆中申请的空间没有被释放;二是对象已不再被使用,但还仍然在内存中保留着。垃圾回收机制的引人可以有效地解决第一种情况;而对于第二种情况,垃圾回收机制则无法保证不再使用的对象会被释放。因此,Java 语言中的内存泄露主要指的是第二种情况。

6. Java 中的堆和栈有什么区别

在Java 语言中,堆与栈都是内存中存放数据的地方。变量分为基本数据类型和引用类型,基本数据类型的变量(例如 int、short、long、byte、float、double、boolean 以及 char 等)以及对象的引用变量,其内存都分配在栈上,变量出了作用域就会自动释放,而引用类型的变量,其内存分配在堆上或者常量池(例如字符串常量和基本数据类型常量)中,需要通过 new 等方式进行创建。

具体而言,栈内存主要用来存放基本数据类型与引用变量。栈内存的管理是通过压栈和弹栈操作来完成的,以栈帧为基本单位来管理程序的调用关系,每当有函数调用时,都会通过压栈方式创建新的栈帧,每当函数调用结束后都会通过弹栈的方式释放栈帧。

堆内存用来存放运行时创建的对象。一般来讲,通过 new 关键字创建出来的对象都存放在堆内存中。由于 JVM 是基于堆栈的虚拟机,而每个 Java 程序都运行在一个单独的 JVM实例上,每一个实例唯一对应一个堆,一个 Java 程序内的多个线程也就运行在同一个 JVM。实例上,因此这些线程之间会共享堆内存,鉴于此,多线程在访问堆中的数据时需要对数据进行同步。

在C++中,堆内存的管理都是由开发人员来负责的,也就是说,开发人员在堆中申请的内存,当不再使用时,必须由开发人员来完成堆内存释放的工作。而在 Java 语言中,这个内存释放的工作由垃圾回收器来负责执行,开发人员只需要申请所需的堆空间而不需要考虑释放的问题。

在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。这就是 Java 中引用的用法。

从堆和栈的功能以及作用来比较,堆主要用来存放对象的,栈主要是用来执行程序的。相较于堆,栈的存取速度更快,但栈的大小和生存期必须是确定的,因此缺乏一定的灵活性。而堆却可以在运行时动态地分配内存,生存期不用提前告诉编译器,但这也导致了其存取速度的缓慢。

代码和图示如下:

/**
 * 描述:Java堆栈的测试
 *
 * @author Ye
 * @version 1.0
 * @date 2021/8/11 10:18
 */
public class TestStack {
    public static void main(String[] args) {
        int i = 5;
        Student student = new Student(00001,"张三");
    }
}

class Student{
    private int studentId;
    private String name;
    public Student(int studentId,String name){
        this.studentId = studentId;
        this.name = name;
    }
}

分析图:
在这里插入图片描述

参考:《Java程序员面试笔试宝典》 何昊、薛鹏、叶向阳 编著

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值