1)平台无关性
2)GC
3)语言特性(泛型、反射、lamda表达式)
4)面向对象(继承、封装、多态)
5)类库(集合、并发库、网络库、IO/NIO)
6)异常处理
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文件,如下图:
(1)准备工作:如果我们直接将源码解析成机器码,那么我们每次执行都需要进行语法、句法、语义的检查和校验(即每次执行的结果都不会被保留下来,下次执行都需要重新检查,需要做很多重复工作);
(2)兼容性:引入中间字节码,也可以将别的语言解析成字节码。
简单了解:java虚拟机,是抽象的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完整的硬件架构,如处理器、堆栈、寄存器等,还有相应的指令系统;JVM屏蔽了具体操作平台相关的信息,使得Java程序只需生成在java虚拟机上运行的目标代码,即字节码。
(1)加载:通过ClassLoader加载class文件字节码,生成class对象;
(2)链接:
校验:检查加载的class的正确性和安全性;(如:class的格式)
准备:为类变量分配存储空间,并设置类变量初始化;(类变量存放在方法区)
解析:JVM将常量池内的符号引用转换为直接引用;
(3)初始化:执行类变量赋值和静态代码块。
JVM是内存中的虚拟机,下面主要了解JVM中的内存架构和GC:
(1)Class Loader:依据特定格式,加载class文件到内存;
(2)Execution Engine:(能不能运行由其决定,又名解释器)对命令进行解析;
(3)Native Interface:融合不同开发语言的原生库为java所用;(在需要执行性能较高的时候,需要在java中调用C/C++);例如 Class.forName()底层就是调用native方法;
(4)Runtime Data Area:JVM内存空间结构模型。
理论上: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对象。
ClassLoader主要工作在Class装载的加载阶段,ClassLoader负责通过将Class文件里的二进制流装载进系统,然后交给JJVM进行连接、初始化等操作。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();
}
}
如下图(双亲委派机制):
注:可以通过hg.openjdk.java.net查看C代码
避免多份同样字节码的加载(内存宝贵)
了解:类的加载方式:
隐式加载:new(调用对象时,可以直接调用其对象实例的方法和属性;且支持带参数的构造方法)
显示加载:loadClass,forName等(调用对象时,需要先调用newInstance()方法来生成对应的对象实例;但是newInstance方法不支持传入参,需要利用反射)
首先,它们都能在运行时,知道该类的所有属性和方法。
(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(),为了加快初始化速度,延迟加载。
内存简介:计算机所有程序都是在内存中运行的,只不过这个内存可能包括虚拟内存,同时也离不开硬盘这样的外存知识;在程序执行的过程中,需要不断地将内存的逻辑地址和物理地址映射起来,找到相关的指令以及数据去执行;作为操作系统进程,java运行时面临着和其他进程相同的内存限制,即受限于操作系统架构提供的可寻址空间,其由处理器的位数决定;(32位处理器:2^32=4GB的可寻址范围)
地址空间的划分:
内核空间:是主要的操作系统程序和C运行时的空间,包含用于连接计算机硬件、调度程序、以及提供联网和虚拟内存服务的逻辑,和基于C的进程;
用户空间:java进程实际运行时使用的内存空间(32位系统用户进程最多可以访问3GB,内核代码可以访问所有物理内存;64位系统用户进程最多可以访问512GB,内核代码也可以访问所有物理内存)
线程私有(程序计数器、虚拟机栈、本地方法栈),线程共享(元空间MetaSpace、堆);
(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;
}
如下图(虚拟机栈例子):
写一个递归函数(斐波那契数列):
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异常。
解决方法: 限制递归的次数,或者使用循环的方法代替递归
可以参考如下代码(计算机会假死):
public void stackLeakByThread(){
while(true){
new Thread(){
public void run(){
while(true){
}
}
}.start()
}
}
(3)本地方法栈:
与虚拟机栈类似,主要作用于标记了native的方法;
(4)元空间 MetaSpace:
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) 和老年代;
(1)-Xms:堆的初始值;
(2)-Xmx:堆能扩容达到的最大值;(一般设置-Xms -Xmx相等,以免扩容时内存抖动)
(3)-Xss:规定了每个线程虚拟机栈(堆栈)的大小(一般情况下256k就够了),该参数将会影响此进程中线程并发数的大小;
—程序运行时有三种内存分配策略:
静态存储:编译时确定每个数据目标在运行时的存储空间需求;
栈式存储:数据区需求在编译时未知,运行时模块前确定;
堆式存储:编译时或运行时模块入口都无法确定,动态分配;
联系:引用对象、数组时,栈里定义变量保存在堆中目标的首地址;
区别:
(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(行号)
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