【JavaEE初阶】JVM内存划分和类加载过程以及垃圾回收

2024-09-04 00:28

本文主要是介绍【JavaEE初阶】JVM内存划分和类加载过程以及垃圾回收,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

🌲内存划分

🚩堆(线程共享)

🚩栈

🚩元数据区

🍃类加载过程

🚩双亲委派模型

🎄垃圾回收机制(GC)

🚩找到谁是垃圾(不被继续使用的对象)

🚩释放对应的内存

🏀标记-清除

🏀复制算法

🏀标记-整理

🏀分代回收


🌲内存划分

JVM也就是Java进程,这个进程一旦跑起来之后,就会从操作系统这里,申请一大块内存空间,JVM接下来就要进一步的对这个大的空间进行划分,划分成不同区域,每个区域都有不同的作用。

具体如何划分的呢?

JVM运行时数据区域也叫内存布局,但需要注意的是它和Java内存模型((JavaMemoryModel,简 称JMM)完全不同,属于完全不同的两个概念,它由以下5大部分组成:

🚩堆(线程共享)

堆的作用:程序中创建的所有对象都在保存在堆中。(也就是new出来的对象)

成员变量也是在堆中,new出来的对象包含了成员变量,这些东西是一起的

对于里面的新生代来年代后续讲述

🚩栈

分为Java虚拟机栈和本地方法栈。

保存了方法的调用关系,例如写代码时A调用B,B调用C......,这里的调用就是使用栈来维护,只不过虚拟机栈放的Java代码的调用关系,而本地方法栈是针对JVM内部的调用关系,也就是C++代码的调用关系

注意:上述的栈和堆与数据结构的栈和堆没有任何关系,只是名字相同

🚩元数据区

以前叫做方法区,从Java8开始,叫做元数据区。

里面放的是"类对象"。

还放了方法相关信息,类中有一些方法,每个方法都代表了一系列指令集合(JVM字节码指令),还有常量池,编译出来的字节码。

🚩程序计数器(PC) 

他是内存区域中最小的区域,只需要保存当前要执行的下一条指令(JVM字节码)的地址。

具体代码实现:

基本原则:

一个对象在哪个区域,取决于对应变量的形态。

  • 1)局部变量  =>栈上
  • 2)成员变量  =>堆上
  • 3)静态成员变量  =>元数据区/方法区

补充:上述四个区域中,堆和元数据区是整个进程只有一份,栈和程序计数器是每个线程都有一份,则堆和元数据区都是多个线程共享同一份数据,每个线程的局部变量,则不是共享的,每个线程都是有自己的一份。

🍃类加载过程

当前写的Java代码,是一个 .java文件,是在硬盘上的,一个Java进程要跑起来,需要先把 .java文件变成 .class文件,还是在硬盘上,在加载到内存中,得到"类对象"。

一个Java进程要跑起来,也就是要执行指令,要执行的cpu指令,都是通过字节码让JVM翻译出来,也就需要让字节码进入到内存中。

接下来我们来看下类加载的执行流程。

对于一个类来说,它的生命周期是这样的:

  • 1)加载

在硬盘上,找到对应的 .class文件,读取文件内容

  • 2)验证

检查 .class文件的内容是否符号要求。

.class文件是由javac编译器生成的,具体生成的 .class文件里面具体是什么样的格式,在Java官方文档中是有明确定义的。

  • 3)准备

给类对象分配内存空间。

  • 4)解析

针对字符串常量进行初始化,把刚才 .class文件中的常量的内容取出来,放到元数据区

  • 5)初始化

针对"类对象"中的各个部分进行初始化(不是针对对象初始化,和构造方法无关),给执行静态成员,执行静态代码块进行初始化等。

面试:记住上述5个步骤,以及各个变量的内存区域

🚩双亲委派模型

双亲委派模型出现在上述"加载"这个环节,根据代码中写的"全限定类名"找到对应的 .class 文件。

全限定类名指 包名 + 类名。例如String => java.long.String ,List => java.util.List。      

双亲委派模型描述了JVM加载 .class文件过程中,找文件的过程。这就涉及到"类加载器"

"类加载器"在JVM中包含了一个特定的模块/类,这个类负责完成后续类加载的过程。

JVM中内置了三个类加载器:负责加载不同的类

  • 1)BootstrapClassLoader:负责加载标准库的类
  • 2)ExtentionClassLoader:负责加载JVM扩展库的类(前面学习过程中没有涉及到任何扩展类,历史遗留,本身很少使用)
  • 3)ApplicationClassLoader:负责加载第三方库的类和你自己写的代码的类

他们三个类存在一个父子关系:这个父子关系不是继承表示的,而是通过类加载器中存在一个"parent"这样的字段指向自己的"父亲"。

注意:"双亲委派模型"本身翻译是不标准的,更准确的翻译为"父亲委派模型"。

工作过程:

例如,给定了一个类的"全限定类名",自己写的类 => java111.Test

这就是双亲委派模型,拿到任务,先交给父亲处理,父亲处理不了,再自己处理。

上述过程主要为了应对场景:

