接上篇结构图如下
1. 堆区
1.1 概述
其大小通过
- Xms
(最小值)和-Xmx
(最大值)参数设置(例如:java -Xms=1M -Xmx=2M HackTheJava
),-Xms
为 JVM 启动时申请的最小内存,默认为操作系统物理内存的 1/64 但小于 1G,-Xmx
为 JVM 可申请的最大内存,默认为物理内存的 1/4 但小于 1G,默认当空余堆内存小于 40% 时,JVM 会增大 Heap 到-Xmx
指定的大小,可通过- XX:MinHeapFreeRation
来指定这个比列;空余堆内存大于 70% 时,JVM 会减小 heap 的大小到-Xms
指定的大小,可通过XX:MaxHeapFreeRation
来指定这个比列,对于运行系统,为避免在运行时频繁调整 Heap 的大小,通常-Xms
与-Xmx
的值设成一样。
内部分区如下
1.2 核心概述
Java堆( Java Heap): 是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域, 在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例, 几乎所有的对象实例都在这里分配内存。也是垃圾收集器管理的主要区域, 因此很多时候也被称做“GC堆”( GarbageCollected Heap)。Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError
异常。(新生代和老年代都满)
jvm启动时,堆空间的大小也就确定了,一个jvm实例对应一个堆
类对象创建后不会被自动移除,等待垃圾回收器回收。
遇到 new 创建对象 并在堆中开辟一个空间。
对象存放
栈、堆、方法区的关系如下:
设置内存大小
-Xms 用来设置堆空间初始内存大小
-X 是jvm的运行参数
-Xmx 用来设置堆空间最大内存大小
建议最小最大一样
命令行查看参数: jps 查看进程 、 jstat -gc 进程id
或者在 在参数设置时设置 -XX:+PrintGCDetails
初始内存大小:物理电脑内存大小/64
最大内存大小:物理电脑内存大小/4
public class HeapDemo {
public static void main(String[] args) {
//返回java虚拟机中堆的内存总量
long initMemory = Runtime.getRuntime().totalMemory();
//最大堆内存
long maxMemory = Runtime.getRuntime().maxMemory();
System.out.println("initMemort:"+initMemory+"m");
System.out.println("MaxMemort:"+maxMemory+"m");
System.out.println("系统内存大小"+initMemory*64.0/1024+"G");
System.out.println("系统内存大小"+maxMemory*4.0/1024+"G");
}
}
/*
initMemort:255328256m
MaxMemort:3779067904m
系统内存大小1.5958016E7G
系统内存大小1.4761984E7G
* */
1.3 内部分区
java堆进一步划分为年轻代和老年代,其中年轻代又分为eden空间(几乎所有对象在此new出来)、survivor0和survivor1空间(有时也称为from区,和to区),如上图所示。
配置新老代的堆结构比例,一般不改默认:-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆内存的三分之一,其中存活区比例8:1:1
-XX:SurvivorRatio=8 存货为2 新生代为8
自定义:-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆内存的五分之一
1.4 对象分配
具体步骤如下:
1.new的对象先放在伊甸园去。辞去大小限制。
2.当伊甸园满时,程序又创建对象,jvm的垃圾回收器将对伊甸园进行垃圾回收,minor GC ,将伊甸园中不再被其他对象引用的对象进行销毁。在加载新的对象放到伊甸园区。
3.然后将伊甸园中的剩余对象移动到幸存者0区。
4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区,如果没有回收,就会放到幸存者1区。
5.如果再次经理垃圾回收,此时会重新放回幸存者0区,再接着去幸存者1区。
6.幸存次数达到一定的值后会放入养老区。
1.5 垃圾回收分类
minor GC/ mahor Gc/ full gc 新生代收集,老年代收集、整堆收集
新生代收集:eden代满时触发,幸存区满不触发
老年代收集:当老年代内存不足时,会经常伴随一次年轻代回收,之后后空间还不足,会触发老年代收集
老年代收集比新生代慢十倍且stw(停止一切)时间长。
** full GC:触发条件**
1.调用system.gc()时,系统建议执行full gc,但是不是必然执行的。
2.老年代空间不足时
3.方法区空间不足时
4.通过eden区,存活0向1区复制时对象大于1的可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象的大小
1.6 内存分配策略
内存分配策略主要有以下几点:
1.对象优先分配在Eden区,如果Eden区没有足够的空间进行分配时,虚拟机执行一次MinorGC。
2.大对象直接进入老年代(需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
3.长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄(Age Count)计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值(默认15次),对象进入老年区。
4.动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
5.空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
为对象分配内存:TLAB
TLAB 分配
TLAB,全称Thread Local Allocation Buffer, 即:线程本地分配缓存。这是一块线程专用的内存分配区域。TLAB占用的是eden区的空间。在TLAB启用的情况下(默认开启),JVM会为每一个线程分配一块TLAB区域。
为什么需要TLAB?
这是为了加速对象的分配。由于对象一般分配在堆上,而堆是线程共用的,因此可能会有多个线程在堆上申请空间,而每一次的对象分配都必须线程同步,会使分配的效率下降。考虑到对象分配几乎是Java中最常用的操作,因此JVM使用了TLAB这样的线程专有区域来避免多线程冲突,提高对象分配的效率。
局限性
TLAB空间一般不会太大(占用eden区),所以大对象无法进行TLAB分配,只能直接分配到堆上。
分配策略
一个100KB的TLAB区域,如果已经使用了80KB,当需要分配一个30KB的对象时,TLAB是如何分配的呢?
此时,虚拟机有两种选择:第一,废弃当前的TLAB(会浪费20KB的空3.4 间);第二,将这个30KB的对象直接分配到堆上,保留当前TLAB(当有小于20KB的对象请求TLAB分配时可以直接使用该TLAB区域)。
JVM选择的策略是:在虚拟机内部维护一个叫refill_waste的值,当请求对象大于refill_waste时,会选择在堆中分配,反之,则会废弃当前TLAB,新建TLAB来分配新对象。
【默认情况下,TLAB和refill_waste都是会在运行时不断调整的,使系统的运行状态达到最优。】
1.7 堆空间参数设置
参数 | 作用 | 备注 |
---|---|---|
-XX:+UseTLAB | 启用TLAB | 默认启用 |
-XX:TLABRefillWasteFraction | 设置允许空间浪费的比例 | 默认值:64,即:使用1/64的TLAB空间大小作为refill_waste值 |
-XX:-ResizeTLAB | 禁止系统自动调整TLAB大小 | |
-XX:TLABSize | 指定TLAB大小 | 单位:B |
1.8 逃逸分析
基本原理
方法逃逸和线程逃逸
分析对象的动态作用域,当一个对象在方法里面被定义后,他可能会被外部方法引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸。
如果能证明一个对象不会逃逸到其他方法或者线程之外,或者逃逸程度底则能为这个对象实例采取不同程度的优化JVM判断新创建的对象是否逃逸的依据有:
一、对象被赋值给堆中对象的字段和类的静态变量。
二、对象被传进了不确定的代码中去运行。
如果满足了以上情况的任意一种,那这个对象JVM就会判定为逃逸。对于第一种情况,因为对象被放进堆中,则其它线程就可以对其进行访问,所以对象的使用情况,编译器就无法再进行追踪。第二种情况相当于JVM在解析普通的字节码的时候,如果没有发生JIT即时编译,编译器是不能事先完整知道这段代码会对对象做什么操作。保守一点,这个时候也只能把对象是当作是逃逸来处理。
下面举几个例子
public class EscapeTest {
public static Object globalVariableObject;
public Object instanceObject;
public void globalVariableEscape(){
globalVariableObject = new Object(); //静态变量,外部线程可见,发生逃逸
}
public void instanceObjectEscape(){
instanceObject = new Object(); //赋值给堆中实例字段,外部线程可见,发生逃逸
}
public Object returnObjectEscape(){
return new Object(); //返回实例,外部线程可见,发生逃逸
}
public void noEscape(){
synchronized (new Object()){
//仅创建线程可见,对象无逃逸
}
Object noEscape = new Object(); //仅创建线程可见,对象无逃逸
}
}
jvm优化(对象栈分配)
当判断出对象不发生逃逸时,编译器可以使用逃逸分析的结果作一些代码优化
1.将堆分配转化为栈分配。如果某个对象在子程序中被分配,并且指向该对象的指针永远不会逃逸,该对象就可以在分配在栈上,而不是在堆上。在有垃圾收集的语言中,这种优化可以降低垃圾收集器运行的频率。
2.同步消除。如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。
3.分离对象或标量替换。如果某个对象的访问方式不要求该对象是一个连续的内存结构,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
优化一:将堆分配转化为栈分配,这个优化也很好理解。下面以代码例子说明:
虚拟机配置参数:-XX:+PrintGC -Xms5M -Xmn5M -XX:+DoEscapeAnalysis
-XX:+DoEscapeAnalysis表示开启逃逸分析,JDK8是默认开启的
-XX:+PrintGC 表示打印GC信息
-Xms5M -Xmn5M 设置JVM内存大小是5M
public static void main(String[] args){
for(int i = 0; i < 5_000_000; i++){
createObject();
}
}
public static void createObject(){
new Object();
}
运行结果是没有GC。
把虚拟机参数改成 -XX:+PrintGC -Xms5M -Xmn5M -XX:-DoEscapeAnalysis。关闭逃逸分析得到结果的部分截图是,说明了进行了GC,并且次数还不少。
[GC (Allocation Failure) 4096K->504K(5632K), 0.0012864 secs]
[GC (Allocation Failure) 4600K->456K(5632K), 0.0008329 secs]
[GC (Allocation Failure) 4552K->424K(5632K), 0.0006392 secs]
[GC (Allocation Failure) 4520K->440K(5632K), 0.0007061 secs]
[GC (Allocation Failure) 4536K->456K(5632K), 0.0009787 secs]
[GC (Allocation Failure) 4552K->440K(5632K), 0.0007206 secs]
[GC (Allocation Failure) 4536K->520K(5632K), 0.0009295 secs]
[GC (Allocation Failure) 4616K->512K(4608K), 0.0005874 secs]
这说明了JVM在逃逸分析之后,将对象分配在了方法createObject()方法栈上。方法栈上的对象在方法执行完之后,栈桢弹出,对象就会自动回收。这样的话就不需要等内存满时再触发内存回收。这样的好处是程序内存回收效率高,并且GC频率也会减少**,程序的性能就提高了。**
优化二 同步锁消除
如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。
虚拟机配置参数:-XX:+PrintGC -Xms500M -Xmn500M -XX:+DoEscapeAnalysis。配置500M是保证不触发GC。
public static void main(String[] args){
long start = System.currentTimeMillis();
for(int i = 0; i < 5_000_000; i++){
createObject();
}
System.out.println("cost = " + (System.currentTimeMillis() - start) + "ms");
}
public static void createObject(){
synchronized (new Object()){
}
}
运行结果
cost = 6ms
把逃逸分析关掉:-XX:+PrintGC -Xms500M -Xmn500M -XX:-DoEscapeAnalysis
运行结果
cost = 270ms
说明了逃逸分析把锁消除了,并在性能上得到了很大的提升。这里说明一下Java的逃逸分析是方法级别的,因为JIT的即时编译是方法级别。
优化三 分离对象或标量替换。
-XXX:+EliminateAllocations打开标量替换,默认是打开的
这个简单来说就是把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈上。这样的好处有,一、减少内存使用,因为不用生成对象头。 二、程序内存回收效率高,并且GC频率也会减少,总的来说和上面优点一的效果差不多**。
2 方法区
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分, 但是它却有一个别名叫做Non-Heap( 非堆) , 目的应该是与Java堆区分开来.
2.1 图解栈,堆,方法区的交互关系
2.2 方法区jvm参数设置
jdk8以前,-XX:PermSize来设置永久代初始分配空间默认值20.75
-XX:Max’PermSize来设定永久代最大可分配空间,32位机默认64m,64位机默认82m
超出最大内存会出现oom
jdk8以后
window下
-XX:MetaspaceSize来设置永久代初始分配空间默认值21
-XX:MaxMetaspaceSize来设定永久代最大可分配空间,默认-1,没有限制
超出最大内存会出现oom
默认最小值为 16MB,最大值为 64MB,可以通过 - XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。
自定义大小:
-XX:MetaspaceSize=100m
-XX:MaxMetaspaceSize=100m
注:当类占用方法空间达到初始值会触发gc,根据gc后初始值(也称高水平线)会变化,变化的大小取决于释放空间的多少如果释放得多降低该值,释放的少减少该值。
2.3 内部结构
方法区( Method Area) :与Java堆一样, 是各个线程共享的内存区域, 它用于存储已被虚拟机加载的类信息、域信息、常量、 静态变量、 即时编译器(jit)编译后的代码等数据。
类型信息
对每个加载的类型( 类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于interface或是java. lang.Object,都没有父类
- 这个类型的修饰符(public, abstract, final的某个子集)
- 这个类型直接接口的一个有序列表
域信息(成员变量)
- JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
- 域的相关信息包括:域名称、 域类型、域修饰符(public, private, protected, static, final, volatile, transient的某个子集
方法信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
-
方法名称
-
方法的返回类型(或void)
-
方法参数的数量和类型(按顺序)
-
方法的修饰符(public, private, protected, static, final, synchronized, native , abstract的一个子集)
-
方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native 方法除外)
-
异常表( abstract和native方法除外)
- 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
示例
Last modified 2020-4-22; size 1626 bytes
MD5 checksum 69643a16925bb67a96f54050375c75d0
Compiled from "MethodInnerStrucTest.java"
//类型信息会被加载到方法区
public class com.atguigu.java.MethodInnerStrucTest extends java.lang.Object // 类的全限定名以及父类
implements java.lang.Comparable<java.lang.String>, java.io.Serializable //类实现的接口信息
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER // 类的权限修饰符
Constant pool:
#1 = Methodref #18.#52 // java/lang/Object."<init>":()V
#2 = Fieldref #17.#53 // com/atguigu/java/MethodInnerStrucTest.num:I
#3 = Fieldref #54.#55 // java/lang/System.out:Ljava/io/PrintStream;
...
{
//域信息会被加载到方法区
public int num; // 域名称
descriptor: I // 域类型
flags: ACC_PUBLIC // 域权限
private static java.lang.String str;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC
//方法信息会被加载到方法区
public com.atguigu.java.MethodInnerStrucTest(); // 方法名称
descriptor: ()V //方法参数及方法返回值类型
flags: ACC_PUBLIC //方法权限修饰符
Code: //方法对应的字节码
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field num:I
10: return
LineNumberTable:
line 10: 0
line 12: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/atguigu/java/MethodInnerStrucTest;
......
}
Signature: #49 // Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
SourceFile: "MethodInnerStrucTest.java"
2.4 运行时常量池
运行时常量池( Runtime Constant Pool) 是方法区的一部分, Class文件中除了有类的版本、 字段、 方法、 接口等描述信息外, 还有一项信息是常量池表( Constant Pool Table) , 用于存放编译期生成的各种字面量和符号引用, 这部分内容将在类加载后进入方法区的运行时常量池中存放 .
1.JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
2.运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
3.运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性
- String.intern()
4.运行时常量池类似于传统编程语言中的符号表(symbol table) ,但是它所包含的数据却比符号表要更加丰富一些。
5.当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛OutOfMemoryError异常。
6.一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Poo1 Table),包括各种字面量和对类型域和方法的符号引用
7.一个 java 源文件中的类、接口,编译后产生一个字节码文件。而 Java 中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池这个字节码包含了指向常量池的引用。
8.在动态链接的时候会用到运行时常量池. 比如如下代码,虽然只有 194 字节,但是里面却使用了 string、System、Printstream 及 Object 等结构。这里代码量其实已经很小了。如果代码多,引用到的结构会更多。
9.几种在常量池内存储的数据类型包括:数量值、字符串值、类引用、字段引用、方法引用
10.常量池可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型、字面量等信息
Public class Simpleclass {
public void sayhelloo() {
System.out.Println (“hello”) }
}
2.5 方法区的8和以前的区别
1.6前有永久代,静态变量放在永久代上
1.7没有永久代,字符串常量池、静态变量移除,保存在堆中
1.8及以后无永久代,类信息、字段、方法、常量保存在本地内存中的元空间,但字符串常量池、和静态变量仍旧在堆中
2.6 方法区垃圾回收
1.Java
虚拟机规范对方法区是否实现垃圾回收没有做出强制的规定。存在未实现或未能完整实现方法区类型卸载的垃圾回收器(例如JDK 11的zGC收集器)。
2.方法区的回收效果比较难令人满意,条件很苛刻,但是回收又是很有必要的。
3.垃圾回收主要回收两部分内容:**常量池中废弃的常量以及不在使用的类型。
4.HotSpot
虚拟机对常量池的回收策略很明确,只要常量池中的常量没有被任何地方使用,就可以被回收。
满足了上面的3个条件,只是被允许回收,不是一定会被回收。还需要设置一些参数进行控制。
2.7 直接内存
直接内存:在JDK 1.4中新加入了NIO( New Input/Output) 类, 引入了一种基于通道( Channel) 与缓冲区( Buffer) 的I/O方式, 它可以使用Native函数库直接分配堆外内存, 然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能, 因为避免了在Java堆和Native堆中来回复制数据。