JVM你了解?

1.谈谈你对JAVA的理解

  • 平台无关性(一次编译,到处运行)
  • GC(不必手动释放堆内存)
  • 语言特性(泛型、lambda)
  • 面向对象(继承,封装,多态)
  • 类库
  • 异常处理

2.平台无关性怎么实现

0
Java源码首先被编译成字节码,再由不同平台的JVM解析,Java语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。

补充:为什么JVM不直接将源码解析成机器码去执行?

如果直接解析成机器码去执行,那么每次执行,都需要重新做语法分析等操作。
  • 准备工作:每次执行都需要各种检查
  • 兼容性:也可以将别的语言解析成字节码

3.Java虚拟机

一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

0
JVM主要由Class Loader、Runtime Data Area、Execution Engine以及Native Interface这四个部分组成。
它主要通过Class Loader将指定格式的class文件加载到内存,并通过Execution Engine去解析class文件里的字节码并提交给操作系统去执行。
Class Loader:依据特定格式,加载class文件到内存
Execution Engine:对命令进行解析
Native Interface:融合不同开发语言的原生库为Java所用
Runtime Data Area:JVM内存空间结构模型

4.反射

JAVA反射机制是在运行状态中,
对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性;
这种动态获取信息以及动态调用对象的方法的功能称为java语言的反射机制。

补充:

public Method[] getDeclareMethods():返回Class对象表示的类或接口的所有的成员方法对象数组,包括pubic,protected,default和private的方法对象,不包括从父类继承的方法
public Methods[] getMethods(): 返回Class对象表示的类或接口的所有公有成员方法对象数组,包括已声明的、从父类继承,实现接口的方法
反射的例子:
package com.interview.javabasic.reflect;

public class Robot {
    private String name;
    public void sayHi(String helloSentence){
        System.out.println(helloSentence + " " + name);
    }
    private String throwHello(String tag){
        return "Hello " + tag;
    }
}

  

package com.interview.javabasic.reflect;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectSample {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException {
        Class rc = Class.forName("com.interview.javabasic.reflect.Robot");
        
        Robot r = (Robot) rc.newInstance();
        System.out.println("Class name is " + rc.getName());
        
        Method getHello = rc.getDeclaredMethod("throwHello", String.class);
        getHello.setAccessible(true); // 调用私有方法需要加,不然会报错Exception in thread "main" java.lang.IllegalAccessException
        Object str = getHello.invoke(r, "Bob");
        System.out.println("getHello result is " + str);
        
        Method sayHi = rc.getMethod("sayHi", String.class);
        sayHi.invoke(r, "Welcome");
        
        Field name = rc.getDeclaredField("name");
        name.setAccessible(true);
        name.set(r, "Alice");
        sayHi.invoke(r, "Welcome");
    }
}

5.谈谈classloader

(1)类从编译到执行的过程

  • 编译器将Forlan.java 源文件编译为Forlan.class 字节码文件
  • ClassLoader将字节码转换为JVM中的 Class<Forlan>对象
  • JVM利用Class<Forlan>对象实例化为Forlan对象

  ClassLoader在java中有着非常重要的作用,Java的核心组件中所有class都是由ClassLoader进行加载的.它主要工作在class装载的加载阶段,其主要作用是从外部系统获得class二进制数据流,通过将class文件里的二进制数据流装进系统,然后交给java虚拟机进行连接,初始化等操作。
种类

  • BootStrapClassLoader:C++ 编写,加载核心库java.*
  • ExtClassLoader:Java编写,加载扩展库javax.*(不是一次性加载,用到才加载)
  • AppClassLoader:Java编写,加载程序所在目录(class.path)
  • 自定义ClassLoader:Java编写定制加载

(2)自定义ClassLoader的实现

关键函数
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
    protected final Class<?> defineClass(byte[] b, int off, int len) throws ClassFormatError{
        return defineClass(null, b, off, len, null);
    }

