java深入理解jvm,JVM详解

  java深入理解jvm,JVM详解

  本文已经给大家带来了一些java的知识,主要是梳理了JVM的相关问题,包括JVM内存区域划分、JVM类加载机制、VM的垃圾回收等。来看看吧,希望对你有帮助。

  如何解决写爬虫IP受阻的问题?立即使用。

  

一.JVM内存区域划分

   JVM为什么要划分这些区域?JVM的内存是从操作系统申请的,JVM根据功能需求把这些分成一些小模块,这样一个大的领域就可以分成一些小的模块,然后每个模块负责自己的功能。那我们来看看这些区域的功能是什么!

  

1.程序计数器

  程序计数器是内存中最小的区域,主要存储下一条要执行的指令的地址(指令是字节码,一般程序要运行时,JVM需要将字节码加载到内存中,然后程序从内存中取出一条指令放入CPU执行,所以需要记住当前执行的是哪条指令,下一条指令在哪里。因为CPU不仅为一个进程提供服务,还为所有进程提供服务,并发执行程序,而且因为操作系统是以线程为单位调度和执行程序的,所以每个线程都必须有自己的执行位置,也就是每个线程都需要一个程序计数器来记录位置!)

  

2.栈

  堆栈主要存储局部变量和方法调用信息。每当一个新方法被调用,就会有一个‘推入’操作。每次执行一个方法,都会有一个‘推出’操作,每个线程都有一个栈的副本。

  所以对于递归,一定要控制好递归的条件,否则很可能会出现StackOverflowException!

  

3.堆

  堆是内存中空间最大的区域,每个进程只有一个堆。一个进程中的多个线程共享一个堆,堆主要存储新对象及其成员变量,比如String s=new String()。如果这里的s是一个方法中的局部变量,它就在堆上。如果s是成员变量,它就在堆上。后面的new String()是对象的本体,对象在堆上,容易混淆。另外,堆的另一个重要点是关于垃圾回收的,后面会详细介绍!

  

4.方法区

  “类对象”存储在方法区域。正常书写的。java代码将成为。类(二进制字节码),然后。类会被加载到内存中,内存会被JVM构造成类对象(加载过程称为‘类加载’),这些类对象会被存储在方法区,方法区具体描述了类的样子(这里是类成员及其成员名,成员类型,类方法,其方法名,方法类型,以及一些指令……另外,类对象中还存储了一个很重要的东西,就是静态成员。一般用static修饰的成员就成了类属性,而普通的方法就叫实例属性,差别很大)!

  

二.JVM类加载机制

  类加载其实是设计一个运行时环境的重要核心功能,很重量级,这里简单介绍一下!

  那是类加载的具体过程,最后使用和卸载都是使用的过程,就不介绍了。只介绍前三大步骤:

  

1.Loading(加载)

  在加载阶段,你会先找到对应的。类文件,然后打开并读取(根据字节流)该。类文件,并最初生成一个类对象。这和成品类加载是不一样的,不要混淆!

  类文件的具体格式(如果要实现Java编译器,就得用这种格式构造,要实现JVM,就得用这种格式加载!):

  通过观察这种格式,您可以看到。类文件表达了。java文件,但是组织格式发生了变化,所以加载链接会初步将读取的信息填充到类对象中。

  

2.Linking(连接)

  连接一般是在多个实体之间建立良好的连接。

  

2.1.Verification(验证)

  验证是一个验证过程,主要验证读取的内容是否与规范中规定的格式完全匹配。如果发现读取的数据格式不符合规范,则类加载会失败并抛出异常!

  

2.2.Preparation(准备)

  准备阶段是正式为定义变量(静态变量,即静态修改变量)分配内存,设置类变量初始值的阶段。内存将分配给每个静态变量,并设置为0!

  