比如你自己代码中写了一个类,这个类的名字和标准库/扩展库冲突了,JVM就会确保加载的类是标准库中的类(就不加载你自己写的类了)。相当于我自己写了一个java.long.String,那么这套模型就能够确保最终在JVM中加载原有的java.long.String了。

类加载过程中的双亲委派模型也是一个经典面试题。

🎄垃圾回收机制(GC)

垃圾回收机制,是Java提供的对于内存(变量或者对象)自动回收的机制。

GC回收的是"内存",更准确的说是对象,回收的是堆上是内存。

一定是一次回收一个完整的对象,不能回收"一部分对象"。

GC的具体流程,主要有两个步骤:

🚩找到谁是垃圾(不被继续使用的对象)

谁是垃圾这个事情,并不太好找,一个对象什么时候创建这个是明确的,但什么时候不在使用,这个时机往往很模糊。在编程中,一定要确保代码中使用的每个对象,都得是有效的,千万不要出现"体现释放"的情况。

因此判定一个对象是否是垃圾,判定方式就比较保守。比如,如果使用"上次使用时间"的方式来判定垃圾,就是不行的,这就容易错杀。

此处就引入了一个比较保守的做法,判定某个对象,是否存在引用指向它。在代码中都是通过对象的引用来使用的,那么如果没有引用指向这个对象,意味着这个对象注定无法在代码中被使用了。这就可以视为这个对象是垃圾了。

例如:Test t = new Test(); t = null; => 修改t的指向,new Test对象没有引用指向了,就视为垃圾。

具体怎么判定某个对象是否有引用的指向呢?方式有很多,此处介绍两种方式:

  • 1)引用计数(不是JVM采取的方案,而是Python / PHP的方案)

引用计数描述的算法为:
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。

看似比较好用,但是存在两个缺陷:

a)消耗额外的存储空间

如果你的对象比较大浪费空间还好,如果对象比较小,并且对象数目还多,空间占用多了,空间的浪费的就多了。

b)存在"循环引用"的问题(面试官考引用计数,也就是靠你循环引用问题)

例如:

两行实例代码对应的图示

接下来:a.t = b,进行引用复制,把b里面的地址复制给Test类中的引用类型的成员,也就是把b地址复制给a对象中t成员,此时就有两个引用指向0x200了,那么0x200中的引用计数器就为2。b.t = a也是同理。

然后再执行 a = null; b = null;,此时a中就为null,意味着0x100中的引用计数器就为1,b为null,意味着0x200中的引用计数器就为1,这时候这两个对象相互指向对方,就导致了两个对象的引用计数都为1(不为0,不是垃圾),但是你外部代码也无法访问这两个对象!!!

  • 2)可达性分析(是JVM采取的方案)

这个解决了空间的问题,也解决了循环引用问题,也付出了时间上的代价。

核心思想:"遍历",JVM把对象之间的引用关系理解成了一个"树形结构",JVM就会不停的遍历这样的结构,把所有能遍历访问到的对象标记成"可达",剩下的就是"不可达"。

在这里面,是有很多课这样的树(不一定是二叉树),这些树的根节点如何确定的?(GC roots)

🚩释放对应的内存

🏀标记-清除

直接把标记为垃圾的对象对应的内存,释放掉(简单粗暴)。

"标记-清除"算法的不足主要有两个 :

  1. 效率问题 : 标记和清除这两个过程的效率都不高
  2. 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中

需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收

🏀复制算法

"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。

当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。

此算法实现简单,运行高效。算法的执行流程如下图 :

这里面最大的问题,空间浪费的太多了,另一方面要保留的对象比较多,时间花费也不少。

🏀标记-整理

能解决内存碎片,也能解决

标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动(类似于顺序表删除中间元素),然后直接清理掉端边界以外的内存。流程图如下:空间利用率的问题。

这样的搬运时间开销更大。在JVM中实际的方案,是综合上述的方案,更复杂的策略。

🏀分代回收

也就是分情况讨论,根据不同的场景/特点,选择合适的方案。根据对象的年龄来讨论的(我们说GC有一组线程会进行周期性的扫描,某个对象经历了一轮GC扫描之后,还是存在,没有成为垃圾,那么年龄 +1,依此内推)。

这篇关于【JavaEE初阶】JVM内存划分和类加载过程以及垃圾回收的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1134472

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

NameNode内存生产配置

Hadoop2.x 系列,配置 NameNode 内存 NameNode 内存默认 2000m ,如果服务器内存 4G , NameNode 内存可以配置 3g 。在 hadoop-env.sh 文件中配置如下。 HADOOP_NAMENODE_OPTS=-Xmx3072m Hadoop3.x 系列,配置 Nam

作业提交过程之HDFSMapReduce

作业提交全过程详解 (1)作业提交 第1步:Client调用job.waitForCompletion方法,向整个集群提交MapReduce作业。 第2步:Client向RM申请一个作业id。 第3步:RM给Client返回该job资源的提交路径和作业id。 第4步:Client提交jar包、切片信息和配置文件到指定的资源提交路径。 第5步:Client提交完资源后,向RM申请运行MrAp

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory