0%

JVM一---运行时数据区

运行时数据区

写在前面,感谢尚硅谷宋红康老师的讲解,本文根据宋红康老师的ppt完成,仅供个人学习参考,更多的JVM相关的知识请访问Oracle官网

1.内部结构

2.线程

1.说的一个线程准备好之后,那这个准备是什么意思:

每个java线程都有自己的程序计数器,虚拟机栈,本地方法栈等,都需要准备

2.如果在run方法中碰到了未捕获的异常,java线程就会终止,但是操作系统本地线程不会立即终止,由它来决定jvm进程要不要终止,jvm要不要终止,又取决于当前的java线程是不是最后一个非守护线程

3.pc寄存器(程序计数器)

两个常见的问题

4.虚拟机栈

概述

一个线程,对应一个java虚拟机栈

java虚拟机栈常见的异常

栈的存储结构和运行原理

遇到异常如果在当前栈帧中没有处理,则会向上抛,如果还没有,在向上抛,直到main方法,main方法如果还没有处理,当前线程就结束了

栈帧的内部结构

局部变量表的结构的认识

局部变量表是一个数字数组:里面的变量都可以用数值来表示

这里的这个Maximum local variables是在编译的时候就已经确定了长度了

字节码中方法内部结构

上图中的L指的是引用类型的变量

Access flags:访问标识

slot理解

注意,如果是非静态的方法,该对象引用this将会存放在index为0的slot处,静态方法中不可以引用this,所以在静态方法中没有

静态变量和局部变量的对比

操作数栈的特点

操作数栈的字节码指令分析

栈顶缓存技术Top of Stack Cashing

动态链接

大部分字节码指令在执行的时候,都需要进行常量池的访问,帧数据区中,就保存着可以访问常量池中的一个指针,方便程序访问常量池,这里就是我们所说的动态链接

以上这些符号就是所谓的动态链接,常量池在运行的时候被放到方法区,所以叫做运行时常量池。

那为什么不把“方法”直接放在栈帧中呢?java对象中有很多的方法,所以,总不可能每个栈帧,都放一个符号方法吧

方法的调用–静态绑定与动态绑定

如果在编译器就知道符号引用对应方法的直接引用,就称为静态链接

在多态中可以体现

虚函数 == 具备晚期绑定的特点的方法

虚方法和非虚方法

非虚方法,不涉及方法的重写

注意:子类自己声明的普通方法也叫做invokevirtual,父类中子类没有重写的方法,在子类中没有super.显示调用也叫做invokevirtual

invokedynamic指令的使用

方法重写的本质

IllegalAccessException在maven管理jar包的时候经常会出现,因为jar包冲突,或者没有部署到服务器上,都有可能会出现这个异常,但是具体情况具体分析。

虚方法表在链接的解析阶段创建:将常量池内的符号引用(类、属性、方法的符号等)转换为直接引用的过程

方法返回地址

在一个A方法中调用了B方法,当B返回的时候,pc寄存器的值当作方法返回地址返回给A,A就按照下一条指令的地址执行B方法之后的代码,此处又分为异常退出和正常退出。

引用类型时areturn

虚拟机栈的五道面试题

5.本地方法栈

6.本地方法接口

native不意味着是抽象方法,和abstract关键字不能共用

堆空间

1.概述

堆和方法区对一个进程来说,都是唯一的,一个进程就对应一个JVM的实例,一个JVM实例中就会有一个运行时数据区,一个进程有多个线程,所以线程共享一个堆空间和方法区

-Xms 初始的堆内存空间;-Xmx最大允许的堆内存空间

在jdk自带的工具包中可以使用jvisualvm,可以查看运行的java进程

每个进程都各自有一份堆内存

堆内存逻辑上连续(实现比较简单,存储也比较高效),但是硬盘上不一定连续,物理内存和虚拟内存可以建立一张映射表,这张表,可以将物理上不连续的内存在虚拟内存中看作是连续的,方便存储数据。

上面所说的所有的线程都共享堆空间,那么这肯定会带来线程安全的问题,如果用同步的方式处理,堆空间的并发性就会变得非常差,如何处理?

在堆空间中分配出一些小块,每个线程一人一份,每个小块就称为TLAB,在这里就体现了另外一点,就是并发性更好一些

注意:

①这里所说的是“几乎”所有的对象都会在堆内存中分配内存,表示还有一部份对象可能不会在堆内存中分配(还有可能在栈上分配)。

