涉及原理的东西比较复杂,且Java中很多native方法,JDK版本差异等,难免有误。面试有地方错了也不要紧,错也要笃定,但不要犟
Java程序运行基本过程(windows)
- java.exe调用jvm.dll创建虚拟机
- 创建引导类加载器实例
- 创建Java启动器实例(sun.misc.Launcher,该类创建其他类加载器:
sun.misc.Launcher$ExtClassLoader 和sun.misc.Launcher$AppClassLoader 两种实现) - 调用sun.misc.Launcher.getLauncher()获取类加载器,自己写的类都是AppClassLoader
- 类加载器加载(loadClass)类
- 加载完成,JVM执行类的main()方法入口
- 程序结束,JVM销毁
类加载过程(loadClass)
- 加载
- 验证
- 准备
- 解析
- 初始化
- 使用
- 卸载
加载: 1.通过一个类的全限定名来获取此类的二进制字节流 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证(校验字节码的正确性): 1.文件格式验证 2.元数据验证 3.字节码验证 4.符号引用验证
准备: 1.为类变量分配内存并设置类变量初始值
解析:(将常量池内的符号引用替换为直接引用) 1. 类或接口的解析 2. 字段解析 3. 类方法解析 4. 接口方法解析
初始化: 1. 执行类构造器<clinit>() 方法
类加载器
- 引导类加载器(启动类加载器):负责加载JRE的
lib 路径下的内容 - 扩展类加载器:负责加载jre的
lib/ext 路径下的内容 - 应用类加载器:负责加载classPath路径下的类包
- 自定义类加载器
双亲委派机制
默认情况下(没有自定义类加载器打破双亲委派),要加载一个类先由AppClassLoader检查是否加载了类,如果没有则委派给ExtClassLoader,加载了就返回,以此类推,如果BootStrapClassLoader也没有加载过,就会去lib 下搜索,找到则加载返回,未找到则委派给ExtClassLoader,以此类推。 双亲委派的双在于向上委派搜索和向下委派加载
双亲委派的作用
- 沙箱安全机制:BootstrapClassLoader有加载类的最高优先权,ExtClassLoader次之,保证Java的核心类库不被篡改
- 避免类重复加载(效率更高):不用双亲委派,每次都从BootstrapClassLoader开始搜索加载也可以保证安全,但是通常我们自己写了很多类,都是AppClassLoader加载的,这样就被置于加载链的末端,显然是不合理的
全盘委托机制
当一个ClassLoader加载一个类时,除非显式使用另一个ClassLoader,否则其依赖类和引用类也会使用这个类加载器加载
打破双亲委派
自定义一个类加载器,集成ClassLoader 类,重写loadClass() 方法,不向上委派
Tomcat打破双亲委派
双亲委派有一个特性是不会重复加载类,即全限定名相同。但是tomcat中极有可能部署多个应用,甚至是不同的Spring版本等,显然必须打破双亲委派才能运行应用。
JDK体系结构
JDK包含了jre和一些工具(javac,javap,javadoc…),jre是运行已编译好的Java程序所需的所有内容的集合,JVM是Java虚拟机,包含于jre。
Java语言的跨平台特性
Java语言跨平台的关键是JVM针对不同的操作系统有特定的实现,因此,只要将Java代码编译成JVM能够识别处理的字节码就可以在不同的操作系统运行。JVM屏蔽了不同操作系统底层特性
jvm体系结构/组成
- 类加载子系统:根据类的全限定名加载类
- 运行时数据区:JVM的内存
- 字节码执行引擎:执行.class文件中的指令
- 本地接口:由C/C++等其它编程语言实现的功能
JVM内存模型
组成:
- 堆
- 虚拟机栈
- 本地方法栈
- 方法区
- 程序计数器
堆: ??堆是jvm内存中非常重要的一块,存放了几乎所有对象,也是主要的jvm调优对象。堆区分为年轻代和老年代(默认1:2),年轻代又分为Eden区,Survivor0区和Survivor1区(默认8:1:1)。 ??一个对象在堆区中的完整生命历程是:在Eden区分配空间,因为minor gc对象被移动到Survivor区并在survivor0和survivor1区来回复制,分代年龄达到15后进入老年代。如果对象进入老年代后失去引用变垃圾,则在full gc时被回收。 ??需要补充的是很多对象在年轻代就会被回收,也有些情况会直接进入老年代,如大对象。实际情况很复杂,有多种可能。
虚拟机栈: ??虚拟机栈这里也可以称作线程栈。虚拟机栈被分为很多栈帧,这些栈帧都是线程独享的,也就是说每个线程都有自己专属的栈帧区域。 ??栈帧又分为局部变量表、操作数栈、动态链接和方法出口。局部变量表存方法中的局部变量,操作数栈用于辅助计算,动态链接存放一些在编译期无法确定的方法的指向,方法出口是指调用其它方法后执行完成后需要回到的位置继续运行。
本地方法栈 与虚拟机栈类似,但是它是服务于本地方法的
方法区 存放静态变量,常量和类信息等
程序计数器 线程独有,记录线程所执行字节码的行号。(想想方法调用,肯定不是依次执行的,那总需要一个标记吧)程序计数器内容由字节码执行引擎负责修改。
jvm内存参数
X越多说明越不稳定
堆: -Xms:初始堆空间大小,默认物理内存1/64 -Xmx:最大堆空间大小,默认物理内存1/4 -Xmn:年轻代大小 -XX:NewRatio:新生代与老年代的比例,如–XX:NewRatio=2(新:老=1:2),老年代大full gc慢,老年代小频繁full gc。 -XX:SurvivorRation:如-XX:SurvivorRation=8,一个survivor区占Eden的1/8
栈: -Xss:每个线程栈空间大小,-Xss1m,每个线程都有独立的栈空间
方法区: -XX:MaxMetaspaceSize:设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小 -XX:MetaspaceSize:指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。
JVM参数设置
没有固定公式,只能根据程序内对象的大小,存活时间等分析,然后避免频繁full gc等。没有高并发场景其实都不用太管。
Java对象的创建过程
-
类加载检查:如果未加载则加载类,如果已加载则开始分配内存 -
分配内存:在新生代(年轻代)为对象划分一块内存空间。 划分内存:
- 指针碰撞:内存完全规整,将指针往后挪对象需要空间大小即可
- 空闲列表:内存并不规整,虚拟机维护一个未使用空间的列表,分配空间时在列表中查找空间
解决并发问题:
- CAS+失败重试
- 本地线程分配缓冲TLAB:每个线程预先在堆中分配一小块内存
-
初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 -
设置对象头:初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding) -
执行<init> 方法:对象按照程序员的意愿进行初始化
查看对象信息
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
ClassLayout layout = ClassLayout.parseInstance(new Object());
System.out.println(layout.toPrintable());
指针压缩
启用指针压缩:-XX:+UseCompressedOops(默认开启) 禁止指针压缩:-XX:-UseCompressedOops
为什么要指针压缩
指针压缩:在64位平台的HotSpot中使用32位指针,实际存储用64位。 主要是提高64位平台的效率,大指针在内存中和缓存中的移动代价更大,gc压力也更大
堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间 .堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址
对象在栈内存分配及逃逸分析
??为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力
对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,也可能直接使用销毁。
大对象直接进入老年代
大对象不再新生代分配空间,直接在老年代分配。 因为新生代空间本来就小,更容易触发minor gc,且大对象复制操作对效率影响更大。
分代年龄
对象在新生代经历一次minor gc,分代年龄就加1。默认15进老年代。
对象动态年龄判断
主要是希望那些可能是长期存活的对象,尽早进入老年代 -XX:TargetSurvivorRatio:目标存活率,默认为50% 一批survivor区中对象分代年龄从小到大累加其占用内存大小,超过这块survivor区内存大小的总和的50%(-XX:TargetSurvivorRatio)即停止,设此时的分代年龄是N,那么将N及以上年龄的对象移入老年代。
老年代空间分配担保机制
每次minor gc之前jvm计算老年代剩下可用空间大小; 如果老年代可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象) ; ??如果:设置了-XX:-HandlePromotionFailure(jdk8默认设置),则会比较老年代可用空间和之前每次minor gc进入老年代空间的对象总大小的平均值。小于平均值则触发full gc,如果full gc后依然小于则OOM。 ??如果没设置,直接full gc。
对象回收算法
- 引用计数法:有地方引用一次就+1,失效就-1。简单高效,但无法解决循环引用问题。
- 可达性分析算法:从GC ROOTS出发向下搜索,可到达的都标记为非垃圾对象,其余都是垃圾对象。
GC ROOTS:线程栈的本地变量、静态变量、本地方法栈的变量等等
常见引用类型
- 强引用:普通的变量引用
- 软引用:将对象用SoftReference软引用类型的对象包裹
- 弱引用:将对象用WeakReference软引用类型的对象包裹
- 虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
finalize()
对象可以自救的一次性方法
如果在回收对象的时候,某些对象没有与GC ROOTS引用链链接:
- 第一次标记并筛选:未覆写finalize()方法直接回收
- 第二次标记:执行finalize()方法,如果与GC ROOTS引用链建立连接则复活
垃圾收集算法
基于分代收集理论:
- 复制算法:内存平均分成两份,来回复制;内存规整但空间浪费一半。
- 标记清除算法:将内存中的垃圾对象清除掉,简单,但存在内存碎片化问题,另外标记过程也会影响效率
- 标记整理算法:将存活对象向一端移动,然后直接清理掉边界以外的内存
垃圾收集器
新生代: Serial、ParNew、Parallel、Epsilon
新生代&老年代: G1、ZGC、Shenandoah
老年代: CMS、Serial Old、Parallel old
垃圾收集器——Serial
单线程 新生代:复制算法 老年代:标记整理
Serial Old收集器是Serial收集器的老年代版本,它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。
垃圾收集器——parallel
多线程,JDK8默认的新生代和老年代收集器 Parallel收集器其实就是Serial收集器的多线程版本 新生代:复制算法 老年代:标记整理
垃圾收集器——parnew
ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。
垃圾收集器——CMS(重点)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
回收过程:
- 初始标记:STW,记录GC ROOTS直接能引用的对象(很快啊)
- 并发标记:根据
初始标记 阶段的记录,并发遍历对象图。(存在问题:因为是并发的,所有有些对象的状态会变化) - 重新标记:修正
并发标记 阶段由于程序运行导致标记变动的对象的标记记录。(三色标记) - 并发清理:并发清理掉未标记的对象
- 并发重置:重置本次gc的标记
优点:并发收集,低停顿时间 缺点:(尽管如此,CMS还是一款优秀的收集器) 对CPU资源敏感 无法处理浮动垃圾(并发标记、并发清理阶段产生的) 采用标记清除算法,内存碎片化(-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理)
参数 -XX:+UseConcMarkSweepGC:启用CMS -XX:ConcGCThreads:并发的GC线程数 -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片) -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次 -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比) -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整 -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,降低CMS GC标记阶段(也会对年轻代一起做标记,如果在minor gc就干掉了很多对垃圾对象,标记阶段就会减少一些标记时间)时的开销,一般CMS的GC耗时 80%都在标记阶段 -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW
三色标记
- 黑:被垃圾收集器访问过,且所有对象都已经扫描
- 灰:被垃圾收集器访问过,但至少还有一个引用未被扫描
- 白:未被垃圾收集器访问过
|