本人也刚刚参加完秋招,一直打算把在秋招中遇到的面试常考点做一个总结,但是一直都没着手。近期,好多朋友都在问我关于秋招春招要怎么准备,我也刚好闲来没事,就帮大家收集了网上各大博客的精华,并且结合自己所学和理解做了一些简单的整理,希望可以帮助到现在正在春招的朋友们。(大多数知识点是建立在已经学习过大致了解的基础上才能看懂,如果没有学习过可能不太好理解) 根据个人理解总结!仅供参考!欢迎纠错!
加载—链接—初始化—使用—卸载
由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个字节码文件,当Java程序需要使用到某个类的时候,JVM会确保这个类已经被加载、连接、初始化。
类加载阶段:
(1)类加载器通过一个byte数组把我们的.class文件(class文件开头有特定的文件标示,以便于虚拟机可以识别他)读取到内存当中,然后把他存储到方法区中,并且给他创建一个Class对象。
(2)任何类被使用时系统都会为其创建一个且仅有一个Class对象。
(3)这个Class对象描述了这个类创建出来的对象的所有信息,比如有哪些构造方法,都有哪些成员方法,都有哪些成员变量等。
类加载完成后,ClassLoader对象还不完整,所以此时的类还不可用,类被加载后就进入链接阶段。
链接包括验证、准备以及解析三个阶段。
(1)验证阶段。主要的目的是确保被加载的类(.class文件的字节流)满足Java虚拟机规范,不会造成安全错误。
- 这个类的父类是否继承了不允许被继承的类
- 如果这个类不是抽象类, 是否实現了其父类或接口之中要求实现的所有方法
- 方法中的类型转换是否正确
(2)准备阶段。负责为类变量分配内存,并设置默认初始值。
(3)解析阶段。将类的二进制数据中的符号引用替换为直接引用。
举个例子来说,现在调用方法hello(),这个方法的地址是0xaabbccdd,那么hello就是符号引用,0xaabbccdd就是直接引用。
一个java类(假设为People类)被编译成一个class文件时,如果People类引用了Tool类,但是在编译时People类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,及直接引用地址。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
初始化,则是为标记为常量值或类的静态变量赋值的过程。换句话说,只对static修饰的变量或语句块进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
类加载过程只是一个类生命周期的一部分,在其前,有编译的过程,只有对源代码编译之后,才能获得能够被虚拟机加载的字节码文件;在其后还有具体的类使用过程,当使用完成之后,还会在方法区垃圾回收的过程中进行卸载(垃圾回收)
什么是类加载器:把我们的.class的字节码文件加载到内存中,然后存放在方法区中。
有几种?
类的加载过程是由类加载器完成的,类加载器包括:BootStrapClassLoader、ExtensionClassLoader、ApplicationClassLoader、UserClassLoader。
BootstrapClassLoader:负责加载JVM基础核心类库(加载rt.jar包(String、ArrayList…))(
object.getClass().getClassLoader()
返回的是null,因为他是C++写的,是根加载器)ExtensionClassLoader:负责加载目录下的扩展类(随着Java的发展,在原来的rt的jar包基础上进行了扩展,对于后面添加的扩展包用这个加载器加载,javax的包就是扩展的包)
ApplicationClassLoader:复制加载应用类,他从环境变量classpath或java.class.path所指定的目录中加载类(加载的是我们自己写的类)
UserClassLoader:用户可以定制类的加载方式
双亲委派机制
从Java2开始,类加载过程采用双亲委派机制,该过程是如果一个类加载器收到了一个类加载请求,他首先不会去加载这个类,而是把这个加载请求委托给父加载器去加载,直到启动类加载器,如果父类加载器加载不了了这个请求(在他的加载路径下没有找到所需加载的Class),子类加载器才会自己去加载。作用就是保证Java源代码不受污染,保证他的安全,这就叫沙箱安全。
俗语:我们在加载一个类的时候,会先从根加载器去加载,找得到直接用,找不到就去扩展类加载器中找,一直找到自己,如果自己也找不到,就会报ClassNotFoundException异常。
沙箱安全机制:沙箱机制是由基于双亲委派机制上,采取的一种JVM的自我保护机制
为什么要使用双亲委派机制:
- 我们写一个java.lang.String的类,然后运行。加载器会把他传到bootStrapClassLoader去加载,bootstrapClassLoader会在自己的rt.jar包中找到一个String.class,但是rt.jar中的String就没有main方法,所以会抛出异常。所以我们的双亲委派机制就是为了保证我们写的代码不污染Java出厂自带的源代码,保证沙箱安全。
Java内存模式是符合计算机内存模型规范的。计算机都是多核的 ,每个CPU也都有一个高速缓存,这种设计保证了数据的访问性能,但是也造成了数据不一致的问题,在CPU层面上内存模型有一个规范就是保证CPU的写入动作对其他CPU是可见的,为了实现这种特性,计算机采用的是内存屏障的方式实现的,可以保证一个写操作可以被其他多个CPU是可见的。在Java内存模型中,他也是抽象的一个概念,具体不存在,也是符合这样的规范的,这组规范描述了在多线程代码中,哪些行为是正确的、合法的,以及多线程之间如何进行通信。每当创建一个线程JVM都会给这个线程分配一个工作空间,每个线程的私有数据都存放在这里,这块空间是线程私有的,Java内存模型规定所有的变量都要存储在主物理内存中,主物理内存是线程共享的区域,所有线程都可以访问,但线程对变量的操作必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量操作,操作完再写回给主内存。只要主物理内存的值变了,就要有一种通知机制,通知其他线程,同步最新的数据。这也就是可见性。 因为不同线程间无法访问对方的工作内存,所以线程间的通信必须通过主内存来完成。
什么是内存屏障:刷新高速缓存中的数据到内存,禁止指令重排序。通过内存屏障的功能,我们可以禁止一些不必要、或者会带来负面影响的重排序优化,在内存模型的范围内,实现更高的性能,同时保证程序的正确性。
什么是指令重排序:比如编译器会觉得把一个变量的写操作放在最后会更有效率,编译后,这个指令就在最后了
//对JMM可见性的验证 class ShareValue { volatile int num = 10; public void add10() { num += 10; } } public class Test { public static void main(String[] args) { ShareValue shareValue = new ShareValue(); new Thread(() -> { System.out.println("Come.in"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } shareValue.add10(); System.out.println("修改了值,这时num" + shareValue.num); },"AAA").start(); while (shareValue.num == 10) { } System.out.println("mission over, num" + shareValue.num); } }
程序计数器:
- 他是线程私有的,就是一个行号指示器,记录了方法之间的调用和执行情况,存储的是将要执行的指令代码
虚拟机栈:
线程私有的。每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 局部变量表:一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量。局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的
- 动态链接:执行某个方法时,会找到这个方法的直接引用,形成指向这个方法的动态链接,找到了这个方法的入口
- 操作数栈:运算某个值时会将这个值先压入操作数栈中
栈中存放8种基本类型的变量,对象的引用变量。
本地方法栈:
- 线程私有的。与虚拟机栈作用类似,虚拟机栈是为虚拟机执行Java方法服务,而本地方法栈是为虚拟机执行native方法服务(装native方法的栈),所有的native方法的实现靠的是C语言实现的第三方函数库。
堆:
线程共享的。存放的是对象实例及数组。堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。他也是垃圾回收的主要区域。
堆内存逻辑上分为:新生代+老年代+元空间。物理上有:新生代+老年代。
方法区:
用于存储已被虚拟机加载的类信息、常量、静态变量。
方法区只是一个规范,在不同虚拟机里面实现是不一样的,最典型的就是永久代(1.8以前)和元空间(1.8以后)
永久代是存放在JVM的堆内存中的,而元数据区是放在计算机的物理内存当中,所以元数据区不存在内存溢出的概念,因为启动jvm虚拟机的时候有可能只会给他分配256MB内存,但是元数据区不占用这个内存,它占用的是物理内存,有可能是4G、8G甚至更大。
元空间是一个常驻的内存区域,用于存放JDK自身携带的Class、Interface的元数据,也就是说存储的是运行环境必须的类信息,被装载在此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
为什么要移除永久代?
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
rt.jar包加载在元空间中,让我们随时可以使用。
内存溢出:程序在申请内存时,没有足够的内存空间
指系统内存不够用了
堆溢出:不断创建对象
栈溢出:不合理的递归,不断创建线程
方法去和运行时常量池溢出:通过String.itern()方法不断向常量池中添加常量。
内存泄露:程序在申请内存后,无法释放已申请的内存空间
分配出去的内存回收不了
原因:
长生命周期的对象持有短生命周期对象的引用
比如将ArrayList设置为静态变量,则容器中的对象在程序结束之前都不能被释放,从而造成内存泄露
连接未关闭:数据库连接,网络连接,IO连接
JVM性能调优
-Xms
:设置堆内存初始分配大小,默认为物理内存的1/64(等价于-XX:InitialHeapSize
)
-Xmx
:最大堆内存分配,默认为物理内存的1/4(等价于-XX:MaxHeapSize
)
-XX:+PrintGCDetails
:输出详细的GC处理日志注意:在我们的生产环境下,初始内存和最大内存一定是配置为一样大的,避免内存忽高忽低产生停顿(主程序运行时会使用内存,GC垃圾回收线程也要用内存,会产生抢占,导致停顿),理由是避免GC和应用程序争抢内存。所以默认和最大设置一样大小。
JVM的XX参数说明
Boolean类型
公式:
-XX: + 或者 -某个属性值
+表示开启,- 表示关闭
case
- 是否打印GC收集细节
-XX:+PrintGCDetails
(冒号后没空格)
- 上面表示是开启了,具体怎么用呢?
java -XX:+PrintGCDetails -version XXX
- 是否使用串行垃圾回收器
-XX:+UseSerialGC
KV设值类型
- 公式:
-XX:属性key=属性值value
- case:
-XX:MetaspaceSize=128m
(修改元空间的大小)-Xms1024m
-Xmx1024m
-XX:MaxTenuringThreshold=15
jinfo:查看当前运行程序的配置
- 公式:
jinfo -flag/flags 配置项 进程编号
(进程编号通过jps -l查询)
jinfo -flags
:打印出所有的参数- eg:
jinfo -flag PrintGCDetails 15185
如何查看一个正在运行中的Java程序,他的某个Jvm参数是否开启?值是多少?
- jps:
查看Java的后台进程
jps-l
- jinfo:
查询正在运行中的Java程序的信息
- jinfo -flag PrintGCDetails 4765
- jinfo -flag MetaspaceSize 4765
查看JVM系统默认值
- 主要查看初始参数默认值:
java -XX:+PrintFlagsInitial
- 主要查看修改和更新:
java -XX:+PrintFlagsFinal -version
java -XX:+PrintFlagsFinal -Xss128k 运行的Java类名字
:运行Java命令的同时打印出参数- = 表示没有改过,初始值;:= 表示人为改过或者JVM修改过的参数值
- 主要查看使用的是那种GC:‘java -XX:+PrintCommandLineFlags -version’
JVM常用的基本配置参数
-Xms
:设置堆内存初始分配大小,默认为物理内存的1/64(等价于-XX:InitialHeapSize
)-Xmx
:最大堆内存分配,默认为物理内存的1/4(等价于-XX:MaxHeapSize
)-Xss
:设置单个线程栈的大小,一般默认为512k~1024k(等价于-XX:ThreadStackSize
)
- 如果我们查看ThreadStackSize的值为0,就代表他的大小是系统的默认值(跟平台有关,1024K),如果是具体的数字,那就代表设置的值
-Xmn
:设置年轻代大小典型案例:
-Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC
-XX:+PrintGCDetails
:打印GC收集细节-XX:SurvivorRatio
:设置新生代中eden和survivor0、survivor1区的比例,默认-XX:SurvivorRatio=8
,即8:1:1-XX:NewRatio
:配置年轻代与老年代在堆结构的占比,默认-XX:NewRatio=2,新生代占1,老年代占2,新生代占整个堆的1/3-XX:MaxTenuringThreshold
:设置垃圾最大年龄,默认是15。设置的值只能设置在0-15之间。如果设置为0的话,则年轻代对象不经过survivor区,直接进入老年代,对于老年代较多的应用,可以提高效率。如果设置为一个较大的值,则年轻代对象会在Survivor区进行多次的复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概论。
对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。
1.Full GC
会对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。
2.导致Full GC的原因
1)年老代(Tenured)被写满
调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 。
2)持久代Pemanet Generation空间不足
增大Perm Gen空间,避免太多静态对象 , 控制好新生代和旧生代的比例
3)System.gc()被显示调用
垃圾回收不要手动触发,尽量依靠JVM自身的机制
面向对象有三大特征:封装继承多态,对于封装来说是建立在抽象基础上的,而继承建立在封装的基础上,而多态是建立在继承的基础上的。封装就是将相似对象的相同属性和方法提取出来重新装箱的过程,对数据的访问只能通过已定义的接口。(生成一个类)(包含设置访问控制的过程),提取相似对象的相同属性和方法的过程就是抽象的过程,与抽象相对应的就是实例化,实例化就是将这个箱子当做模板复制一份真实存在的东西,继承其实就是扩展,在原有的基础上改变或者增加原来的属性或方法,继承基于封装来做的。继承以后有了父子类才有了多态的概念,一个父类生成了两个子类,两个子类表现出来的状态不一样(龙生九子各有不同),这就是继承多态的概念,还有一个重载的概念,针对同名方法,通过参数列表的不一致,导致同名方法在执行过程中,参数的传递不同,执行的逻辑也不同。多态的体现就是重写重载和父类引用子类实例化。多态可以提高代码的可扩展性和可维护性
多态有两种实现:编译时多态(重载)和运行时多态(重写)。在运行时根据实际情况决定调用函数。编译时多态。他们的调用地址在编译期就绑定了。运行时多态,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法
面向过程是具体的,流程化的,在解决一个问题时,他需要一步一步分析,一步一步执行。
面向对象是模型化的,我们只需要抽象出一个类,这是一个封闭的盒子,这里拥有解决问题的方法,我们需要什么功能调用就行了,不需要具体实现,更不需要知道他是怎么实现的。
面向对象的有点的就是易扩展,易维护,易复用,可设计出低耦合的系统,是系统更加灵活。面向过程的有点就是性能好。
BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。BIO如果需要同时做很多事情(例如同时读很多文件,处理很多tcp请求等),就需要系统创建很多线程来完成对应的工作,因为BIO模型下一个线程同时只能做一个工作,(如果线程在执行过程中依赖于需要等待的资源,那么该线程会长期处于阻塞状态)所以一个线程可能会长期处于阻塞状态,我们知道在整个操作系统中,线程是系统执行的基本单位,在BIO模型下的线程阻塞就会导致系统线程的切换,从而对整个系统性能造成一定的影响。所以不适合高并发的应用。
多线程的上下文切换是指 CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程。
- 举例:这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。
NIO:他是同步非阻塞I/O模型。主要就是为了解决BIO的高并发问题,在BIO模型中,如果需要并发处理多个I/O请求,那就需要多线程来支持,NIO使用了多路复用器机制,对于我们的网络编程来说,多路复用器通过不断轮询各个连接的状态,(看有没有就绪的,如果有就直接去处理,不需要一个链接一个线程了。)只有在socketchannel有流可读或者可写时,才需要去处理它,就不需要一个链接一个线程了。而只是一个有效请求才会使用一个线程去处理。对于BIO来说,BIO是面向流的,而且是单向的,而NIO是面向缓冲区的。NIO抽象出了一个Channel通道,负责数据的传输的通道,并且是双向的。而数据是存储在缓冲区Buffer中传送的,通过缓冲区实现数据的存和读。对于NIO还有一个很重要的组件就是Selector,他是NIO相对于BIO实现多路复用的基础,Selector 运行单线程处理多个 Channel,通过不断轮询的方式看有没有就绪的链接。
- 举例:还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。
- 使用NIO可以做一个聊天服务器
AIO:异步非阻塞I/O模型。异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理,这样使用者就不需要不停地轮询了。
- 举例:对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。
JDK1.4开始引入了NIO类库,这里的NIO指的是Non-blcok IO,主要是使用Selector多路复用器来实现。Selector在Linux等主流操作系统上是通过epoll实现的。
NIO的实现流程,类似于select:
- 创建ServerSocketChannel监听客户端连接并绑定监听端口,设置为非阻塞模式。
- 创建Reactor线程,创建多路复用器(Selector)并启动线程。
- 将ServerSocketChannel注册到Reactor线程的Selector上。监听accept事件。
- Selector在线程run方法中无线循环轮询准备就绪的Key。
- Selector监听到新的客户端接入,处理新的请求,完成tcp三次握手,建立物理连接。
- 将新的客户端连接注册到Selector上,监听读操作。读取客户端发送的网络消息。
- 客户端发送的数据就绪则读取客户端请求,进行处理。
NIO采用可分配大小的缓冲区Buffer实现对数据的读写操作。
- 因为断开的发起者可以是服务器或者是客户端,链接的发起者只能是客户端
- 由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当客户端完成数据发送任务后,发送一个FIN来终止这一方向的连接,客户端收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上,服务器仍然能够发送数据,直到服务器一方向也发送了FIN,并且收到了ACK才是完全的断开链接了。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。
- 发送Fin只能表示我以后不发数据了,但不代表我数据收完了。ACK表示我的数据收完了,并且不再收数据了
CountDownLatch类似于一个计数器,它一般用于某个线程A等待其他多个线程执行完了之后它才能执行。这个类有两个方法,countDown()方法,每次调用他,count值就会-1,还有一个await()方法,某个线程调用他了以后就会被挂起,直到count值减为0时当前线程才能执行。这个类还有一个重载方法,就是可以设置一个超时时间,等到达某个时间点count还没减到0当前线程就向下执行。
CyclicBarrier,他一般用于一组线程互相等待到达某种状态时然后这组线程在同时执行。它里面有一个awiat()方法,当某个线程调用了await方法那么这个线程就处于barrier状态了。只有当一组所有的线程都到达barrier了才能同时执行
CountDownLatch不能重复使用,而CyclicBarrier可以重复使用
Semaphore,可以控制同事访问线程的个数,通过acquire()获取一个许可,如果没有许可能够或得到,那他就一直等待,知道获得到许可。然后通过release释放一个许可。这两个方法都会被阻塞,还有一种类似的非阻塞的方法就是tryAcquire()方法,他会返回一个boolean值,如果获得到了许可就返回true,反之亦然。(可以通过一个例子理解:一个工厂有5台及其,但是有8个工人,一台机器同时只能被一人使用。可以把机器数理解为Semaphore对象创建时的参数,即许可,把工人数当做线程)Semaphore类似于锁,如果创建他时参数设置为1那他就跟sync差不多了。( synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源)
public class Main { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(5); for (int i = 0; i < 5; i++) { new Thread(){ @Override public void run() { System.out.println(Thread.currentThread().getName() + "走啦"); countDownLatch.countDown(); } }.start(); } countDownLatch.await(); System.out.println(Thread.currentThread().getName()+"锁门"); } } public class Main { public static void main(String[] args) throws InterruptedException { CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() { @Override public void run() { System.out.println("出发"); } }); for (int i = 0; i < 5; i++) { new Thread(){ @Override public void run() { System.out.println(Thread.currentThread().getName() + "到啦"); try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } }.start(); } } } public class Main { public static void main(String[] args) { Semaphore semaphore = new Semaphore(3); for (int i = 0 ; i < 6; i++) { new Thread(()->{ try { semaphore.acquire(); System.out.println(Thread.currentThread().getName()+"停车拉"); Thread.sleep(3000); System.out.println(Thread.currentThread().getName()+"走啦"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); } },"name:"+i).start(); } } }
这里所谓的垃圾指的是在系统运行过程当中所产生的一些无用的对象,这些对象占据着一定的内存空间
在Java中,后台专门有一个独立的低优先级的用于垃圾回收的线程来进行监控、扫描,自动将一些无用的内存进行释放,这就是垃圾收集的一个基本思想,目的在于防止由程序猿引入的人为的内存泄露。