文档章节

JVM调优之:内存模型

贾峰uk
 贾峰uk
发布于 2017/04/24 01:56
字数 2908
阅读 16
收藏 0

1. JVM内存模型

2.程序计数器

程序计数器是一个很小的内存空间。由于Java是支持多线程的语音,当线程数量超过cpu数量,线程之间根据时间片轮询抢夺CPU资源。对于单核CPU而言,每一时刻只能有一个线程在执行,而其他线程必须被切换出去。为此每一个线程必须有一个独立的程序计数器,用于记录下一条要执行的指令。各个线程之间的计数器互不影响,独立工作。是一块线程私有的内存空间。

如果当前线程正在执行一个Java方法,程序计数器中存放的就是正在执行的Java字节码地址,如果正在执行的是native方法,则程序计数器为空。

3.虚拟机栈

Java虚拟机栈也是线程的私有的内存空间,它和Java线程在同一时刻创建,它保存方法的局部变量、部分结果,并参与方法的调用和返回。

JAVA规范中允许Java栈的大小是动态的或者是固定不变的;Java虚拟机中定义了两种异常与栈空间有关

StackOverflowError:如果线程在计算过程中,请求的栈深度大于最大可用的栈深度,则抛出该错误。

OutOfMemoryError:如果Java栈可以动态扩展,在扩展过程中没有足够的内存空间来支持栈的扩展,则抛出该错误。

JVM中可是使用-Xss参数来设置栈大小,栈的大小直接决定了函数调用的可达深度。以下代码通过递归调用查看方法调用可达深度。在JDK5.0及其以前栈默认大小为256k,JDK5.0之后jvm栈默认大小为1m。

public class StackTest {
    private int count = 0;//记录栈可达深度
    public void recursion(){
        count ++;
        recursion();
    }

    public static void main(String[] args) {
        StackTest stackTest = new StackTest();
        try {
            stackTest.recursion();
        }catch (Throwable e){
            System.out.println("deep of stack is "+stackTest.count);
            e.printStackTrace();
        }
    }
}

默认执行结果:

deep of stack is 17127
java.lang.StackOverflowError

如果系统需要更深的栈调用,设置-Xss2m

执行结果:

deep of stack is 73391
java.lang.StackOverflowError

可以看到增加栈空间后,函数调用的栈深度明显增加。

虚拟机栈在运行时使用一种叫做栈帧的数据结构保存上下文数据,在栈帧中存放了方法局部变量表、操作数栈、动态连接方法和返回地址等信息。每个方法的调用都伴随着入栈,方法的返回伴随着出栈。如果方法调用时方法的参数和局部变量越多那么栈帧中局部变量表就越大,栈帧占用的空间就越大,那么方法调用嵌套次数就越小。

如下方法和上面例子比较

public class StackTest {
    private int count = 0;//记录栈可达深度
    public void recursion(long a,long b,long c){
        long d = 0,f = 0,e = 0;
        count ++;
        recursion(d,f,e);
    }

    public static void main(String[] args) {
        StackTest stackTest = new StackTest();
        try {
            stackTest.recursion(1L,2L,3L);
        }catch (Throwable e){
            System.out.println("deep of stack is "+stackTest.count);
            e.printStackTrace();
        }
    }
}

默认栈大小-Xss1m时执行结果,可以看到当方法参数和局部变量增加时,调用深度明显减小。

deep of stack is 5445
java.lang.StackOverflowError

在栈帧中,与性能调优关系最为密切的的部分就是局部变量表。局部变量表存放方法参数和内部变量,非static方法虚拟机还会把当前对象(this)作为参数通过局部变量表传递给当前方法。

通过jclasslib工具可以查看class文件中每个方法所分配的最大局部变量表的容量。打开StackTest.calss文件找到方法recursion(),将其展开后查看Code属性,选择Misc页面,可以查看该方法最大局部变量。局部变量表以“字”为单位进行划分内存空间。long/double类型占两个“字”,其他类型占一个“字”。

