JVM原理分析及面试题

讲解概况:

 

JVM:将java字节码翻译成其他操作系统可以识别的语言(将class翻译成机器码(110101010))。

跨平台、跨语言、

java程序通过javac编译成字节码。java字节码通过JVM翻译,成为了其他操作系统可以识别的语言。

在下载的jdk的bin目录下,有很多exe后缀的文件,这些文件就是java运行时需要用到的一些工具,比如javac.exe就是用来将java程序转成Java字节码的工具,javadoc.exe为文档工具,javap.ext反编译工具等等。

JDK是个大合集,本质上就是提供很多工具,很多工作都是依靠于JDK,比如反编译、javac等

JRE是java的运行环境

JVM仅仅是在javac和操作系统之间做一个翻译的事情。

如上图:class文件通过类加载器ClassLoader加载到JVM内存(运行时数据区),然后class文件中的方法、变量等操作通过执行引擎,去解释执行或者JIT的执行给操作系统。

解释执行:即在class文件中的内容进行执行时,每执行一行,JVM就翻译一行为机器码,此行为叫做解释执行。

JIT:解释执行效率低,为了提高效率,将一块热点数据通过JIT直接在本地执行

白色区域为线程私有(虚拟机栈、本地方法栈、程序计数器)

偏移量的多少决定code的数值,一般来说,偏移量都是1,但是有时偏移量比较大,比如从7变成9,就是偏移量为2的原因。

面试题:

1、为什么需要程序计数器?

由于时间片轮转机制(操作系统层)的存在,可能JVM执行此线程一部分时被时间片轮转机制给切走了,这时就需要程序计数器记录此时运行的字节码的位置,等到切到此线程执行时,根据程序计数器记录的地址位置,继续执行。

2、JVM内存区域中,程序计数器是唯一块不会发车OOM的内存,为什么?

程序计数器只占据非常小的一块内存,因为记录的是数值的叠加,只需要一个int类型的内存大小。JVM肯定能够分配一个int类型大小的内存给程序计数器。

栈:先进后出(后进先出)(比如成手枪安装子弹,最先打出的是安装的最后一个子弹)

虚拟机栈(JVM内存栈):在大部分版本中的限制是1M(子弹不能无限多),线程私有,每个线程都会配有一个虚拟机栈。(平常大家说的栈其实就是虚拟机栈)

操作数栈:用来存放、操作数据的

java的解释执行是基于栈的(操作数栈),c是寄存器运算。两者的优缺点?

1、寄存器是基于硬件的,所以c运行会快一些

2、c移植性差(make install)

3、栈兼容性好,效率低

动态连接:多态的静态分派(编译期确定)和动态分派(运行期确定):动态分派时在编译期无法确定其真实对象,在栈帧的存储区域中有一个动态连接,通过它来确定你应该调用哪一个对象。

完成出口(又叫:返回地址):把运行过程中的符合引用变成具体引用。

Java 虚拟机规范:

本地方法栈:

堆:对象、数组,可以频繁的回收

方法区懒回收:(由于方法区的数据都是偏向于静态的,比较难回收)

方法区:运行时数据区的划分。

方法区在不同的版本中的实现是不一样的,叫法也不一样,在JDK1.7以前,方法区的实现叫做永久代

在1.8以后叫做元空间。

永久代受制于堆的大小,回收比较困难,

元空间可以使用堆外空间,

元空间的好处:可以使用机器内存,不受大小限制,数据可以不断的添加,方便扩展

坏处:由于不受限制,可能会挤压堆空间

举例:

假如机器大小只有20G,设置堆内存最大上限为10G,如果此时元空间的堆外内存使用了17G,那么堆内存只剩下3G了。

NDK用的就是本地内存

主动调用垃圾回收:System.gc();

举例:看如下代码:

首先设置初始化的值:(方便看运行时数据区的数据)

-Xms堆的初始值30M,-Xmx堆的最大值30M,-XX:UseConcMarkSweepGc设置一种垃圾回收器为使用很多种回收器去回收垃圾。-XX:-UseCompressedOops设置内存不压缩(JVM有自动压缩内存的功能)

-Xss 设置虚拟机栈内存大小为1M(默认也是1M)

