java的平台无关性

(三)Java底层知识:JVM

谈谈你对Java的理解:

1)平台无关性

2)GC

3)语言特性(泛型、反射、lamda表达式)

4)面向对象(继承、封装、多态)

5)类库(集合、并发库、网络库、IO/NIO)

6)异常处理

下面展开讲解:

一、平台无关性

1.Java如何实现"一次编译,到处运行"(compile once, run anywhere)?

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

(1)编译:“javac java文件的路径.java”:该指令将源码编译,生成字节码,并存入.class文件中;

(2)运行:

“java java文件的路径”;

“javap -c java文件的路径”:该指令可以反汇编,将JVM解析后的执行指令返回到class文件的状态;

例子:
public class ByteCodeSample{
	public static void main(String[] args){
		int i=1,j=5;
		i++;
		++j;
		System.out.print(i);
		System.out.print(j);
	}
}

在终端调用指令显示代码执行过程:
-javabasic ls
javabasic.iml src
-javabasic cd src
-src javac com.imooc.javabasic.bytecode.ByteCodeSample.java
-src java com.imooc.javabasic.bytecode.ByteCodeSample
2
6
-src javap -help//会输出javap相关的一些指令
-src javap -c com.imooc.javabasic.bytecode.ByteCodeSample//反汇编
Complied from "ByteCodeSample.java"
public class com.imooc.javabasic.bytecode.ByteCodeSample{
	public com.imooc.javabasic.bytecode.ByteCodeSample();//当我们不指定构造函数的时候,编译器会为我们生成一个不带参的默认构造函数
		Code:
		   0: aload_0
		   1: invokespecial  #1
		   0: return
	public static void main(java.lang.String[]);
		Code:            //虚指令,即字节码
		   0: iconst_1   //将常量1放入栈顶
		   1: istore_1   //将栈顶的值放入局部变量1(即i)当中
		   2: iconst_5
		   3: istore_2
		   4: iinc          1, 1  //将变量1(即i)加上1
		   7: iinc          2, 1
		  10: getstatic     #2    //获取PrintStream的静态对象
		  13: iload_1   
		  14: invokevirtual #3 
		  17: getstatic     #2
		  20: iload_2      
		  21: invokevirtual #3    
		  return
}

如图(JAVA跨平台):

演示在另一个Linux服务器上执行ByteCodeSample.class文件,如下图:
java的平台无关性_第1张图片

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

(1)准备工作:如果我们直接将源码解析成机器码,那么我们每次执行都需要进行语法、句法、语义的检查和校验(即每次执行的结果都不会被保留下来,下次执行都需要重新检查,需要做很多重复工作);

(2)兼容性:引入中间字节码,也可以将别的语言解析成字节码。

2.JVM如何加载.class文件?

简单了解:java虚拟机,是抽象的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完整的硬件架构,如处理器、堆栈、寄存器等,还有相应的指令系统;JVM屏蔽了具体操作平台相关的信息,使得Java程序只需生成在java虚拟机上运行的目标代码,即字节码。

  • 类的装载过程:

(1)加载:通过ClassLoader加载class文件字节码,生成class对象;

(2)链接:

校验:检查加载的class的正确性和安全性;(如:class的格式)
准备:为类变量分配存储空间,并设置类变量初始化;(类变量存放在方法区)
解析:JVM将常量池内的符号引用转换为直接引用;

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

JVM是内存中的虚拟机,下面主要了解JVM中的内存架构和GC:

2.1 JVM架构(如下图):

java的平台无关性_第2张图片

(1)Class Loader:依据特定格式,加载class文件到内存;

(2)Execution Engine:(能不能运行由其决定,又名解释器)对命令进行解析;

(3)Native Interface:融合不同开发语言的原生库为java所用;(在需要执行性能较高的时候,需要在java中调用C/C++);例如 Class.forName()底层就是调用native方法;

(4)Runtime Data Area:JVM内存空间结构模型。

3. 什么是反射?

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

写一个反射的例子:

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

