JVM学习笔记之面试题解析(初学者复习笔记)

7 篇文章 0 订阅

百度 三面:说一下JVM内存模型吧,有哪些区?分别干什么的?

	JVM内存模型: 方法区 虚拟机栈 本地方法栈 堆 程序计数器
    
		方法区:《Java虚拟机规范》中明确说明:"尽管所有的方法区在逻辑上是属于堆的一部分,
            但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。
            ”但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
            --Java 虚拟机有一个在所有Java虚拟机线程之间共享的方法区域。
            方法区域类似于传统语言编译代码的存储区域,或类似于操作系统进程中的“文本”段。
            它存储每个类的结构,比如运行时常量池、字段和方法数据,以及方法和构造函数的代码,
            包括在类和实例初始化以及接口初始化中使用的特殊方法
            方法区域是在虚拟机启动时创建的。虽然方法区域在逻辑上是堆的一部分,
            但简单的实现可能选择不进行垃圾收集或压缩。
            此规范不强制规定用于管理已编译代码的方法区域或策略的位置。
            方法区域可以具有固定的大小,也可以根据计算的需要进行扩展。
            如果不需要更大的方法区域,则收缩。方法区域的内存不需要是连续的
            Java虚拟机实现可以为程序员或用户提供对方法区域初始大小的控制,
            以及在方法区域大小可变的情况下,对最大和最小方法区域大小的控制。
            以下异常条件与方法区域相关联
            如果方法区域中的内存不能满足分配请求,Java虚拟机将抛出一个OOM
            --JDK8后 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆
            
     	 虚拟机栈 :由于跨平台性的设计,Java指令集是根据栈来设计的.不同平台的CPU架构不同,所以不能设计成指令寄存器
            栈是运行时的单位,而堆是存储的单位。
            即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
            Java虚拟机栈(Java Virtual Machine Stack),早起也叫Java栈.
            每个线程在创建时都会创建一堆虚拟机栈,其内部保存一个个栈帧(Stack Frame),对应着一次次的Java方法调用.
            生命周期:和线程保持一致
            作用:主管Java程序的运行,它保持方法的局部变量、部分结果、参与与方法的调用和返回.
            每个栈帧:
                局部变量表(Local Variables)
                操作数栈(Operand Stack)
                动态链接(Dynamic Linking) (或指向运行时常量池的方法引用)
                方法返回地址(Return Address)  (或方法正常退出或者异常退出的定义)
                一些附加信息 (如JVM信息)
            符号引用:字符串,能根据这个字符串定位到指定的数据,比如java/lang/StringBuilder
            直接引用:内存地址
        本地方法栈:
            本地方法栈是线程私有的,和Java虚拟机栈所发挥的作用非常相似,
            他们之间的区别就是Java虚拟机栈执行的Java方法,本地方法栈执行的是native方法(使用c/c++编写的方法),
            本地方法栈和Java虚拟机栈一样都会有StackOverflowError和OutOfMemoryError异常。
           
        堆(Heap):
        		新生代/老年代 -XX:NewRatio=2 表示新生代占1,老年代占2,新生代占整个堆的1/3
                -XX:NewRatio=4 表示新生代占1,老年代占4,新生代占整个堆的1/5
	             	Java堆是JVM内存中最大的一块,它是线程共享的,唯一的作用就是存放对象的实例,
	                几乎所有的对象实例都在堆里面分配内存。Java堆是垃圾收集器管理的主要区域,
	                Java堆可以细分为:新生代和老年代,这样分配主要目的是为了更好地回收内存。
	                Java堆可以处于物理上不连续的空间,只要逻辑上连续即可,
	                Java堆可以通过-Xmx和-Xms来控制大小。
	                当堆没有内存完成分配的时候,会抛出OutOfMemoryError异常。

            新生代(YoungGen):
                    默认8/1/1 虚拟机有动态分配技术所以不设置情况下达不到默认比例
                    -XX:SurvivorRatio=8
                伊甸区(Eden):
                幸存者0区(Survivor0/from):
                幸存者1区(Survivor1/to):
                    new的对象先放伊甸园区。此区有大小限制。
                    当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),
                    将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
                    然后将伊甸园中的剩余对象移动到幸存者0区。
                    如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会I放到幸存者1区。
                    如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。啥时候能去养老区呢?可以设置次数。默认是15次。
                    ·可以设置参数: -XX:MaxTenuringThreshold=<N>进行设置。
                --从伊甸园中GC后的数据进入幸存者区,不断来回存放来回存放的过程中,增加代数到达一定临界后进入老年代
                 如果对象内存大于新生代区,会直接进入老年区。

