目录
一、JAVA程序的跨平台特性
-
在Java虚拟机中执行的指令, 称之为Java字节码指令
-
下面显示了同一个Java程序,被编译为一组Java字节码的集合之后,可以通过Java虚拟机运行于不同的操作系统上, 它以Java虚拟机为中介, 实现了跨平台的特性
二、JVM的基本结构
1.类加载⼦系统
负责从⽂件系统或者⽹络中加载Class信息
2.方法区
-
加载的Class信息并存放到方法区内存空间
-
运⾏时常量池(字符串字⾯量和数字常量)也会放到其中
-
公有的
3.Java堆
-
虚拟机启动时建立
-
基本上所有的对象实例都放在Java堆中
-
公有的
4.Java栈、虚拟机栈
-
与方法调用息息相关
-
Java栈——栈帧
-
方法入参
-
局部变量
-
中间计算的临时数据
-
常量池指针
-
正常方法返回处理
-
异常的状态处理
-
-
私有的——方法和线程息息相关
5.本地方法栈
-
与Java栈类似且私有
-
native method都放在本地方法栈中
6.计数器
-
普通方法
指向正在被执行的字节码的位置
-
native方法
undefined
7.直接内存
-
与NIO是息息相关的
-
是堆外内存,直接向系统申请内存空间
-
与Java堆相比
-
优点
读写访问快
-
缺点
内存空间申请上速度慢
-
-
应⽤场景
内存空间申请次数少,并且访问较为频繁的情况
三、JVM类加载流程
1.主动加载的方式
-
new对象
-
利用反射、clone
-
初始化子类时, 父类会被初始化
-
调用类的静态方法
2.类加载内容
-
加载Classloader
读取文件
-
通过类的全路径名, 获取到类的二进制数据流
-
获得类信息后进行解析, 转化为方法区内部的数据结构
-
创建java.lang.Class的实例
-
-
验证
判断字节码是否合法、规范
-
验证项
-
格式
-
语义
-
字节码
-
查看指令: javap -verbose Xxx.class
-
-
准备
分配相应的内存空间
-
解析
将符号引用转化为直接引用
-
初始化
类已经被加载到系统中了, 开始执行字节码
3.符号引用和直接引用
-
符号引用
以一组符号来描述应用的目标直接的关系
D:\...\target\classes>javap -verbose Student.class Classfile /D:/.../target/classes/Student.class Last modified 2021-11-26; size 1039 bytes MD5 checksum abe7a514b71443d5b5136583dbc4dead Compiled from "Student.java" public class day1.Student minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #12.#35 // java/lang/Object."<init>":()V #2 = Fieldref #11.#36 // day1/Student.name:Ljava/lang/String; #3 = Fieldref #11.#37 // day1/Student.age:I #4 = Fieldref #38.#39 // java/lang/System.out:Ljava/io/PrintStream; #5 = Class #40 // java/lang/StringBuilder #6 = Methodref #5.#35 // java/lang/StringBuilder."<init>":()V #7 = String #41 // name= #8 = Methodref #5.#42 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #9 = Methodref #5.#43 // java/lang/StringBuilder.toString:()Ljava/lang/String; #10 = Methodref #44.#45 // java/io/PrintStream.println:(Ljava/lang/String;)V . . .
-
直接引用
通过对Class文件加载到内存后, 对符号引用的转换, 最终得到直接引用(即地址)
-
为什么有符号引⽤
-
java类被编译成Class⽂件后, 并不知道具体的引⽤地址, 所以就以符号引⽤作为代替
-
在
解析阶段
, 才把符号引⽤转换为了真正的地址, 即直接引⽤
-
四、JMM内存模型
1.程序计数器
-
是当前线程所执行的字节码的行号指示器, 指向虚拟机字节码指令的位置
-
被分配了一块较小的内存空间
-
针对于方法
-
非Native方法
是当前线程执行的字节码的行号指示器
-
Native方法
是undefined
-
-
每个线程都有自己独立的程序计数器, 所以该内存是线程私有的
-
该区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域
2.虚拟机栈&本地方法栈
-
虚拟机栈
-
为执行Java方法服务的, 是描述方法执行的内存模型
-
线程私有的
-
每次函数调用的数据都是通过栈传递的
-
栈中保存的主要内容是
栈帧
, 数据结构是先进后出
-
入栈 —— 方法调用
-
出栈 —— 结果返回
-
栈顶 —— 正在执行的函数
-
-
每个方法在执行的同时都会创建一个栈帧用于存储
局部变量表
、操作数栈
、帧数据区
,动态链接
等信息
-
-
本地方法栈
- 为native方法服务的
2.1.栈内存测试
-
无参
public class StackOverflowTest1 { private static int count=0; public static void main(String[] args) { try { while (true) { count(); } } catch (Throwable e) { System.out.println("count = " + count); throw e; } } private static void count() { count++; count(); } }
-
设置最大栈内存为-Xss160K
count = 1900 Exception in thread "main" java.lang.StackOverflowError at stack.StackOverflowTest.count(StackOverflowTest.java:30) at stack.StackOverflowTest.count(StackOverflowTest.java:31)
-
设置最大栈内存为-Xss256K
count = 2737 Exception in thread "main" java.lang.StackOverflowError at stack.StackOverflowTest.count(StackOverflowTest.java:30) at stack.StackOverflowTest.count(StackOverflowTest.java:31)
-
-
有参
public class StackOverflowTest2 { private static int count = 0; public static void main(String[] args) { try { while (true) { count(1L, 2L, 3L, 4L, 5L); } } catch (Throwable e) { System.out.println("count = " + count); throw e; } } private static void count(long arg1, long arg2, long arg3, long arg4, long arg5) { long num1 = 1; long num2 = 2; long num3 = 3; long num4 = 4; long num5 = 5; long num6 = 6; long num7 = 7; long num8 = 8; count++; count(arg1, arg2, arg3, arg4, arg5); } }
-
设置最大栈内存为-Xss160K
count = 532 Exception in thread "main" java.lang.StackOverflowError at stack.StackOverflowTest2.count(StackOverflowTest2.java:27) at stack.StackOverflowTest2.count(StackOverflowTest2.java:36)
-
设置最大栈内存为-Xss256K
count = 755 Exception in thread "main" java.lang.StackOverflowError at stack.StackOverflowTest2.count(StackOverflowTest2.java:27) at stack.StackOverflowTest2.count(StackOverflowTest2.java:36)
-
3.Java堆
-
运行数据时,
几乎所有的对象实例都在Java堆中
-
完全自动化管理
, 通过垃圾回收机制, 垃圾对象会被自动清理, 不需要显示地释放 -
堆是垃圾收集器
进行GC的最重要的内存区域
-
Java堆划分
-
新生代
((Eden区, S0区, S1区)(默认8:1:1))(1/3) -
老年代
(2/3)
-
-
分配机制
-
绝大数情况下, 对象首先被分配在Eden区
-
在一次新生代GC后, 如果对象还存活则会进入S0区或S1区
-
之后, 每进行一次新生代回收, 对象如果存活, 年龄就会加一
-
当对象年龄达到一定条件后, 就会被认为是老年代对象, 从而进入老年代
-
4.方法区
-
逻辑定义, 是
JVM的规范
, 所有虚拟机必须遵守的 -
是JVM所有线程共享的、用于存储类信息、常量等
-
方法区大小决定着系统可以保存多少个类
-
JDK8之前——永久代
-
内存的永久保存区域, 主要存放Class和Meta(元数据)的信息, Class在被加载的时候被放入永久区域, 它和存放实例的区域不同,
GC不会在主程序运行期对永久区域进行清理
, 所以这也导致了永久代的区域会随着加载的Class的增多而胀满, 最终抛出OMM异常如果场景使用了
动态代理
等, 会产生大量的类时, 设置合适的永久代大小有利于系统的稳定 -
设置初始永久代大小 —— -XX:permSize
-XX:permSize=5m
-
设置最大永久代大小 —— -XX:MaxPermSize(默认64MB)
-XX:MaxPermSize=5m
-
-
JDK8之后——元空间
-
永久代已经被移除, 被"元数据区"(元空间)所取代
-
元空间的本质和永久代类似, 元空间与永久代之间的最大的区别在于: 元空间并不在虚拟机中, 而是使用的
堆外直接内存
-
与永久代不同, 如果不指定大小,
默认情况下, 虚拟机会耗尽所有的可用系统内存
-
设置元空间初始大小 —— -XX:MetaspaceSize
-XX:MetaspaceSize=40m
-
设置最大元空间大小 —— -XX:MaxMetaspaceSize
-XX:MaxMetaspaceSize=40m
-
5.槽位复用
在
当前方法内
的局部变量
跳出作用域后, 后续变量将不会创建新的槽位, 而是复用先前的槽位
例如:
slot | value | 状态 |
---|---|---|
1 | a | 跳出自身作用域(失效) |
1 | b | 复用 |
6.对象分配
-
栈上分配
JVM提供的一种优化技术——对
线程私有对象
, 可以尝试打散分配到栈上, 而不是分配到堆上-
优点
分配到栈上, 调用结束后会自行销毁, 而不用GC去回收
-
缺点
栈空间相对较小, 不适合大对象进行栈上分配
-
开启条件(缺一不可)
-XX:+PrintFlagsFinal, 可查看所有系统参数
-
逃逸分析(默认开启)
逃逸: 变量脱离了当前函数体, 即在函数体外部也有引用
-
标量替换(默认开启)
-
标量
不可被进⼀步分解的量, 例如int, long, byte…
-
聚合量
可被进⼀步分解的量, 例如创建的对象…
把聚合量分解为标量, 对应的成员变量存储在栈帧或寄存器上
-
-
server模式(默认模式)
-
-
-
TLAB——(-XX:±UseTLAB)(默认开启)
-
堆上分配
五、JVM垃圾回收
1.什么是垃圾回收
垃圾回收动作, 通常称之为GC(Garbage Collection)
-
垃圾
特指存在于内存中的、不会再被使用的对象
-
回收
清除内存中的"垃圾"对象
2.可触及性
在GC时, 根据可触及性来确定对象是否可被回收的
即: 从根节点开始是否可以访问到某个对象, 也说明这个对象是否被使用
-
状态
-
可触及
从根节点开始,可以到达某个对象
-
可复活
对象引用被释放,但是可能在finalize()函数中被初始化复活
-
不可触及
由于finalize()只会执行一次,所以,错过这一次复活机会的对象,则为不可触及状态
-
-
状态测试
public class GcAccessibilityTest { private static GcAccessibilityTest gcAccessibilityTest; public static void main(String[] args) { // 创建实例 // 可触及 gcAccessibilityTest = new GcAccessibilityTest(); for (int i = 0; i < 2; i++) { System.out.println("--------------------num = " + i + "--------------------"); // 第一次循环: 可复活 // 第二次循环: 不可触及 // 将gcAccessibilityTest对象设置为垃圾对象 gcAccessibilityTest = null; // 通知JVM可以执行GC了 System.gc(); try { // 睡眠100ms Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if (gcAccessibilityTest == null) { System.out.println("gcAccessibilityTest is null"); } else { System.out.println("gcAccessibilityTest is not null"); } } } /** * finalize只会被调用一次,给对象唯一一次重生的机会 * 关闭资源还是得使用try-catch-finally */ @Override protected void finalize() throws Throwable { System.out.println("finalize is called!"); super.finalize(); // 使对象复生, 添加引用 gcAccessibilityTest = this; } }
// 结果 --------------------num = 0-------------------- finalize is called! gcAccessibilityTest is not null --------------------num = 1-------------------- gcAccessibilityTest is null
3.引用级别
-
强引用
一般程序中的引用
Student student = new Student();
-
软引用(java.lang.ref.SoftReferenct)
当堆空间不足时, 才会被回收, 所以软引用对象不会引起内存溢出
SoftReference<Student> softReference = new SoftReference<>(new Student());
-
弱引用(java.lang.ref.WeakReferenct)
GC时, 只要发现存在弱引用, 无论堆空间是否不足, 都会将其回收
WeakReference<Student> weakReference = new WeakReference<>(new Student());
-
虚引用
-
与没有引用是一样的
-
虚引用必须和引用队列在一起使用
-
作用: 用于追踪GC回收过程, 可以将一些资源释放操作放置在虚引用中执行, 进行记录
-
4.主要的垃圾回收算法
4.1.引用计数法
对于对象A
只要有任何一个对象引用了A, A的引用计数器就会加1
反之引用失效了, 则引用计数器将减1
只要引用计数器的值变为0, 对象A就不可能再被使用
-
存在的问题
- 对于循环引用的情况, 无法进行处理
- 引用计数器要求在每次引用产生和消除的时候, 都需要伴随一个加减法操作, 对系统性能会有一定的影响
- 对于循环引用的情况, 无法进行处理
JVM并未选择此算法作为垃圾回收算法
4.2.标记清除法
分为两个阶段:
标记阶段
和清除阶段
-
存在的问题
- 空间碎片
老年代中, 使用了标记清除法
4.3.复制算法
将原有内存空间分为两块, 每次只使用其中一块内存
例如:
A内存, GC时将存活的对象复制到B内存中, 然后清除掉A内存所有对象, 开始使用B内存
-
优点
-
没有空间碎片(解决了标记清除算法空间碎片问题)
-
垃圾对象越多, 效率越高
-
-
存在的问题
内存浪费
新生代中, 使用了复制算法
4.4.标记压缩法
首先标记存活对象, 然后将所有存活对象压缩到内存的一端, 然后再清理所有存活对象之外的空间
-
优点
-
没有空间碎片
-
没有内存浪费
-
性价比较高
-
-
存在的问题
- 效率相对较低
老年代中, 同时也使用了标记压缩法
4.5.分代算法
将堆空间划分为新生代和老年代, 根据它们之间的不同特点,执行不同的回收算法,提升回收效率
4.6.分区算法
1.将堆空间划分成连续的不同小区间, 每个区间独立使用、回收
2.由于当堆空间大时, 一次GC的时间会非常耗时, 所以可以控制每次回收多少个小区间, 而不是整个堆空间, 从而减少一次GC所产生的停顿
5.JVM垃圾收集器
默认启用ParallelOldGC
-
串行回收器——Serial
-
启用指定收集器
JVM参数 新生代 老年代 -XX:+UserSerialGC 串行回收器 串行回收器 -XX:+UserParNewGC ParNew 串行回收器 -XX:+UserParallelGC ParallelGC 串行回收器
-
-
并行回收器——ParNew & ParallelGC & ParallelOldGC
-
启用指定收集器
JVM参数 新生代 老年代 -XX:+UserParNewGC ParNew 串行回收器 -XX:+UserParallelGC ParallelGC 串行回收器 -XX:+UserParallelOldGC(默认启用) ParallelGC ParallelOldGC
-
-
并行回收器——CMS
JVM参数 新生代 老年代 -XX:+UseConcMarkSweepGC ParNew CMS -
新生代GC——G1
JVM参数 作用 -XX:+UseG1GC 启用G1收集器 -XX:MaxGCPauseMillis 指定目标最大停顿时间 -XX:PartallelGCThreads 设置并发线程数量 -XX:InitiatingHeapOccupancyPercent 指定堆的使用率是多少时, 触发并发标记周期(默认45)
6.JDK工具
-
JPS
用于列出Java的进程
执行语法: jps [-options]
-
jstat
用于查看堆中的运行信息
执行语法: jstat <-option> [-t] [-h<lines>] <vmid> [<interval> [<count>]
-
jinfo
用于查看运行中java进程的虚拟机参数
执行语法: jinfo [option] <pid>
-
jmap
用于生成指定java进程的dump文件; 可以查看堆内对象实例的统计信息, 查看ClassLoader信息和finalizer队列信息
执行语法: jmap [option] <pid>
-
jhat
用于分析jmap生成的堆快照
执行语法: jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-debug <int>] [-version] [-h|-help] <file>
-
jstack
用于导出指定java进程的堆栈信息
执行语法: jstack [-l] <pid>
-
jcmd
用于导出指定java进程的堆栈信息, 查看进程, GC等
执行语法: jcmd <pid | main class> <command ...|PerfCounter.print|-f file>
附录
-
并行回收器JVM参数
收集器 JVM参数 作用 ParNew -XX:ParallelGCThreads 指定GC时工作的线程数量 ParallelGC -XX:MaxGCPauseMillis 最大的垃圾收集暂停时间 -XX:GCTimeRatio 设置垃圾收集吞吐量 -XX:+UseAdaptiveSizePolicy 打开自适应垃圾收集策略 ParallelOldGC -XX:ParallelGCThreads 指定GC时工作的线程数量 CMS -XX:-CMSPrecleaningEnable 禁用预清理操作 -XX:ConcGCThreads 设置并发线程数量 -XX:PartallelCMSThreads 设置并发线程数量 -XX:-CMSInitiatingOccupancyFraction 老年代空间使用达到某百分比, 执行CMS(默认68) -XX:+CMSCompactAtFullCollection GC后, 进行一次碎片整理 -XX:CMSFullGCsBeforeCompaction 指定执行多少次GC后, 进行一次碎片整理 -
JVM常用参数
执行语法: Java [-options] [package+className] [arg1,arg2,…,argN]
options 作用 -Xms128m 设置初始化堆内存为128M -Xmx512m 设置最大堆内存为512M -Xmn160m 设置新生代大小为-Xmn160M(堆空间1/4~1/3) -Xss128m 设置最大栈内存为128M -XX:SurvivorRatio 设置新生代eden区与from/to空间的比例关系(默认值8:2, 即8:1:1) -XX:PretenureSizeThreshold 设置大对象直接进入老年代的阈值(默认值0, 即都先进入eden区) -XX:MaxTenuringThreshold 设置对象进入老年代的年龄阈值(默认值15) -XX:PermSize=64M 设置初始永久区64M -XX:MaxPermSize=128M 设置最大永久区128M -XX:MaxMetaspaceSize 设置元数据区大小(JDK1.8 取代永久区) -XX:+DoEscapeAnalysis 启用逃逸分析(Server模式)(JDK1.7开始, 默认开启) -XX:+EliminateAllocations 开启标量替换(默认开启) -XX:+TraceClassLoading 跟踪类的加载 -XX:+TraceClassUnloading 跟踪类的卸载 -Xloggc:gc.log 将gc日志信息打印到gc.log文件中 -XX:+PrintGC 打印GC日志 -XX:+PrintGCDetails 打印GC详细日志 -XX:+PrintGCTimeStamps 输出GC发生的时间 -XX:+PrintGCApplicationStoppedTime GC产生停顿的时间 -XX:+PrintGCApplicationConcurrentTime 应用执行的时间 -XX:+PrintHeapAtGC 在GC发生前后, 打印堆栈日志 -XX:+PrintReferenceGC 打印对象引用信息 -XX:+PrintVMOptions 打印虚拟机参数 -XX:+PrintCommandLineFlags 打印虚拟机显式和隐式参数 -XX:+PrintFlagsFinal 打印所有系统参数 -XX:+PrintTLAB 打印TLAB相关分配信息 -XX:+UseTLAB 打开TLAB -XX:TLABSize 设置TLAB大小 -XX:+ResizeTLAB 自动调整TLAB大小 -XX:+DisableExplicitGC 禁用显示GC (System.gc()) -XX:+ExplicitGCInvokesConcurrent 使用并发方式处理显式GC