6-1 谈谈你对Java的理解
Java思维导图
谈谈你对Java的理解:
-
平台无关性 — 一次编译到处运行
-
GC — 垃圾回收机制
-
语言特性 — 泛型、反射、lambda表达式
-
面向对象 — 封装、继承、多态
-
类库 — 集合、并发库、网络、IO、NIO
-
异常处理
6-2 平台无关性如何实现
-
Compile Once,Run AnyWhere如何实现
-
编译时 使用javac指令编译java源码,即将源码编译生成字节码并存入到对应的.class文件中
-
运行时
答:Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令
-
-
为什么JVM不直接将源码解析成机器码去执行
-
准备工作:每次执行都需要进行各种检查,如语法校验
-
兼容性:也可以将别的语言解析成字节码
-
-
如何查看.class字节码:使用javap -c指令
6-3 JVM如何加载class文件
Java虚拟机:JVM内存结构模型、GC垃圾回收机制
-
Class Loader:依据指定格式,加载class文件到内存
-
Execution Engine:对命令进行解析
-
Native Interface:融合不同开发语言的原生库为Java所用
-
Runtime Data Area:JVM内存空间结构模型
Class.forName()
JVM架构主要由Class Loader、Runtime Data Area、Execution Engine、Native Interface四部分组成
答:JVM通过Class Loader将符合其格式要求的class文件加载到内存里,并通过Execution Engine去解析class文件里边的字节码,并提交给操作系统去执行。
6-4 什么是反射
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 Exception { Class<?> rc = Class.forName("com.tuyrk.reflect.Robot"); Robot robot = (Robot) rc.newInstance(); System.out.println("Class name is " + rc.getName()); //private私有方法 Method getHello = rc.getDeclaredMethod("throwHello", String.class);//可以获取所有(包括私有方法)自定义的方法,不能获取继承或者实现接口的方法 getHello.setAccessible(true);//throwHello方法为私有方法,默认为false Object str = getHello.invoke(robot, "Bob"); System.out.println("getHello result is " + str); //public公有方法 Method sayHi = rc.getMethod("sayHi", String.class);//只能获取public方法,但是可以获取继承或接口的方法 sayHi.invoke(robot, "Welcome"); //私有属性 Field name = rc.getDeclaredField("name"); name.setAccessible(true); name.set(robot, "Alice"); sayHi.invoke(robot, "Welcome"); } }
6-5 谈谈ClassLoader
-
类从编译到执行的过程:
-
编译器将Robot.java源文件编译为Robot.class字节码文件
-
ClassLoader将字节码转换为JVM中的Class<Robot>对象
-
JVM利用Class<Robot>对象实例化为Robot对象
-
-
谈谈什么是ClassLoader
-
ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获取Class二进制数据流。他是Java的核心组件,所有的Class都是由ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。
-
-
ClassLoader的种类:
-
BootStrapClassLoader:C++编写,加载核心库java.*
-
ExtClassLoader:Java编写,加载扩展库javax.*
-
AppClassLoader:Java编写,下载程序所在目录
-
自定义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(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 protected Class<?> findClass(String name) { byte[] b = loadClassData(name); return defineClass(name, b, 0, b.length); } //用于加载类文件 private 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 b = 0; while ((b = in.read()) != -1) { out.write(b); } } 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) throws Exception { MyClassLoader m = new MyClassLoader("E:\\java\\imooc_coding\\303-java-offter\\src\\main\\resources\\", "Wali"); Class c = m.loadClass("Wali"); System.out.println(c.getClassLoader()); c.newInstance(); } }
6-6 ClassLoader的双亲委派机制
谈谈类加载器的双亲委派机制:
Xbootclasspath是java -jar命令的一个参数
为什么要使用双亲委派机制去加载类:
-
避免多份同样字节码的加载,内存是相当宝贵的,没有必要保存两份一样的类对象,即Class对象。
6-7 loadClass和forName的区别
类的加载方式:
-
隐式加载:new
-
显式加载:loadClass,formName等
显式加载当获取到Class对象之后需要调用Class对象的newInstance方法来生成对象的实例,而通过隐式加载则无需调用Class对象的newInstance方法即可获取对象的实例,并且new支持带参数的构造器生成对象实例,而Class对象的newInstance方法则不支持传入参数,需要通过反射调用构造器的newInstance方法才能支持参数。
类的装载过程:
-
加载:通过ClassLoader加载class文件字节码,生成Class对象
-
链接:检验:检查加载的class的正确性和安全性,如检查class文件的格式是否正确 准备:为类变量分配存储空间并设置类变量初始值,类变量随类型信息存放在方法区中,生命周期很长。 解析:JVM将常量池内的符号引用转换为支持引用
-
初始化:执行类变量赋值和静态代码块
loadClass和forName的联系: 他们都能够在运行时对任意一个类进行动态加载,都能够知道该类的所有属性和方法,对于任意一个对象都能够调用它的任意方法和属性。
loadClass和forName的区别: Class.forName得到的class是已经初始化完成的,即已经完成装载的整个过程 ClassLoader.loadClass得到的class是还没有链接的,即只完成装载的第一步
public class Robot { static { System.out.println("Hello Robot"); } }
public class LoadDifference { public static void main(String[] args) throws ClassNotFoundException { // ClassLoader cl = Robot.class.getClassLoader(); Class c = Class.forName("com.tuyrk.reflect.Robot"); } }
区别产生的作用: 例如程序中需要使用MySQL需要加载driver,这里只能使用forName,而不能使用loadClass
有了forName装载类,为什么还要使用loadClass呢? 在Spring IOC中,在资源加载器获取要读入的资源的时候,即读取Bean的配置文件的时候,如果是以ClassPath的方式来加载就需要使用ClassLoader.loadClass来加载。这是因为SpringIOC使用了lazy loading延迟加载,Spring ICO为了加快初始化速度因此大量使用了延迟加载技术,而使用ClassLoader不需要执行类中的初始化代码和链接步骤,从而加快了加载速度,把类的初始化工作留到实际使用到这个类的时候再做。
6-8 Java内存模型之线程独占部分
内存简介:
32位处理器:2^32的可寻址范围 64位处理器:2^64位可寻址范围
地址空间划分: 内核空间:内核是主要操作系统程序和运行时空间,包含用于连接计算机调度程序以及提供联网,虚拟内存等服务的逻辑和进程 用户空间:Java进程实际运行时的内存空间
你了解Java的内存模型么
JVM内存模型-JDK8
线程私有:程序计数器、虚拟机栈、本地方法栈 线程共享:MetaSpace、Java堆
程序计数器(Program Counter Register)
-
当前线程所执行的字节码行号指示器(逻辑)
-
改变计数器的值来选取下一条需要执行的字节码指令,包括分支、循环、跳转、异常处理、线程恢复等基础功能
-
和线程是一对一的关系,即“线程私有”
-
对Java方法技术,如果是Native方法则计数器值为Undefined
-
不会发生内存泄漏
程序计数器是逻辑计数器,而非物理计数器;为了线程切换后都能恢复正确的执行位置,每个线程都有一个独立的程序计数器,是线程独立的,只对Java方法计数,对Native方法则为Undefined,不会发生内存泄漏。
Java虚拟机栈(Stack)
-
Java方法执行的内存模型
-
包含多个栈帧,方法运行期间的基础数据结构,用于存储局部变量表、操作栈、动态链接、返回地址等。
局部变量表和操作数栈
-
局部变量表:包含方法执行过程中的所有变量
-
操作数栈:入栈、出栈、复制、交换、产生消费变量
递归为什么会引发java.lang.StackOverflowError异常?
-
递归过深,栈帧数超过虚拟机栈深度。限制递归次数,使用循环替代。
public class Fibonacci { //F(0) = 0, F(1) = 1, 当n >= 2的时候, F(n) = F(n - 1) + F(n - 2), //F(2) = F(1) + F(0) = 1, F(3) = F(2) + F(1) = 1 + 1 = 2 //F(0) - F(N)依次为 0, 1, 1, 2, 3, 5, 8, 13, 21, 34... 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(100000)); } }
虚拟机栈过多会引发java.lang.OutOfMemoryError异常
public static void stackLeakByThread() { while (true) { new Thread() { public void run() { while (true) { } } }.start(); } }
虚拟机栈也是Java虚拟机自动管理的,栈类似于一个集合,但是有固定的容量,是由多个栈帧合起来的。每调用一个方法Java虚拟机就会在内存中分配对应的一块空间,这块空间也就是一块栈帧,当方法调用结束后,对应的栈帧就会被自动释放掉。栈的内存不需要通过GC去回收,而会自动释放。
本地方法栈:
-
与虚拟机栈相似,主要作用于标注了native的方法。
6-10 Java内存模型之线程共享部分
元空间(MetaSpace)和永久代(PermGen)的区别:
-
元空间使用本地内存,而永久代使用的是jvm的内存
MetaSpace相比PermGen的优势:
-
字符串常量池存在于永久代中,容易出现性能问题和内存溢出
-
类和方法的信息大小难以确定,给永久代的大小指定带来困难
-
永久代会为GC带来不必要的复杂性,并且回收效率低
-
方便HotSpot与其他JVM如Jrockit的集成
Java堆(Heap):
-
对象实例的分配区域
-
GC管理的主要区域-新生代、老年代
6-11 Java内存模型之 常考题解析
JVM三大性能调优参数-Xms、-Xmx、-Xss的含义: java -Xms128m -Xmx128m -Xss256k -jar xxx.jar
-
Xss:规定了每个线程虚拟机栈(堆栈)的大小,256k足够。影响并发线程数的大小。
-
Xms:堆的初始值,
-
Xmx:堆能扩展达到的最大值,
一般讲Xms和Xmx设置成一样的,因为当Heap不够用发生扩容时会发生内存抖动,从而影响程序运行的稳定性。
Java内存模型中堆和栈的区别-内存分配策略
-
静态存储:编译时确定每个数据目标在运行时的存储空间需求,不允许有可变数据结构、嵌套、递归存在
-
栈式存储:数据区需求在编译时未知,运行时模块入口前确定
-
堆式存储:编译时或运行时模块入口都无法确定,动态分配
Java内存模型中堆和栈的区别: 联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址 区别: 管理方式:栈自动释放,堆需要GC 空间大小:栈比堆小 碎片相关:栈产生的碎片远小于堆 分配方式:栈支持动态和静态分配,而堆只支持动态分配 效率:栈的效率比堆高
元空间、堆、线程独占部分间的联系-内存角度
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.sayHelo(); } }
.intern()添加进字符串常量池,JDK6在永久代引发OutOfMemoryError异常,JDK7+则不受永久代约束。
/* VM options:-XX:MaxPermSize=6M -XX:PermSize=6M */ public class PermGenErrTest { public static void main(String[] args) { for (int i = 0; i <= 1000; i++) { //将返回的随机字符串添加到字符串常量池中。 getRandomString(1000000).intern(); } System.out.println("Mission Complete"); } //返回指定长度的随机字符串 private static String getRandomString(int length) { //字符串源 String str = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"; Random random = new Random(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < length; i++) { int number = random.nextInt(62); sb.append(str.charAt(number)); } return sb.toString(); } }
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); } } /* JDK7+:false true JDK6:false false */
不同JDK版本之间的intern()方法的区别-JDK6:
不同JDK版本之间的intern()方法的区别-JDK7+:
JDK6使用intern()向字符串常量池添加的是字符串的副本;而JDK7+添加的是字符串的引用
知识点结构图: