Java的理解
- 平台无关性
- GC 垃圾回收机制
- 语言特性:反射、范型、lambda表达式等
- 面向对象:多态、继承、封装
- 类库:集合、并发库、IO库等
- 异常处理
平台无关性
Jvm可以从软件层屏蔽不同的操作系统在底层硬件与指令上的区别。
.java文件通过javac编译生成.class文件,.class文件中包含:编译生成的二进制码、java类中的属性、方法和静态属性。可以通过java自带的javap命令反编译查看.class文件,了解java编译器内部机制。
- javap指令(javap -help查看), -c 对代码进行反汇编。
JVM架构
Java虚拟机JVM,抽象化的、运行在内存中的虚拟化计算机,通过模拟仿真各种计算机功能。JVM屏蔽了与具体操作系统平台相关的信息,使得java编译后可以在各种平台运行。
Class Loader(类装载系统):依据特定格式,加载class文件到Runtime Data Area。
Execution Engine(字节码执行引擎):对命令进行解析。
Native Interface(本地方法接口):融合不同开发语言的原生库为java所用。
Runtime Data Area(运行时数据区):JVM内存空间结构模型。
JVM如何加载class文件?
Java反射机制
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有方法和属性;对于任意一个对象都能够调用任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
定义反射类Robot,包括私有属性name、公共方法sayHi、私有方法throwHello。
package com.Interview.javabasic;
public class Robot {
private String name;
public void sayHi(String helloSentence){
System.out.println(helloSentence + "" +name);
}
private String throwHello(String tag){
return "hello" + tag;
}
}
定义反射实例类,通过forName获取反射类、newInstance实例化反射类、getDeclaredMethod和getMethod获取反射类的方法、getDeclaredField获取反射类的属性、invoke调用反射类的方法并且可以传入参数。
package com.Interview.javabasic;
import javax.swing.*;
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, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
//forName根据类的路径获取类对象
Class rc = Class.forName("com.Interview.javabasic.Robot");
//通过newInstance获取类的实例,因为newInstance返回的实例类型是范性,需要强转。
Robot r = (Robot) rc.newInstance();
System.out.println("class name "+rc.getName());
//getDeclaredMethod能够获取反射类的所有包括公共和私有方法,但是不能获取继承的方法和实现接口的方法。
Method getHello = rc.getDeclaredMethod("throwHello",String.class);
//getMethod只能获取公共public方法,但是可以获取反射类继承的方法和实现接口的方法。
Method sayHi = rc.getMethod("sayHi", String.class);
Field name = rc.getDeclaredField("name");
//反射类的throwHello为私有方法、name为私有属性,因此需要设置setAccessible为true
getHello.setAccessible(true);
name.setAccessible(true);
//给name赋值,需要传入实例化的反射类对象
name.set(r,"alice");
//使用Object接受返回的值,invoke()返回的是Object对象,
Object str = getHello.invoke(r,"Bob");
sayHi.invoke(r,"welcome");
System.out.println("getHello result" + str);
}
}
类从编译到执行的过程
反射获取类的class对象,必须先获取该类的字节码文件对象。
- 编译器将Robot.java源文件编译为Robot.class字节码文件。
- ClassLoader将字节码(byte数组格式)转换为JVM中的Class对象。
- JVM利用Class对象实例化为Robot对象。
ClassLoader简介
ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。他是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载近系统,然后交给Java虚拟机进行连接、初始化等操作。
ClassLoader的种类
- BootStrapClassLoader:C++编写,加载核心库java.*,由JVM实现。
- ExtClassLoader:Java编写,加载扩展库javax.*,可以将自定义的class进行加载。
- AppClassLoader:Java编写,加载程序所在目录。用于加载ClassPath路径下的内容 ,例如上述的Robot.class和ReflectSample.class文件。
- 自定义ClassLoader:Java编写,定制化加载。
findClass()根据名称和路径加载.class字节码,然后它会调用defineClass()解析定义clss字节流并返回class对象。
自定义ClassLoader实例
目的:自定义一个类加载器,实现加载自定义claa文件的功能。
流程:
1.在工程以外创建一个Wail类(Wail.java),使用javac编译为Wail.class文件。
public class Wail{ static{ System.out.println("Hello wail"); } }
2.编写一个自定义的ClassLoader类加载器,通过类的路径和名称,读取class文件的字节流解析返回class对象。
package com.Interview.javabasic; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; public class MyClassLoader extends ClassLoader { private String path; private String classLoaderName; public MyClassLoader(String path, String classLoaderName){ this.path = path; this.classLoaderName = classLoaderName; } public Class findClass(String name){ byte [] b = loadClassData(name); return defineClass(name,b,0, b.length); } public byte[] loadClassData(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(); } }
3.编写ClassLoaderChecker.java文件进行验证。
package com.Interview.javabasic; public class ClassLoaderChecker { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { MyClassLoader m = new MyClassLoader("class文件所在目录","MyClassLoader"); Class c = m.loadClass("Wail"); System.out.println(c.getClassLoader()); //newInstance()触发自定义的加载类里的static代码块 c.newInstance(); } }
类加载机制的双亲委派机制
- 1.自底向上检查类是否已经加载,如果最终在Bootstrap ClassLoader中没有检查到该类,则会尝试加载类。
- 2.自顶向下尝试加载类,首先在-Xbooclasspath路径下的Bootstrap ClassLoader管理的Jar包中进行加载,最终在自定义的Custom ClassLoader按照findClass的方式寻找对应的class文件的类,如果有直接加载到JVM中。
- 3.如果在顶向下还没有加载到类,就会抛出异常。
为什么会使用双亲委派机制去加载类?
避免同样字节码的加载从而浪费内存,例如:对于system.out字节码,如果没有双亲委派机制,类A打印会加载一份,类B打印也会加载一份,那JVM中会出现两份同样的字节码,浪费内存。如果有在类A打印时,system.out字节码已经从bootstrap ClassLoader中加载到内存,类B打印时,会自底向上进行检查,检查到bootstrap ClassLoader中system.out字节码已经被加载到内存中,直接引用就行。
类的加载方式
1.隐式加载:new
隐式加载使用new,将类加载到JVM中,支持带参数的构造器生成对应的实例。
2.显式加载:loadClass、forName等
通过显式加载到类的class对象后,需要调用class对象的newInstance()方法来获取类的实例,不支持传参。
loadClass和forName的区别
1.在运行时,都能够调用类的任意属性和方法。
2.Class.forName得到的class是已经初始化完成的。
Class r = Class.forName("com.Interview.javabasic.Robot"); //使用forName()方法查看静态代码块是否被执行
3.ClassLoader.loadClass得到的class是还没有链接的。
ClassLoader cl = Robot.class.getClassLoader(); //使用loadClass()方法查看静态代码块是否被执行
4.例如,准备好Robot.java文件,并编译为Robot.class文件。如果Robot类被初始化,则静态代码块会被执行。
package com.Interview.javabasic; 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"); } }
类的两种显式加载的应用场景
1.loadClass()应用场景
SpringIOC中 ,在资源加载器获取要读入bean的配置文件时,如果以classpath的加载方式就需要使用loadClass()进行加载。因为这与SpringIOC的(lazz loader)懒加载机制有关,SpingIOC为了加快初始化速度,大量使用了懒加载技术,使用loadClass()不需要执行装载类的链接和初始化,并将类的初始化工作留在使用的时候在进行。
2.forName应用场景
目的:程序连接Mysql时,需要加载对应的数据库驱动Driven,此时需要用forName进行Driven类的加载。
流程:1.导入jar包,file->Structure->Modules->Dependencies->"+"进行导入。
2.使用forName加载Driver类。
package org.gjt.mm.mysql; import java.sql.SQLException; public class Driver extends com.mysql.jdbc.Driver { public Driver() throws SQLException { } }
类的装载过程
类的加载过程是类的装载过程的一部分。
1.加载,通过ClassLoader加载class文件字节码,生成Class对象。
ClassLoader通过loadClass方法将class文件字节码加载到内存中,并将这些静态数据转化为Runtime Data Area(运行时数据区)中Method Area(方法区)的类型数据。在Runtime Data Area(运行时数据区)的Heap(堆)中生成一个代表这个类的java.lang.class对象,作为Method Area(方法区)类数据的访问入口。
2.链接
- 校验:检查加载的class的正确性和安全性。
- 准备:为类变量分配存储空间并设置类变量初始值。
- 解析:JVM将常量池内的符号引用转换为直接引用。
3.初始化,执行类变量赋值和静态代码块。
Java内存模型
线程私有和线程共享
- 线程私有:程序计数器、虚拟机栈、本地方法栈
- 线程共享:MetaSpace(元空间)、Java堆
PC Register(程序计数器)
- 当前线程所执行的字节码行号指示器(逻辑)。
- 改变计数器的值来选取下一条需要执行的字节码指令(分支、循环、跳转、异常处理、线程恢复等功能都需要依赖PC计数器)。
- 线程私有,每个线程独有程序计数器,程序计数器在线程挂起时记录当前位置,线程调用CPU时从当前记录位置运行。
- 对Java方法计数,记录的是虚拟机执行的字节码指令的地址。如果是Native方法则计数器值为Undefined。
- 不会发生内存泄漏。
Stack(Java虚拟机栈)
- Java方法执行的内存模型
- JVM中的栈(虚拟机栈)由一个个栈帧组成,每个方法执行时都会产生一个栈帧
- 栈帧中包括局部变量表、操作数栈、动态链接、方法出口(返回地址)等。
- 首先压入到虚拟机栈中的一般时main()栈帧,依次是main()中调用的函数(compute())栈帧。
栈帧结构
1.局部变量表,包含方法执行过程中的所有变量(基本数据类型的变量)。存放的是方法体中定义的局部变量,如果局部变量为对象,则局部变量中存放的是堆中对象存储的地址。
函数方法中的局部变量会存储在栈内存空间中,当main()中的线程启动时会在虚拟机栈中获得一小块内存空间存放局部变量,main()中调用其他函数会再开启一个线程,同时获得栈中内存空间存放局部变量。
2.操作数栈,在执行字节码指令时被用到,包括入栈、出栈、复制、交换、产生消费变量。
3.动态链接
4.方法出口,被调方法存放方法执行完成后,回到调用方法代码执行的位置
实例
编写Math.java文件
public class Math{
public static final int d = 100;
public static User user = new User();
public int compute(){
int a = 1;
int b = 2;
int c = (a + b)*10;
return c;
}
public static void main(String[] args){
Math math = new Math();
math.compute();
System.out.println("test");
}
}
class User{
}
通过 javap -verbose Math.class口水话语言反编译Math.class
Last modified 2020-10-7; size 675 bytes
MD5 checksum f1cfc18c54f979f9383988d20a27e696
Compiled from "Math.java"
public class Math //描述类信息,公有、继承Object
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool: //常量池信息
#1 = Methodref #11.#29 // java/lang/Object."<init>":()V
#2 = Class #30 // Math
#3 = Methodref #2.#29 // Math."<init>":()V
#4 = Methodref #2.#31 // Math.compute:()I
#5 = Fieldref #32.#33 // java/lang/System.out:Ljava/io/PrintStream;
#6 = String #34 // test
#7 = Methodref #35.#36 // java/io/PrintStream.println:(Ljava/lang/String;)V
#8 = Class #37 // User
#9 = Methodref #8.#29 // User."<init>":()V
#10 = Fieldref #2.#38 // Math.user:LUser;
#11 = Class #39 // java/lang/Object
#12 = Utf8 d
#13 = Utf8 I
#14 = Utf8 ConstantValue
#15 = Integer 100
#16 = Utf8 user
#17 = Utf8 LUser;
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 compute
#23 = Utf8 ()I
#24 = Utf8 main
#25 = Utf8 ([Ljava/lang/String;)V
#26 = Utf8 <clinit>
#27 = Utf8 SourceFile
#28 = Utf8 Math.java
#29 = NameAndType #18:#19 // "<init>":()V
#30 = Utf8 Math
#31 = NameAndType #22:#23 // compute:()I
#32 = Class #40 // java/lang/System
#33 = NameAndType #41:#42 // out:Ljava/io/PrintStream;
#34 = Utf8 test
#35 = Class #43 // java/io/PrintStream
#36 = NameAndType #44:#45 // println:(Ljava/lang/String;)V
#37 = Utf8 User
#38 = NameAndType #16:#17 // user:LUser;
#39 = Utf8 java/lang/Object
#40 = Utf8 java/lang/System
#41 = Utf8 out
#42 = Utf8 Ljava/io/PrintStream;
#43 = Utf8 java/io/PrintStream
#44 = Utf8 println
#45 = Utf8 (Ljava/lang/String;)V
{
public static final int d;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 100
public static User user; //类的初始化信息
descriptor: LUser;
flags: ACC_PUBLIC, ACC_STATIC
public Math(); //类的初始化信息
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public int compute();
descriptor: ()I //表示不接收变量 ,返回值为Int类型
flags: ACC_PUBLIC //public类型的方法
Code: //代码前面的编号方便程序计数器调用
stack=2, locals=4, args_size=1 //操作数栈的深度为2、本地变量容量为4、参数数量为1
0: iconst_1 //将int类型常量1压入操作数栈
1: istore_1 //将int类型值存入局部变量1 将常量1赋值给局部变量表中的第一个变量a
2: iconst_2 //将int类型常量2压入操作数栈
3: istore_2 //将int类型值存入局部变量2 将常量2赋值给局部变量表中的第二个变量b
4: iload_1 //从局部变量1中装载int类型值 将常量1装载到操作数栈
5: iload_2 //从局部变量2中装载int类型值 将常量2装载到操作数栈
6: iadd //执行int类型的加法 常量1与常量2相加
7: bipush 10 //将一个8位带符号整数压入栈 将10压入操作数栈
9: imul // 执行int类型的乘法 常量3
10: istore_3 //将int类型常量3压入操作数栈
11: iload_3 //从局部变量3中装载int类型值
12: ireturn //从方法中返回int类型的数据
LineNumberTable: //行号表
line 5: 0 //代码第5行对应字节码的第0行
line 6: 2
line 7: 4
line 8: 11
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #6 // String test
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
LineNumberTable:
line 11: 0
line 12: 8
line 13: 13
line 14: 21
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #8 // class User
3: dup
4: invokespecial #9 // Method User."<init>":()V
7: putstatic #10 // Field user:LUser;
10: return
LineNumberTable:
line 3: 0
}
SourceFile: "Math.java"
栈的内存会自动释放,不需要GC。
为什么递归会引发java.lang.StackOberflowError异常?
当线程执行一个方法时,就会创建一个栈帧并压入虚拟机栈,方法执行完毕后,将栈帧出栈。正在运行的方法,位于虚拟机栈的栈顶,递归函数会不断的调用自身,并往虚拟机栈中压入。当递归过深,栈帧数超出虚拟机栈深度时,就会抛出异常。
本地方法栈
与虚拟机栈相似,主要作用于标注了native的方法,是调用本地方法时所需的内存空间。
本地方法栈中的方法底层用C语言来实现,当java运行到native方法时,会到操作系统方法库中调用xx.dll(类似于jar包)方法,来实现跨语言的方法调用,现在一般不常用。常用的是基于服务来实现跨语言方法调用,直接调用服务的接口。
private native void start0(); //用native来修饰的方法
MetaSpace元空间
jdk1.8版本之前称之为方法区(又叫永久方法区),jdk1.8之后称之为元空间。元空间使用本地内存,而永久代使用的是JVM的内存;字符串常量池存在永久代中,容易出现性能问题和内存溢出,元空间中将常量池移到堆中。
- 方法区
- 常量
- 静态变量:静态变量如果为对象,那静态变量中存储的是对象在堆中的地址
- 类信息
Heap堆
堆,存放对象实例的内存区域,内存区域可划分为:old区、eden区、from-to survivor区。同样也是GC主要操作的区域。
- eden区:新创建的对象都会放在eden区,当eden区的存储空间不足时,会触发minor GC(范围为eden、from或to)。通过垃圾回收,释放掉垃圾对象,未被回收的对象全部存放至survivor区的from或to中并且分代年龄+1.
- from-to区:from和to区统称为survivor区,经过minor GC后,from区和eden区未被回收的对象全部移入to区或者to区和eden区未被回收的对象移入from区。当survivor区中由对象的分代年龄超过15次,会移入到old区。
- old区 :随着程序的不断运行,old区空间会越来越少。当空间不足以存储survivor区移入的对象,会进行一次full GC(范围为整个堆),如果仍然空间不足,就会抛出异常。
Java的内存模型
JVM三大性能调优参数
- -Xms,堆能达到的最大值(一般为128M)。通常将Xms和Xmx大小设置为一样,因为当堆扩容时,会产生内存抖动,影响程序的稳定性。
- -Xmx,堆的初始值(128M),该进程刚创建时专属Java堆的大小,会自动扩容,扩容至堆的最大值。
- -Xss,规定了每个线程虚拟机栈的大小(一般为256K),配置会影响此进程中并发线程数的大小。
Java内存模型中堆和栈的区别
1.内存分配策略
- 静态存储,编译时确定每个数据目标在运行时的存储空间要求。
- 栈式存储,数据区要求在编译时未知,运行时模块入口前确定。
- 堆式存储,编译时或运行时模块入口都无法确定,动态分配(如:可变字符串、实例化对象等)。
2.联系
引用对象、数组时,栈里定义变量保存对象、数组在堆中目标的首地址,可以运用栈中的引用变量访问堆中的对象和数组。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外会被释放掉。数组和对象本身会在堆中进行分配,分配的内存区域只有在没有引用变量指向的时候,才会被垃圾回收机制释放。
3.区别
- 管理方式,栈自动释放(编译器就可以通过pull操作释放),堆需要GC(Execution Engine执行引擎不会对内存进行释放操作)。
- 空间大小,栈比堆(需要存放大量java对象数据)小。
- 碎片相关,栈产生的碎片远小于堆(内存结构较复杂)。
- 分配方式,栈支持静态和动态分配,而堆仅支持动态分配。
- 效率,栈的效率比堆高。
元空间、堆、线程独占部分间的联系
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-Fiels:name Class:System
- Java堆:存储的是类对象的实例。Object:String(“test”) Object:HelloWorld
- 线程独占:程序执行时,main线程会分配对应的虚拟机栈、本地方法栈和程序计数器。栈里面会存储:
- Parameter reference:“test” to String object
- Variable reference:“hw” to HelloWorld object
- Local Variables:a with 1,lineNo
不同版本之间的intern()方法的区别-JDK6和JDK6+?