JVM:Java Virtual Machine java虚拟机
虚拟机:使用软件技术模拟出与具有完整硬件系统功能、运行在一个隔离环境中的计算机系统。
JVM官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
java 一些命令
javac 将文件编译成.class文件
java 执行 .class文件,若类中没有main函数,则不能执行。如 java -cp User.jar User 运行User.jar
javap 反编译器,显示编译类中可以访问的方法和数据。 如 javap -c 打开.class
jar cvf User.jar User.class :将User.class打jar包 , 全部打包: jar -cvf xx.jar *
jar xvf 解压jar
JVM内存结构
- 类加载器子系统
- 执行引擎
- 本地方法库
- 运行时数据区:
-
程序计数器(program counter register --pc):记录每个线程指令执行顺序、当前位置等。线程私有。
-
java虚拟机栈stack:每个线程有自己的栈,栈的创建同线程创建一起。栈有一系列栈帧组成,帧可以描述当前线程的一个方法的执行过程(每一个方法从调用直至执行完成的过程, 就对应着一个栈帧在虚拟机栈中入栈和出栈的过程),方法执行完就释放这个帧,也就释放了局部变量(基本数据类型、对象引用)的内存地址。
-
堆heap:存放对象实例和数组,线程共享。gc的区域,新生代和老年代。
-
方法区 :已被加载的类信息(类的元数据),常量,静态变量,静态代码块,编译器编译后的代码等。
1.8以后字符串常量池和静态变量移到堆;运行时常量池留在方法区中;删除永久代,替换为元空间(存数据的数据)。
-
本地方法栈(native method --底层其他语言,如C++):执行本地方法需要的栈。
-
堆与方法区为线程共享,栈和程序计数器为线程私有。程序计数器标记了线程执行到哪儿了,线程切换了也可以回到它本来运行中断的地方;每个线程都要有个独立的程序计数器。
栈内存与堆内存
数据结构的栈 ≠ jvm栈内存;数据结构的堆 ≠ jvm堆内存。
数据结构:队列(Queue)和栈(Stack) 、链表、线性表、Map、Tree
数据结构:队列先进先出(排队)、栈先进后出
栈(Stack) :执行程序用,比如:基本类型的变量和对象的引用变量。
堆(Heap):存储java中的对象和数组, 存取速度较慢。gc的区域,新生代和老年代。
栈的空间大小远远小于堆。
栈内存线程私有 (局部变量存在于栈内存);堆内存所有线程共有(成员变量存在于堆内存所以有线程安全一说)。
栈:内存溢出 StackOverFlowError、OutofMemoryError
堆:内存溢出 OutofMemoryError
方法区:也会有 OutofMemoryError
常量池
静态常量池(class文件常量池)
存储在.class文件中。
每一份class文件都有一份自己的静态常量池。类和接口名字,字段名,和其他一些在class中引用的常量,如 CONSTANT_Class_info、CONSTANT_Utf8、CONSTANT_String、CONSTANT_Integer...
静态常量池是在编译时就存入且不变更。
Class对象是存放在堆区的,不是方法区。而类的元数据(元数据并不是类的Class对象),即类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的,存在方法区。
运行时常量池
是方法区的一部分。
在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中。每个class都对应有一个运行时常量池,类在解析之后将符号引用替换成直接引用。具备动态性,运行期间也可能将新的常量放入池中。
字符串常量池
存放字符串对象的实例(堆)。在每个JVM中都只会维护一份,所有类共享。
字符串常量池保存的是"字符"的实例,供运行时常量池引用。
-XX:StringTableSize=1009 这个参数可以指定字符串常量池的容量。(JDK1.6默认为1009,JDK1.7之后默认为60013,字符串常量池底层为HashTable,合理增大常量池大小会解决Hash冲突问题,JDK1.8开始1009是可以设置的最小值)
字符串常量池、运行时常量池:关系、位置演化
JDK1.7之前,字符串常量池是运行时常量池的一部分,一起存在方法区中。
JDK1.7,字符串常量池和静态变量,移到堆中。运行时常量池还在方法区。字符串常量池不属于运行时常量池的一部分。
JDK1.8,字符串常量池和静态变量还在堆中。但运行时常量池跟随方法区一起变成元空间,进入主内存。
验证字符串常量池的位置,是在heap堆中:
java
ArrayList<String> list = new ArrayList<String>();
for (int i = 0; i < 100000000; i++) {
String temp = String.valueOf(i).intern();
list.add(temp);
}
java
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
...
JVM内存模型(JMM)
Java线程之间的通信由Java内存模型控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见,线程私有内存和主内存之间的抽象关系。
JMM主要围绕可见性、原子性和有序性这三个特性而建立。具体来说:
-
可见性与共享变量
在Java程序中,多个线程可能同时访问和修改共享变量。为了确保每个线程都能看到其他线程对共享变量所做的修改,Java内存模型提供了一系列规则。例如,volatile关键字可以确保变量的可见性,即当一个线程修改了一个volatile变量的值后,其他线程能够立即看到这个修改。此外,synchronized块也可以保证可见性,它确保在进入和退出synchronized块时,线程对共享变量的操作对其他线程是可见的。
-
有序性
为了优化程序性能,编译器和处理器可能会对指令进行重排序。然而,这种重排序可能会导致多线程程序出现意想不到的结果。为了解决这个问题,Java内存模型定义了happens-before规则来确保多线程之间的操作顺序符合预期。简单来说,如果一个操作happens-before另一个操作,那么第一个操作的结果将对第二个操作可见。
-
原子性
Java内存模型还规定了某些操作具有原子性。原子性意味着这些操作在执行过程中不会被其他线程中断。例如,对volatile变量的读写操作具有原子性。但是,需要注意的是,并非所有操作都具有原子性。对于非原子性操作,我们需要使用锁等机制来保证线程安全。
垃圾回收机制 之 如何判断对象已"死"
-
引用计数法
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。
但是, 对象之间存在相互引用, 就不会被回收. JAVA并没有采用此算法.
-
可达性分析算法
通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为"引用链",当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。
当一个对象 与 GC Roots 没有任何引用链项链, 证明此对象不可用.
可作为GC Root的对象包含以下几种:
- 虚拟机栈(栈帧中的本地变量表)中的引用
- 方法区中静态属性
- 方法区中常量
- 本地方法栈中(Native方法)引用的对象
垃圾回收机制 之 垃圾回收算法
标记-清除算法
算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。后续的收集算法都是基于这种思路并对其不足加以改进而已。
- 标记和清除这两个过程的效率都不高
- 标记清除后会产生大量不连续的内存碎片,以后需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
复制算法(新生代回收算法)
它将可用内存按容量划分为两块,每次只使用其中一块(并不是50%).当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。
现在的商用虚拟机(包括HotSpot)都是采用这种收集算法来回收新生代。
新生代中98%的对象很快就需要回收, 经过一次 复制活着的对象后, 只需要少量的空间来存放这部分活着的对象, 所以不需要1:1 ,而是将内存(新生代内存)分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor, 当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
默认Eden与Survivor的大小比例是8 : 2 (8:1:1) (两个Survivor是一叫From区,另一个To区)
部分对象会在From区域和To区域中复制来复制去,如此交换15次(默认),最终如果还存活,就存入老年代。
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法, 要使用标记-整理算法
标记-整理算法(老年代回收算法)
标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法
不是指某种特定算法。是说针对新生代合老年代采用不同GC算法,(用不同的垃圾收集器)
-
Minor GC又称为新生代GC :
指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
-
Full GC 又称为老年代GC或者Major GC :
指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
new Object() 申请堆内存空间过程
先在 Eden 申请,可用就创建对象;不够 MinorGC(新生代-复制算法)。
GC后再次判断Eden,可用就创建, 不够再判断Survivor(from) 还有空间? 有就创建,否则判断 Old,可用就在 Old创建,不够 FullGC。 再次判断Old,可用就创建,否则GC。
垃圾收集器
语义:
-
serial 【串行】 启用的时候会暂停所有线程 只有一个线程来GC 不适用服务器环境
-
Parallel 【并行】 启用的时候会暂停所有线程 多个线程来GC
-
CMS(Concurrent Mark Sweep) 【并发】 一部分线程GC, 其他线程继续运行 (初始标记的时候也会stw,只是收集的过程中并发)
-
G1 (Garbage-First)
指定垃圾收集 :
-XX:+UseSerialGC
新生代 + 老年代 都是串行,即Serial + Serial old(新是复制,老是整理)
-XX:+UsePerNewGC
PerNew + Serial old
-XX:+UseParalleGC
Parallel Sce + Parallel Old 【jdk8的默认]
-XX:+UseConcMarkSweepGC
老年代开启CMS。新生代开启PerNew
-XX:+UseG1GC
G1 同时适用 young 和 old 适用大内存,多core。追求更短的stw的时间,在jdk1.7u4支持。在jdk1.9的默认收集器 把整个Heap都重新分配了,划分成很多Region,2048个,每个大小范围[1M,32M] 所以Heap最大64G。 Region: Eden、Servivor、Old、Humongous(巨大的:超过region一半大小) 没有碎片
其他的收集器:ZGC(jdk11)、Shenandoan(openJDK)
新生代和老年代大小默认比例大约1:2
新生代中eden:s0:s1 的比例也和垃圾收集器有关,通常说的是 8:1:1 其实是 SerialGC 的时候
而Java8默认Parallel GC 的话,比例大约是 4:1:1
也可以手动改比例:-XX:ServivorRatio=8
垃圾收集器如何选择
对1.8来说,CMS/G1中选一个。
- 程序很小,单进程:串行就行
- 多核、高吞吐:ps
- 多核、少暂停时间:cms
- 多核、内存连续:G1
CMS 收集器
Concurrent Mark Sweep:可并发的标记-清除 (也有碎片)
sh
-XX:+UseConcMarkSweepGC # 老年代开启CMS。新生代开启PerNew
CMS参考:https://plumbr.io/handbook/garbage-collection-algorithms-implementations/concurrent-mark-and-sweep
-
Phase 1: Initial Mark. (会stw)
mark all the objects in the Old Generation that are either direct GC roots or are referenced from some live object in the Young Generation.
会在old中标记直接由GC root 可达对象、和由young中存活对象引用的old 中的对象
-
Phase 2: Concurrent Mark. (不会stw)
在根据上阶段标记的对象,沿着引用链标记
-
Phase 3: Concurrent Preclean.(不会stw)
清除
-
Phase 4: Concurrent Abortable Preclean. (不会stw)
-
Phase 5: Final Remark.(会 stw)
最后标记,因为是上一阶段的标记是并发的,标记的过程中可能同时发生新的引用变更,最后需要stw,再标记一次。
-
Phase 6: Concurrent Sweep.(不会 stw)
-
Phase 7: Concurrent Reset.
合并步骤为5步:(三次Mark,一次Sweep)
- Initial Mark.
- Concurrent Mark.
- Final Remark.
- Concurrent Sweep.
- Reset.
G1 收集器
Garbage-First
sh
-XX:+UseG1GC
G1 同时适用 young 和 old
适用大内存,多core。追求更短的stw的时间,在jdk1.7u4支持。在jdk1.9的默认收集器
把整个Heap都重新分配了,划分成很多Region,2048个,每个大小范围[1M,32M] 所以Heap最大64G。
Region: Eden、Servivor、Old、Humongous(巨大的:超过region一半大小)
没有碎片
G1如何做到可预测停顿时间:通过自己管理的大小不一的region,判断可回收的空间,以及回收的开销大小,做到优先回收回收效率最高的部分region(如 region1 10M,500ms,region2 50M,100ms,优先回收region2)
GC 日志解析
日志中语义:
DefNew:新生代串行 (Serial )
Tenured:老年代串行 (Serial old)
PerNew: 新生代并行 (PerNew )
PSYoungGen:新生代并行 (Parallel Sce)
ParOldGen:老年代并行 (Parallel Old )
JVM参数
-
X 参数
java -Xmixed -version 【# 开启 mixed mode 】
java -Xint -version 【# 开启 interpreted mode (解释模式,字节码直接执行)】
-
XX 参数
-XX:[+/-] 属性名 (启用/禁用某属性)如: -XX:+UseG1GC
-XX:name=value (为某属性设置值) 如:-XX:MaxTenuringThreshold=10
举例:设置最大、最小堆内存
-Xmx -XX:MaxHeapSize 的简写,设置最大堆内存,默认为物理内存的1/4
sh
-Xms1024m
-Xms -XX:InitialHeapSize 的简写,设置最小堆内存。默认为物理内存的1/64
sh
-Xms1024k
# 或
-Xms2m
举例:设置栈的大小
-Xss -XX:ThreadStackSize 的简写。(不写单位就默认单位字节)
sh
-Xss1024k
举例:查看这个进程是否开启了GC明细输出的开关
jinfo -flag 命令可以查看JVM参数
sh
jinfo -flag PrintGCDetails <pid>
如果输出:
-XX:-PrintGCDetails # 代表没打开
所以在启动前,可以设置开启GC日志输出
sh
-XX:+PrintGCDetails
举例:开启类加载过程日志
sh
-XX:+TraceClassLoading
会发现先从 jre/lib/rt.jar 加载依赖包,再从当前项目target/classes下加载自己创建的类