• 【从面试出发学习java】- Java - JVM


    平台无关性如何实现

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

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

    如果每次将源码解析成机器码去执行,那么每次执行都需要各种语法,句法,语义的检查。每次都要重新分析,做重复的事情。
    还可以把别的语言解析成字节码,摆脱Java的束缚。

    JVM如何加载class文件

    JVM基本概念
    JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。 JVM 是运行在操作系统之上的,它与硬件没有直接
    的交互。

    JVM运行过程
    Java源文件,通过编译器,能够生产相应的.class文件,也就是字节码文件而字节码文件又通过Java虚拟机中的解释器,编译成特定机器上的机器码
    也就是如下:
    Java源文件 -> 编译器 -> 字节码文件
    字节码文件 -> JVM -> 机器码

    每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够
    跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会
    存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不
    能共享。
    在这里插入图片描述
    类加载器:依据特定格式,加载class文件到内存
    执行引擎:对命令进行解析
    本地库接口:融合不同开发语言的原生库为java所用
    运行时数据区:JVM内存空间结构模型

    谈谈反射

    在这里插入图片描述

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

    关于反射的例子

    public class ReflectExample{
    	public static void main(String[] args) throws ClassNotFoundException, IllegalArgument{
    		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);
    		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");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    类从编译到执行的过程

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

    或者

    java从源码到执行的过程,从JVM的角度看可以总结为四个步骤:编译-加载-解释-执行

    1. 编译」经过 语法分析、语义分析、注解处理 最后才生成会class文件
    2. 加载」又可以细分步骤为:装载->连接->初始化。装载则把class文件装载至JVM,连接则校验class信息、分配内存空间及赋默认值,初始化则为变量赋值为正确的初始值。连接里又可以细化为:验证、准备、解析
    3. 解释」则是把字节码转换成操作系统可识别的执行指令,在JVM中会有字节码解释器和即时编译器。在解释时会对代码进行分析,查看是否为「热点代码」,如果为「热点代码」则触发JIT编译,下次执行时就无需重复进行解释,提高解释速度
    4. 执行」调用系统的硬件执行最终的程序指令

    谈谈ClassLoader 类加载器

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

    JVM 提
    供了 3 种类加载器:

    1. 启动类加载器(Bootstrap ClassLoader)
      负责加载 JAVA_HOME\lib 目录中的,或通过**-Xbootclasspath** 参数指定路径中的,且被
      虚拟机认可(按文件名识别,如 rt.jar)的类。
    2. 扩展类加载器(Extension ClassLoader)
      负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类
      库。
    3. 应用程序类加载器(Application ClassLoader)
      负责加载用户路径(classpath)上的类库。
      JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader
      实现自定义的类加载器。

    在这里插入图片描述

    自定义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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    自定义ClassLoader的例子

    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[] loadClassDate(String name){
    		name = path + name + ".class";
    		InputStream in = null;
    		ByteArrayOutputStream out = null;
    		try{
    			in = new FileInputStream(new File(name));
    			out = new ByteArrayOutputStream();
    			int i = 0;
    			while((i = in.reader) != -1){
    				out.write(i);
    			}
    		}catch(Exception e){
    			e.printStackTrace()
    		}finally{
    			try{
    				in.close();
    				out.close();
    			}catch(Exception e){
    				e.printStackTrace()
    			}
    		}
    		
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    测试代码

    public class ClassLoaderChecker{
    	public static void main(String[] args) throws ClassNotFoundException, IllegalAcceessException {
    		MyClassLoader m = new MyClassLoader("/User/baidu/Desktop/","myClass");
    		Class c = m.loadClass("Wali");
    		System.out.println(c.getClassLoader());
    		c.newInstance();
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    谈谈类加载器的双亲委派机制

    当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父
    类去完成
    ,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,
    只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的
    Class),子类加载器才会尝试自己去加载

    采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载
    器加载这个类,最终都是委托给顶层的启动类加载器进行加载这样就保证了使用不同的类加载
    器最终得到的都是同样一个 Object 对象
    。(采用双亲委派机制的好处)

    在这里插入图片描述

    类的加载方式

    类加载有三种方式:

    1、命令行启动应用时候由JVM初始化加载 (new)
    2、通过Class.forName()方法动态加载
    3、通过ClassLoader.loadClass()方法动态加载

    Class.forName()和ClassLoader.loadClass()区别?

    Class.forName() 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
    ClassLoader.loadClass() 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
    Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

    你了解Java的内存模型吗

    JVM 内存区域
    在这里插入图片描述
    JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区
    域【JAVA 堆、方法区】、直接内存。
    线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 Hotspot
    VM 内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的
    生/死对应)。
    线程共享区域随虚拟机的启动/关闭而创建/销毁。
    直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提
    供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用
    DirectByteBuffer 对象作为这块内存的引用进行操作
    (详见: Java I/O 扩展), 这样就避免了在 Java
    堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。

    程序计数器(线程私有) Program Counter Register

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

    虚拟机栈(线程私有)
    是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)
    用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成
    的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程

    **栈帧( Frame)**是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接
    (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异
    常)都算作方法结束。
    在这里插入图片描述
    虚拟机栈过多会引发java.lang.OutOfMemoryError异常
    在这里插入图片描述

    本地方法区(线程私有)
    本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为
    Native 方法服务
    , 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个
    C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。

    方法区/永久代(线程共享)
    即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静
    态变量、即时编译器编译后的代码等数据
    . HotSpot VM把GC分代收集扩展至方法区, 即使用Java
    堆的永久代来实现方法区
    , 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存,
    而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收类型
    的卸载,
    因此收益一般很小)。

    运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版
    本、字段、方法、接口等描述等信息外,还有一项信息是常量池
    (Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加
    载后存放到方法区的运行时常量池中。 Java 虚拟机对 Class 文件的每一部分(自然也包括常量
    池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会
    被虚拟机认可、装载和执行。

    递归为什么会引发java.lang.StackOverflowError异常

    例如:斐波那契函数

    int feibo(int x)
    {
        if(x==1||x==2) return 1;
        else return feibo(x-1)+feibo(x-2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    递归到一个很大的数量时,会引发Java.lang.StackOverflowError异常
    递归过深,栈帧数超出虚拟栈深度

    元空间(MetaSpace)与永久代(PermGen)的区别

    元空间使用本地内存,而永久代使用的是jvm的内存
    永久代容易发生溢出
    java.lang.OutOfMemoryError: PermGen space

    元空间相比永久代的优势

    字符串常量池存在永久代中,容易出现性能问题和内存溢出
    类和方法的信息大小难以确定,给永久代的大小执行带来困难
    永久代会为GC带来不必要的复杂性
    方便HotSpot与其他JVM如Jrockit的集成

    JVM三大性能调优-Xms -Xmx -Xss的含义

    -Xss 规定了每个线程虚拟机栈(推栈)的大小
    -Xms 堆的初始值
    -Xmx 堆能达到的最大值

    https://blog.csdn.net/fcclzydouble/article/details/123095908

    Java内存模型中堆和栈的区别 —— 内存分配策略

    静态存储在编译时就能确定每个数据目标,在运行时的存储空间需求,因而在编译时就可以给他们分配固定的内存空间,这种分配策略要求程序代码中不允许有可变数据结构的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算程序的存储空间。

    栈式分配:该分配可称为动态的存储分配,是由一个类似于堆栈的运行栈来实现的,和静态存储的分配方式相反,在栈式存储方案中,程序对数据区的要求在编译时是完全未知的,只有到了程序运行时才能知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需数据区的大小才能分配其内存和我们在数据结构中所熟知的栈一样,栈式存储按照先进后出的原则进行分配。

    堆式存储专门负责在编译时或运行时模块入口处都无法确定的内存分配,比如可变长度串和对象实例堆有大片的可利用空间或空闲块组成,堆中内存可按照任意顺序分配和释放。

    Java内存模型中堆和栈的区别在这里插入图片描述

    主要区别体现在以下五个方面

    管理方式:栈自动释放内存,堆需要GC进行自动回收

    空间大小一般情况下栈空间相对于堆空间较小,这是由栈空间里存储的数据,以及本身需要的数据特性决定的,而堆空间在JVM堆实例进行分配时,一般大小都比较大。因为堆空间在JAVA程序中需要存储较多的JAVA对象数据

    碎片相关栈产生的碎片远小于堆。针对堆空间而言,即使GC能够进行自动堆内存回收,但堆空间活动量相对栈空间而言比较大,很有可能存在长期堆空间分配和释放操作,而且GC不是实时的,它有可能使堆中内存碎片逐渐积累起来,针对栈空间而言,它本身就是堆栈的数据结构,它的操作都是一一对应的,而且每一个最小单位的结构栈帧和堆空间内复杂的内存结构不一样,所以它在使用过程中很少出现内存碎片。

    分配方式一般栈空间有两种分配方式,静态分配和动态分配,静态分配是本身由编译器分配好了,而动态分配可能根据情况有所不同,而堆空间却是完全的动态分配的,是一个运行时级别的内存,而栈空间分配的内存,不需要我们考虑释放的问题,对空间即使在有GC的前提下,还是要考虑其释放问题。

    效率:因为内存本身快的排列就是一个典型的堆栈结构,所以栈比堆效率高很多而且计算机底层基本内存结构使用堆栈结构,使得栈空间和底层结构更加符合,操作也简单,只涉及到了最简单操作,入栈和出栈,栈帧对堆的弱点是灵活程度不够,特别在动态管理时。堆最大优点在于动态分配,因为他在计算机底层实现可能是一个双向链表结构,所以在管理内存时操作比栈空间复杂很多,自然灵活度就高了,但这样的设计使得堆空间不如栈空间,而且要低很多。

    Java 元空间、堆、线程

    元空间
    在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间
    的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用
    本地内存
    。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native
    memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由
    MaxPermSize 控制, 而由系统的实际可用空间来控制


    是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行
    垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以
    细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。

    线程
    这里所说的线程指程序执行过程中的一个线程实体JVM 允许一个应用并发执行多个线程
    Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓
    冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。

    Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可
    用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。当线程结束时,
    会释放原生线程和 Java 线程的所有资源。

    Hotspot JVM 后台运行的系统线程主要有下面几个:
    虚拟机线程(VM thread)
    这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当
    堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-the-world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。

    周期性任务线程
    这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。
    GC 线程
    这些线程支持 JVM 中不同的垃圾回收活动。
    编译器线程
    这些线程在运行时将字节码动态编译成本地平台相关的机器码
    信号分发线程
    这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理

    intern()方法的区别

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

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

    例子
    jdk 1.6

    public class InternDifference{
    	public static void main(String[] args){
    		String s = new String("a"); // java堆中
    		s.intern();
    		String s2 = "a"; //字符串常量池
    		System.out.println(s==s2);
    
    		String s3 = new String("a")+new String("a");
    		s3.intern();
    		String s4 = "aa";
    		System.out.println(s3 == s4);
    	}
    }
    
    结果
    false
    false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    jdk1.8

    public class InternDifference{
    	public static void main(String[] args){
    		String s = new String("a");
    		s.intern();
    		String s2 = "a";
    		System.out.println(s==s2);
    
    		String s3 = new String("a")+new String("a");
    		s3.intern();
    		String s4 = "aa";
    		System.out.println(s3 == s4);
    	}
    }
    
    结果
    false
    true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    JDK6
    在这里插入图片描述

    JDK6+
    在这里插入图片描述
    总结
    jdk6 new String(“a”)+new String(“a”) 调用intern()方法后 放在常量池的是“aa”的副本,而“aa”和“aa”的副本指向的地址不同

    jdk6+ new String(“a”)+new String(“a”) 调用intern()方法后 放在常量池的是“aa”的引用 指向的是堆里面"aa"的地址

  • 相关阅读:
    IP代理安全吗?如何防止IP被限制访问?
    【threejs教程9】threejs加载360全景图(VR)的两种方法
    P02014186陈镐镐
    【JavaEE重点知识归纳】第7节:类和对象
    Python对excel文件批量加密(GUI选择)
    《算法设计与分析》第四章:贪心算法
    华为云新用户:定义,优惠券及专享活动
    机器学习10—多元线性回归模型
    oracle中使用rownum作为条件的失效问题的原因和解决方法
    Linux 安装 Maven
  • 原文地址:https://blog.csdn.net/loyd3/article/details/125161110