//下面做一个反射的例子,来获取上面类里面的属性和方法
public class ReflectSample{
	public static void main(String[] args) throws ClassNotFoundException{
		Class rc=Class.forName("Robot类的全路径");
		Robot r=(Robot)rc.newInstance();
		System.out.println("Class name is"+rc.getName());

		Method getHello=rc.getDeclaredMethod(name:"throwHello",String.class);//getDeclaredMethod()方法可以获取类中的所有方法(包括私有方法),但是不能获取继承类的方法、所实现接口方法
		getHello.setAccessible(true);//注意获取private方法一定要设置
		Object str=getHello.invoke(r,...args:"Bob");//.invoke(实例对象,方法参数)调用获取的方法
		System.out.println("getHello result is"+str);

		Method sayHi=rc.getMethod(name:"sayHi",String.class);//getMethod()方法可以获取类中的public方法(不包括私有方法),且能获取继承类的方法、所实现接口方法
		Object sayHi.invoke(r,...args:"Welcom");

		Field name=rc.getDeclaredField(name:"name");//getDeclaredField()获取类中的属性
		name.setAccessible(true);
		name.set(r,"Alice");
		sayHi.invoke(r,...args:"Welcome");		
	}
}

类从编译到执行的过程:

(1)编译器将Robot.java源文件编译为Robot.class字节码文件;

(2)ClassLoad将字节码转换为JVM中的Class对象;

(3)JVM利用Class对象实例化为Robot对象。

4. 谈谈ClassLoader:

ClassLoader主要工作在Class装载的加载阶段,ClassLoader负责通过将Class文件里的二进制流装载进系统,然后交给JJVM进行连接、初始化等操作。ClassLoader是抽象类。

4.1 ClassLoader的种类

(1)BootStrapClassLoader:主要加载java自带的核心类java.*(C++编写)

(2)ExtClassLoader:主要加载扩展库javax.*,是用户可见的 (java编写)

(3)AppClassLoader:加载程序所在目录,是用户可见的(java编写)

(4)自定义ClassLoader:定制化加载(Java编写)

如下是自定义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(name:null,b,off,len,protectionDomain:null);
}

具体的:

public class Wali{
	static{
		System.out.println("Hello Wali");
	}
}

//自定义类加载器
public class MyClassLoader extends ClassLoader{
	private String path;
	private String classLoaderName;
	
	public MyClassLoader(String path,String classLoaderName){
		this.path=path;
		this.classLoaderName=calssLoaderName;
	}

	//用于寻找类文件
	@Override
	public class findClass(String name){
		byte[] b=loadClassDate(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.read())!=-1){
				out.write(i);
			}
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			try{
				out.close();
				in.close();
			}catch(Exception e){
				e.printStackTrace();
			}
		}
		return out.toByteArray();
	}
}

//检测类
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();
	}
}

4.2 谈谈类加载器的双签委派机制:

如下图(双亲委派机制):

java的平台无关性_第3张图片

注:可以通过hg.openjdk.java.net查看C代码

为什么要使用双亲委派机制去加载类?

避免多份同样字节码的加载(内存宝贵)

了解:类的加载方式:

隐式加载:new(调用对象时,可以直接调用其对象实例的方法和属性;且支持带参数的构造方法)

显示加载:loadClass,forName等(调用对象时,需要先调用newInstance()方法来生成对应的对象实例;但是newInstance方法不支持传入参,需要利用反射)

4.4 loadClass和forName的区别:

首先,它们都能在运行时,知道该类的所有属性和方法。

(1)Class.forName得到的class是已经初始化完成的;

(2)Classloader.loadClass得到的class是还没有链接的。

public class Robot{
	private String name;
	public void sayHi(String helloSentence){
		System.out.println(helloSentence+" "+name);
	}
	private String throwHello(String tag){
		return "Hello"+tag;
	}
	//静态代码块
	static{
		System.out.println("Hello Robot");
	}
}

public class LoadDifference{
	public static void main(String[] args){
		ClassLoader cl=Robot.class.getClassLoader();//什么都不会输出
		Class c=Class.forName("com.interview.javabasic.reflect.Robot");//输出 Hello Robot
	}
}

forName和loadClass的作用:

(1)JDBC中加载数据库驱动需要用到Class.forName(“com.mysql.jdbc.Driver”),Driver中有静态代码块所以需要用到forName();

(2)Spring IOC资源加载器获取资源(即读取配置文件时),需要用到class.getClassLoader(),为了加快初始化速度,延迟加载。

5. 你了解JAVA的内存区域划分吗?

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

地址空间的划分:

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

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

JVM的内存区域划分----JDK8

5.1 从线程的角度看:

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

如下图(JVM内存模型):
java的平台无关性_第4张图片

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

当前线程所执行的字节码行号指示器(逻辑);

改变计数器的值来选取下一条需要执行的字节码指令;

和线程是一对一的关系,即"线程私有";

对java方法计数,如果是native方法则计数器值为undefined;

不会发生内存泄漏;

(2)java虚拟机栈(stack):

java方法执行的内存模型;

包含多个栈帧;(一个栈帧中包含:局部变量表、操作栈、动态连接、返回地址)

