前言:在慕课网上学习剑指Java面试-Offer直通车时所做的笔记
目录
7.1 JVM三大性能调优参数 -Xms -Xmx -XSS的含义
7.5 不同jdk版本之间的intern方法的区别JDK6 VS JDK6+
第一章 对Java的理解
平台无关性 GC 语言特性(泛型,反射,lambda表达式) 面向对象(封装,继承,多态) 类库(Java本身自带的集合,并发库,网络库,io与nio)
异常处理
第二章 Java平台无关性如何实现
Compile Once, Run Anywhere
分为两块,编译时和运行时.
编译时会用到javac指令,javac编译的是java的源码,即将java的源码编译成二进制字节码,并存入到.class文件.
我们平时运行javac能执行是因为已经配置了对应的环境变量,这样调用javac时系统会到jdk对应的bin目录下去找到javac并执行.javac会对程序的语法句法进行检查,
通过javap -c指令可以实现对字节码文件的反编译
为什么JVM不直接将源码解析成机器码去执行:
准备工作:每次执行都需要各种检查
兼容性:也可以将别的语言解析成字节码
第三章 JVM如何加载class文件
java虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的,JVM有自己完善的硬件架构,如处理器,堆栈,寄存器等,还具有相应的的指令系统,这边屏蔽了与具体操作系统平台相关的信息,使得java程序只需生成在java虚拟机上运行的目标代码即字节码就可以在多种平台上无加修改的运行.
一般情况下,我们不需要知道虚拟机的运行原理,只要专注写java代码就行了,这也是java虚拟机存在的原因,屏蔽底层平台的不同并且减少基于原生语言的复杂性,只要虚拟机厂商在特定操作系统上实现了虚拟机,定义如将字节码解析成本操作系统可执行的二进制码,java这门语言便能实现跨越各种平台.
对于JVM来讲,最值得我们学习的两点分别是JVM内存结构模型和GC.
JVM是内存中的虚拟机,JVM的存储就是内存.我们所写的所有类常量变量方法都在内存中.
JVM通过ClassLoader加载字节码文件到内存,并通过Execution Engine去解析Class文件中的字节码,并提交给操作系统去执行.
Java的执行性能在绝大多数情况下并没有C,C++高,主流的JVM也是基于java实现的,因此在涉及到一些需要较高执行性能的运算操作的时候,是需要在java里直接调用它们的,此外本着不重复造轮子的原则,在实际生产中某个库已经由别的语言进行开发了,我们java可以调用别的库.
因此为了满足上述需求,JVM在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution执行时加载Native Libraies.
第四章 什么是反射
4.1 反射的概念
4.2 反射的简例
Class.forName
第五章 ClassLoader
类从编译到执行的过程:
什么是ClassLoader:ClassLoader是一个抽象类,定义了一些接口,用于自定义加载流程,
其中最重要的方法是如下所示,即给定一个类名,加载一个类,返回代表这个类的class的实例.
ClassLoader的种类:
BootstrapClassLoader:C++编写,加载核心库 java.*,加载一些java的核心类,通常这些核心类不能被替换掉,它是由JVM内核实现的,在目前主要的java虚拟机中是用C++实现的,有了它加载最核心的内容,才会有后面的ClassLoader存在.
自定义ClassLoader的实现:
关键函数:
自定义代码:
验证:
结论:我们不仅可以,只要传入的是二进制的流,是合法的,便能通过不同的形式去加载,比如通过findClass去访问某个远程的网络,去获取二进制流并生成我们需要的类,又比如说可以对某些敏感的class文件加密,可以对生成的二进制流代码做手脚,修改二进制流的代码,增加类的信息.
5.1 ClassLoader的双亲委派机制
首先是自底而上的,也就是从自定义ClassLoader开始去查找有没有加载过该类,比如说我们要加载Robot.class,Custom ClassLoader就会去找找曾经有没有加载过Robot.class,如果加载过直接返回,如果没加载过,就会委派给它的parent即AppClassLoader,如果也没有,就会委派给Extension ClassLoader,直到最顶层,如果还是没有的话,Bootstrap ClassLoader就会去规定的路径rt.jar中或者Xbootclasspath(我们启动程序的时候指定的参数)中去寻找,如果没有会从上向下委派给下面,直到Custom ClassLoader,让它依照的它的findClass方法去寻找class文件的byte数组,如果有就把字节装载到JVM中.
代码讲解:
首先是ClassLoader中的loadClass方法
这个方法会调用一个重载的loadClass,传入类名和一个false的参数,打开方法,第一行是大家熟悉的同步锁,为了锁住Class,因为可能涉及到多个线程调用同一个ClassLoader去加载同一个类,为了避免冲突就加一个同步锁.然后是findLoadedClass,判断之前是否加载过了,如果加载过了就直接返回,因为resolve是false.如果是null,就会判断其parent是否为空,如果不为空继续调用loadClass,直到最上层,因为BootstrapClassLoader是C++编写的,所以在ExtensionClassLoader中找parent的时会跳转到else中去执行findBootstrapClassLoader,尝试在BootstrapClassLoader目录下去扫描,如果有装载进来,如果没有,ExtensionClassLoader就会到if里面去调用自己实现的findClass,去对应的路径找找有没有这个类,如果有就返回,如果没有就返回AppClassLoader,去调用它自定义的findclass,去到对应的path中找找有没有对应的类文件,有的话装载进来返回,没有的话最终会到用户自定义的findclass,如果没有就抛出ClassNotFoundException异常.
如何确定AppClassLoader与ExtensionClassLoader的父类都是图片中描述的那样呢?去调用parent
输出的结果为:
bootstrapClassLoader输出的是null,因为它是调用C++的,如何证明呢?进入findBoodstrapClssOrNull方法中
打开findBootstrapClass,发现它的前缀是native,native会去调用本地的库.查看类调用的java.lang的库,我们可以去openjdk网上去查看代码,可以看到ClassLoader.c,打开可以找到具体的C的代码.
为什么要用双亲委派机制去加载类:
避免多份同样字节码的加载.
5.2 loadClass和forName的区别
隐式加载:new
显式加载:loadClass,forName
对于显式加载来讲,当我们获得对象的class之后,要调用对象的newInstance方法来生成对象的实例,而通过new隐式加载则无需调用对象的newInstance方法即可获取对象的实例,而且new支持带参数的构造器构造对象实例,而class对象的newInstance方法则不支持传入参数.需要通过反射调用对象的newInstance方法才能支持参数,
类的装载过程:
准备是为类变量随类型信息存储在方法区中,生命周期很长,类变量是static变量,初始值指的是类变量类型的默认值而不是世纪指,是用解析步骤时可选的,
同为显式加载loadClass与forName的区别
相同点: 都能直到类的所有属性和方法,对于任意一个对象,都能调用其任意方法和属性.
不同点: class.forName得到的class是已经初始化完成的.classLoader.loadClass得到的class是还没有链接的.
实际代码;
首先看ClassLoader.loadclass这个方法,看到其中有个resolve,点进去后发现resolve参数是决定是否解析的.
点开resolveClass,发现它是为了链接这个类,置为false,代表我们的loadClass不会解析这个类.
打开forName的代码,表示我们将初始化这个类.
进一步验证:
实际中的应用:
Driver是静态的代码段,
forName:
LoadClass:
在SpringIOC中,为了加快加载速度,大量使用了延迟加载技术,而使用ClassLoader不需要使用类中的初始化代码还有链接的步骤,
第六章 Java内存模型
6.1 Java内存模型之线程独占部分
在程序执行的过程中,需要不断将内存的逻辑地址与物理地址进行映射,找到相关的指令以及数据去执行,java运行时面对着与其它进程完全相同的内存限制即受限于操作系统架构提供的可寻址地址空间,操作系统架构提供的可寻址地址空间由操作系统的位数决定,32位处理器提供了2^32的可寻址范围即4GB.
内核空间:内核是主要的操作系统程序和C运行时的空间,包含用于连接计算机硬件,调度程序,提供联网和虚拟内存等服务的逻辑和基于C的进程.
用户空间:用户空间是java进程实际运行时使用的内存空间,32位系统用户最多可以访问3GB,内核可以访问所有物理内存,而64位系统用户进程最大可以访问超过512gb,内核代码同样可以访问所有物理空间.
java内存模型:
虚拟机执行java程序的过程中,会把它管理的内存划分为不同的内存,方便管理,
程序计数器:程序计数器占一个较小的内存空间,是当前线程所执行的字节码的行号指示器(逻辑),字节码解释器工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令.包括分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖计数器完成.由于JVM的多线程是由于线程切换并分配处理器执行时间来实现的,在一个确定的时候,处理器只会执行一个线程中的指令,为了每次执行时处理及都能定位到正确的位置,每个线程都需要一个独立的程序计数器.和线程时一对一的关系.如果正在执行java方法,这个计数器记录的是正在执行的虚拟机字节码的地址.如果是Native方法则计数器的值为Undefined,不用担心内存泄露的问题.
Java虚拟机栈:java虚拟机栈是java方法执行的内存模型,每个方法被执行时都会创建一个栈帧,即方法运行期间的基础数据结构,栈帧用于存储局部变量表,操作栈,动态连接,返回地址,当方法调用结束时帧才会被销毁.局部变量表包含方法执行过程中的所有变量,包括diss引用(没听清),方法参数,局部变量,操作数栈在执行在执行字节码指令中会被用到.类似于原生CPU寄存器.大部分JVM字节码把时间花费在操作数栈的操作上,包括入栈,出栈,复制,交换,产生消费变量,
下面是虚拟机栈:
虚拟机栈有七个栈帧,程序计数器会从大到小依次压入栈中,最后会从小到大执行.
递归为什么引发栈溢出异常? 递归过深,栈帧数超出虚拟站深度.
总的来说虚拟机栈也是由虚拟机来管理的,但是它是有固定的容量的,是由多个虚拟机栈帧合起来的,在编写代码的时候每调用一个方法,在运行的时候JVM就会分配一个对应的空间,这个空间就是栈帧,方法调用结束中栈帧就会销毁,这就是为什么栈的内存不用gc回收的原因了.
本地方法栈:与虚拟机栈相似,主要用于标注了native的方法.
6.2 Java内存模型之线程共享部分
元空间(MetaSpace)与永久代(PerGen)的区别
在jdk8以后,开始把类的元数据放在本地堆内存中,这一块区域就叫做MetaSpace,这一块区域在jdk7及以前是属于永久代的,元空间与永久代都是用来存储class的相关信息,包括class的method与field等,元空间与永久代都是方法区的实现,只是实现方法有所不同.所以说方法区只是JVM的规范,原先位于方法区的JVM常量池已被移动到堆中,并且在jdk8以后用元空间替代了永久代,元空间使用本地内存,永久代使用的是jvm的内存.
替换掉优势:
java堆(Heap):
java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,所有的对象实例都在这里分配内存.也是GC管理的主要区域.
32位处理器java进程的内存布局.
现在收集器基本都采用分代收集算法,所以java堆还可以分为新生代和老年代.
第七章 Java内存模型之常考习题
7.1 JVM三大性能调优参数 -Xms -Xmx -XSS的含义
通常将-Xms与-Xmx写成一样的,避免内存抖动
7.2 Java内存模型中堆和栈的区别->内存分配策略
程序运行时有三种内存分配策略,静态的,栈式的,堆式的
静态存储:静态存储是指在编译时能确定每个数据目标在运行时的存储空间需求,因而在编译时就能分配给它们固定的内存空间,这种程序分配策略要求代码中不能有可变数据集,以及嵌套,递归的结构出现.
栈式存储:该程序可被动态的存储分配,程序对数据区的要求是编译时是完全未知的,运行时才能知道,但是规定在运行到数据模块时必须知道该程序所需内存的大小以分配其内存.
堆式存储:编译时或运行时模块入口都无法确定,动态分配.
7.3 堆和栈的区别
创建好的数组和对象实例都会保存在堆中,想要引用堆中的某个对象或者数组,可以在栈中定义一个特殊的变量,让栈中变量的值为对象或者数组的地址,可以在程序中用栈中的引用变量访问堆中的数组,引用变量在栈中分配,在用到其作用域之外后被释放掉,而数组和对象本身在堆中分配,即使在作用域之外也不会释放,在没有其的引用变量时,会被gc回收.
所以区别主要有以下五点:
7.4 元空间,堆,线程独占部分间的关系-内存角度
我们根据如下的类判断一下其存储情况.
lineNo是lineNumber用来记录代码的执行的行号.以便JVM的执行.
7.5 不同jdk版本之间的intern方法的区别JDK6 VS JDK6+
字符串自带的方法
区别就是在jdk6+中还能在堆中添加常量的引用. 常量池在jdk6+中已经移动到了堆中.
下面的代码的输出:
jdk6: false false
上面的:第一步original:''a''会在常量池中创建一个a,new的话会在堆中创建一个a.s与s2分别代表的是堆中与常量池中a的地址所以不同.
下面的:s3代表的是堆中的''aa''的地址,s3.intern会在常量池中创建''aa''副本,所以s4的地址是常量池中的''aa''的地址.所以不同.
jdk6+: flase true
上面的:第一步original:''a''会在常量池中创建一个a,new的话会在堆中创建一个a.s与s2分别代表的是堆中与常量池中a的地址所以不同.
下面的:s3代表的是堆中的''aa''的地址,s3.intern会在常量池中创建堆中'aa''的引用,所以s4的地址是常量池中的''aa''的地址也就是s3的地址.所以不同.
彩蛋:找工作的最佳时期
金三银四未必是跳槽的好时间:
临近年末的时候也不错:
此时的公司虽说不会降低用人标准,但往往会降低用人标准
不过只要底子过硬,什么时候都处于稀缺,都不怕