②堆中的对象不会在方法结束以后马上被移除,而是在垃圾回收的时候才会被移除。(在方法执行结束后对象依次弹出栈,但是是在GC中判断是否这两个对象当前是不是垃圾,等到相应的空间内存不足时JVM会主动的调用GC),如果是像我们所理解的方法执行结束就进行垃圾回收,那这样的话,垃圾回收的频率就会特别高,会影响用户线程去执行(在《深入java虚拟机》中提到的“stop the world”的问题就出现了)

堆空间中的实例,所属的类类型信息,以及类中的方法都存放于方法区中

一旦执行了new这个字节码指令的时候,就会在堆空间中创建对象,开辟空间,之后JVM为堆空间的实例变量初始化。

2.内存细分

分代垃圾回收算法就是针对于堆的分代来讲的

现在所说的堆空间,主要就分为了新生区和养老区,这里所说的永久区是在方法区中的实现。

上文所看的对的内存分配的图可以看出

堆内存中只有新生代和老年代,并没有所谓的永久区。

运行java程序时指定:-XX: +PrintGCDetails,即可打印垃圾回收器的执行细节

3.堆空间的大小的设置和查看

这里所说的指令,也可以使用等价于后面的哪个指令,他们的效果是一样的。

默认情况下,对内存的初始化大小是……最大内存大小是……

但是,一般情况而言,这里的内存总是会比电脑的实际内存要小一点:原因之一是实际上使用的时候,有很多必须要加载的数据,他要占用一点内存。

再手动设置堆内存的大小的时候,通常将最大值和最小值设置成一样大小的值:初始的时候要设置的一个值之后,如果堆空间不够会进行扩容,后续如果内存用不到,他也会往下降,我们如果在服务器中不设置成一样大小的值,就会出现频繁的扩容和释放的情况,会造成不必要的系统的压力。

出现的一个问题?这个数怎么算出来的?

查看设置的参数,方式一、JPS jstat -gc 进程ID

老年代:OC(其中C是总量) OU(U是use)

伊甸园区:EC EU

幸存者区:S0C S1C S0U S1U

将各个区域的总量加起来(S0C+S1C+EC+OC/1024),发现正好是600M,那为什么在上面的程序中是575呢?

伊甸园区能放对象,S0C或者S1C他们只能二选一的去用,所以如果少加一个S区,会发现,正好是575,主要用到的是垃圾回收,新生代使用的复制算法,S0C、S1C他俩始终有一个区域是空的

方式二、运行java程序的时候加上-XX: +PrintGCDetails

其中PSYoungGen中的total是通过eden总量加上from/to其中一个得出。

4.oom的说明和举例

关于异常相关的类叫做Throwable

我们这里所说的oom指的是 error,在面试中我们讲到的异常,实际上就是Throwable(包括Error或者Exception)

5.年轻代与老年代中相关参数的设置

注意:幸存者0区不一定是from 1区也不一定就是to

前面我们讲到了设置堆空间的大小,那么如何控制新生代和老年代的大小呢?

-XX:NewRatio 设置新生代和老年代的比例。默认值是2,一般情况下不会去修改这个空间的。

如果你知道有很多对象声明周期比较长,这个时候你就可以把老年代的空间调大一点

伊甸园区和幸存者0、1区的比例在官方给出的默认值是8:1:1,但是我们在jvisualvm中看到的却是下图所示的结果

这是为什么?

所以我们想到jvm底层有一个自适应的机制,可以使用

1
-XX:-UseAdaptiveSizePolicy // 减号代表关闭自适应

但是我们操作了之后,发现还是不起效果。

要想比例是原来默认的8:1:1,我们仍需要显示地指定一下才行。

1
-XX:SurvivorRatio=8

注意:设置了NewRatio制定了新生代和老年代的比例,又设置了-Xmn参数,这时候就会有矛盾,以-Xmn为准(-Xmn一般不设置)

6.对象分配的过程

对象刚开始放在伊甸园区,如果伊甸园区放满了,这时候会进行垃圾回收(YGC/Minor GC),那谁是垃圾,谁不是垃圾,怎么进行判断,(涉及到引用计数算法、可达性分析算法,在java中我们使用后者)

垃圾回收完成之后,如果还有对象被引用,这时候就把存活的对象放进幸存者区(s0、s1都是空的,这时候先把他放在0区,为幸存的对象的年龄计数器赋值为1)

这个to表示,下一次YGC的时候,一点远去的对象要往哪里放

这里有个问题,伊甸园区空间放满了的时候,触发YGC,我们知道伊甸园区和幸存者区的比利时8:1:1,那万一幸存者区满了怎么办?会触发YGC吗?

不会触发,这不意味着幸存者区不会有垃圾回收(当伊甸园区发生YGC的时候,伊甸园区和幸存者区都会发生垃圾回收),幸存者区满了,这里会存在特殊的规则(会直接晋升到老年代)

有没有可能对象一上来就被分配到老年代?

有可能的,对象所占用的内存比较大的情况

7.对象分配的特殊过程

注意,YGC之后,Eden区一定是空的,如果这个时候Eden区还是放不下的话,那只有可能对象是超大对象,这时候会考虑直接一步到老年区是不是能够放下这个对象,如果这个时候放不下,会Full GC(Major GC),如果还是放不下那么就OOM。

8.常用的调优工具

电脑上安装Jprofiler

idea下载Jprofiler插件

9.Minor GC ,Full GC和Major Gc的对比

YGC == Minor GC

调优的目的就是希望垃圾回收的次数少一些,注意的是,Major GC和Full GC所占用时间比Minor GC占用的时间多十倍以上(出现“Stop The World”)。

上图中所说的三个内存区域分别指的是:新生代、老年代;方法区。

但是因为新生代比较小的原因,“STW”的时间也不会很长,虽然频率特别高,但是执行的速度特别快。

为什么会有STW:

如果出现,垃圾回收的线程和制造垃圾的线程同时运行,在垃圾回收线程判断完那个是垃圾,准备回收垃圾的时候,制造垃圾的线程这时候又制造了几个对象, 不知道是不是垃圾,这个时候就不合适了。

Full GC覆盖了整个堆空间,包括我们所讲的方法区。

10.GC举例和日志分析

字符串常量池是存放在堆空间的,在jdk8之后修改成了存放在堆空间,以前是在方法区。

堆空间报OOM之前通常会进行一次Full GC,垃圾回收之后仍然发现空间不足,才会报这个OOM

后面的secs是此次GC花费的时间

11.小结内存分配的策略

​ 为什么要将java堆分代,分代的好处是什么?

为了优化GC的性能

一些不容易销毁的对象放在老年代中,不让他进行频繁的垃圾回收,这样就可以优化性能

注意:如果年龄相同的所有对象的综合大于Survivor中空间的一般,这些对象就可以直接进入老年代,空间分配担保指的是进行了minor GC之后,对象在Survivor中放不下,这时候就会直接进入老年代,但是它的前提是老年代必须有这个空间来容纳这个对象(需要用到-XX:HandlePromotionFailure)

12.堆空间为每个线程分配的TLAB

那么这个时候TLAB就应运而生

所以在面试中就会有个问题,堆空间的内存都是共享的吗?

不一定,因为Eden区为每个线程分配了TLAB。

13.总结

小结堆空间的参数设置

如果Eden区设置的比较大,会出现什么问题?

Eden区太大,Survivor就会比较小,那么当进行YGC的时候,Survivor只能存放一小部分的对象,这会导致某一部分的对象,直接就进入Old区,即时这个对象的生命周期非常短,这样不利于系统性能 (导致minor GC失去意义了)

Eden区设置的不够大,Survivor设置的反而比较大,出现的问题?

YGC触发地更加频繁,会影响用户线程

关于参数-XX:HandlePromotionFailure的说明:

14.拓展

堆是分配对象存储的唯一选择吗?

我们堆JVM性能地调优,主要是针对于老年代的GC,因为老年代的GC的‘STW’时间比较长

最终目的都是希望在堆上分配的对象少一些(因为不用进行垃圾回收)

代码优化

栈上分配

通过逃逸分析,发现对象没有逃逸的,才作为栈上分配

逃逸:通常是返回一个对象,这个对象在另一个方法外可能会被使用得到,此时就分配在堆空间中

开启逃逸分析:-XX:+DoEscapeAnalysis,(当开启逃逸分析的时候,如果对象没有发生逃逸,那么就会考虑栈上分配)关闭逃逸分析:-XX:-DoEscapeAnalysis。(JDK7之后默认是开启的)

当开启逃逸分析的时候,因为是在站上分配的,所以根本不会发生GC,真正维护的对象的个数也不足我们创建的个数

同步省略

如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以考虑不同步

