JVM八股

java运行时数据区

首先,jvm运行的时候,内部的内存会分为5个部分:程序计数器、java虚拟栈、本地方法栈、方法区、堆。

其中,线程私有的是:程序计数器、java虚拟栈、本地方法栈。线程公有的是方法区、堆。

Java8使用元空间(MetaSpace)代替永久代,它们都是方法区的实现,最大的区别是:元空间并不在JVM中,而是使用本地内存。

程序计数器:

程序计数器就是一块比较小的内存,主要是用来保存当前执行的程序的字节码文件的信号灯,有2个作用。作用1、表明当前线程运行的逻辑,顺序、循环等。作用2、记录当前运行的位置,为了保证在线程切换的时候能够正确继续上一次停止的位子。

特点:

唯一一个没有OutOfMemory的内存区域

生命周期,随着线程的创建而创建,随着线程的结束死亡。

线程私有

执行native方法的时候,计数器数值是空。(这个的概念并不是这个意思,因为native方法就不是java的方法,可理解成并不是在jvm上运行的,因为如果是java写的方法,会在jvm中的线程映射到os上找一个线程运行,native方法并不是在jvm中运行,而是直接在os上运行,所以这个程序计数器并不是空的概念,而是未定义,这个未定义自然可以是所有值。)

java虚拟栈:

虚拟机栈,描述的就是java方法执行的内存模型。

每个方法都在执行的时候,会弄一个栈帧出来,然后这个栈帧就会入栈,入jvm的java虚拟机栈,然后这个栈帧就保存了类似局部变量、方法出口等方法的信息,然后在运行结束的时候,对应这个栈帧出栈的过程,所以这个内存的模型我们叫他是一个栈。

特点:

是线程私有的

生命周期和线程一样,伴随线程产生,伴随线程死亡

异常:

StackOverflowError:线程请求的栈深度超过了虚拟机允许的栈的深度,这就会抛出这个错误,比如,如果你是一个递归函数,递归函数的递归层次太多了就会出现这个情况。

OutOfMemoryError:虚拟机可以动态拓展,但是拓展的时候无法申请到足够的内存。例如,一个jvm线程 -Xss分配多大的内存,你申请的超过了这个。(默认线程是1M大小,但是实际情况比这个小)

栈帧:

用于支持虚拟机方法调用和方法执行的数据结构,

作用:存储方法的局部变量、方法出口、操作数栈等

编译代码的时候,方法需要多大的局部变量表和多深的操作数栈都已经确定,在运行阶段不会变化

java虚拟栈中,占用内存最多的就是:局部变量表。

局部变量表:是一组变量值存储空间,用于存放方法参数和局部变量。

局部变量表中存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)和 returnAddress类型(指向了一条字节码指令的地址)

其中64位长度的 long 和 double 类型占用两个局部变量空间(Slot),其余数据类型只占用1个。

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法的运行期间不会改变局部变量表的大小。

java方法有2种返回的方法:return语句和抛出异常,都是让栈帧出栈。

本地方法栈:

本地方法栈和java虚拟机栈的作用类似的,区别就是java虚拟机栈就是为了java方法服务,然后本地方法栈是为了native方法服务。在HotSpot虚拟机中,二者是合二为一的。

那么特点和异常跟上面一样的

特点:

是线程私有的

生命周期和线程一样,伴随线程产生,伴随线程死亡

异常:

StackOverflowError:线程请求的栈深度超过了虚拟机允许的栈的深度,这就会抛出这个错误,比如,如果你是一个递归函数,递归函数的递归层次太多了就会出现这个情况。

OutOfMemoryError:虚拟机可以动态拓展,但是拓展的时候无法申请到足够的内存。例如,一个jvm线程 -Xss分配多大的内存,你申请的超过了这个。(默认线程是1M大小,但是实际情况比这个小)

java堆:

java堆是最大的一块内存,是被所有线程共享的一块内存,在虚拟机运行的时候创建,线程共享,主要存放对象实例和数组。