//共13个“字”
public void recursion(long a,long b,long c){
    long d = 0,e = 0, f = 0;
    count ++;
    recursion(d,e,f);
}

局部变量表中字空间是可以重用的,因为在一个方法体内,局部变量的作用范围并不是一定是整个方法体。

public void test1(){
    {
        long a = 0;
    }
    long b = 0;
}

比较

public void test2(){
    long a = 0;
    long b = 0;
}

局部变量表的字对系统GC也有一定影响,如果与几个变量被保存在局部变量表中,那么GC根就能引用到这个局部变量所指向的内存空间,从而GC时无法回收这部分空间。

1. 

public static void test1(){
    {
        Byte[] b = new Byte[1024*1024*6];
    }
    System.gc();
    System.out.println("first explict gc over");
}

虽然系统GC时已经超出了b变量作用范围,但是不会被回收

[GC (System.gc())  26542K->25256K(62976K), 0.0043875 secs]
[Full GC (System.gc())  25256K->25191K(62976K), 0.0326676 secs]
first explict gc over

2. 

public static void test1(){
    {
        Byte[] b = new Byte[1024*1024*6];
        b = null;
    }
    System.gc();
    System.out.println("first explict gc over");
}

手动把b变量设置为null,可以使GC时回收b所占用的内存空间。

[GC (System.gc())  26542K->25328K(62976K), 0.0020175 secs]
[Full GC (System.gc())  25328K->627K(62976K), 0.0074723 secs]
first explict gc over

3. 

public static void test1(){
    {
        Byte[] b = new Byte[1024*1024*6];
    }
    int a = 0;
    System.gc();
    System.out.println("first explict gc over");
}

更多的方法是新声明的变量,会复用变量b的字,使b所占的内存空间被GC回收。

[GC (System.gc())  26542K->25384K(62976K), 0.0015061 secs]
[Full GC (System.gc())  25384K->627K(62976K), 0.0077682 secs]
first explict gc over

4. 本地方法栈

本地方法栈和Java虚拟机栈的功能很相似,Java虚拟机栈用于管理Java函数的调用,而本地方法栈用于管理本地方法的调用。因此和Java虚拟机栈一样本地方法栈也会抛出StackOverflowError和OutOfMemoryError。

本地方法栈是使用C语言实现的,在SUN的Hot Spot虚拟机中不区分本地方法栈和Java虚拟中栈。

5. Java堆

Java堆可以说是Java运行时内存中最为重要的部分,几乎所有的对象和数组都在堆中分配空间。Java堆分为新生代和老年代两部分,新生代用来存放新产生的对象和年轻的对象,如果一个对象经过多次GC都没有被回收,则该对象会被存放到老年代。

新生代

Eden:刚刚产生的对象存放该空间。

s0(from space)/s1(to space):至少经过一次GC没有被回收的对象存放在该空间。

老年代:多次GC都没有被回收的对象最终会被存放在该空间。

6. 方法区(JDK6)

方法区也是JVM内存中非常重要的的一块内存区域。与堆空间类似,它也是被JVM中所以的线程共享。方法区中主要保存的信息是类的元数据。

方法区存放

类的类型信息:包括类的完整名称,父类的完整名称,类型修饰符,类型的直接接口类表。

常量池:包括这个类方法、域等信息所引用的常量信息。

域信息:包括域名称,域类型和修饰符。

方法信息:包括方法名称,返回类型,方法参数,方法修饰符,方法字节码,操作数栈和方法帧栈的局部变量区大小以及异常表。

在Hot Spot虚拟机中,方法区也成为永久区,是一块独立的内存空间。虽然叫做永久区,但是在永久区中的对象也是可以被回收的。对永久区的回收主要从两个方面分析:一是GC对永久区常量池的回收;二是永久区对类元数据的回收。Hot Spot虚拟机对常量池的回收,只要常量池中的常量没有被任何地方引用就可以被回收。

常量池演示:

String.intern():如果常量池存在当前String,返回常量池中的String;如果常量池中不存在当前String,将当前String添加到常量池,并返回池中对象。

