JVM内存结构

一、虚拟机历史

三大主流虚拟机:Sun 的 HotSpot VM、BEA公司的JRockit VM 和 IBM公司的J9 VM

二、JVM内存结构

主要包括堆区,方法区(元数据空间),虚拟机栈(JAVA线程栈),本地方法栈,程序计数栈


RuntimeDataArea.png
  • 堆:是虚拟机所管理的内存中最大的一块,主要是存放对象实例。参数设置:-Xms1g -Xmx2g
    堆= 新生代+老年代,比例是1:2, 新生代可以用 -Xmn设置, 新生代=Eden+FromSurvior+ToSurvior,比例是8:1:1

分代是为了解决提升垃圾回收的速度

  • 方法区:也叫永久区(永久代),用于存储类信息,常量、静态变量等数据,为了与Java堆区分开,也叫"非堆区"。 参数设置:-XX:PermSize-XX:MaxPermSize 默认值是64MB

  • 元空间: 从JDK1.8开始,方法区变成元空间,直接占用本地内存,去掉方法区设置参数,新参数为-XX:MaxMetaspaceSize默认值是21M, -XX:MetaspaceSize 默认值-1,上限受限于宿主机内存

字符串存在永久代中,容易出现性能问题和内存溢出。
类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

永久代会为 GC 带来不必要的复杂度,并且回收效率偏低

  • 直接内存:不是虚拟机运行时数据区的一部分。如果使用NIO,在java堆内可以用DirectByteBuffer对象直接引用并操作,直接使用堆外内存。受宿主机总内存限制,频繁使用也会抛OutOfMemoryError异常。参数设置:-XX:MaxDirectMemorySize 默认值与堆内存最大值一样

  • 运行时常量池:原本是方法区的一部分,用于存放编译期生成的各种字面量(“abcd”,“12345”等)和符号引用,从JDK1.7开始,运行时常量池移到堆中。

  • 程序计数器:是当前线程所执行的字节码的行号指示器,较小的内存空间,唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

  • 本地方法栈:该区域是为虚拟机使用到的本地方法服务

  • Java栈:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法返回地址等信息(从调用直到完成的过程,对应一个栈帧在java栈中的入栈出栈的过程)。参数设置:-Xss

设置-XX:+PrintCommandLineFlags 参数,启动程序会先打印jvm启动参数

栈的特点

  • 栈是线程私有的,即每个线程都会有一个栈空间,同栈中数据可以共享,但线程之间不能共享栈中数据
  • 栈的生命周期短且小,超出变量作用域后,会自动释放所分配的内存空间,该内存空间立即能被使用
  • 当栈内存空间不够,会抛出java.lang.StackOverFlowError异常

主要是局部变量和函数形参,以及对象的引用存放在栈中

堆的特点

  • 堆是线程共享的,一个JVM只有一个堆内存
  • 堆的生命周期较长且大,当没有引用指向对象时,对象并不会马上回收,需要等待JVM垃圾收集器来回收内存
  • 当堆中内存不足,则会抛出java.lang.OutOfMemoryError异常

除了栈,元空间,其余都在堆中

运行时常量池

运行时常量池原先是方法区的一部分,从JDK1.7开始运行时常量池从方法区中移出,放在堆中。常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享,提升性能。

  • 1.字符串常量池
    字符串常量池是属于运行时常量池的一块,对于字符串,其对象的引用都是存储在栈中的,直接用双引号定义String str="test"存储在字符串常量池中,运行时new String("test"), 存储在堆中。
String str = "te";
String str1 = "test";
String str2 = "te" + "st";
String str3 = "test";
String str4 = str+"st";
String str5 = "te"+"st";

String newStr1 = new String("test");
String newStr2 = new String("test");

System.out.println(str1 == str3);       //true   str1和str3字符串常量池"test"
System.out.println(str1 == str2);       //true
System.out.println(str1 == newStr1);    // false.
System.out.println(newStr1 == newStr2); //false
System.out.println(str2 == str4); // false
System.out.println(str2 == str5); // true

'equals'相等的字符串,常量池只有一份,堆中有多份

  • 2.包装类常量池技术的运用
    java 8种基本数据类型都有自己的包装类,其中Byte,Short,Integer,Long,Character,Boolean都实现了常量池技术;而Byte,Short,Integer,Long类型在装箱时会缓存了范围[-128,127]的数据到数组中,Character会缓存[0,127]范围的数据到数组中进行缓存。查看对应valueOf()方法实现

Integer为例, valueOf方法源码如下:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

当整数在[-128,127]的范围在自动装箱时会直接使用常量池里面(不存在会创建一个),当整数不在[-128,127]范围内时,就会在堆中创建对象

int i = 100;
Integer i1 = 100;        //范围是[-128,127]的数在自动装箱时加入常量池里面
Integer i2 = 100;
Integer i3 = new Integer(100);
Integer i4 = new Integer(100);
System.out.println(i==i1);      //true   包装类型跟基本类型比较都会先拆箱转成基本类型再比较,因此比较的是值,类似使用equals
System.out.println(i1==i2);     //true.  都指向常量池的100
System.out.println(i2==i3);     //false
System.out.println(i3==i4);     //false

案例分析(JDK1.8)

class Student{

    private static final String PREFIX = "STUDENT:";    //静态字符串常量,字符串常量池
    private String name;
    private int age;

    public Student(String n, int a) {
        this.name = n;
        this.age = a;
    }

