1.2.5 JVM

1、JVM回收算法和回收器,CMS采用哪种回收算法,怎么解决内存碎片问题?

1 标记-清除算法(Mark-Sweep)

  标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

2 复制算法(Copying)

  复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。

3 标记-整理算法(Mark-compact)

  标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。具体流程见下图:

4 分代收集算法

  分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

4.1 年轻代(Young Generation)的回收算法 (回收主要以Copying为主)

a) 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

b) 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。

d) 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。

4.2 年老代(Old Generation)的回收算法(回收主要以Mark-Compact为主)

a) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

b) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

4.3 持久代(Permanent Generation)的回收算法

  用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区,具体的回收可参见上文2.5节。

常见的垃圾收集器

下面一张图是HotSpot虚拟机包含的所有收集器,图是借用过来滴:

  • Serial收集器(复制算法)
    新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
  • Serial Old收集器(标记-整理算法)
    老年代单线程收集器,Serial收集器的老年代版本。
  • ParNew收集器(停止-复制算法) 
    新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
  • Parallel Scavenge收集器(停止-复制算法)
    并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
  • Parallel Old收集器(停止-复制算法)
    Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。
  • CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
    高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。

参考:https://www.cnblogs.com/aspirant/p/8662690.html

CMS的详解:https://www.cnblogs.com/aspirant/p/8663911.html

2、类加载过程

从JVM的角度来看,类加载的过程就是查找解析.class文件,并提取其中类信息以某种数据结构存储在方法区中,并在堆内存中创建一个Class对象的过程。细节上又可以分为装载、连接、初始化三个操作,其中装载对应.class文件的寻址及相关编译验证,连接表示把加载后的类数据动态合并到运行中的状态中去(比如分配空间、符号引用的替换等等),初始化表示对Class对象、类变量的初始化等等。加载过程图解如下

 

3、JVM分区

(重读)

4、eden区,survial区?

1、Eden区

    Edeb区位于JVM中的新生代,是新对象分配内存的地方,由于堆是所有线程共享的,所以在堆上分配内存需要加锁。

而Sun JDK为了提升效率,会为每个新建的线程分配一个独立的内存区域,这块区域称之为TLAB(Thread Location Allocation

Buffer).在TLAB上分配内存是不需要进行加锁的,所以Eden区域的对象内存分配会优先在TLAB上进行.若是对象过大或者是TLAB的内存空间使用完,则对象的内存分配会在堆上进行。如果Eden区内存耗尽,则会触发Minor GC(Young GC)

2、From Survivor和To Survivor区

针对新生代对象"朝夕生死"的特点,将新生代划分为3块区域u,分别为Eden、From Survior、ToSurvior,比例为8:1:1。

From和To是相对的,每次Eden和From发生Minor GC时,会将存活的对象复制到To区域,并清除内存。To区域内的对象每存活一次,它的"age"就会+1,当达到某个阈值(默认为15)时,ToSurvior区域内的对象就会被转移到老年代。

  可以通过设置参数-XX:MaxTenuringThreshold来设置晋升的年龄。

虚拟机提供了一个参数:-XX  PertenureSizeThreshold 使得大于这个参数的对象直接在老年代中分配内存,这样就避免了在Eden区域以及Survior区域进行大量的内存复制。

3、老年代

    老年代中是存活时间久的大对象(很长的字符串或者是数组),因此老年代使用标记-整理算法。当老年代容量满的时候,会触发一次MajorGC (FullGC)
参考:https://blog.csdn.net/weixin_30300689/article/details/79888642

5、JAVA虚拟机的作用?

Java Virtual Machine(Java虚拟机),它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

6、GC中如何判断对象需要被回收?

1. 引用计数算法(已被淘汰的算法)

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器的值为0的对象就是不可能再被使用的,将会被回收。

目前主流的java虚拟机都摒弃掉了这种算法,最主要的原因是它 很难解决对象之间相互循环引用的问题 。尽管该算法执行效率很高。

2. 可达性分析算法