在这里插入图片描述

    			老年代(OldGen):
					年老代里存放的都是存活时间较久的,大小较大的对象,因此年老代使用标记整理算法。当年老代容量满的时候,会触发一次Major GC(full GC),回收年老代和年轻代中不再被使用的对象资源。

在这里插入图片描述

        元空间/非堆(Metaspace):
            方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,
            它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
            虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,
            但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
            很多人都更愿意把方法区称为“永久代”(Permanent Generation)。

            从jdk1.7已经开始准备“去永久代”的规划,jdk1.7的HotSpot中,
            已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中。

            在jdk1.8中,永久代已经不存在,存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,
            元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)

妈蚁金服:

Java8的内存分代改进

  1. 首先明确:只有Hotspot才有永久代(JDK8后元空间/非堆(Metaspace))。BEA JRockit、IBMJ9等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。

2.Hotspot中方法区的变化:
在这里插入图片描述

JVM内存分哪几个区,每个区的作用是什么?

1、程序计数器
程序计数器是一块较小的内存,通过这个计数器来选取下一条需要执行的字节码指令。Java虚拟机的多线程是通过线程之间切换来轮流获得处理器的执行时间的,每个线程都有自己独立的程序计数器,它们互不影响,也就是线程私有的。如果执行的是Java方法,这里存放的是指令地址,如果执行的是Native方法,这里的值为空(Undefined)。并且这个区域不会出现OutOfMemoryError
2、虚拟机栈
虚拟机栈也是线程私有的,虚拟机栈描述的是Java方法执行的内存模型,执行每个方法的时候都会创建一个栈帧,用来存储局部变量表、操作数栈、方法出口等信息。局部变量表中存放了各种基本数据类型,对象引用类型,局部变量表在编译期间完成分配。
这个区域有两种异常情况:如果线程请求的栈深度大于虚拟机容许的最大深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,但是扩展时无法申请到足够的内存,抛出OutOfMemoryError异常。
3、本地方法栈
本地方法栈与虚拟机栈的作用类似,只不过是虚拟机栈是为执行Java方法服务,而本地方法栈是为执行Native方法服务。
4、堆
Java堆是由所有线程共享的一块内存区域,在虚拟机启动时创建,并且也是Java虚拟机管理管理的内存中最大的一块。Java堆主要用于存放对象实例以及数组,Java堆是垃圾回收的主要区域,也被成为“GC”堆。
5、方法区(JVM规范方法区定义,在Hotspot中实际存储有所区别)
方法区也是各个线程共享的内存区域,用于存储已经被虚拟机加载的类信息、常量、静态变量等数据。这个区域也会存在垃圾回收,不过回收的目标主要是针对常量池和对类型的卸载,不过由于类型的卸载条件比较苛刻,所以回收成绩难以令人满意。
最后再介绍一下对象的访问方式,上面已经介绍,在执行每个Java方法的时候都会创建一个栈帧,其中的局部变量表用来存储基本类型和对象引用(reference),而对象实例存储在堆中,那么如何通过引用访问对应的实例呢?介绍两种常用的方式:使用句柄访问和使用直接指针访问

一面:JVM内存分布/内存结构?栈和堆的区别?堆的结构?为什么两个survivor区?