具体实现

    public class MyClassLoader extends ClassLoader{
    
        /** 路径 */
        private String path;
    
        /** 名称 */
        private String classLoaderName;
    
        /** 全参构造函数 */
        public MyClassLoader(String path, String classLoaderName) {
            this.path = path;
            this.classLoaderName = classLoaderName;
        }
    
        @Override
        public Class findClass(String name){
            byte[] b = loadClassData(name);

            return defineClass(name,b,0,b.length);
        }

        /** 用于加载类文件 */
        private byte[] loadClassData(String name) {
            /** 全路径1 */
            name = path + name + ".class";
            /** jdk1.8新特性,不用手动关闭 */
            try(
                InputStream in = new FileInputStream(new File(name));
                ByteArrayOutputStream out = new ByteArrayOutputStream();) 
                {

                int i = 0;
                while ((i = in.read()) != -1){
    
                    out.write(i);
                }

                return out.toByteArray();
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        
        }
    }
    //检测类
    public class ClassLoaderChecker{
		public static void main(String[] args){
		MyClassLoader m=new MyClassLoader(path:"Users/baidu/Desktop/",classLoaderName:"myClassLoader");
		Class c=m.loadClass(name:"Wali");
		System.out.println(c.getClassLoader());//结果是MyClassLoader
		System.out.println(c.getClassLoader().getParent());//结果是AppClassLoader
		System.out.println(c.getClassLoader());//结果是ExtClassLoader
		System.out.println(c.getClassLoader());//结果是null
		c.newInstance();
	}

  

 (3)双亲委派机制

0

当类加载器收到类加载请求时,会去判断是否加载过,如果加载过,直接返回,否则,就委派给它上级,层层这样去判断,到达最底层后,还是没加载过,就去判断是否可以被加载到,可以就返回,否则,就层层往下判断能否加载到,如果最终还是加载不到就抛出异常。

作用

避免多份同样字节码加载,加载过了就不会再加载,重新加载它一定是不同的。

(4)类的加载方式

隐式加载:new
显示加载:Classloder.loadClass,Class.forName等获取Class对象,再通过newInstance方法获取对象

(5)类的装载过程

主要有三个步骤:装载(Load),链接(Link)和初始化(Initialize)。
加载:通过ClassLoader加载class文件字节码,生成Class对象
链接:

  • 校验:检查加载的class的正确性和安全性
  • 准备:为类变量分配存储空间并设置类变量初始值
  • 解析:JVM将常量池内的符号引用转换为直接引用

初始化:执行类变量赋值和静态代码块

(6)loadClass和forname区别

forname得到的class已经完成初始化
loadclass只是加载,并没有链接和初始化

  • JDBC中加载数据库驱动用到Class.forName(“com.mysql.jdbc.Driver”),Driver中有静态代码块,所以需要用到forName()
  • Spring IOC资源加载器获取资源(即读取配置文件时),用到class.getClassLoader(),是为了加快初始化速度,延迟加载

(7)类什么时候才被初始化

  • new对象
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射——Class.forName方法
  • 初始化子类,会首先初始化父类)
  • JVM启动时标明的启动类,即文件名和类名相同的那个类

6.Java内存模型

(1)内存相关(了解)

简介:

计算机所有程序都是在内存中运行的,只不过这个内存可能包括虚拟内存,同时也离不开硬盘这样的外存知识;在程序执行的过程中,需要不断地将内存的逻辑地址和物理地址映射起来,找到相关的指令以及数据去执行;作为操作系统进程,java运行时面临着和其他进程相同的内存限制,即受限于操作系统架构提供的可寻址空间。
可寻址空间由处理器的位数决定:

  • 32位处理器:2^32的可寻址范围(4GB)
  • 64位处理器:2^64的可寻址范围
0

(2)地址空间的划分:

内核空间:是主要的操作系统程序和C运行时的空间,包含用于连接计算机硬件、调度程序、以及提供联网和虚拟内存服务的逻辑,和基于C的进程;

用户空间:java进程实际运行时使用的内存空间(32位系统用户进程最多可以访问3GB,内核代码可以访问所有物理内存;而64位系统用户进程最多可以访问512GB,内核代码也可以访问所有物理内存)

 

0

(3)JVM内存模型——JDK8

线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享:元空间(MetaSpace)、Java堆
0

1)程序计数器(逻辑计数器,而非物理计数器)

  • 当前线程所执行的字节码行号指示器(逻辑)
  • 改变计数器的值来选取下一条需要执行的字节码指令
  • 和线程是一对一的关系即“线程私有”
  • 对Java方法计数,如果是Native方法则计数器值为Undefined
  • 不会发生内存泄露

