Java进阶(一)
一. JVM
1.1 为什么学习JVM
首先面试需要
高级程序员也更需要了解JVM
1.2 JVM作用
JVM负责把编译后的字节转换为机器码
1.3 JVM内部构造
1.3.1 类加载部分:
负责把硬盘上字节码加载到内存中(运行时数据区)
1.3.2 运行时数据区:
负责把存储运行时产生的各种数据:类信息,对象信息,方法信息
1.3.3 执行引擎:
负责将字节码转为机器码
1.3.4 本地方法接口:
调用本地方法 Object类中的hashCode()–对象的的内存地址
java">public native int hashCode(){}
private native int read()throws IOException{}
1.3.5 垃圾回收部分
二. JVM类加载
类加载系统,负责将硬盘上的字节码文件加载到jvm中,生成类的Class对象,存储在方法区
2.1 类加载过程
2.1.1 加载
以二进制文件流进行读取
在内存中为类生成Class对象
2.1.2 链接
验证:验证字节码的结构是否正确
准备:为类的静态属性进行初始化赋值
解析:把字节码的符号引用替换成内存中的直接引用地址
2.1.3 初始化
初始化阶段主要是为类中静态成员进行赋值
因为类加载执行完初始化阶段才说明类加载完成了
2.2 类在哪些情况下会被加载
1.调用类中静态成员(变量,方法)
2.new一个类的对象
3.在类中执行main函数
4.反射加载类,Class.forName(“地址”);
5.子类被加载
类在以下两种情况,是不会被加载的:
java">//1.类作为数组类型
Demo demo[] =new Demo[10]; //new的数组对象不是Demo对象//2.只是访问类中的静态的常量
System.out.println(Demo.P); //java给优化了不加载整个类了,只获取到用到的静态常量
2.3 类加载器分类
站在JVM角度上看,类加载器可以分为两种:
1.引导类加载器(启动类加载器 Bootstrap ClassLoader)
2.其他所有类加载器,这些类加载器由java语言实现,独立存在于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader
站在开发人员角度来看,类加载器分的更为细致:
-
引导类加载器(启动类加载器 BootStrap ClassLoader):
使用C/C++语言实现,嵌套在JVM内部,用来加载java核心类库
负责加载扩展类加载器和应用类加载器,并为他们指定父类加载器
引用类加载器出于安全考虑只存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存储放的类
-
扩展类加载器(Extension ClassLoader):
由Java语言编写,派生于ClassLoader类
从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK系统安装目录的jre/lib/ext子目录(扩展目录下加载类库),如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载
-
用用程序类加载器(系统类加载器 Application ClassLoader)
Java语言编写,由sun,misc.Launcher$AppClassLoader实现
派生于ClassLoader类
加载我们定义的类,用于加载用户类路径( classpath)上所有的类
该类加载器是程序中默认的类加载器
ClassLoader类是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
2.4 双亲委派:
2.4.1 双亲委派机制:
当加载一个类时,总是先让他的父级类加载器去加载,确保把系统中类优先加载
直到父类加载器找不到类时,再逐级向下,让子级类加载器加载
如果子级也找不到,最终抛类找不到异常
为什么要如此?
因为要防止我们自己写的类替换了系统中的核心类
2.4.2 如何避免双亲委派?
在ClassLoader 类中涉及类加载的方法有两个,loadClass(String name), findClass(String name),这两个方法并没有被final修饰,也就表示其他子类可以 重写
重写findClass 方法 我们可以通过自定义类加载重写方法打破双亲委派机制
再例如tomcat等都有自己定义的类加载器
3. JVM运行时数据区
3.1 运行时数据区组成概述
JVM的运行时数据区,不通虚拟机实现可能会略微不同
Java8虚拟机规范规定,Java虚拟机所管理的内存将会包括以下几个区域
3.2 程序计数器
用来记录每一个线程执行的指令位置,由执行引擎读取下一条指令
速度是最快的,是线程私有的(每一个线程都有一个程序计数器)
此区域不会出现内存溢出(不够用),也不会出现垃圾回收
3.3 虚拟机栈
3.3.1 作用:
栈是运行的,解决程序方法执行
调用方法入栈,运行结束后开始出栈,main方法通常在栈底
一个方法就是一个栈帧,在栈帧中存储局部变量,运行结果等
3.3.2 特点:
虚拟机栈也是线程私有的,线程之间互相隔离
栈区域不存在垃圾回收,但是存在溢出问题
3.3.3 栈中存储什么内容?
局部变量表 int a=1;
操作数栈(计算过程)
方法返回地址
3.4 本地方法栈
是用来执行调用的本地方法的
是线程私有的,不会存在垃圾回收,可能会出现内存溢出
本地方法是用c语言写的
具体做法是在Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库
3.5 Java堆内存
3.5.1 堆内存概述
所有对象的实例都应当在运行时分配在堆上
堆也是Java内存管理的核心区域,是JVM管理的最大一块内存空间
堆内存的大小可以调节,例如:-Xms:10m(堆起始大小)-Xmx:30m(堆最大内 存大小)
所有的线程共享Java堆
堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
堆是GC(Garbage Collection,垃圾收集器)执行垃回收的重点区域
3.5.2 堆内存区域划分
Java8 及之后堆内存分为:新生区(新生代)+老年区(老年代) 新生区分为Eden(伊甸园)区和Survivor(幸存者)区
3.5.3 为什么进行分区?
将对象根据存活概率进行分类,对存活时间长的对象放到一个固定区,从而减少扫描垃圾时间及GC概率。针对分类进行不同的垃圾回收算法。
3.5.4 对象创造内存分配过程
JVM的设计者们不仅需要考虑内存如 何分配,在哪分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考 虑GC执行完内存回收后是否会在内存空间中产生内存碎片.
- new的新对象先放到伊甸园,此区大小有限制
- 当伊甸园达到空间限制时,会垃圾回收,把不再引用的对象进行销毁,然后再加载新的对象
- 然后将伊甸园中的剩余对象移动到幸存者0
- 如果再次发起垃圾回收,如果上次进入到幸存者0的对象没有被回收就会被放到幸存者1,每次保证要有一个幸存者区是空的
- 如果又垃圾回收,就会重新放回幸存者0,然后又去幸存者1
- 什么时候去老年代呢,默认是进行15次这样的情况之后,也可以设置参数,最大值是15.在对象头中,它是由4位数据堆GC年龄进行保存的,所以最大值是1111(15),当对象的GC年龄达到15时,就会去老年代
- 在老年代内存不足时,会触发老年代的内存清理
- 若老年代执行了GC之后发现依然无法进行对象存储,会对堆进行GC,之后依然无法进行对象存储,就会发生OOM异常(Java.lang.OutOfMemoryError:Java heap space)。比如死循环
3.5.5 堆空间的参数设置
官网地址: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial查看所有参数的默认初始值
-Xms:初始堆空间内存
-Xmx:最大堆空间内存
-Xmn:设置新生代的大小
-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails 输出详细的 GC处理日志
3.6 方法区
3.6.1 方法区的基本理解
方法区是一个被线程共享的内存区域,主要存储加载类字节码,class/method/field等元数据,static final常量,static变量,即时编译器编译后的代码等数据
尽管所有的方法区在逻辑上是属于堆的一部 分,但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的 就是要和堆分开。
java_294">所以,方法区看作是一块独立于java堆的内存空间。
方法区在JVM启动时被创建,并且它的实际物理内存空间中和Java堆区一样不连续
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出的错误
关闭JVM就会释放这个区域的内存
3.6.2 方法区大小设置
Java方法区大小不固定,JVM可以根据应用的需要动态调整
元数据区大小可以使用参数-XX:MetaspaceSize指定
3.6.3 方法区的垃圾回收
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被 使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
1.该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子 类的实例。
2.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加 载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
3.该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通 过反射访问该类的方法。
4. 本地方法接口
4.1 什么是本地方法
本地方法就是一个java调用非java代码的接口,一个Native Method底层是C语言或其他底层语言实现的。
定义一个Native Method并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面用C/C++实现的。
关键字native可以与其他所有java标识符连用,但是abstract除外。
java">Object hashCode() //获取对象内存地址的方法
read(); //IO中读文件(输入文件 操作硬盘)
native void start(); //启动线程,就是把这个线程注册到操作系统
4.2 为什么要使用Native Method
Java属于应用层语言,Java应用需要与java外面的环境交互时(对硬件系统资源进行调用),就需要本地方法,它提供了一个非常简洁的接口,我们就无需去了解java应用之外的繁琐细节
5. 执行引擎
5.1 概述
执行引擎时java虚拟机核心部件之一,主要是将字节码转为机器码,可以通过解释/编译两种方式实现字节码转为机器码
java程序执行过程中经过两次编译:
5.2 字节码转为机器码两种方式:
解释器(解释执行): 对字节码逐行进行解释翻译,重复性代码也需要每次都解释执行,效率很低
编译器(JIT 编译执行): 对某段字节码进行整体编译,然后存储起来,以后使用就不用编译了。编译器会针对执行过程中的热点代码进行编译,然后缓存起来。
5.3 为什么要使用解释和编译两种并存的设计?
程序开始运行时,需要解释器这种直接立即发挥作用的投入使用
而编译器虽然效率高,但是前期需要花费时间先对热点代码进行跟踪和编译
6. 垃圾回收
6.1 什么是垃圾对象
就是一个不被任何引用指向的对象,垃圾对象如果不清理,新的对象就没用足够的空间,会导致内存溢出问题
6.2 垃圾回收发展
早期c/c++内存管理都是使用时申请,使用完手动释放。虽然这样对内存管理更加精确和效率高,但是这样使得程序员负担变大,需要总关注内存是否释放。
后来发展为自动回收:
java,c#都是自动回收,这样虽然解放了程序员,但是会占用一些内存空间存放垃圾,也降低了程序员管理内存的能力
6.3 哪些区域会出现垃圾回收
堆:对象,频繁回收年轻代,较少回收老年代。
方法区:类信息卸载,整堆收集时,会进行回收 FULL GC
6.4 内存溢出与内存泄露
内存溢出:内存不够用了。
内存泄露:系统中用不到的但是又不能回收的对象
eg:单例对象
数据库连接对象,IO流,socket (只要是提供close()的类)
用完后如果没有关闭,垃圾回收器也不能主动回收这些对象
//内存泄露虽然不能直接导致内存溢出发生,但是长期不处理最终会使内存溢出
6.5 Stop the World
垃圾回收时,会经历两个阶段:一个是标记阶段,一个是回收阶段(这是垃圾回收需要用到的算法,处理何时进行垃圾回收)
标记和回收阶段,都需要我们的用户线程暂停,不暂停可能会出现错标和漏标的情况
6.6 垃圾回收相关算法
6.6.1 垃圾标记阶段
将虚拟机中不被任何引用指向的对象标记出来,等垃圾回收的时候就会将其回收
java_443">6.6.2 引用计数算法(存在缺陷,没有被java虚拟机使用)
设计思想:在对象中维护一个整数计数器变量 当有引用指向对象就++,当失去指向就–,当减少为0时说明此对象没有被引用便可以垃圾回收了。
优点:设计简单,容易分辨
缺点:需要维护一个变量存储引用数量,频繁修改引用计数器变量,占空间还耗时,还无法解决循环引用问题
循环引用问题:a引用b,b引用c,c引用a。此时他们的计数器都是1但是他们独立于其他对象,已经不被需要了,这时候垃圾回收缺回收不了它们
6.6.3 可达性分析算法(根搜索算法)
设计思想:从GCRoots对象开始向下寻找,只要能找到某个对象说明此对象是被需要的,与这个GCRoots根对象没有联系的对象说明是需要回收的
可以作为GCRoots(根对象)的对象:
- 虚拟机栈中(被调用的方法)所使用的对象
- 类中的静态属性
- 虚拟机中使用的系统类对象
6.6.4 finalize()
这个方法是对象被回收前,虚拟机自动调用的。在某些对象被回收前还需要执行一些操作就可以在此方法中编写。
finalize()可以在子类中重写
finalize()只会被调用一次。就在第一次被判定为垃圾时,调用finalize()是对象复活,然后对象有可能再次被回收,当下次再被判定为垃圾对象时就不会再调用finalize()了。
可以将对象分为三种状态:
- 可触及:被GCRoots引用的,不是垃圾对象
- 可复活的:被判定为垃圾但是finalize()方法还没有被diaoyongguo
- 不可触及的:被判定为垃圾并且finalize()方法已经被调用过。
6.7 垃圾回收阶段算法
6.7.1 标记-复制算法:
把内存中的存活对象复制到未被使用的内存块中,然后直接把正在使用的内存块中的所有对象清除,再交换两个内存的角色,就完成了垃圾回收。
复制算法适合于存活对象少,垃圾对象多的情况(新生代)
6.7.2 标记-清除算法:
把需要清除的对象地址保存在空闲的地址列表里,下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够就存放(也就是覆盖原有的地址)
6.7.3 标记-压缩算法:
面对老年代多数都是存活对象的情况,如果再使用复制算法成本高,所以我们基于老年代的特性需要其他算法。标记清除算法可以应用再老年代,但是在执行完后内存回收后还会产生内存碎片。
所以我们需要标记压缩算法:
将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外的所有空间。
//标记压缩算法等于标记清除算法执行完后再进行一次内存碎片整理,因此可以称为标记-清除-压缩算法。
二者的本质在于清除算法是非移动式的,标记压缩算法是移动式的。
6.7.4 什么时候用什么算法?
新生代:标记-复制
老年代:标记-清除------>标记-压缩
先使用标记清除,当老年代空间不足,或者不能存储一个较大的对象时使用标记压缩
6.8 垃圾回收器
6.8.1 什么是垃圾回收器?
垃圾回收器是对垃圾回收过程的实践者
6.8.2 垃圾回收器分类
线程数量:单线程和多线程
工作模式:
-
独占式:垃圾回收线程执行时,其他用户线程需要STW
-
并发式:可以并发执行
分区角度:新生代和老年代
6.8.3 性能指标
吞吐量 用户线程暂停时间(重点) 回收时内存开销
6.8.4 各种垃圾回收器
Serial: 单线程,新生代
Serial Old:单线程,老年代
Parallel Scavenge, ParNew:多线程,新生代
Parallel Old:多线程老年代
CMS(并发标记清除):多线程,老年代。并开创了垃圾收集线程与用户线程并行的先例
CMS过程:
初始标记—独占执行
并发标记—并发执行
重新标记—独占执行
并发清除—并发执行
6.8.5 G1(Garbage First)
是一个并行回收器,继承了CMS中,垃圾收集线程和 用户线程并行执行的特点,减少了用户线程暂停的时间。同时,将新生代和老年代的各个区域又划分成多个更小的区域,对每个区域进行跟踪。优先回收简直高的区域(垃圾多的区域,例如可以把伊甸园区分成好几个小的区域)
提升了回收效率,提高了吞吐量,不再区分年轻代和老年代,可以做到对整个堆进行回收。非常适合服务器端程序,大型项目。
6.8.6 查看和设置JVM垃圾回收器
- 打印默认垃圾回收器:-XX:+PrintCommandLineFlags-version
JDK 8 默认的垃圾回收器
年轻代使用 Parallel Scavenge GC
老年代使用 Parallel Old GC
-
打印垃圾回收详细信息:-XX:+PrintGCDetails-version
-
设置默认垃圾回收器
Serial 回收器:-XX:+UseSerialGC 年轻代使用Serial GC, 老年代使用Serial Old GC
ParNew 回收器-XX:+UseParNewGC 年轻代使用 ParNew GC,不影响老年代。
CMS回收器-XX:+UseConcMarkSweepGC 老年代使用 CMS GC。
G1回收器-XX:+UseG1GC 手动指定使用 G1 收集器执行内存回收任务。
-XX:G1HeapRegionSize 设置每个 Region 的大小