public static void main(String[] args) {
    for (int i = 0;i< Integer.MAX_VALUE;i++){
        String t = String.valueOf(i).intern();
    }
}

使用JVM参数-XX:PermSize=2M -XX:MaxPermSize=4M -XX:+PrintGCDetails运行,每当常量池满时就进行回收,确保程序正常运行。

结果显示,当常量池空间不足时,没有被引用的常量会被回收。

元数据演示:

与常量池的回收相比,类的元数据回收,稍微复杂一些,使用javassist类库,产生大量类占用元数据。观察元数据的回收情况。

动态类父类,生成的子类都要继承给父类

//定义演示动态类的父类,后面使用Javassist产生的动态类都是该类的子类
public class JavaBeanObject{
    private String name="java";
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

动态类生成的

public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
    for (int i=0;i<Integer.MAX_VALUE;i++){//循环动态生成大量类
        CtClass c = ClassPool.getDefault().makeClass("Geym"+i);//定义类名
        c.setSuperclass(ClassPool.getDefault().get("com.eaju.jvm.JavaBeanObject"));//设置父类
        Class clz = c.toClass();//新建类
        JavaBeanObject v = (JavaBeanObject)clz.newInstance();
    }
}

运行参数:-XX:PermSize=2M -XX:MaxPermSize=4M -XX:+PrintGCDetails

以上代码运行会产生大量的JavaBeanObject类的子类,占用元数据,导致永久区空间不足,运行一段时间之后会抛出“java.lang.OutOfMemoryError:PermGen space”显示持久带溢出。

事实上类元数据也是可以被回收的,需要满足以下两个条件:1.该类的所有实例均已经被回收;2.该类的加载器ClassLoader也已经被回收。

7.  方法区(JDK8)

在Java7之前,HotSpot虚拟机中将GC分代收集扩展到了方法区,使用永久代来实现了方法区。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。但是在之后的HotSpot虚拟机实现中,逐渐开始将方法区从永久代移除。

Java7中已经将运行时常量池从永久代移除,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。而在Java8中,已经彻底没有了永久代,将方法区直接放在一个与堆不相连的本地内存区域,这个区域被叫做元空间。 

总之:jdk1,6常量池放在方法区,jdk1.7常量池放在堆内存,jdk1.8放在元空间里面,和堆相独立。

验证常量池

同样的方法验证元数据

//定义演示动态类的父类,后面使用Javassist产生的动态类都是该类的子类
public class JavaBeanObject{
    private String name="java";
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

动态创建类方法

public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
    for (int i=0;i<Integer.MAX_VALUE;i++){//循环动态生成大量类
        CtClass c = ClassPool.getDefault().makeClass("Geym"+i);//定义类名
        c.setSuperclass(ClassPool.getDefault().get("com.eaju.jvm.JavaBeanObject"));//设置父类
        Class clz = c.toClass();//新建类
        JavaBeanObject v = (JavaBeanObject)clz.newInstance();
    }
}

运行参数:-XX:MaxMetaspaceSize=8m  -XX:+PrintGCDetails

限制元空间大小,在不断创建元数据时元空间很快就会被占用完就会抛出异常

[GC (Last ditch collection) [PSYoungGen: 0K->0K(44544K)] 8631K->8631K(132608K), 0.0075906 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Last ditch collection) [PSYoungGen: 0K->0K(44544K)] [ParOldGen: 8631K->8631K(127488K)] 8631K->8631K(172032K), [Metaspace: 7758K->7758K(1056768K)], 0.0336065 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
Heap
 PSYoungGen      total 44544K, used 1662K [0x00000000eb180000, 0x00000000eee80000, 0x0000000100000000)
  eden space 41984K, 3% used [0x00000000eb180000,0x00000000eb31fab0,0x00000000eda80000)
  from space 2560K, 0% used [0x00000000edd00000,0x00000000edd00000,0x00000000edf80000)
  to   space 10240K, 0% used [0x00000000ee480000,0x00000000ee480000,0x00000000eee80000)
 ParOldGen       total 127488K, used 8631K [0x00000000c1400000, 0x00000000c9080000, 0x00000000eb180000)
  object space 127488K, 6% used [0x00000000c1400000,0x00000000c1c6de78,0x00000000c9080000)
 Metaspace       used 7790K, capacity 8098K, committed 8192K, reserved 1056768K
  class space    used 1936K, capacity 1997K, committed 2048K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
    at javassist.ClassPool.toClass(ClassPool.java:1099)
    at javassist.ClassPool.toClass(ClassPool.java:1042)
    at javassist.ClassPool.toClass(ClassPool.java:1000)
    at javassist.CtClass.toClass(CtClass.java:1224)