将初始的信息设置进运行代码中:

运行代码:

1、向操作系统申请上述设置的内存(堆、栈、方法区等),

2、将编译期编译的class文件通过Java加载器ClassLoader加载到方法区(上述的JVMObject.class、Teacher.class)。

3、将常量、静态变量也加载进方法区(MAN_TYPE、WOMAN_TYPE)

4、开始运行时首先运行的是main方法(解释执行,首先Main),为其设置main的虚拟机栈,并压入main方法的栈帧。

5、栈帧的执行(解释执行,一步一步的转化为机器码执行):main方法中的Teacher t1=new Teacher();首先将对象Teacher放入堆中,然后将引用t1放入栈中。并且不断的执行下面的方法,一直入栈出栈的操作。

HSDB工具:

打开方式:此目录下,找到sa-jdi.jar,然后选中->右键->打开命令行->

在命令行中输入:java -cp .\sa-jdi.jar sun.jvm.hostspot.HSDB

回车后出现如下HSDB界面:

作用:用可视化工具监控JVM运行情况、找到我们的对象。

之后再命令行中输入jps,去查看所有进程,然后找到我们需要的进程,输入其进程号:

之后在HSDB界面上打开如下:

然后输入进程号进行绑定。

选中main,点击鼠标位置,打开main方法的虚拟机栈的详情。(虚拟栈帧就是多物理内存虚拟化,如左边的数字码就是物理地址)

1代表的栈针中真实的内存地址,2的蓝色区域就是分配给main方法的栈针内存地址。

如上图:sleep方法输入本地方法(native),在上面的紫色区域就是Thread.sleep方法,所以很好的验证了下面这句话:

方法区的查看方式:

比如JVMObject.class的查看:

然后输入全类名:

然后点进去Teacher,

堆的查看方式:

Gen 0为新生代、Gen 1为老年代

经历过多次垃圾回收,而没有被回收掉的对象就会进入老年代。

栈溢出:报错:StackOverFlowError 和OOM(OOM很难演示)

如上图:king()方法的执行,一直分配栈帧,每个栈帧是占内存的,所以虚拟机栈1M的栈内存很容易被用完。

栈OOM:

假设机器内存500M,为每个虚拟机栈分配1M的空间,如果此时同时开启1000个线程,那么就会创建1000个虚拟机栈,那么机器内存肯定不够,就会发生栈溢出。此种情况会造成死机现象,所以很难模拟。

堆溢出:

数组是存储在堆内存中的,如上图,设置的最大堆内存才30M,数据初始化内存是35M,所以很容易溢出。

一直创建对象,直到堆溢出。

方法区内存溢出:

cglib动态生成(导入的第三方jar包),会不断的编译代码,而编译代码的过程是发生在方法区的。

本机直接内存(堆外)溢出

Android基本上都是堆内存溢出。

1、编译期优化技术:方法内联:减少进栈和出栈的的次数,直接进行代码执行。如下图main方法中调用max(1,2);正常流程需要设置max栈帧,然后将1,2压入操作数栈,然后将1,2存入局部变量表,然后再取出1,2进行计算,将计算结果压入操作数栈中。最后再通过return返回地址进行出栈。

通过方法内联优化后:在编译期确定max方法就是一个固定的表达式,优化后相当于在main方法中直接进行计算,少一步入栈出栈的操作。

2、栈的优化技术:栈帧之间数据共享:

加入A方法和B方法发生了数据传递,此时操作B方法,A方法的数据在局部变量表,B的数据在操作数栈。此时这些数据就可以共享,只是在A、B栈帧中的叫法不同。(一个女的既可以是妻子也可以是女儿)

如下例子:此时共享数据为10。

通过HSDB工具可以看出,黑色的区域是work()方法的局部变量表(Locals area)、蓝色区域为main的操作数栈。此时两者共享0096(为10)

扩展:

一个方法就是一个栈帧。

普通成员变量属于对象的内容,所以放在堆中。

类的回收是件非常难的问题,首先要满足上述的三个条件,然后JVM的参数-Xnoclassgc还不能禁用类的垃圾回收。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

HMP*

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

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

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

打赏作者

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

抵扣说明:

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

余额充值