    public void changeAge(int a) {
        this.age = a;
    }
}
public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        Student student = new Student("zhangsan", 12);
        int num = 13;
        student.changeAge(13);
    }
}
  • Student,Test类及方法成员等类信息存放在元空间
  • Test test = new Test();test为对象引用,存放栈中,对象(new Test())存在堆中
  • Student student = new Student("zhangsan", 12);student为对象引用,存放栈中,对应的值也就是对象new Student("zhangsan", 12) 存放在堆中,其中n,a为局部变量存储在栈中,n为字符串类型对象存放字符串常量池中,a为基本类型存储在栈中;name,age为成员变量,它们存储在堆中;当Student构造方法执行完之后,n,a将从栈中消失。
  • num 为局部变量,且是基本类型,引用和值都存在栈中
  • main 方法执行完成后,栈中test,num,student变量消失,值new Test(), new Student()将等待垃圾回收

三、常用垃圾回收器

gc.png
  • Serial 串行回收器(新生代)
    单线程垃圾收集器,采用复制算法、串行回收和STW机制进行内存回收,

  • Serial Old 串行回收器(老年代)
    单线程垃圾收集器,采用标记整理(压缩)算法、串行回收和STW机制进行内存回收,可以作为老年代CMS回收器的后备垃圾收集方案, 可以通过 -XX:+UseSerialGC参数启用Serial回收器,表示新生代使用Serial,老年代使用Serial Old

  • ParNew回收器(新生代)
    Parallel New是Serial的多线程版本垃圾回收器,采用复制算法、并行回收和STW机制进行内存回收,可以通过-XX:+UseParNewGC参数启用ParNew回收器,表示新生代使用ParNew

  • CMS回收器 (老年代)
    并发收集,采用标记清除算法,CMS的关注点在于尽可能缩短垃圾收集时用户线程停顿的时间
    可以通过-XX:+UseConcMarkSweepGC 开启,开启后-XX:+UseParNewGC会自动打开。(JDK9 该收集器被标记过时,JDK14被删除)

  • Parallel Scavenge回收器(新生代)
    采用复制算法,并行回收和STW机制,以吞吐量优先的垃圾回收器。可以让用户设置最大垃圾收集停顿时间-XX:MaxGCPauseMillis以及吞吐量大小-XX:GCTimeRatio, 是 jdk1.7 和 1.8 的默认的年轻代垃圾收集器。可以通过-XX:+UseParallelGC指定, 默认老年代使用Parallel Old收集器,如果老年代想采用串行回收器,可以增加-XX:-UseParallelOldGC 配置。

  • Parallel Old收集器(老年代)
    Parallel Scavenge 收集器的老年代版本,基于标记-整理算法, 是 jdk1.7 和 1.8 的默认的老年代垃圾收集器。可以通过 -XX:+UseParallelOldGC指定,默认年轻代使用Parallel Scavenge回收器

  • Garbage First(G1)收集器
    垃圾收集器技术发展历史上的里程碑式的结果,没有在物理空间上去区分新生代和老年代,而是把堆内存分割成很多不相关的区域(region,物理上不连续),使用不同区域来表示伊甸园区,幸存者区和老年代区,大对象区,尽量避免对整个Java堆进行垃圾收集,它会跟踪各个region里垃圾回收的价值大小(回收所获得的空间大小及所需时间的经验值),在后台维护一个优先列表(额外占用内存空间),每次根据允许收集时间,优先回收价值最大的region。可以通过-XX:+UseG1GC 开启。JDK1.9 默认的垃圾收集器。适合堆内存很大的应用。

垃圾回收器 分类 作用位置 使用算法 特点 适用场景
Serial 串行 新生代 复制算法 响应速度优先 适用于单CPU环境下的Client模式
ParNew 并行 新生代 复制算法 响应速度优先 多CPU环境Server模式下与CMS配合使用
Parallel 并行 新生代 复制算法 吞吐量优先 适用于后台运算而不需要太多交互的场景
Serial Old 串行 老年代 标记-压缩算法 响应速度优先 单CPU环境下的Client模式
Parallel Old 并行 老年代 标记-压缩算法 吞吐量优先 适用于后台运算而不需要太多交互的场景
CMS 并发 老年代 标记-压缩算法 响应速度优先 适用于互联网或B/S业务
G1 并行与并发 新生代、老年代 复制算法 标记-压缩算法 响应速度优先 面向服务端应用

GC日志打印配置

  • JDK 1.8
-Xms2G -Xmx2G -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -Xloggc:/logs/myGC.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/
  • JDK1.9以上(包含JDK1.9)
-Xms2G -Xmx2G -Xlog:gc*=info,gc+heap=debug,gc+age=trace,safepoint:/logs/gc_%t.log:time,level,tags:filecount=5,filesize=20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/

从上面例子对比,可以看出,JDK1.8及更早版本,设置gc日志显得比较麻烦,JDK1.9开始,统一使用一个Xlog 参数来设置gc日志打印。

java -Xlog:help  //查看参数所有所有选项

格式如下:

-Xlog[:[selections][:[output][:[decorators][:output-options]]]]

官网:https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/tutorial-Xlog/html/index.html
https://docs.oracle.com/en/java/java-components/enterprise-performance-pack/epp-user-guide/printing-jvm-information.html
https://docs.oracle.com/en/java/javase/11/jrockit-hotspot/logging.html#GUID-33074D03-B4F3-4D16-B9B6-8B0076661AAF

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容