JDK8之后内存模型图

© 著作权归作者所有

贾峰uk
粉丝 2
博文 110
码字总数 171435
作品 0
深圳
私信 提问
《成神之路-基础篇》JVM——JVM参数及调优(已完结)

Java内存模型,Java内存管理,Java堆和栈,垃圾回收 本文是[《成神之路系列文章》][1]的第一篇,主要是关于JVM的一些介绍。 持续更新中 JVM参数及调优 JVM实用参数系列 成为Java GC专家(5)...

2018/05/05
0
0
成为Java GC专家(5)—Java性能调优原则

这是“成为Java GC专家”系列的第五篇文章。在第一篇深入浅出Java垃圾回收机制中,我们已经学习了不同的GC算法流程、GC的工作原理、新生代(Young Generation)和老年代(Old Generation)的...

stefanzhlg
2014/12/05
1K
1
JVM性能优化, Part 5:Java的伸缩性

ImportNew注: JVM性能优化系列文章前4篇由ImportNew翻译(第一篇,第二篇,第三篇, 第四篇)。本文由新浪微博:吴杰 (@WildJay) 投稿至ImportNew。感谢吴杰! 如果你希望分享好的原创文章或...

梁杰_Jack
2014/10/30
224
0
大型互联网架构必备技术——性能调优专题

性能调优 深入内核,直击故障 ,拒绝蒙圈 性能优化如何理解 1、性能基准 2、什么是性能优化 3、衡量标准 JVM调优 1、Jvm虚拟机内存剖析 2、垃圾收集器 3、实战调优案例与解决方案 4、Jvm运行...

Java高级架构
2018/04/15
0
0
JVM系列篇:JVM性能调优的6大步骤,及关键调优参数详解

本系列会持续更新。 一、JVM内存调优 对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。 1.Full GC 会对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个...

mikechen优知
03/26
267
0

没有更多内容

加载失败,请刷新页面

加载更多

web前端开发高级

前端高效开发框架技术与应用 Vue 基础 Vue 框架简介 MVX 模式介绍 Vue 框架概述 如何使用 Vue.js 基础语法 实例对象 生命周期 模板语法 计算属性 Methods 方法 渲染 列表渲染 条件渲染 事件与...

达达前端小酒馆
21分钟前
3
0
PostgreSQL 11.3 locking

rudi
今天
5
0
Mybatis Plus sql注入器

一、继承AbstractMethod /** * @author beth * @data 2019-10-23 20:39 */public class DeleteAllMethod extends AbstractMethod { @Override public MappedStatement injectMap......

一个yuanbeth
今天
20
1
一次写shell脚本的经历记录——特殊字符惹的祸

本文首发于微信公众号“我的小碗汤”,扫码文末二维码即可关注,欢迎一起交流! redis在容器化的过程中,涉及到纵向扩pod实例cpu、内存以及redis实例的maxmemory值,statefulset管理的pod需要...

码农实战
今天
4
0
为什么阿里巴巴Java开发手册中不建议在循环体中使用+进行字符串拼接?

之前在阅读《阿里巴巴Java开发手册》时,发现有一条是关于循环体中字符串拼接的建议,具体内容如下: 那么我们首先来用例子来看看在循环体中用 + 或者用 StringBuilder 进行字符串拼接的效率...

武培轩
今天
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部