2)java虚拟机栈(stack)

  • Java方法执行的内存模型
  • 包含多个栈帧(栈帧包含:局部变量表、操作栈、动态连接、返回地址)
0

3)局部变量表和操作数栈

  • 局部变量表:包含方法执行过程中的所有变量
  • 操作数栈:入栈、出栈、复制、交换、产生消费变量
执行add(1,2)的过程
public static int add(int a, int b){
    int c = 0;
    c = a + b;
    return c;
}
0
递归为什么会引发java.lang.StackOverflowError异常
①原因:递归过深,栈帧数超出虚拟栈深度

  当线程执行一个方法时,就会随之创建一个栈帧,并将栈帧压入虚拟机栈,当方法执行完后,便会将栈帧出栈,因此可知,线程当前执行的方法所对应的栈帧位于栈的顶部,而我们的递归函数不断去调用自身,
每一次方法调用会涉及以下操作:

第一:每新调用一个方法,就会生成一个栈帧;

第二:它会保存当前方法栈帧的状态,将它放入虚拟机栈中;

第三:栈帧上下文切换的时候,会切换到最新的方法栈帧当中,而由于我们虚拟机栈深度是固定的,递归实现将导致栈的深度增加;如果栈帧数超过了最大深度,就会抛出java.lang.StackOverflowError异常。

②解决方法
  • 循环方法替代
  • 限制递归次数

3)本地方法栈

  • 与虚拟机栈相似,主要作用于标注了native的方法

4)元空间和永久代

①区别
元空间使用本地内存,永久代使用的是JVM内存
②MetaSpace相比PermGen的优势
jdk1.8之后,jvm移除了永久代,使用元空间。
  • 字符串常量池存在永久代中,容易出现性能问题和内存溢出
  • 类和方法的信息大小难以确定,给永久代的大小指定带来困难
  • 永久代会为GC带来不必要的复杂性
  • 方便HotSpot与其他JVM如Jrockit的集成

5)Java堆

  • 对象实例的分配区域
  • GC管理的主要区域
0
①JVM三大性能调优参数-Xms -Xmx -Xss的含义
  • -Xms:堆的初始值(该进程刚创建出来的时候,它的专属Java堆的大小。一旦对象容量超过Java堆的初始容量,Java堆将会自动扩容,最大扩容大小的-Xmx)
  • -Xmx:堆能达到的最大值(在很多情况下,-Xms和-Xmx设置成一样的。这么设置,是因为当Heap不够用时,会发生内存抖动,影响程序运行稳定性)
  • -Xss:规定了每个线程虚拟机栈(堆栈)的大小(一般256k足够,此配置会影响此进程中并发线程数的大小)
②内存分配策略
程序运行时,有三种内存分配策略:静态的、栈式的、堆式的。
  • 静态存储:编译时确定每个数据目标在运行时的存储空间需求(因而在程序编译时就可以给它们分配固定的内存空间。这种分配策略要求程序代码中不允许有可变数据结构的存在,也不允许有嵌套或者递归的结构出现,因为他们都会导致编译程序无法计算准确的存储空间)
  • 栈式存储:数据区需求在编译时未知,运行时模块入口前确定(动态的存储分配,由一个堆栈的运行栈实现的。规定在进入一个程序模块的时候,必须知道这个程序模块所需要的内存大小。按照先进后出的原则进行分配)
  • 堆式存储:编译时或运行时模块入口都无法确定,动态分配(比如可变长度串以及对象实例,堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放)
