定义: Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境)
官方文档:https://docs.oracle.com/javase/8/docs/
从图中总结两点:
1、java程序的执行过程
2、JDK,JRE,JVM的关系
JRE是运行时环境,除了包含JVM外还提供了很多基础类库,JDK在此基础之上还提供了很多的开发工具包给程序员使用,比如:javac(编译代码),java(执行),jar(打包),javap(反汇编),jconsole(性能分析),jvisualvm(监控)等等。
而JVM全称 Java Virtual Machine,能识别 .class 后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作,JVM 的作用是从软件层面屏蔽不同操作系统在底层硬件和指令的不同,同时 JVM 是一个虚拟化的操作系统,类似于 Linux 或者 Windows 的操作系统,只是它架在操作系统上,接收字节码也就是 .class ,把字节码翻译成操作系统上的机器码且进行执行。
跨平台:java是跨平台的,能实现所谓的 write once , run anywhere 正是基于其JVM在不同平台的实现,不同操作系统有对应的 JDK 的版本,
https://www.oracle.com/java/technologies/javase/javase-jdk8-downloa
ds.html
跨语言:也叫语言无关性,JVM 只识别字节码,所以 JVM 其实跟语言是解耦的,JVM 运行不是翻译 Java 文件。还有像 Groovy 、Kotlin、Scala 等等语言,它们其实也是编译成字节码,所以它们也可以在 JVM 上面跑,这个就是JVM 的跨语言特征。Java的跨语言性一定程度上奠定了非常强大的java语言生态圈。
JVM严格来说是一套规范,不同公司在这套规范下做了自己的实现
可以查看自己目前所用的实现
C:\Users\22863>java -version
java version "1.8.0_281"
Java(TM) SE Runtime Environment (build 1.8.0_281-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed
mode)
JVM 能涉及非常庞大的一块知识体系,比如内存结构、垃圾回收、类加载、性能调优、JVM 自身优化技术、执行引擎、类文件结构、监控工具等。
但是在所有的知识体系中,都或多或少跟内存结构有一定的关系: 比如垃圾回收回收的就是内存、类加载加载到的地方也是内存、性能优化也涉及到内存优化、执行引擎与内存密不可分、类文件结构与内存的设计有关系,监控工具也会监控内存。所以内存结构处于 JVM 中核心位置。也是属于我们入门 JVM学习的最好的选择。
同时JVM是一个虚拟化的操作系统,所以它要虚拟处理器(执行引擎),虚拟操作系统指令(基于操作数栈的指令集),虚拟操作系统内存(jvm内存区域),等等
java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,不同的区域存储不同的数据,Java 引以为豪的就是它的自动内存管理机制,相比于 C++的手动内存管理、复杂难以理解的指针等,Java 程序写起来就方便的多。所以要深入理解 JVM 必须理解JVM虚拟内存的结构划分。
这样的划分只是JVM的一种规范,至于具体的实现是不是完全按照规范来?这些区域是否都存在?这些区域具体在哪儿?不同的虚拟机不同的版本在实现上略有不同,譬如:方法区, hotspot 1.7和1.8中是大不相同的(特别注意!!!)
较小的内存空间,存储当前线程执行的字节码的偏移量;各线程之间独立存储,互不影响,
程序计数器主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。由于 Java 是多线程语言,当执
行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运
行的指令。
因为 JVM 是虚拟机,内部有完整的指令与执行的一套流程,所以在运行Java 方法的时候需要使用程序计数器(记录字节码执行的地址或行号),如果是遇到本地方法(native 方法),这个方法不是 JVM 来具体执行,所以程序计数器不需要记录了,这个是因为在操作系统层面也有一个程序计数器,这个会记录本地代码的执行的地址,所以在执行 native 方法时,JVM 中程序计数器的值为空(Undefined)。
另外程序计数器也是 JVM 中唯一不会 OOM(OutOfMemory)的内存区域。
设计角度:jvm中的程序计数器和操作系统中的程序计数器是对等的,也能体现jvm叫虚拟机的含义,它要虚拟出一个机器,就要从设计上虚拟的匹配一点。
案例:从反编译回来的字节码指令中可以看到,每条指令前都有其对应的字节码偏移量
作用
用于保存JVM中下一条所要执行的指令的地址
特点
虚拟机栈顾名思义首先是一个栈结构,线程每执行一个方法时都会有一个
栈帧入栈,方法执行结束后栈帧出栈,栈帧中存储的是方法所需的数据,指
令、返回地址等信息,虚拟机栈的结构如下:
1、虚拟机栈是基于线程的:哪怕你只有一个 main() 方法,也是以线程的
方式运行的。在线程的生命周期中,参与执行的方法栈帧会频繁地入栈和出
栈,虚拟机栈的生命周期是和线程一样的。
2、栈大小:每个虚拟机栈的大小缺省为 1M,可用参数 –Xss[size] 调整大小,可以参考官方文档(jdk1.8):https://docs.oracle.com/javase/8/doc
s/technotes/tools/unix/java.html
不同操作系统平台还略有不同
演示
代码
public class Main {
public static void main(String[] args) {
method1();
}
private static void method1() {
method2(1, 2);
}
private static int method2(int a, int b) {
int c = a + b;
return c;
}
}
在控制台中可以看到,主类中的方法在进入虚拟机栈的时候,符合栈的特点
问题辨析
垃圾回收是否涉及栈内存?
不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。
栈内存的分配越大越好吗?
不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
方法内的局部变量是否是线程安全的?
如果方法内局部变量没有逃离方法的作用范围,则是线程安全的
如果如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题
内存溢出
Java.lang.stackOverflowError 栈内存溢出
发生原因
Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时需要定位占用CPU过高的线程
top命令,查看是哪个进程占用CPU过高
ps H -eo pid, tid(线程id), %cpu | grep 刚才通过top查到的进程号 通过ps命令进一步定位是哪个线程占用CPU过高
jstack 进程id 通过查看进程中的线程的nid,刚才通过ps命令看到的tid来对比定位,注意jstack查找出的线程id是16进制的,需要转换,发现是whlie(true)死循环了,编码要规范
栈帧大体都包含四个区域:(局部变量表、操作数栈、动态连接、返回地址),如下图所示:
1、局部变量表:存放我们的局部变量的(方法内的变量)。首先它是一个32 位的长度,主要存放我们的 Java 的八大基础数据类型,一般 32 位就可以存放下,如果是 64 位的就使用高低位占用两个也可以存放下,如果是局部变量是一个对象,存放它的一个引用地址即可。
2、操作数栈:存放 java 方法执行的操作数的,它也是一个栈,操作的的元素可以是任意的 java 数据类型,一个方法刚刚开始的时候操作数栈为空,操作数栈本质上是JVM执行引擎的一个工作区,方法在执行,才会对操作数栈进行操作。
3、动态链接:Java 语言特性多态(后续细讲)
4、完成出口:正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)
以一个案例来演示jvm在执行指令过程中局部变量表和操作数栈的作用:
/**
* 演示:
* 栈帧中局部变量表,操作数栈在方法执行过程中的数据变化情况
*
* 1、找到 .class(二进制文件)
* 2、javap 反汇编
* 3、对照指令
https://cloud.tencent.com/developer/article/1333540 模拟方
法的执行
*/
public class Gucci {
public int work()throws Exception{
int x =1;
int y =2;
int z =(x+y)*10;
return z;
}
public static void main(String[] args) throws
Exception{
Gucci gucci = new Gucci();//gucci 栈中 , new
Gucci()对象是在堆
gucci.work();
}
}
反编译得到的字节码指令结果如下:
public int work() throws java.lang.Exception;
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
对比:https://cloud.tencent.com/developer/article/1333540 相关指令的含义在虚拟机栈可模拟出jvm的执行过程。
注意:
再次理解操作数栈的作用:
1、主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
2、操作数栈就是JVM执行引擎的一个工作区, Java虚拟机的解释执行引擎
被称为"基于栈的执行引擎",其中所指的栈就是指-操作数栈
3、为了实现java的跨平台,选择了面向操作数栈的指令集架构而没有选择直接基于CPU寄存器的指令集架构(由执行引擎面向更底层),基于栈的指令集主要的优点就是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束,但是栈架构指令集的主要缺点是执行速度相对来说会稍慢一些,因为栈实现在内存之中,频繁的栈访问也就意味着频繁的内存访问,相对于CPU来说,内存始终是执行速度的瓶颈;当然也有基于CPU寄存器的虚拟机,如Google Android平台的Dalvik VM。
4、前面说JVM虚拟机也可看作一个虚拟的操作系统(跑在物理OS之上的)它就要虚拟出自己的处理器(执行引擎),指令(基于操作数栈的指令集),内存(jvm内存区域),那这里的操作数栈其实就相当于它虚拟出来的CPU高速存。
1、本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点
2、不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机
栈服务的是JVM执行的java方法
3、虚拟机规范里对这块所用的语言、数据结构、没有强制规定,虚拟机可
以自由实现它
4、hotspot把它和虚拟机栈合并成了1个
5、和虚拟机栈一样,大小有限
堆(Heap)是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是
在这里分配存储的。我们常说的垃圾回收,操作的对象就是堆。 堆空间一般是
程序启动时,就申请了,但是并不一定会全部使用,堆一般设置成可伸缩的,
随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的
对象进行回收,这个在 Java 中,就叫作 GC(Garbage Collection)。
1、堆被划分为新生代和老年代( Tenured ),
2、新生代与老年代的比例的值为 1:2 ,即:新生代 ( Young ) = 1/3 的堆空
间大小。老年代 ( Old ) = 2/3 的堆空间大小;该值可以通过参数 – XX:NewRatio 来指定 。
3、新生代又被进一步划分为 Eden 和 Survivor 区, Survivor 由 From Survivor 和 To Survivor 组成,
4、eden,from,to的大小比例为:8:1:1;可通过参数 - XX:SurvivorRatio 来指定
5、JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
堆的大小可由如下两个参数设置:
1、-Xmssize:堆的初始大小,等价于 -XX:InitalHeapSize
2、-Xmxsize:最大堆大小,等价于 -XX:MaxHeapSize
JHSDB 是一款基于服务性代理实现的进程外调试工具。服务性代理是HotSpot 虚拟机中一组用于映射 Java 虚拟机运行信息的,主要基于 Java 语言实现的API 集合。
下面我们依次来看它的使用方式
Jdk1.8 启动 JHSDB 的时候必须将 sawindbg.dll (一般会在 JDK的 %JAVA_HOME%\jre\bin 目录下)复制到和 %JAVA_HOME% 同级目录下的
jre\bin 目录下
然后进入到 %JAVA_HOME%/lib 目录下执行如下命令启动
java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB
/**
* 堆的内存结构:
* 新生代:
* Eden
* from
* to
* 老年代:
*
* 永久代(1.8已废弃)
*/
public class ObjectHeapLocation {
private static String MAN_POSI = "123";
private final static String WOMAN_POSI = "222";
private static int age = 99;
/**
* VM参数:
* -Xms30m -Xmx30m -XX:MaxMetaspaceSize=30m -XX:+UseConcMarkSweepGC -XX:-UseCompressedOops
*/
public static void main(String[] args) throws Exception{
//System.out.println(Integer.toHexString(System.identityHashCode(MAN_POSI)));
//System.out.println(Integer.toHexString(System.identityHashCode(WOMAN_POSI)));
Person p1 = new Person();
p1.setName("xx");
p1.setPosition(MAN_POSI);
p1.setAge(30);
for (int i = 0; i < 15; i++) {
System.gc(); //主动触发GC 垃圾回收 15次
}
Person p2 = new Person();
p2.setName("yy");
p2.setPosition(WOMAN_POSI);
p2.setAge(29);
Thread.sleep(Integer.MAX_VALUE);//线程休眠
}
static class Person {
String name;
String position;
int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getPosition() {
return position;
}
public void setPosition(String position) {
this.position = position;
}
}
}
因为jvm启动后有一个进程,通过 jps 可查看进程号,找到我们的进程号
复制该进程号,去 JHSDB 中 attach 到该进程
然后可看到该进程中所有运行的线程情况,并找到我们的 main 线程
查看堆参数如下:
上图中可以看到实际 JVM 启动过程中堆中参数的对照,可以看到堆空间里
面的分代划分情况。
在搜索框中搜索我们的对象
com.jvm.ObjectHeapLocation.Person ,按照全路径名搜索
能发现 Person 对象有两个,通过对比分析,我们发现 p1 在老年代, p2 在
年轻代。
从图中能看出来 hotspot 将虚拟机栈和本地方法栈合二为一了。
对于上面的代码,JVM在运行时,
1、JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间
2、JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小 3、完成类加载(加载细节看后续章节),收集所有类的初始化代码,包括静态变量赋值语句、 静态代码块、静态方法;静态变量和常量放入方法区。
4、启动 main 线程执行 main 方法,开始执行,该创建对象创建对象,对象引用放入栈,然后调用其他方法等等,如下图
方法区(Method Area)是可供各线程共享的运行时内存区域,主要用来存储已被虚拟机加载的类信息、常量、静态变量、JIT编译器编译后的代码缓存等等,它有个别名叫做:非堆(non-heap),主要是为了和堆区分开。
方法区中存储的信息大致可分以下两类:
1、类信息:主要指类相关的版本、字段、方法、接口描述、引用等
2、运行时常量池:编译阶段生成的常量与符号引用、运行时加入的动态变量
注意:
方法区在虚拟机规范里这是一个逻辑概念,它具体放在哪没有严格的规定,拿 hotspot 虚拟机为例,在 jdk1.7- 和 jdk1.8+ 是不相同的。
永久代是 hotspot 在1.7及之前才有的设计,1.8+,以及其他虚拟机并不存在这个东西。可以说,永久代是1.7的 hotspot 偷懒的结果,他在堆里划分了一块来实现方法区的功能,叫永久代。因为这样可以借助堆的垃圾回收来管理方
法区的内存,而不用单独为方法区再去编写内存管理程序。懒惰!
同时代的其他虚拟机,如 J9 , Jrockit 等,没有这个概念。后来 hotspot
认识到,永久代来做这件事不是一个好主意。1.7已经从永久代拿走了一部分数
据(静态变量和运行时常量池转移到了堆中),直到1.8+彻底去掉了永久代,
方法区大部分迁移到了 metaspace (注意不是全部,不是全部)
永久代大小参数设置:https://docs.oracle.com/javase/8/docs/technote
s/tools/unix/java.html
-XX:PermSize :初始大小
-XX:MaxPermSize :最大值
案例:方法区溢出错误,我们拿 jdk1.6 来演示说明
在1.6里,字符串常量是运行时常量池的一部分,也就是归属于方法区,放
在了永久代里。这个时候经常会出现的一个错误就是:java.lang.OutOfMemoryError: PermGen space
为了演示出这种错误,让方法区溢出,只需要可劲造往字符串常量池中造
字符串即可,看如下代码
/**
* 1.6演示:OutOfMemoryError: PermGen space
*
* String.intern():
* 如果字符串常量池里有这个字符串,直接返回引用,不再额外添加
* 如果没有,加进去,返回新创建的引用
*
* 方法区溢出,注意限制一下永久代的大小
* 编译的时候注意pom里的版本,要设置1.6,否则启动会有问题
* jdk1.6 : -XX:PermSize=6M -XX:MaxPermSize=6M
**/
public class PermGenOOM {
public static void main(String[] args) {
Set<String> stringSet = new HashSet();
int i = 0;
while (true) {
System.out.println(++i);
stringSet.add(String.valueOf(i).intern());
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError:
PermGen space
at java.lang.String.intern(Native Method)
at com.itheima.jvm.PermGenOOM.main(PermGenOOM.java from
InputFileObject:25)
从jdk1.8开始已经将方法区中实现的永久代去掉了,并用元空间( class metadata space )代替了之前的永久代,元空间的存储位置是:本地内存/直
接内存,并且将方法区大部分迁移到了元空间,注意不是方法区的全部。
参数设置
-XX:MetaspaceSize :元空间初始大小
-XX:MaxMetaspaceSize :可从本地内存为元空间分配出的最大值,默认
是没有限制的
元空间的内存溢出 -xx:MaxMetaspaceSize=8m
问题:Java8 为什么使用元空间替代永久代,这样做有什么好处呢?
官方给出的解释是:
1、移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。
2、永久代内存经常不够用或发生内存溢出,抛出异常java.lang.OutOfMemoryError: PermGen 。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,但回收率都偏低,成绩很难令人满意;
3、为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等,而jdk1.8以后的元空间大小就只受本机总内存的限制(如果不设置参数的话),因为它使用的是本地内存。
扩展:
前面提到,方法区的一部分迁移到了元空间,具体哪些在元空间呢?还有
什么不在元空间?
1、元空间里主要存的是类的元数据(方法代码,变量名,方法名,访问权限,返回值等等)
2、类加载的最终产品 Class 对象, static 成员,运行时常量池从物理存储上是在堆空间,逻辑上属于方法区。
在jvm规范中,方法区除了存储类信息之外,还包含了运行时常量池。这里
首先要来讲一下常量池的分类
常量池可分两类:
1、Class常量池(静态常量池)
2、运行时常量池
3、字符串常量池(没有明确的官方定义,其目的是为了更好的使用String )
也叫静态常量池,在 .class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池 ( Constant Pool Table ),用于存放编译期间生成的各种字面量和符号引用,之所以说它是静态的常量池是因为这些都只是躺在 .class 文件中的静态数据,此时还没被加载到内存呢!,这点一定要理解。
比如:
/**
* Class常量池(静态常量池)
*
* 1、编译,注意使用jdk1.8
* 2、找到.class,使用 javap -v ClassConstantPool.class
* 3、查看输出的信息
*/
public class ClassConstantPool {
private static String a = "abc";
private static int b = 123;
private final int c = 456;
private int d = 789;
private float e ;
private String f = "def";
private Gucci gucci = new Gucci();
public static void main(String[] args) {
}
}
反编译过来得到如下所示:
字面量:给基本类型变量的赋值就叫做字面量或者字面值。 字面量是编译后生成的产物。
比如: String a=“b” ,这里“b”就是字符串字面量,同样类推还有整
数字面值、浮点类型字面量、字符字面量。
符号引用 :符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,JAVA 在编译的时候一个每个 java 类都会被编译成一个 class 文件,但在编译的时候虚拟机并不知道所引用类的地址(实际地址),就用符号引用来代替,而在类的解析阶段(类加载的一个过程)就是为了把这个符号引用转化成为真正的地址。
比如: ClassConstantPool 类被编译成一个 class 文件时,发现引用了 Gucci 类,,但是在编译时并不知道 Gucci 类的实际内存地址,因此只能使用符号引用( com.jvm.Guuci )来代替。而在类装载器装载 Guuci 类时,此时可以通过虚拟机获取 Guuci类 的实际内存地址,因此便可以将符号com.jvm.Guuci 替换为 Guuci 类的实际内存地址。
运行时常量池( Runtime Constant Pool )是每一个类或接口的常量池
( Constant_Pool )的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。运行时
常量池相对于 Class 常量池的另外一个重要特征是具备动态性。(这个是虚拟
机规范中的描述,很生涩)
运行时常量池是在类加载完成之后,将 Class 常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。 另外运行时常量池的物理存储位置要注意两点:
1、运行时常量池在 JDK1.7 版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)。
2、在 JDK1.8 中,使用元空间代替永久代来实现方法区,但是方法区从定义上并没有改变,所谓 “Your father will always be your father” 。变动的只是方法区中内容的物理存放位置,运行时常量池和字符串常量池被移动到了堆中而并没有在元空间。但是不论它们物理上如何存放,逻辑上还是属于方法区的。
字符串常量池这个概念是有争议的,很多正式的虚拟机规范文档,都没有
对这个概念作一个明确的官方定义,所以字符串常量池与运行时常量池的关系
不去抬杠,我们从它的作用和 JVM 设计它用于解决什么问题的点来分析它。
以 JDK1.8 为例,字符串常量池是存放在堆中,并且与 java.lang.String类有很大关系。设计这块内存区域的原因在于: String 对象作为 Java 语言中重要的数据类型,是内存中占据空间最大的一个对象。高效地使用字符串,可以提升系统的整体性能。
StringTable特性
1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
1.6将这个字符串对象尝试放入串池,如有有则并不会放入,没有没有则会把**对象复制一份**(深克隆),放入串池,会把串池中的对象返回
1、存储位置
在1.8中验证字符串常量池真实的存储位置,还是之前的 PermGenOOM 代
码,但是运行时添加如下参数
-XX:PermSize=6M -XX:MaxPermSize=6M -XX:MetaspaceSize=10M - XX:MaxMetaspaceSize=10M
控制台输出,发现很难出现 OOM ,理论上只要堆内存够,可以一直打下
去,然后我们再加一个限制堆大小的参数:
-Xms10M -Xmx20M 1
再次运行,过一会会报 OOM 。
这个现象说明字符串常量池的物理存储其实是在堆中
StringTable垃圾回收
在内存紧张时,会自动进行
调优
底层是数组+链表的结构
1.桶大小,默认6000,增加桶数(增加数组长度)
2.有大量字符串,而且有重复,入池再添加会减少很多内存占用
2、String类分析(1.8)
先看String的定义
public final class String
implements java.io.Serializable, Comparable<String>,
CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
....
}
String 对象是对 char 数组进行了封装实现的对象,主要有 2 个成员变量: char 数组, hash 值。 这点证明了String对象的不可变性,因为他们被priavte+final 修饰,java这样做有什么好处呢?
1、保证 String 对象的安全性,假设 String 对象是可变的,那么String 对象将可能被恶意修改
2、保证 hash 属性值不会频繁变更,确保了唯一性,使得类似 HashMap容器才能实现相应的 key-value 缓存功能
3、可以实现字符串常量池。在 Java 中,通常有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,如 String str=“abc” ;另一种是字符串变量通过 new 形式的创建,如 String str = new String(“abc”) 。
下面来看String的创建方式及内存分配的方式
public static void main(String[] args) {
/**
* 1、
* 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,不在则创建并放入
*/
String a = "abc";
String a_1 ="abc";
System.out.println(a==a_1);
/**
* 2、
* 编译时"abc"作为字面量在Class常量池,类加载时创建到字符串常量池
* new String时会在堆中创建String对象,通过构造传入字符串常量池中"abc"的引用
*/
String b = new String("abc");
System.out.println(a==b);
//3、编译器会优化 String c= "abcdef" 可以查看字节码
String c = "ab"+"cd"+"ef";
//4、编译器会通过 StringBuilder.append 优化
String d = "wo yao";
String f = "mama " + d +" chi nai";
//5、
String g = "xx";
User user = new User();
user.setName("xx");
user.setAddress("北京");
System.out.println(g==user.getName());
/**
* 6、intern
* 1、new Sting() 会在堆内存中创建一个 h 的 String 对象,ts"将会在常量池中创建
* 2、在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。
* 3、调用 new Sting() 会在堆内存中创建一个 i 的 String 对象。
* 4、在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。
* 所以 h 和 i 引用的是同一个对象
*/
String h = new String("ts").intern();
String i = new String("ts").intern();
System.out.println(h==i);
}
static class User{
private String name;
private String address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
false
true
true
1.6 false
1.8 true
常量池中的信息,都会被加载到运行时常量池中,这时的a,b,ab都是常量池的符号,还没有变为java字符串对象,当运行到的时候会先到StringTable[](串池)中找,没有就放入串池 (hashtable结构)
直接内存有一种更加科学的叫法,堆外内存(off-heap)。
1、JVM 在运行时,会从操作系统申请大块的堆内存,进行数据的存储;同
时还有虚拟机栈、本地方法栈和程序计数器,这块称之为栈区。操作系统剩余的内存也可以申请过来一块,也就是堆外内存。
2、它不是虚拟机运行时数据区的一部分,也不是 java 虚拟机规范中定义
的内存区域;
3、如果使用了 NIO,这块区域会被频繁使用,主要是通过DirectByteBuffer 申请,在 java 堆内可以用 DirectByteBuffer 对象直接引用并操作;
4、开发者也可以通过 Unsafe 或者其他 JNI 手段直接直接申请。
5、这块内存不受 java 堆大小限制,但受本机总内存的限制,可以通过 -XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出
现 OOM 异常。
6、堆外内存的泄漏是非常严重的,它的排查难度高、影响大,甚至会造成主机的死亡
注意:
Oracle之前计划在Java 9中去掉 sun.misc.Unsafe API 。这里删除sun.misc.Unsafe 的原因之一是使Java更加安全,并且有替代方案。
1.NIO操作时,用于数据缓冲区
2.分配回收成本较高,但读写性能高
3.不受JVM内存回收管理
使用了Unsafe对象完成直接内存的分配回收,并且回收需要住的调用freeMemory方法
ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMenory来释放直接内存
可以用unsafe直接管理
又叫内存溢出,是特别需要注意的一个问题
1、对于 java.lang.StackOverflowError 一般的方法调用是很难出现的,如果出现了可能会是无限递归。
虚拟机栈带给我们的启示:方法的执行因为要打包成栈桢,所以递归天生要比实现同样功能的循环慢,所以树的遍历算法中:递归和循环都有存在的意义。递归代码简洁,非递归代码复杂但是速度较快。
2、 OutOfMemoryError Stack 的出现需要不断建立线程,JVM 申请栈内存,机器没有足够的内存。(一般演示不出,演示出来机器也死了)
3、栈区的总空间大小 JVM 没有办法去限制的,因为 JVM 在运行过程中会有线程不断的运行,没办法限制,所以只限制单个虚拟机栈的大小,可通过 - Xss 设置
堆内存溢出:申请内存空间,超出最大堆内存空间。
1、如果是内存溢出,则通过 调大 -Xms,-Xmx 参数。
2、如果是内存泄漏(Memory Leak),就是说内存中的对象一直回收不
掉,要使用工具查看泄露对象到 GC Roots 的引用链,找到泄露对象是通过怎样
的路径与 GC Roots 相关联并导致垃圾收集器无法自动回收它们。
3、如果不是内存泄露,也就是说内存中的对象的确都是必须存活的,那么
应该检查 JVM 的堆参数设置,与机器的内存对比,看是否还有可以调整的空
间, 再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存
储结构设计不合理等情况,尽量减少程序运行时的内存消耗。
1、运行时常量池溢出
2、方法区中保存的 Class 信息占用的内存超过了我们配置或者 Class 对象该回收时没有被及时回收,比如有个参数:
-Xnoclassgc ,就是禁用 Class 的回收
注意:Class 要被回收,条件比较苛刻(仅仅是可以,不代表必然,因为还有一些参数可以进行控制):
1、该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
2、 加载该类的 ClassLoader 已经被回收。
3、 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
代码示例:使用 cglib 不断生成 Class 并加载到内存,如何设置MetaspaceSize 的大小
-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
出现以下错误!
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
直接内存的容量可以通过 -XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异常;
由直接内存导致的内存溢出,一个比较明显的特征是在 HeapDump 文件中不会看见有什么明显的异常情况,如果发生了 OOM ,同时 Dump 文件很小,可以考虑重点排查下直接内存方面的原因。
我们从源码,字节码来看对象的创建过程
1、先看如下源码
public class ObjectCreate {
private int age = 18;
private boolean isGay;
private String friend;
ObjectCreate(){
isGay = true;
friend ="yyds";
}
public static void main(String[] args) {
ObjectCreate obj = new ObjectCreate();
}
}
2、查看字节码,这里字节码分两个方法的字节码,一个是 main 一个是
init
先看 main 的字节码
0 new #7 <com/itheima/jvm/ObjectCreate>
3 dup
4 invokespecial #8 <com/itheima/jvm/ObjectCreate.<init> :
()V>
7 astore_1
8 return
可以看出源码中 new 一个对象其实对应着多条字节码指令,证明对象的创建分好几个过程,其中 invokespecial 指令就是去执行 init 函数的,对应的字节码如下
0 aload_0
1 invokespecial #1 <java/lang/Object.<init> : ()V>
4 aload_0
5 bipush 18
7 putfield #2 <com/itheima/jvm/ObjectCreate.age : I>
10 aload_0
11 iconst_1
12 putfield #3 <com/itheima/jvm/ObjectCreate.isGay : Z>
15 aload_0
16 ldc #4 <yyds>
18 putfield #5 <com/itheima/jvm/ObjectCreate.friend :
Ljava/lang/String;>
21 return
首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用(符号引用 :以一组符号来描述所引用的目标),并且检查类是否已经被加载、 解析和初始化过。
虚拟机将为新生对象分配内存。为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来,主要有两种分配内存的方式:
如果 Java 堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。
如果 Java 堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。
选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用
的垃圾收集器是否带有压缩整理功能决定。 如果是 Serial、ParNew 等带有压
缩的整理的垃圾回收器的话,系统采用的是指针碰撞,既简单又高效。如果是
使用 CMS 这种不带压缩(整理)的垃圾回收器的话,理论上只能采用较复杂的
空闲列表。
除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题主要依靠以下两种解决方案:
CAS
对分配内存空间的动作进行同步处理——虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性
TLAB
另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块私有内存,也就是本地线程分配缓冲( Thread Local Allocation Buffer,TLAB ),JVM 在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer ,如果需要分配内存,就在自己的 Buffer 上分配,这样就不存在竞争的情况,可以大大提升分配效率,当 Buffer 容量不够的时候,再重新从 Eden区域申请一块继续使用。
TLAB 的目的是在为新对象分配内存空间时,让每个 Java 应用线程能在使
用自己专属的分配指针来分配空间,减少同步开销。
另外 TLAB 只是让每个线程拥有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个 TLAB用满时就重新申请一个 TLAB 。
涉及到的参数: -XX:+UseTLAB ,允许在年轻代空间中使用线程本地分配块
( TLAB )。默认情况下启用此选项。要禁用 TLAB ,请指定 -XX:-UseTLAB
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为默认值(如 int 值为 0, boolean 值为 false 等等)。这一步操作保证了对象 的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的默值。
所以在创建对象的过程中, 对象的成员属性存在着一个中间状态值,就是默认值。
虚拟机要对对象头( object header )进行必要的设置,例如markword ,这个对象是哪个类的实例、数字的长度等等这些信息!
上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚刚开始,所有的字段都还为默认值。所以,一般来说,执行 new 指令之后会接着把对象按照程序员的意愿进行初始化(构造方法),执行 init 函数,这样一个真正可用的对象才算完全产生出来。
当一个对象在堆内存中分配好并且初始化完成之后的结构是什么样的呢?
经常在大厂面试中看到这样的题目:
1、说一说对象在内存中的布局?
2、 new Object() 在内存中占多少字节?
要回答出这些问题,就要来学习对象的内存布局结构!!!
JOL:java object layout,java对象布局,是openjdk提供的一个工具
要使用这个工具,首先引入坐标
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
<scope>provided</scope>
</dependency>
其次使用方法如下:
/**
* 认识 JOL
* https://mvnrepository.com/artifact/org.openjdk.jol/jol-core
*
* org.openjdk.jol
* jol-core
* 0.9
*
*
*
* VM参数:关于是否开启指针压缩,默认开启
* -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
* 如果要关闭:
* -XX:-UseCompressedClassPointers -XX:-UseCompressedOops
*/
public class KnowJOL {
public static void main(String[] args) {
Object o = new Object();
//打印对象的内存布局
System.out.println(ClassLayout.parseInstance(o).toPrintable());
User user = new User(18,"ts");
System.out.println(ClassLayout.parseInstance(user).toPrintable());
Coupons c = new Coupons(100L);
System.out.println(ClassLayout.parseInstance(c).toPrintable());
int[] arr = new int[]{1,2,3};
System.out.println(ClassLayout.parseInstance(arr).toPrintable());
User[] users = new User[3];//示例数据占12字节,因为存的是引用
System.out.println(ClassLayout.parseInstance(users).toPrintable());
}
static class User{
private int age;
private String name;
public User(){};
public User(int age,String name) {
this.age = age;
this.name = name;
}
}
static class Coupons{
private long id;
public Coupons(){};
public Coupons(long id) {
this.id = id;
}
}
}
打印输出的结果如下
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
com.itheima.jvm.KnowJOL$User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 60 6a 0e 00 (01100000 01101010 00001110 00000000) (944736)
12 4 int User.age 18
16 4 java.lang.String User.name (object)
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
com.itheima.jvm.KnowJOL$Coupons object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) b8 6e 0e 00 (10111000 01101110 00001110 00000000) (945848)
12 4 (alignment/padding gap)
16 8 long Coupons.id 100
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 10 0c 00 00 (00010000 00001100 00000000 00000000) (3088)
12 4 (object header) 03 00 00 00 (00000011 00000000 00000000 00000000) (3)
16 12 int [I.<elements> N/A
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
[Lcom.itheima.jvm.KnowJOL$User; object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 38 75 0e 00 (00111000 01110101 00001110 00000000) (947512)
12 4 (object header) 03 00 00 00 (00000011 00000000 00000000 00000000) (3)
16 12 com.itheima.jvm.KnowJOL$User KnowJOL$User;.<elements> N/A
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
结合JVM规范,以及hotspot的实现,我们来看对象的内存布局:https://o
penjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html通过前面打印对象内存布局代码的输出结果来看,一个对象在内存中的布局如下:
1、添加对求填充是为了保证对象的总大小是8的整数倍个字节。
2、类型指针占4个字节是因为默认开启了指针压缩,如果不开启指针压缩,则占8个字节,通过如下命令行参数可查看
java -XX:+PrintCommandLineFlags -version
其中: -XX:+UseCompressedClassPointers 表明开启了类型指针压缩,另外 -XX:+UseCompressedOops 则表明开启了普通对象指针压缩, oops: ordinary object pointer ,
什么叫普通对象指针压缩?比如对象A中有一个对象B的引用, 这个引用就是一个指针。
建立对象是为了使用对象,我们的 Java 程序需要通过栈上的 reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种
句柄方式:栈指针指向堆里的一个句柄的地址,这个句柄再定义俩指针分别指向类型和实例。
好处是:垃圾回收时遇到对象内存地址的移动只需要修改句柄即可,不需要修改栈指针
弊端是:寻址时多了一次操作。
直接地址:栈指针指向的就是实例本身的地址,在实例里封装一个指针指向它自己的类型。
很显然,垃圾回收要移动对象时要改栈里的地址值,但是它减少了一次寻址操作。
备注:hostspot使用的是直接地址方式
在 JVM 开启逃逸分析后,如果对象没有逃逸,结合对象的大小等因素决定对象分配在栈上。其本质是Java虚拟机提供的一项优化技术。
JVM会分析对象的动态作用域,当一个对象在方法中定义后,它可能被外部所引用,称之为逃逸。
比如:通过调用参数传递到其他方法中,称之为方法逃逸;赋值给其他线程中访问的变量,称之为线程逃逸。 从不逃逸到方法逃逸到线程逃逸,称之为对象由低到高的不同逃逸程度。
开启逃逸分析需要配置以下参数:
-XX:+DoEscapeAnalysis :,默认开启
如果开启逃逸分析,那么即时编译器( Just-in-time Compilation, JIT )在运行期就可以对代码做如下优化:
(1)同步锁消除:如果确定一个对象不会逃逸出线程,即对象被发现只能被一个线程访问到,无法被其它线程访问到,那该对象的读写就不会存在竞争,对这个变量的同步锁就可以消除掉
public static void f() {
Object obj = new Object();
synchronized(obj) {
System.out.println(obj);
}
}
使用 synchronized 的时候,如果 JIT 经过逃逸分析之后发现并无线程安全问题的话,就会做锁消除(注意是在运行时),
public static void f() {
Object obj = new Object();
System.out.println(obj);
}
锁消除在jdk1.8默认开启,可通过如下参数配置
-XX:+EliminateLocks :开启锁消除,锁消除基于逃逸分析基础之上,开启锁消除必须开启逃逸分析
-XX:-EliminateLocks : 关闭锁消除
(2)分离对象或标量替换。Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解,它们就可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化, 可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。
/**
* 2、标量替换
* 标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
* 相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
*
* 在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换
*
* VM参数:
* -XX:+DoEscapeAnalysis -XX:+EliminateAllocations
*/
static void scalar() {
Point point = new Point(1,2);
System.out.println("point.x="+point.x+"; point.y="+point.y);
}
// 标量替换为栈上分配提供了很好的基础
static class Point {
private int x;
private int y;
Point(int x,int y) {
this.x = x;
this.y = y;
}
}
以上代码中, point 对象并没有逃逸出 scalar 方法,并且 point 对象是可以拆解成标量的。那么在运行时 JIT 就会不会直接创建 Point 对象,而是直接使用两个标量 int x ,int y 来替代 Point 对象,如下
//标量替换后
static void scalar2() {
int x = 1;
int y = 2;
System.out.println("point.x="+x+"; point.y="+y);
}
标量替换需要添加以下 VM 参数
-XX:+EliminateAllocations ,但前提是开启逃逸分析。并由此可见标量替换为栈上分配提供了很好的基础。
-XX:+PrintEliminateAllocations 查看标量替换情况(Server VM非Product版本支持)
(3)将堆分配转化为栈分配:栈上分配就是把方法中的变量和对象分配到栈上,方法执行完后栈自动销毁,而不需要垃圾回收的介入,从而提高系统性能。栈上分配基于逃逸分析和标量替换。
栈上分配的优点:
1、可以在函数调用结束后自行销毁对象,不需要垃圾回收器的介入,有效避免垃圾回收带来的负面影响
2、栈上分配速度快,提高系统性能
栈上分配的局限性: 栈空间小,对于大对象无法实现栈上分配
public static void main(String[] args) throws Exception{
//f();
stackAlloc();
//scalar();
}
/**
* 3、栈上分配
* VM参数:
* -Xms220M -Xmx220M -XX:+DoEscapeAnalysis -XX:+EliminateAllocations -XX:+PrintGC
*
*/
static void stackAlloc() throws Exception{
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {//创建1000万个对象,内存占:10000000 * 24 约228M
allocate();
}
System.out.println((System.currentTimeMillis() - start) + " ms");
Thread.sleep(Integer.MAX_VALUE);
}
//逃逸分析(不会逃逸出方法)
static void allocate() {
//这个escape引用没有出去,也没有其他方法使用
Escape escape = new Escape(2021, 100D);
//bom(escape);
}
static void bom(Escape e) {
Escape es = e;
e = new Escape(2022,250d);
}
// 一个Escape对象占24个字节
static class Escape {
int id;
double score;
Escape(int id,double score) {
this.id = id;
this.score = score;
}
}
1、开启逃逸分析和标量替换,运行,通过 jps 查找到进程id,然后通过如下命令查看java堆上的对象分布情况
jmap -histo 4904 1
控制台没有输出GC日志,方法整体耗时6 ms,
2、关闭逃逸分析或者标量替换后,可以发现有GC的日志,时间明显变长
控制台输出了GC日志,方法整体耗时69ms
由此可见:关闭逃逸分析后堆内存中的 Escape 对象要比开启多 11107728-1971048=9136680 字节,
证明了启用了逃逸分析,可以减少堆内存的使用和减少GC。
大多数情况下,对象在新生代 Eden 区中分配。
public static void main(String[] args) throws Exception {
//test1();
test2();
}
/**
* 大多数情况下对象优先Eden分配,空间不够触发 Minor GC
* VM参数:-Xms30M -Xmx30M -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+PrintGCDetails
*/
static void test1() throws Exception{
//1、创建一个普通对象 优先分配在 eden 区
Eden e = new Eden(1,"xx");
//2、空间不够分配 触发 Minor GC eden 大概8M 测试时先注释第3步
int num = 349525;
for (int i=0;i<num-1;i++) { // 内存占:10000000 * 24 约228M
Eden ed = new Eden(i,"ts"+i);
}
/**
* 大对象直接进入老年代
*/
byte[] arr = new byte[1024*1024*10];//数组长度是 10,485,760
Thread.sleep(Integer.MAX_VALUE);
}
// 一个 Eden 对象占24个字节
static class Eden {
int id;
String name;
Eden(int id,String name) {
this.id = id;
this.name = name;
}
}
当 Eden 区没有足够空间分配时,虚拟机将发起一次 Minor GC
使用线程本地分配缓冲会加快在 Eden 中的分配效率,测试如下,查看控制台时间的消耗情况
/**
* 在 eden 分配时是使用TLAB 性能更高
* VM参数:-Xms30M -Xmx30M -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+UseTLAB
* 不适用线程本地分配缓冲则性能有所下降
* -XX:-UseTLAB
*/
static void test2() {
long start = System.currentTimeMillis();
int num = 10000000;
for (int i=0;i<num;i++) { // 内存占:10000000 * 24 约228M
Eden ed = new Eden(i,"ts"+i);
}
System.out.println((System.currentTimeMillis() - start) + " ms");
}
大对象就是指需要大量连续内存空间的 Java 对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。
大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,我们写程序的时候应注意避免。
在 Java 虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复制对象时,大对象就意味着高额的内存复制开销。
HotSpot 虚拟机提供了 -XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的:1.避免大量内存复制,2.避免提前进行垃圾回收,明明内存有空间进行分配。
-XX:PretenureSizeThreshold 参数只对 Serial 和 ParNew 两款收集器有效。 -XX:PretenureSizeThreshold=4m
HotSpot 虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。 为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。
有以下几点要注意:
1、如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1,对象在 Survivor区中每熬过一次 Minor GC ,年龄就增加 1,当它的年龄增加到一定程度(并发的垃圾回收器默认为 15),CMS 是 6 时,就会被晋升到老年代中。
2、可以通过参数: -XX:MaxTenuringThreshold=threshold 调整
3、为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold 中要求的年龄
1、在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。
2、如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC ,尽管这次Minor GC 是有风险的,如果担保失败则会进行一次 Full GC;如果小于,或者HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC 。