局部变量表和操作数栈:

局部变量表:包含方法执行过程中的所有变量;

操作数栈:入栈、出栈、复制、交换、产生消费变量

举例如下:执行add(1,2),javap -c反汇编如图(java虚拟机栈)
public static int add(int a,int b){
	int c=0;
	c=a+b;
	return c;
}

如下图(虚拟机栈例子):

java的平台无关性_第5张图片

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

写一个递归函数(斐波那契数列):

public class Fibonacci{
	public static int fibonacci(int n){
		if(n==0){
			return 0;
		}
		if(n==1){
			return 1;
		}
		return fibonacci(n-1)+fibonacci(n-2);
	}
	public static void main(String[] args){
		System.out.println(fibonacci(1000000));
	}
}
//结果报错:java.lang.StackOverflowError

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

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

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

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

解决方法: 限制递归的次数,或者使用循环的方法代替递归

虚拟机栈过多会引发java.lang.OutOfMemoryError异常:

可以参考如下代码(计算机会假死):

public void stackLeakByThread(){
	while(true){
		new Thread(){
			public void run(){
				while(true){

				}
			}
		}.start()
	}
}

(3)本地方法栈:

与虚拟机栈类似,主要作用于标记了native的方法;

(4)元空间 MetaSpace:

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

JDK 1.7之前,元空间是属于永久代的,元空间和永久代是用来向存储class的相关信息,包括class对象的method和field; 实际上,元空间和永久代都是方法区的实现,只是实现有所不同;java7之后,原先在方法区中的字符串常量池已被移动到java堆中;在JDK1.8之后,使用元空间代替了永久代,这一替代并非仅仅是名字上的替代,两者最大的区别是:元空间使用本地内存,而永久代使用的是JVM的内存

元空间使用本地内存的最大好处是:java.lang.OutOfMemoryError:PermGen space 这个异常就不复存在了。

  • 元空间相比永久代的优势:

字符串常量池存在于永久代中,容易出现性能问题和内存溢出;

类和方法的信息大小难以确定,给永久代的大小指定带来困难;

永久代会为GC带来不必要的复杂性;

(5)java堆(Heap):

  • 对象实例的分配区域:【例如:32位计算机的可寻址内存为4GB,其中OS and C占用1GB,JVM占用一定内存,java heap占用2GB,剩余的由native heap(即metaSpace)占用;】堆的大小是可扩展;

  • GC管理的主要区域:堆可以分为新生代(Eden, From survivor, To survivor) 和老年代;

5.2 从存储角度看:

  • JVM三大性能调优参数 -Xms -Xmx -Xss的含义:(可以调整堆栈的大小)

(1)-Xms:堆的初始值;

(2)-Xmx:堆能扩容达到的最大值;(一般设置-Xms -Xmx相等,以免扩容时内存抖动)

(3)-Xss:规定了每个线程虚拟机栈(堆栈)的大小(一般情况下256k就够了),该参数将会影响此进程中线程并发数的大小;

—程序运行时有三种内存分配策略:

静态存储:编译时确定每个数据目标在运行时的存储空间需求;

栈式存储:数据区需求在编译时未知,运行时模块前确定;

堆式存储:编译时或运行时模块入口都无法确定,动态分配;

  • java内存模型中堆和栈的区别:

联系:引用对象、数组时,栈里定义变量保存在堆中目标的首地址;

区别:

(1)管理方式:栈自动释放,堆需要GC;

(2)空间大小:栈比堆小;

(3)碎片相关:栈产生的碎片远小于堆;

(4)分配方式:栈支持静态和动态分配,而堆仅支持动态分配;

(5)效率:栈的效率比堆高。

  • 元空间、堆、线程独占部分间的联系(内存角度):

举例如下:

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();
	}
}

元空间:Class:HelloWorld -Method:sayHello\setName\main -Field:name Class:System

java堆:Object:String(“test”); Object:HelloWorld

线程独占:Parameter reference:“test” to String object;
Variable reference:“hw” to HelloWorld object; Local Variables: a with 1, lineNo(行号)

  • 不同JDK版本之间的intern()方法的区别(JDK6 VS JDK6+):

String s=new String(“a”);
s.intern();

JDK6:当调用intern()方法时,仅会在字符串常量池里添加字符串对象;

JDK6+:当调用intern()方法时,不但会在字符串常量池里添加字符串对象,还能添加字符串变量在堆中的引用;

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);
	}
}

JDK 1.6的运行结果是:false false
JDK 1.7/1.8的运行结果是:false true

你可能感兴趣的:(Java基础知识)