image.png

jvm内存分为堆内存和永久代,堆内存分为年轻代和老年代,永久代只有一块。

堆内存分为年轻代和老年代,年轻代分为生成区和幸存区,老年代只有一块。

年轻代分为生成区和幸存区,幸存区分为FromSpace和ToSpace,生成区只有一块。

生成区Eden和幸存区中FromSpace和ToSpace的默认比例是8:1:1

堆内存中存放的就是对象,垃圾回收机制回收的也就是这些东西

非堆内存,也就是永久代,永久代也称呼为方法区,存储线程长期运行的对象,比如常量、属性、元数据等

注意:java8中使用元数据代替了永久代,和永久代的区别就是,元数据在内存中,不在jvm中

java堆是垃圾回收机制的主要区域,所以也叫做CG堆

java堆无需物理连续,只需要逻辑连续就行

异常:OutOfMemoryError

特点:线程共享

方法区:

方法区(Method Area)与Java堆一样,是线程共享的内存区域,它用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码 等数据。它的别名 Non-Heap(非堆)。在HotSpot虚拟机中方法区也被称为 永久代

作用:

主要存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

异常:OutOfMemoryError

特点:

线程共享

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

新生代、老年代等的动态年龄

对象会在Eden 生成区 分配,再一次新生代垃圾回收之后,如果对象还活着,则会进入S0或者S1,也就是幸存区的2个部分,并且对象的年龄会随之增加1,(Eden区到Survivor对象的初始年龄是1,默认到15就进入老年区),对象进入老年代的阈值可以调整,可以通过 -XX:MaxTenuringThreshold

动态老年阈值计算:

根据年龄从小到大的顺序扫描,然后看占用内存的累计,超过了总内存的一半的时候,把这个年龄和目前的阈值对比,取新阈值 = Math.min(老阈值,这个年龄)

堆空间最容易出现的就是OutOfMemoryError

方法区和永久代

首先,明确,方法区是java虚拟机的概念,永久代是Hotspot的概念,所以这个更像是一个接口和实现类的关系,方法区是一个规范,然后永久代是Hotspot的具体实现。之后就是元数据。

元数据更像是干掉了方法区的概念,重新搞了个概念,并且还是自己实现了,使用的就是本地内存的概念。

内存分配方式:

分配方式有:指针碰撞和空闲列表,选择哪种方式由java堆是否规整决定,而java堆是否规整由采取的垃圾收集器是否带有压缩整理功能决定。gc收集器的算法是:标记-清除,还是标记-整理,

指针碰撞:假设Java堆是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放一个指针作为分界点的指示器。为对象分配内存时把那个指针向空闲空间那边挪动一段与对象大小相等的距离。例如(Serial ParNew)

空闲列表:Java堆中的内存不是规整的,已经使用的内存和空闲的内存相互交错,无法使用指针碰撞。这时虚拟机就必须维护一个列表,记录上哪些内存是可用的,在分配内存的时候从列表中找一块足够大的空间划分给对象实例,并更新列表上的记录。例如(CMS)

内存分配并发问题

虚拟机会保存内存分配的时候的并发问题:

方法一、CAS+失败重试:CAS是一个乐观锁,乐观的态度先去分配,然后加上一个预期值的概念,如果失败了就重新尝试,这个方法保证了分配内存的原子操作

方法二、TLAB:为每一个线程预先在生成区 Eden分配一块内存,JVM在给线程中对象分配内存的时候,首先在TLAB分配,当对象大于TLAB预先留下的内存或者TLAB已经耗尽的时候,再采用CAS和失败重试的方法。

java对象的创建过程?

step1、类加载的过程

虚拟机遇到一个new指令的时候,先会去找这个类是否在常量池中已经加载过,如果没有,那么就必须要进行一个类加载的过程

step2、分配内存

在类加载检测通过之后,就需要对于这个类进行分配内存,分配的内存在类加载的时候就已经确定了,为对象分配的内存就相当于是把一块内存从java的堆内存中提取出来。