2.3.Resolution(解析)

  解析阶段是Java虚拟机用直接引用替换常量池中符号引用的过程,也就是初始化常量的过程。中的常数。类文件都是集中放置的,每个常量都会有一个编号,而初始情况在结构中。class file就是记录的编号,然后可以根据这个编号找到相应的内容然后填充到class对象中!

  

3.Initialization(初始化)

  初始化阶段是类对象的真正初始化(根据写好的代码),尤其是静态成员。

  

4.典型的面试题

   A级

  公共A(){

  system . out . println( a的构造方法);

  }

  {

  system . out . println( a的构造块);

  }

  静态{

  system . out . println(“A的静态代码块”);

  } }类扩展了A{

  公共B(){

  system . out . println( B的构造方法);

  }

  {

  system . out . println( B的积木);

  }

  静态{

  system . out . println( B的静态代码块);

  } }公共类测试扩展B{

  公共静态void main(String[] args) {

  新测试();

  新测试();

  }}可以尝试自己先写输出结果。

  做这样的题,我们需要把握几个大原则:

  在类加载阶段,将执行静态代码块。如果要创建实例,必须首先加载该类。

  静态代码块只在类加载阶段执行一次,其他阶段不会再执行。

  构造方法和构造代码块每次实例化都执行,构造代码块在构造方法之前执行~ ~

  首先执行父类,最后执行子类!

  程序是从main(主测试方法)中执行的,所以要执行main,您需要首先加载测试类。

  只有当涉及到这个类时,才会加载该类的内容。

  输出结果:

  的静态代码块

  静态代码块

  a的构造代码块

  的构造方法

  b的构造代码块

  B的构造方法

  a的构造代码块

  的构造方法

  b的构造代码块

  b的构造方法

5.双亲委派模型

  这个东西是类加载中的一个环节,在加载阶段(前期)。父委托模型描述了JVM中的类装入器,以及如何找到。根据类的完全限定名(java.lang.String)创建类文件。这里的类加载器是JVM专门提供的对象,主要负责类加载,所以查找文件的过程也由类加载器负责。类文件可能放在许多位置,有些在JDK目录,有些在项目目录,有些在其他特定位置。因此,JVM提供了多个类装入器,每个类装入器负责一个领域,有三个默认的类装入器:

  BootStrapClassLoader:负责加载标准库中的类(string、ArrayList、random、scanner……)

  ExtensionClassLoader:负责加载JDK扩展的类(现在很少使用)

  Classloader:负责加载当前项目目录中的类。

  此外,程序员可以定制类加载器来加载其他目录中的类,Tomcat已经定制了类加载器来专门加载。webapps中的类。

  父委托模型描述了查找目录的过程,也就是上面的类加载器是如何协作的。

  考虑java.lang.String:

  当程序启动时,它将首先进入ApplicationClassLoader类加载器。

  ApplicationClassLoader的类加载器将检查其父类加载器是否已加载,如果没有,它将调用父类加载器ExtensionClassLoader。

  ExtensionClassLoader类加载器将检查其父类加载器是否已经加载,如果没有,它将调用父类加载器BootStrapClassLoader。

  BootStrapClassLoader类加载器也会检查自己的父加载器是否已经加载,然后发现没有父,于是扫描自己负责的目录。

  然后可以在标准库中找到java.lang.String类,然后BootStrapClassLoader负责后续的加载过程,搜索过程就结束了!

  考虑您自己的测试类:

  当程序启动时,它将首先进入ApplicationClassLoader类加载器。

  ApplicationClassLoader的类加载器将检查其父类加载器是否已加载,如果没有,它将调用父类加载器ExtensionClassLoader。

  ExtensionClassLoader类加载器将检查其父类加载器是否已经加载,如果没有,它将调用父类加载器BootStrapClassLoader。

  BootStrapClassLoader类加载器也会检查它的父加载器是否已经加载,然后发现它没有父,于是扫描自己负责的目录。如果没有,它将返回到子加载器继续扫描。

  ExtensionClassLoader扫描它负责的目录,但是扫描失败,然后回到子加载器继续扫描。

  ApplicationClassLoader还扫描它自己负责的目录,它编写的类在它自己的项目目录中,因此可以找到它。然后后续的类加载由ApplicationClassLoader完成,查找目录的环节结束~ ~(另外,如果没有找到应用类加载器,我们会抛出一个ClassNotFoundException异常异常)

  这组搜索规则被称为父委托模型,那么JVM为什么要这样设计呢?原因是一旦程序员自己写的类和全限定类名重复,就可以成功加载标准库中的类,而不是自己写的类!

  另外,如果是自定义类加载器,要不要遵循这种家长委托模式?

  答案是能不能遵守,主要看要求。比如Tomcat在webapp中加载类,你不遵守,因为遵守了上面的类加载器就不可能找到了!

  