目前主流的编程语言(java、C#等)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路:

通过一系列的称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为 引用链(Reference Chain) ,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

二、被GC判断为“垃圾”的对象一定会被回收吗?

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们 暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程

(1)如果对象在进行可达性分析后,发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。(即意味着直接回收)

(2)如果这个对象被判定为有必要执行finalize()方法,那么 这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。 这里所谓的“执行”是指虚拟机会触发这个方,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己 —— 只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合; 如果对象这时候还没有逃脱,那基本上它就真的被回收了。

参考:https://www.jianshu.com/p/e597a74c4938

有两种方法判断是否是垃圾,但是是垃圾不一定就会立马回收,而有挽救的机会。

7、JAVA虚拟机中,哪些可作为ROOT对象?

常说的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的对象,GC会收集那些不是GC roots且没有被GC roots引用的对象。

在Java语言里,可作为GC Roots对象的包括如下几种: 
a.虚拟机栈(栈桢中的本地变量表)中的引用的对象 
b.方法区中的类静态属性引用的对象 
c.方法区中的常量引用的对象 
d.本地方法栈中JNI的引用的对象

参考:https://blog.csdn.net/bolg_hero/article/details/79344745

8、JVM内存模型是什么?

内存模型

方法区(Method Area)
方法区主要是放一下类似类定义、常量、编译后的代码、静态变量等,在JDK1.7中,HotSpot VM的实现就是将其放在永久代中,这样的好处就是可以直接使用堆中的GC算法来进行管理,但坏处就是经常会出现内存溢出,即PermGen Space异常,所以在JDK1.8中,HotSpot VM取消了永久代,用元空间取而代之,元空间直接使用本地内存,理论上电脑有多少内存它就可以使用多少内存,所以不会再出现PermGen Space异常。

堆(Heap)
几乎所有对象、数组等都是在此分配内存的,在JVM内存中占的比例也是极大的,也是GC垃圾回收的主要阵地,平时我们说的什么新生代、老年代、永久代也是指的这片区域,至于为什么要进行分代后面会解释。

虚拟机栈(Java Stack)
当JVM在执行方法时,会在此区域中创建一个栈帧来存放方法的各种信息,比如返回值,局部变量表和各种对象引用等,方法开始执行前就先创建栈帧入栈,执行完后就出栈。

本地方法栈(Native Method Stack)
和虚拟机栈类似,不过区别是专门提供给Native方法用的。

程序计数器(Program Counter Register)
占用很小的一片区域,我们知道JVM执行代码是一行一行执行字节码,所以需要一个计数器来记录当前执行的行数。

图片描述
参考:https://blog.csdn.net/qq_22152261/article/details/79491536

9、jvm是如何实现线程?

java虚拟机的多线程是通过线程轮流切换分配处理执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条程序中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。

简单点说,对于单核处理器,是通过快速切换线程执行指令来达到多线程的,真正处理器就能同时处理一条指令,只是这种切换速度很快,我们根本不会感知到。

参考:https://www.cnblogs.com/81du/p/8460688.html

10、jvm最大内存限制多少

首先JVM内存限制于实际的最大物理内存(废话!呵呵),假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制了。

参考:https://blog.csdn.net/liugw_768/article/details/69525141

可以使用命令 java -XmxXXXXM -version 进行测试
(如:java -Xmx1024M -version)
逐渐的增大XXXX的值,如果执行正常就表示指定的内存大小可用,否则会打印错误信息。

参考:https://blog.csdn.net/iteye_19741/article/details/82075767

实际上是跟JDK,操作系统,处理器密切相关。

11、什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?

一、什么是java虚拟机?

java虚拟机是执行字节码文件(.class)的虚拟机进程。

java源程序(.java)被编译器编译成字节码文件(.class)。然后字节码文件,将由java虚拟机,解释成机器码(不同平台的机器码不同)。利用机器码操作硬件和操作系统

二、为什么java被称为平台无关的编程语言?

因为不同的平台装有不同的JVM,它们能够将相同的.class文件,解释成不同平台所需要的机器码。正是因为有JVM的存在,java被称为平台无关的编程语言

参考:https://blog.csdn.net/qq_18433441/article/details/78025857

12、描述一下JVM加载class文件的原理机制?

Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。

  类装载方式,有两种 
      1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
      2.显式装载, 通过class.forname()等方法,显式加载需要的类 
    隐式加载与显式加载的区别:两者本质是一样? 

     Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。

   Java的类加载器有三个,对应Java的三种类:(java中的类大致分为三种:   1.系统类   2.扩展类 3.由程序员自定义的类 )

     Bootstrap Loader  // 负责加载系统类 (指的是内置类,像是String,对应于C#中的System类和C/C++标准库中的类)
            | 
          - - ExtClassLoader   // 负责加载扩展类(就是继承类和实现类)
                          | 
                      - - AppClassLoader   // 负责加载应用类(程序员自定义的类)

 三个加载器各自完成自己的工作,但它们是如何协调工作呢?哪一个类该由哪个类加载器完成呢?为了解决这个问题,Java采用了委托模型机制。

委托模型机制的工作原理很简单:当类加载器需要加载类的时候,先请示其Parent(即上一层加载器)在其搜索路径载入,如果找不到,才在自己的搜索路径搜索该类。这样的顺序其实就是加载器层次上自顶而下的搜索,因为加载器必须保证基础类的加载。之所以是这种机制,还有一个安全上的考虑:如果某人将一个恶意的基础类加载到jvm,委托模型机制会搜索其父类加载器,显然是不可能找到的,自然就不会将该类加载进来。

前面是对类加载器的简单介绍,它的原理机制非常简单,就是下面几个步骤:

1.装载:查找和导入class文件;

2.连接:

      (1)检查:检查载入的class文件数据的正确性;

      (2)准备:为类的静态变量分配存储空间;

      (3)解析:将符号引用转换成直接引用(这一步是可选的)

3.初始化:初始化静态变量,静态代码块。

      这样的过程在程序调用类的静态成员的时候开始执行,所以静态方法main()才会成为一般程序的入口方法。类的构造器也会引发该动作。

      我们可以通过这样的代码来获取类加载器:

ClassLoader loader = ClassName.class.getClassLoader();
ClassLoader ParentLoader = loader.getParent();

注意一个很重要的问题,就是Java在逻辑上并不存在BootstrapKLoader的实体!因为它是用C++编写的,所以打印其内容将会得到null。

参考:https://www.cnblogs.com/Qian123/p/5707562.html#_label2

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值