堆(Heap):
堆是Java虚拟机管理的内存中最大的一块区域,Java堆是所有线程共享的区域,在虚拟机启动的时候创建。堆的作用是用来存放几乎所有的对象实例(虚拟机的逃逸分析和TLAB会根据分析,将对象实例分配到栈上,而不是堆上,减轻堆的同步负载和内存分配压力)(!!!实际上即使经过逃逸分析对象还是保存在堆上)。
Java堆是垃圾器管理的主要区域,因此也被称为GC(garbage collect)堆。当要给新的实例分配内存空间的时候,堆空间不足会抛出OOM异常。

虚拟机栈(VM Stack)
Java内存中,划分最粗糙的方法将是堆栈。这里的栈内存也就是虚拟机栈。虚拟机栈是线程私有的(线程私有的意思就是,每个线程都有一个自己的虚拟机栈)。虚拟机栈执行方法的时候,会为每一个方法创建一个栈帧,用于存放局部变量、操作数栈、动态链接、方法出口等信息。每一个方法的调用过程,在虚拟机中就是一个栈帧的一次入栈出栈。所以在用递归的方法来处理量很大的数据的时候,就会 导致StackOverFlowError。
局部变量表中存放了几种类型的数据:

  1. 基本数据类型(boolean、int、double、byte…)
  2. 对象引用,也就是局部变量,但是这里存的只是引用(对象的起始地址,句柄或者其他与对象相关的位置),不是对象实例。
  3. returnAddress,Java的三种原始数据类型(数值、boolean、returnAddress)之一。用来保存当前执行指令的下一条指令,用 来后续执行。

方法区(Method)
方法区也是用于线程共享的区域,用来存储已经被加载的类信息,常量、静态变量、即时编译器编译后的代码数据。
在HotSpot虚拟机中(Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机),方法区的实现在不同版本的jdk中实现有所不同。在JDK7中,HotSpot虚拟机使用永久代来实现方法区,而在JDK8之后移除了永久代,并将方法区移入了meta-space中。

本地方法栈(Navitive Method Stack):
功能和虚拟机栈作用非常相似,虚拟机栈执行的是Java字节码,而本地方法栈执行的是Native方法。

程序计数器(Programmer Counter Register):
程序计数器是一小块内存区域,也是线程私有的。用来储存当前线程执行的字节码的行号。
Java多线程是通过线程轮流切换并分配给处理器执行的方式实现的。所以如果当前线程切换后,要想恢复到正确的位置,就需要程序计数器了。
如果当前线程执行的方法是Java方法,那程序计数器保存的就是字节码指令的地址,如果执行的是Native方法,计数器的值就为空(Undefind)。

栈与堆的区别

 栈内存:栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。

 堆内存:存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。
   
   	区别很明显:
        1.栈内存存储的是局部变量而堆内存存储的是实体;
        2.栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;
        3.栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。
        
  	Java栈是与每一个线程关联的,JVM在创建每一个线程的时候,会分配一定的栈空间给线程,主要存放线程执行过程中的局部变量,方法的返回值,基本类型的变量(,int, short, long, byte, float, double, boolean, char)以及方法调用的上下文。栈空间随着线程的终止而释放,栈的优势是,存取速度比堆要快,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈有一个很重要的特殊性,就是存在栈中的数据不可以共享。
  
	Java中堆是由所有的线程共享的一块内存区域,堆用来保存各种JAVA对象,比如数组,线程对象等,java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等 指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时 动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢
	栈是运行时的单位,而堆是存储的单位。
	即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
		 堆解决的是数据存储的问题,即数据怎么放、放在哪儿。

二面: Eden和survior的比例分配

在虚拟机中默认有设置 -XX:+UseAdaptiveSizePolicy 自适应比例 所以没通过设置不是8:1:1
在这里插入图片描述

新生代老年代默认空间1/2
在这里插入图片描述