③联系:创建的数据和对象实例都保存在堆中,想要引用对象、数组时,可以在栈里定义变量保存堆中目标的首地址
0
④堆和栈的区别:
  • 管理方式:栈自动释放,堆需要GC(JVM可以针对内存栈进行管理操作,而且该内存的释放是编译器就可以操作的内容)
  • 空间大小:栈比堆小(由本身存储的数据特性决定)
  • 碎片相关:栈产生的碎片远小于堆(对于堆空间而言,即使垃圾回收器可以进行自动堆内存回收,但是堆空间的活动量相对于栈空间而言比较大,很有可能存在长时间的堆空间分配和释放操作。而且垃圾回收器不是实时的,它有可能使得堆空间的内存碎片,逐渐累积起来。针对栈空间而言,因为本身就是一个堆栈的数据结构,操作都是一一对应的,而且每一个最小单位的结构栈帧和堆空间中复杂的内存结构不一样,所以在使用过程很少出现内存碎片)
  • 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
  • 效率:栈的效率比堆高(因为内存块本身的排列就是一个典型的堆栈结构,所以栈空间的效率自然比起堆空间要高很多,而且计算机底层内存空间本身就使用了最基础的堆栈结构使得栈空间和底层结构更加符合,它的操作也变得简单,就是最简单的两个指令:入栈和出栈;栈空间相对堆空间而言的弱点是灵活程度不够,特别是在动态管理的时候。而堆空间最大的优势在于动态分配,因为它在计算机底层实现可能是一个双向链表结构,所以它在管理的时候操作比栈空间复杂很多,自然它的灵活度就高了,但是这样的设计也使得堆空间的效率不如栈空间,而且低很多)

7.元空间、堆、栈独占部份间的联系——内存角度

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("forlan");
        hw.sayHello();
    }
}
综上述代码分析:各部分的存储情况

元空间

Class:HelloWorld - Method:sayHello\setName\main - Field:name
Class:System 类对象、成员变量和方法

Java堆

Object:String(“forlan”) 实例
Object:HelloWorld

线程独占

Parameter reference:“forlan” to String object 地址引用
Variable reference:“hw” to HelloWorld object 地址引用
Local Variables:a with 1 值,lineNo 行号

8.不同JDK版本之间的intern()方法的区别——JDK6 VS JDK6+

String s = new Stirng("a");

s.intern();

JDK6:当调用 intern 方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串的引用。

JDK6+:当调用 intern 方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。

注:在JDK1.6的时候,字符串常量池是存放在Perm Space中的(Perm Space和堆是相隔而开的),在1.6+的时候,移到了堆内存中。

public static void main(String[] args) {
   String s1 = new String("a");
   s1.intern();
   String s2 = "a";
   System.out.println(s1 == s2); //Jdk6:false Jdk6+:false 

   String s3 = new String("a") + new String("a");
   s3.intern(); // 不加都是false
   String s4 = "aa";
   System.out.println(s3 == s4); //Jdk6:false Jdk6+:true 
}
Jdk6
s创建的时候,首先在常量池中创建“a”,然后在heap中创建了string对象。当使用intern时,企图将string对象放到常量池中,但是常量池已经有a了,所以放置失败。定义s2时,直接使用了常量池中的a,所以两个地址不一样。 创建s3时,在heap中创建string对象,使用intern函数,将对象的副本放置到常量池中,s4引用常量池中的aa,但是由于是string对象的副本,所以两个的地址不同
Jdk6+
s创建的时候,首先在常量池中创建“a”,然后在heap中创建了string对象。当使用intern时,企图将string对象放到常量池中,但是常量池已经有a了,所以放置失败。定义s2时,直接使用了常量池中的a,所以两个地址不一样。
创建s3时,在heap中创建string对象,使用intern函数,将对象的引用放置到常量池中,s4引用常量池中的aa,由于是string对象的引用,所以两个的地址相同
posted @ 2021-02-07 23:19  程序员Forlan  阅读(121)  评论(0编辑  收藏  举报