step3、初始化零值

内存分配完成之后,虚拟机需要将分配的内存空间都初始化为零值,(不包括对象头),这一步操作是为了保证对象的实例字段在java代码中可以不赋初始值就直接使用,程序可以访问到的这些字段的数据类型所对应的0值

step4、设置对象头

在设置为零值之后,虚拟机需要对对象进行必要的设置,比如这个对象是哪个类的实例,如何找到对象的元数据,hash码,对象的GC年龄分代等问题,这些信息都是在对象头中设置,设置对象头的方法也有多种。

step5、执行init方法

上面的搞定之后,从java虚拟机的角度来说,就已经搞定了一个对象的创建了,但是从java角度,还差最后一个初始化的构造方法,将对象按照意愿进行初始化,这样一个对象才能创建完毕。

对象访问定位有哪俩种方式?

java的程序通过操作栈上的Reference数据来操作堆内的对象,通过这样的方式,来执行对于对象的操作。 对象的访问方式目前主要的是:使用句柄 和 直接指针 2种

使用句柄:堆中划出一部分内存作为句柄池,句柄池中存放各个对象的句柄,句柄包含了队形实例数据和对象类型数据的具体地址信息,而reference中存储的则是对象的句柄地址。

image.png

直接指针:使用直接方式访问对象时,堆中对象存放的是对象的实例数据和指向对象类型数据的指针,reference则存储的是对象地址

image.png

二者的对比:

句柄方法稳定,因为保存的是句柄的东西,reference中保存的是稳定的句柄的地址,如果对象数据变化了,那么不需要改reference的数据,直接指针需要改

直接指针快,因为省去了一个指针的寻址的操作,这种寻址在java中非常的麻烦。

JVM的内存的分配和回收

java自动分配最关键的还是内存的分配和回收,其中最为关键的应该就是堆内存中对象的分配和回收。

java堆是垃圾回收的重点,所以也叫做GC堆,(Garbage Collection 堆),垃圾回收的算法很多是根据年代划分,所以堆里面还有新生代、年老代等概念,还有生产区、幸存区,幸存区中还有toSpace 和 FromSpace等,这些东西都是为了垃圾回收的快。

一种垃圾回收方式,是保证to空间是空的,一直重复,直到from是满了,然后把from的所有的都放进老年代中。

堆内存中对象分配的基本策略

优先在Eden中分配

大对象直接去老年区(数组、字符串)

长期存活的也会在老年区(年龄的概念)

jvm的GC回收机制

说白了只有partial 和 full,也就是只有部分的和整个都的,就这2中情况。

如何判断对象已经死亡?

堆中几乎放着所有的对象的实例。(为什么几乎,因为存在逃逸分析),判断对象死亡有如下2种方法

方法一、引用计数法:

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

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。

方法二、可达性分析算法

使用一系列对象作为GC Root对象,所有的引用关系连接起来,如果跟GC Root没有相连的对象就可以判断它挂了,可以清除

image.png

可作为GC Roots对象有以下几种:

虚拟机栈中引用的对象

方法区中类静态属性引用的对象

方法区中常量引用的对象

本地方法栈中Native方法引用的对象

引用在jvm中的概念?

JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)

强引用

以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

软引用(SoftReference)

如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

弱引用(WeakReference)

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

虚引用(PhantomReference)

"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

虚引用主要用来跟踪对象被垃圾回收的活动。

如何判断一个常量是废弃常量?

假如在常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池。

如何判断一个类是无用的类?

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :

  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。

  2. 加载该类的 ClassLoader 已经被回收。

  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾手机算法以及各自特点?

标记-清除算法
分为标记和清除2个部分:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象,是最基础的收集算法,后续算法都是对这个算法的改进,这种算法会带来2个问题,

