Java JVM【笔记】
Java的平台无关性是如何实现的?
Java源码首先被编译成字节码,再由不同的平台的JVM进行解析,Java语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令
为什JVM不直接将源码解析成机器码去执行?
因为如果JVM将源码直接解析成机器码的话,那么每次执行的时候都需要进行像是语法语义之类的各种检查,也就是说,每次分析的时候这些结果都不会被保留,都要重新去编译,重新去分析,这样一直进行重复的事情,整体的性能就会被影响,因此引用了中间字节码,这样就可以保证能够在被编译成字节码以后,多次执行程序不需要进行多次的检验和补全
同时还可以脱离Java的束缚,像是别的语言解析成字节码以后,同样也可以被JVM执行,这样就可以增加平台的兼容扩展能力
查看字节码的方法有命令行输入以及通过编译器(idea这种)
JVM如何加载.class文件?
首先要明白Java虚拟机是什么,虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行
需要注意的是,JVM是一个内存中的虚拟机,也就意味着JVM的存储就是内存,我们所写的各种类,常量,方法等等都在内存中,这就决定了程序的运行时是否健壮,是否高效
JVM的架构
大致分为四个部分:class loader,runtime data area,execution engine以及native interface
class loader:类加载器,依据特定格式,加载class文件到内存,需要注意的是,不能自己随意创建一个class文件,需要有格式的要求才能执行
execution engine:解析器,其作用就是对命令进行解析
native interface:本地接口,其作用就是融合不同开发语言的原生库为Java所用
runtime data area:JVM内存空间结构模型,其中含有的部分为,方法区(Method Area),虚拟机栈(VM Stack),本地方法栈(Native method stack),堆(Heap),程序计数器(Program Counter Register)
什么是反射?
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意方法和属性,这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制
之所以能够获取到类的属性或者是方法,并对其进行调用,是必须要获取class对象的,而想要获取到该类的class对象,就必须先要获取到该类的字节码文件对象
那么对于类从编译到执行的过程(以robot.java为例子)
首先编译器会将robot.java源文件编译为robot.class字节码文件,然后呢,class loader类加载器就会将字节码转换成JVM中的class
谈谈什么是classloader?
classloader是很重要的,在Java中有着非常重要的作用,它主要工作在class装载的加载阶段,其主要作用是从系统外部获得class二进制数据流,它是java的核心组件,所有的class都是由classloader进行加载的,classloader负责通过将class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接,初始化等操作
classloader的种类?
其主要有四类:
bootstrapclassloader:其是由c++编写,其作用主要是加载核心库java.*,是用户不可见的
extclassloader:其是由Java编写,其作用主要是加载扩展库javax.*,是用户可见的
appclassloader:其是由Java编写,其作用主要是加载程序所在目录,是用户可见的
自定义classloader:其是由Java编写,可以定制化加载,用很多的用途
想要实现自定义classloader,还需要做些事情,并不是想干就能干的,需要覆盖两个关键函数,才可以去定义classloader
关键函数一:findclass函数
关键函数二:defineclass函数
谈谈类加载器的双亲委派机制?
自底向上检查类是不是已经加载,然后就自顶向下的尝试加载类
为什么要使用双亲委派机制去加载类?
就是为了避免多份同样的字节码的加载
类的加载方式?
隐式加载:new
显式加载:loadclass,forname等
对于显式加载来说,当获取到class对象以后,需要调用class对象的newInstance方法来生成对象的实例
通过new来隐式加载,就不需要newInstance的方法就可以获取对象的实例,并且new支持使用带参数的构造器来生成对象实例,而class对象的newinstance方法呢,则不支持传入参数,需要通过反射来调用构造器对象的
类的装载过程(class对象的生成过程)
首先是加载,其通过classloader加载class文件字节码,生成class对象
然后是链接,分为三步,第一步是校验,检查加载的class的正确性和安全性,第二步是准备,为类变量分配存储空间并设置类变量初始值,第三步就是解析,JVM将常量池内的符号引用转换为直接引用
最后是初始化,其就是执行类变量赋值和静态代码块
Java的内存模型?
内存简介
在程序执行的过程中,要不断地将内存的逻辑地址和物理地址进行映射,从而找到相关的指令和数据去执行,作为操作系统进程,Java在运行的时候会有一些限制,即受限于操作系统架构提供的可寻址地址空间,操作系统架构提供的可寻址地址空间由处理器的位数决定
其中32位处理器提供了232的可寻址范围,64位处理器提供了264的可寻址范围
地址空间的划分
地址空间被划分为内核空间和用户空间
内核空间是主要的操作系统程序和c运行时的空间,包含用于连接计算机硬件,调度程序以及提供联网和虚拟内存等服务的逻辑和基于c的进程
用户空间,其是Java运行的时候的实际的运行空间,32位系统的用户,进程最大可以访问3gb,内核代码可以访问所有的物理内存,而64位系统的用户,进程最大可以访问超过512gb,同样的,内核代码也可以访问所有的物理内存
Java的内存模型指的就是JVM架构图中的runtime data area部分
以JDK8为例,从线程的角度去看,其中程序计数器,虚拟机栈以及本地方法栈都是线程私有的,方法区以及堆是所有线程共享的
对每个部分进行解释:
方法区(Method Area)
方法区主要是用来存储 JVM 加载的类信息,其中就包括类的方法(如类的接口以及父类等)、常量、静态变量、即时编译器编译后的代码等数据,其还包括运行时常量池,用于存放静态编译产生的字面量和符号引用
其很少发生 GC(Garbage Collection,垃圾回收),偶尔发生的 GC 主要是对常量池回收和类型的卸载
其是线程共享的
一些问题:元空间(metaspace)与永久代(permgen)的区别
这两个都是用来存储class的相关信息
元空间使用本地内存,而永久代使用的是JVM的内存
元空间比起永久代的优势:
第一,字符串常量池存在永久代中,容易出现性能问题的内存溢出
第二,类和方法的信息大小不好确定,给永久代的大小指定带来一些困难
第三,永久代会为GC带来不必要的复杂性
第四,方便Hotspot与其他的JVM比如JRockit的集成
虚拟机栈(VM Stack)
Java虚拟机栈是线程私有的,可以说是Java方法执行的内存模型
每个方法在被执行的时候都会创建一个栈帧,即方法运行期间的基础结构,栈帧用于存储局部变量表,操作栈,动态链接,返回地址等每个方法执行中对应的虚拟机栈帧从入栈到出栈的过程
Java虚拟机栈用来存储栈帧,而栈帧持有局部变量和部分结果以及参与方法结果的调用语法,当方法调用结束以后,帧才会被销毁,这里就显示出了关键的信息:虚拟机栈包含了单个线程每个方法执行的栈帧,而栈帧则存储了局部变量表,关键数栈,动态链接和方法出口等信息
其中局部变量表包含了方法执行过程中的所有变量
操作数栈在执行字节码指令过程中被用到,JVM会把大部分的时间都花在上面,包括入栈,出栈,复制,交换,产生消费变量的操作
其中的关于异常的问题
递归为什么会引发java.lang.StackOverflow异常?
其实很好想,就是因为递归过深,栈帧数超出了虚拟栈深度,解决方法就是限制递归的次数或者是使用循环的方法区替换递归
此外,虚拟机栈过多还会引发java.lang.OutOfMemoryerror异常
本地方法栈(Native method stack)
和虚拟机栈类似,主要作用于标注了native的方法
堆(Heap)
对于大多数的应用来说,堆是Java虚拟机管理的最大的一块内存,Java堆是被所有的线程共享的一块区域,在虚拟机启动时创建,其唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存
Java堆是垃圾收集器管理的主要区域,因此很多时候又被称为GC堆,Java堆可以细分为新生代和老年代,在细一些还能分为四部分,不再说明
程序计数器(Program Counter Register)
其可以看作是当前线程所执行的字节码行号指示器(逻辑上),也就是说,程序计数器是逻辑计数器,而不是物理计数器
其工作时就是改变计数器的值来选取下一条需要执行的字节码指令,包括跳转等基础功能
由于JVM的多线程是通过线程的轮流切换并分配处理器执行时间的方式来实现的,在任何一个确认的时刻,一个处理器只会执行一条线程中的指令,因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程的计数器之间互不影响,独立存储,我们就称这种为线程私有的内存
如果线程正在执行一个Java方法,这个计数器是记录正在执行的虚拟机字节码指令的地址,如果正在执行的native方法,那么这个计数器值就是undefined
此外,由于只是记录行号,程序计数器不会发生内存泄露的问题
一些面试可能的问题:
JVM三大性能调优参数-Xms -Xmx -Xss的含义?
-Xms :规定了每个线程虚拟机栈(堆栈)的大小
-Xmx :堆的初始值
-Xss :堆能达到的最大值
Java内存模型中堆和栈的区别?
可以从内存分配策略说明
静态存储:编译时确定每个数据目标在运行时的存储空间需求
栈式存储:数据区需求在编译时未知,运行时模块入口前确定
堆式存储:编译时或运行时模块入口都无法确定,动态分配
然后再从堆和栈的联系来说明
创建好的数组和对象实例都会被保存在堆中,那么想要在引用堆中的对象和数组,那么可以在栈中定义一个特殊的变量,让这个变量的取值等于这个数组或是对象在堆内存中的首地址,那么栈中的这个变量就成了数组或是对象的引用变量
然后结合以后就可以得出在Java内存模型中堆和栈的区别
第一,管理方式:栈是自动释放,堆需要GC
第二,空间大小:栈相对来说比堆小
第三,碎片相关:栈产生的碎片远远小于堆
第四,分配方式:栈支持静态和动态分配,而堆仅支持动态分配
第五,效率:栈的效率比堆高
元空间,堆,线程独占部分间的联系?
以这段代码为例
package com.interview.javabasic. jvm.model;
public class Helloworld {
private String name;
public void sayHello( ){
system.out.println("Hello " +name) ;
}
public void setName(String name) {
this.name = name;
}
public static void main(String [] args) {
int a = 1;
Helloworld hw = new Helloworld( );
hw. setName( "test" );
hw.sayHello( ) ;
}
}
元空间,Java堆以及线程独占的情况如下:
不同JDK版本之间的intern()方法的区别(JDK6 VS JDK6+)?
在JDK6中调用intern方法时,如果字符串常量池先前已经创建出该字符串对象,则返回池中的该字符串的引用,否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用
在JDK6以后的版本,当调用intern方法的时候,如果字符串常量池先前已经创建出该字符串对象,则返回池中的该字符串的引用,否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用,如果堆中不存在,则在池中创建该字符串并返回其引用