就是说,如果一个对象没有发生逃逸,就没有必要使用加锁,因为他根本就不会被其他线程访问到

上面的方法,每次线程调用方法是,对象总是自己new出来的,所以这时候加锁就没有必要。JVM虚拟机有了逃逸分析,会自动帮你确定

只是在运行的时候,才考虑把它去掉,加载的时候还是会加载原来的文件

标量替换

对象被称作聚合量,聚合量可以分解成最小的标量,标量是不可以再分的量

相当于,将对象拆解开来,分散在栈上

开启标量替换-XX:+EliminateAllocations

总结

堆空间是对象分配地唯一选择吗?

逃逸分析在java地服务器端模式才会开启的。

逃逸分析本身并不成熟,分析逃逸分析,无法保证其本身的性能一定高于它的消耗,如果经过逃逸分析,发现没有一个对象是不逃逸的,那么逃逸分析的性能就被白白浪费掉了。

注意:这里所说的所有对象都创建在堆上,这是正确的。这里我们先否定了所有的对象都是分配在堆上的,这里我们有否定了。

方法区

栈、堆、方法区的关系

方法区的理解

逻辑上我们把方法区当作堆的一部分,但是实际上,方法区的简单实现既没有GC,也没有压缩。

在实际操作过程中,我们设置堆空间的大小的情况下,实际上也没有影响到方法区空间的大小

因为是共享的,在加载类得对象的时候,多个线程加载对象,只有一个对象可以被加载,其他的线程都必须等待。

如果加载了过多的第三方jar包,这个时候就可能出现oom;Tomcat部署的应用程序太多;大量动态地生成反射类

Hotspot中方法区的演进

注意,方法区说成永久代,主要是对于hotspot而言的。

永久代仍然使用的是java虚拟机的内存,更容易oom。

生产JRockit的BEA公司被Oracle公司收购,JRockit是世界上公认的最快的虚拟机。

注意:方法区无法满足新的内存分配需求时,将会抛出OOM异常,即使它使用的是本地内存。

设置方法区的大小

如果想要设置参数:

如果在jdk8当中设置jdk7中设置的PermSize会报错。

如果解决oom的问题

内存泄露:堆中有对象,栈中存在着引用指向他,但是这个数据我们也没有使用,因为它始终和GC Roots有过关联,所以会导致这个对象始终不会被回收,这样的对象多了之后,就造成了OOM。

方法区的内部结构

字节码文件是通过类的加载器加载进来的,所以方法区中还存放了这个字节码文件是由那个类加载器加载进来的,这个classLoader也会记载他加载过谁(彼此互相记录)。

class文件中常量池的理解

这还仅仅只是一个方法中的符号引用,如果不把他们管理起来的话,每个方法中都有这个结构的话,字节码文件就会非常的大了。我们使用的方法的相关细节都是从常量池中调用的

运行时常量池

字节码中的常量池通过类的加载器放在内存中以后,对应的这个结构称为运行时常量池,运行时常量池就是字节码文件当中每一个类或者接口对应的常量池表对应的一个运行时候的表示形式。并且,运行时常量池具有动态性。

这里所说的动态性的一个具体例子就是String.intern()方法。

图示方法区的使用

针对上图执行过程图示如下

方法区的演进细节

方法区存储的经典结构如上所示,但是jdk在不同版本的演进当中,方法区的实现也发生了一些变化。

注意的是:jdk8之后,网上很多帖子或者书中,说的是静态变量放到了元空间中,实际上仍然在堆中。

永久代为什么要被元空间替换呢?

相当于把jRockit和J9虚拟机结合hotspot虚拟机了,JRockit认为方法区中的类信息和方法信息等,生命周期比较长,所以把他放在虚拟机内存之外,而hotspot虚拟机则是所有的内存都有我自己来分配(完全受虚拟机控制)

为什么JRockit没有永久代?

判断常量或者类是否需要再被使用其实也挺花时间的。

StringTable为什么要调整位置

如何证明静态变量的位置?

使用JHSDB(JDK9引入的工具):监控进程的细节

上图证明的是,三个对象的实体都是放在堆空间当中的(只要是new的对象都放在堆空间当中)

方法区的垃圾回收行为

如果不进行gc的话,就真的成了永久代了(永久也仅限于当前的进程存在的生命周期之内),方法区的垃圾回收也主要是针对于针对运行时常量池和类型信息

类的卸载实际上是很苛刻的。

总结:运行时数据区