效率问题以及空间问题

  • 优点:算法简单,容易实现,后续算法都以标记清楚算法为基础,对其改造

  • 缺点:1、执行效率不稳定。如果Java堆中包含大量的对象,并且大部分对象是需要回收的,此时就需进行大量的标记清楚动作,导致标记 清除两个过程随着对象的数量增长而降低。2、内存空间碎片化问题。标记、清楚之后会产生大量的不连续的内存碎片,空间碎片太多导致以后程序运行过程要分配较大对象时无法找到足够的连续内存而不得不再次进行垃圾回收动作。

image.png

标记-复制算法
复制一个地方,然后每次都是重新放过来,之前的老地方就当做新的,下一次需要复制到的

为解决标记清楚算法面对大量可回收对象时执行效率低的问题,首先出现了半区复制算法。它将内存按容量会分为两块等大的区域,每次使用其中的一块,当这一块内存用完了,就将存活的对象对象复制到另一块上面去,然后把已经使用玩得内存块一次性清除干净。

优缺点:

  • 优点:1、实现简单、运行高效。2、解决了内存碎片问题

  • 缺点:1、如果大多数对象是存活的话或有很高的复制开销。2、将可用内存缩短了原先的一半,空间浪费严重。

image.png

标记-整理(移动)算法
针对老年代对象存亡特征,1974年出现了标记-整理算法。标记过程与标记-清除算法一致。但后续步骤不是直接对可回收对象进行清理,而是让存活对象向内存空间一端移动,然后直接清理掉边界以外的内存。

评价:标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动 式的。

image.png

分代收集算法
就是根据内存的年龄不同,采取不同的收集方法。

小结
比较标记-清除算法和标记-整理算法。前者收集时效率更高,但收集结果存在大量内存碎片,为以后的给对象分配内存带来了很大的负担。后者收集时操作麻烦,在整理的时候还会出现“Stop The World”现象,即全程暂停用户程序,但是将来的给对象分配内存非常简单。

出于吞吐量(吞吐量指的是赋值器与收集器的效率总和)的考虑的话,标记-整理算法更优秀。

出于低延迟考虑的话,标记清除算法更优秀。

出于综合考虑的话,有一种“和稀泥式”解决方案可以不在内存分配和访问上增加太大额外负担,做法是让虚 拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经 大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。

常见的垃圾回收器有哪些?

如果收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现

垃圾收集器就是根据不同的场景选择更加适合的垃圾收集器,这样才能应付各种情况。

常见的垃圾收集器:

Serial收集器

串行,就是一个单线程的垃圾回收器,自己只有一个线程,并且在运行的时候要暂停所有其他的线程,直到自己搞定。

简单高效,但是stop the world的操作非常愚蠢,只能尽量降低这种影响

ParNew收集器

就是串行的多线程版本,

ParallelScavenge收集器

也是多线程的,但是更关注的是吞吐量,也就是高效的利用cpu,CMS更关注的是降低停顿时间,也就是提高用户体验,有自适应的调节策略

CMS收集器

concurrent mark sweep是一种获取最短停顿时间为目的的收集器。

第一次实现了垃圾收集线程和用户线程基本上可以同时工作 是一种标记-清除算法

初始标记:暂停所有其他线程,并记录所有和root直接相连的对象。

并发标记:同时开启GC线程和用户线程,然后记录所有可达对象,这些东西是会变化的,所以尽量去跟踪记录这些发生引用更新的地方

重新标记:也就是更新引用引用变化,导致的标记变化的情况,这个阶段比初始标记要长,但是远远比并发标记短

并发清除:开启用户线程,并且GC线程开始对未标记的清除。

缺点:

对CPU敏感

无法处理浮动垃圾

回收算法是标记-清除,导致收集结束有很多空间碎片

G1收集器

Garbage First收集器

是面向服务器的,主要是高吞吐量,所以对于CPU的性能和服务器的性能要求挺高

并发和并行:就是利用CPU的高性能,利用多cpu和多核心处理线程的GC行为

分代收集:保留了分代的概念,就是为了可以提高性能

空间整合:整体上是标记整理,但是局部又是标记复制

可以实现停顿时间的可预测。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GGUOHHUO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值