三.JVM的垃圾回收

   JVM中的垃圾收集机制(GC)通常涉及到写代码时申请内存,比如创建一个变量、更新一个对象、调用一个方法、加载一个类……而申请内存的时机一般是明确的(如果需要保存一些数据就需要申请内存),但是释放内存的时机就不那么明确了,即使是提前释放了(如果结果已经释放了, 这就使得它没有可用的内存,所以数据‘无处可去’),而且晚释放也没关系(很有可能大量囤积会逐渐减少可用内存,很有可能会出现内存泄漏问题,即没有内存可用)。 所以,内存释放要恰到好处!

  而垃圾收集依靠运行时环境做大量额外的工作来完成内存释放操作,大大减轻了程序员的精神负担,但垃圾收集也有缺点:消耗额外的开销(消耗更多的资源,培养更多的人);可能会影响程序的流畅运行(垃圾收集会经常引入STW问题(停世界))

  让我们具体看看它是如何被回收的:

  

1.找垃圾/判定垃圾

  目前有两种主流方案:

  

1.1.基于引用计数

  这不是Java采用的方案,是Python等语言的方案,所以这里简单介绍一下,但不要太多~

  引用计数的具体思路是,对于每个对象,都会引入一小块内存来存储这个对象有多少次引用指向它。

  然而,这种引用计数有两个缺陷:

  空间利用率比较低!每个新对象都需要配备一个计数器。假设一个计数器是4个字节。如果对象本身很大(几百个字节),就无所谓了。一旦对象本身很小(4个字节),多出来的4个字节就相当于空间利用率的两倍浪费,所以空间利用率会低~存在循环引用的问题。

  所以在使用引用计数器的时候会出现很多问题,而且像Python和PHP这样的语言不仅仅是使用引用计数器来完成GC,还会配合一些其他的机制!

1.2.基于可达性分析

  可达性分析是Java采用的一种方案。可达性分析就是通过一些额外的线程,有规律地扫描整个内存空间中的对象,有一些起始位置(GCRoots),然后就像一个深度优先遍历一样(把它想象成一棵树),在一边标记所有可访问的对象(标记的对象是可访问的对象),未标记的对象是不可访问的对象。

  g此处的根(从这些位置遍历):

  堆栈上的局部变量;常量池中引用指向的对象;方法中静态成员指向的对象;所以可达性分析的优点是解决了引用计数的缺点:空间利用率低,循环引用;可达性分析的缺点也很明显:系统开销大,遍历一次可能会比较慢~

  所以,找垃圾也很简单。核心是确认这个对象以后是否会被使用,看有没有引用指向它,是否应该释放!

  

2.释放垃圾

  既然我们已经明确了什么是垃圾,那就该回收垃圾了。回收垃圾有三个基本策略。让我们来看看!

  

2.1.标记-请除

  这里,标记是可达性分析的过程,清除是释放内存的过程。假设是一块内存,打勾的区域代表垃圾。这时候如果直接释放,虽然内存是返回给系统的,但是释放的内存是离散的,而不是连续的,这样造成的问题就是‘内存碎片’。可能有很多空闲内存,假设加起来总共1G,这时你想申请500MB的空间。申请是有道理的,但是这里有可能申请失败(因为要申请的500MB是连续内存,每次申请的内存都是连续内存空间,这里的1G可能是多个碎片之和),所以这样的问题其实非常影响程序的运行。

  