![在这里插入图片描述](https://img-blog.csdnimg.cn/20201208153557543.png在这里插入图片描述

在这里插入图片描述

小米: jvm内存分区,为什么要有新生代和老年代

因为有的对象寿命长,有的对象寿命短。应该将寿命长的对象放在一个区,寿命短的对象放在一个区。不同的区采用不同的垃圾收集算法。寿命短的区清理频次高一点,寿命长的区清理频次低一点。提高效率。

为什么要有Survivor区?

如果没有Survivor区,那么Eden每次满了清理垃圾,存活的对象被迁移到老年区,老年区满了,就会触发Full GC,Full GC是非常耗时的。
解决办法:
增加老年代内存,那么老年代清理频次减少,但清理一次花费时间更长。减少老年代内存,老年代一次FullGC时间更少,频率增加。
都不行,只有再加一层Survivor。将Eden区满了的对象,添加到Survivor区,等对象反复清理几遍之后都没清理掉,再放到老年区,这样老年区的压力就会小很多。即Survivor相当于一个筛子,筛掉生命周期短的,将生命周期长的放到老年代区,减少老年代被清理的次数。
为什么要加两个Survivor?
先来看一下一个的
在这里插入图片描述
清理内存,很容易产生内存碎片,为了不产生内存碎片,我才用复制算法,将Eden区和Survivor区存活的对象整齐的放到一个空的内存。因为生命周期一般都比较短,所以在存活对象不多的情况下,复制算法效率还是比较高的。
复制算法:
在这里插入图片描述
这样就需要一个空内存,而我如果有三个区,这样就总可以保持一个是空的,这样我清理垃圾的时候,就可以将存活对象全部都整齐的放到一个空的内存中,不产生内存碎片了。

字节跳动: 二面:Java的内存分区? 二面:讲讲jvm运行时数据库区什么时候对象会进入老年代?

首先需要知道Java内存是如何分配的:
1.对象优先在新生代的Eden区进行分配
2.大对象直接进入老年代
3.长期存活的对象将进入到老年代(虚拟机设置值,默认阈值为15)
4.动态对象年龄判定,如果Survivor区中相同年龄所有对象的大小总和大于Survivor区空间一半,年龄大于或者等于该年龄的对象在MinorGC时将复制到老年代
5.空间分配担保,当MinorGC时,如果存活对象过多,无法完全放入Survivor区,就会向老年代借用内存存放对象,已完成MinorGC

四种情况
1.Eden区满时,进行Minor GC
当Eden和一个Survivor区中依然存活的对象无法放入到Survivor中,则通过分配担保机制提前转移到老年代中。

2.对象体积太大, 新生代无法容纳
-XX:PretenureSizeThreshold即对象的大小大于此值, 就会绕过新生代, 直接在老年代分配, 此参数只对Serial及ParNew两款收集器有效。

3.长期存活的对象将进入老年代
虚拟机对每个对象定义了一个对象年龄(Age)计数器。当年龄增加到一定的临界值时,就会晋升到老年代中,该临界值由参数:-XX:MaxTenuringThreshold来设置。
如果对象在Eden出生并在第一次发生MinorGC时仍然存活,并且能够被Survivor中所容纳的话,则该对象会被移动到Survivor中,并且设Age=1;以后每经历一次Minor GC,该对象还存活的话Age=Age+1。

4.动态对象年龄判定
虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor区中相同年龄(设年龄为age)的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄(age)的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

在这里插入图片描述

京东: VM的内存结构,Eden和survivor比例。 JVM内存为什么要分成新生代,老年代,永久代。新生代中为什么要分为Eden和Survivor。

上面已经解答过
为什么要分为永久代 (前说:在JDK8中,永久代被元数据区取代,而元数据区数据保存不保存在虚拟机内存中)
原因如下:
字符串存在永久代中,容易出现性能问题和内存溢出
类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
将 HotSpot 与 JRockit 合二为一

天猫: 一面: Jvm内存模型以及分区,需要详细到每个区放什么。一面:JVM的内存模型,Java8做了什么修改

上面已经解答过

拼多多: VM内存分哪几个区,每个区的作用是什么?

上面已经解答过

美团: java内存分配 jvm的永久代中会发生垃圾回收吗? 一面: jvm内存分区,为什么要有新生代和老年代?

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

为什么有TLAB ( (Thread Local Allocation Buffer ) ?

🐱‍🐉
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,
同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
据我所知所有openJDK衍生出来的JVM都提供了TLAB的设计
在这里插入图片描述

Minor GC、Major GC、FullGC

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值