2.2.复制算法

  由于上述标记清除策略可能会带来内存碎片的问题,所以引入了复制算法来解决这个问题。

  以上是一段回忆。复制算法的策略是使用一半的内存,丢失一半,不使用全部。一般来说,把非垃圾拷贝复制到另一半(这个拷贝是JVM内部处理的,不用纠结),然后把之前用的内存全部释放,这样内存碎片问题就解决了!

  所以复制算法有两个大问题:

  内存利用率低(只使用通用内存);如果要保留的对象很多,要释放的对象很少,那么复制的成本就很高;

2.3.标记-整理

  这是针对复制算法再来一遍,然后做进一步的改进!

  标记的策略是把不是垃圾的内存一起整理出来,然后把后面的内存全部释放。就像删除序列表中间元素的操作一样,有一个运输的过程!

  这种方案空间利用率高,但是还是没有办法解决复制/运输元素成本高的问题!

  以上三种方案虽然可以解决问题,但都有各自的缺陷,所以实际上在JVM中的实现会把各种方案组合起来,也就是‘分代回收’!

  

2.4分代回收

  这里的生成是对对象进行分类(根据对象的年龄,这里的年龄是指一个对象经过一轮GC扫描后老了一岁),而对于不同年龄的对象采用不同的方案!

  这是全代回收流程!

  

3.垃圾回收器

  以上的垃圾搜索和垃圾释放只是算法的思路,并不是真正的实现过程,真正实现上述算法模块的是‘垃圾收集器’。以下是一些特定的垃圾收集器:

  

3.1.Serial收集器和Serial Old收集器

  串行收集器是新一代的垃圾收集器,串行旧收集器是老一代的垃圾收集器。这两个收集器是串联收集的,当垃圾被扫描释放时,业务线程不得不停止工作,所以这种方式扫描全释放慢,还会产生严重的STW!

  

3.2.ParNew收集器,Parallel Scavenge收集器和Parallel Old收集器

   par新收集器和并联扫气收集器都是为新一代提供的。与ParNew收集器相比,Parallel Scavenge收集器增加了一些参数,可以控制STW时间,但它有一些更强大的功能。为老一代提供并行旧收集器,三个收集器并行收集,即引入多线程解决扫描垃圾和释放垃圾的功能!

  以上回收商都是历史遗留下来的,也就是比较老的垃圾回收方式。此外,还引入了两个更新的垃圾收集器!

  

3.3.CMS收集器

   CMS采集器设计巧妙。其初衷是让STW时间尽可能短。Java8使用CMS收集器。下面简单介绍一下CMS collector的流程:

  初始标记:很快,会造成短STW(找GCRoots就行);并发标记:非常快,但是可以和业务线程并发执行,没有STW;重新标记:在2中,业务代码可能会影响并发标记的结果(当业务线程正在执行时,可能会产生新的垃圾),所以这一步是对2的结果进行微调。虽然会造成STW,但只是微调,速度快;

  以上三步都是基于可达性分析的!回收内存:也是和业务线程并发执行,不会产生STW,基于标记排序;当前位置G1清洁工是整个地区唯一的垃圾清洁工。G1收集器从Java11开始使用。这个收集器把整个内存分成很多小区域,对这些区域做不同的标记。一些区域放置新一代对象,一些区域放置旧对象。然后扫描的时候一次扫描几个区域(需要分多次扫描,不需要一次GC)。

  这两个新收藏家的核心思想是把东西分解成零件。G1可以优化,使STW暂停时间小于1ms,这是完全可以接受的!这是对JVM的一些了解。这里的收集者主要侧重于理解,上面的垃圾收集思路很重要!

  推荐:《java视频教程》是Java知识归纳的JVM讲解的详细内容。请多关注我们的其他相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: