jvm(1)内存

越努力越幸运!

转载自https://blog.csdn.net/Scythe666/article/details/51841161 

           http://blog.csdn.net/scythe666/article/details/51700142

 jvm内存

一.运行时内存

二.程序计数器

       可看作当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

在线程创建时创建。执行本地方法时,PC的值为null。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,线程私有

三.Java虚拟机栈

线程私有,生命周期同线程。每个方法在执行同时,创建栈帧。用于存储局部变量表、操作数栈、动态链接、方法出口等信息。栈中的局部变量表主要存放一些基本类型的变量(int, short, long, byte, float,double, boolean, char)和对象句柄。

栈中有局部变量表,包含参数和局部变量。

此外,java中没有寄存器,因此所有的参数传递依靠操作数栈。

栈上分配,小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上。(没有逃逸是指,对象只能给当前线程使用,如果多个线程都要用,则不可以,因为栈是线程私有的。)直接分配在栈上,可以自动回收,减轻GC压力。因为栈本身比较小,大对象也不可以分配,会影响性能。

-XX:+DoEscapeAnalysis 启用逃逸分析,若非逃逸则可栈上分配。

四.本地方法栈

线程私有,与Java虚拟机栈非常相似,区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法(非java语言实现,比如C)服务。Hotspot 直接把本地方法栈和虚拟机栈合二为一。

栈&本地方法栈:线程创建时产生,方法执行是生成栈帧。 

五. Java堆

线程共有(可能划分出多个线程私有的分配缓冲区,Thread Local Allow),Java虚拟机管理内存中最大的一块,此区域唯一目的就是存放对象实例,几乎所有对象实例在此区分配,线程共享内存。可细分为新生代和老年代,方便GC。主流虚拟机都是按可扩展实现(通过-Xmx 和 -Xms 控制)。

注意:Java堆是Java代码可及的内存,是留给开发人员使用的;非堆(Non-Heap)就是JVM留给 自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。

Java堆:在虚拟机启动时创建

关于TLAB

Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配

TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。

六.方法区

线程共有,用于存储已被虚拟机加载的类信息、常量池、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但它却有一个别名Non-Heap(非堆),目的是与Java堆区分开。

注意,通常和永久区(Perm)关联在一起。但也不一定,JDK6时,String等常量信息保存于方法区,JDK7时,移动到了堆。永久代和方法区不是一个概念,但是有的虚拟机用永久代来实现方法区,可以用永久代GC来管理方法区,省去专门写的功夫。

运行时常量池

方法区的一部分,存放编译期生成的各种字面量和符号引用。

直接内存

并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,也可能导致 OOM 异常(内存区域综合>物理内存时)。NIO类,可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。

类加载时 方法信息保存在一块称为方法区的内存中, 并不随你创建对象而随对象保存于堆中。

逻辑内存模型我们已经看到了,那当我们建立一个对象的时候是怎么进行访问的呢?

在Java 语言中,对象访问是如何进行的?对象访问在Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及Java 栈、Java 堆、方法区这三个最重要内存区域之间的关联关系,如下面的这句代码:

Object obj = new Object();

假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java 栈的本地变量表中,作为一个reference 类型数据出现。而“new Object()”这部分的语义将会反映到Java 堆中,形成一块存储了Object 类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java 堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地

址信息,这些类型数据则存储在方法区中。由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针

(1)如果使用句柄访问方式,Java 堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息,如下图所示。

这里写图片描述

(2)如果使用直接指针访问方式,

Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的就是对象地址,如下图所示

这里写图片描述

这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference 本身不需要被修改。使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。就本书讨论的主要虚拟机Sun HotSpot 而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。

七. 堆溢出、栈溢出原因及实例,线上如何排查

除了程序计数器之外的内存区域都可以出现内存溢出

栈会出现栈溢出

 (1)栈溢出

递归,容易引起栈溢出stackoverflow;因为方法循环调用,方法调用会不断创建栈帧。 
造成栈溢出的几种情况: 
1)递归过深 
2)数组、List、map数据过大 
3 ) 创建过多线程

对于Java虚拟机栈和本地方法栈,Java虚拟机规范规定了两种异常状况:

① 线程请求深度>虚拟机所允许的深度,将抛出StackOverFlowError(SOF)异常;

② 如果虚拟机可动态扩展,且扩展时无法申请到足够的内存,就会抛出OutOfMemoryError(OOM)异常。

图的理解:
(1)首先在栈中存储局部变量i和y(line1和Line2)
(2)然后是最开始文章中说的:“应用程序在运行中所创建的所有类实例或数组都放在堆中,并由应用所有的线程共享。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在栈中分配”"在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。”这个就是line3边上小图的意思,在堆中创建了一个cls1的对象,然后在栈中放了一个引用。
(4)最后line4:当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,栈里面就被清空了,而堆中的内容还在,直到被GC清理掉。可能这个就是Java特别消耗内存的原因?

5、至此,再看递归引起的栈溢出,就是如果一个程序结束了,那么栈中的变量是会被清空的,但是递归中的变量值并没有被清空,每调用一次,函数的参数、局部变量等信息就压一次栈,当递归的层级比较深时,最终导致栈溢出。

解决栈溢出的方法 

 1.改变栈的大小。

 2.把递归大小转变成循环算法。

package com.yhj.jvm.memory.stack;

/**

 * @Described:栈层级不足探究

 * @VM args:-Xss128k

 * @FileNmae com.yhj.jvm.memory.stack.StackOverFlow.java

 */

public class StackOverFlow {

    private int i ;

    public void plus() {

       i++;

       plus();

    }


    /**
     * @param args
     * 
     */

    public static void main(String[] args) {

       StackOverFlow stackOverFlow = new StackOverFlow();

       try {

           stackOverFlow.plus();

       } catch (Exception e) {

           System.out.println("Exception:stack length:"+stackOverFlow.i);

           e.printStackTrace();

       } catch (Error e) {

           System.out.println("Error:stack length:"+stackOverFlow.i);

           e.printStackTrace();

       }

    }

}

(2)堆溢出

如果在堆中没有内存完成实例分配,且堆无法扩展时,将抛出OOM异常。

在方法区也会抛出 OOM 异常。

package OOM;

import java.util.ArrayList;
import java.util.List;


public class App1 {

    static class OOMClass {
        long[] num = new long[10240];
    }

    public static void main(String[] args) {
        List<OOMClass> list = new ArrayList<>();
        while (true) {
            list.add(new OOMClass());
        }
    }


}

怎么解决?

Java 堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heapspace”。要解决这个区域的异常,一般的手段是首先通过内存映像分析工具(如EclipseMemory Analyzer)对dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。图2-5 显示了使用Eclipse Memory Analyzer 打开的堆转储照文件。如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确地定位出泄漏代码的位置如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

 -vmargs  //虚拟机设置

     -Xms40m //初始内存

     -Xmx256m //最大内存

     -Xmn16m //最小内存

     -XX:PermSize=128M //非堆内存

     -XX:MaxPermSize=256M

class A{
    int i1 = 1; //i1在堆中A对象里,1在方法区的class文件常量池中
    String s1 = "abc"; //s1在堆中A对象里,"abc"在方法区的class文件常量池中
    String s2 = new String("abc"); //s2在堆中A对象里,"abc"在方法区的class文件常量池中,只有一份,new出来的String对象在堆里

    static int i2 = 2; //i2在方法区,2在方法区的class文件常量池中
    //s3在方法区,"xyz"在方法区的class文件常量池中
    static String s3 = "xyz"; 
    //s4在方法区,"xyz"在方法区的class文件常量池中,只有一份,new出来的String对象在堆里
    static String s4 = new String("xyz");

    public void func(){
        int i3 = 3; //i3在栈中,字面量3也在栈中
        int i4 = 3; //i4在栈中,字面量3已经存在,此时只有一份
        String s5 = "china"; //s5在栈中,"china"在方法区的运行时常量池中
        String s6 = new String("china"); //s6在栈中,"china"在方法区的运行时常量池中已经有一份相同拷贝,不再存, new出来的对象在堆中
    } 
}

那么,当A被classloader装载并且调用func函数的时候,其所在的jvm内存中不同的地方都有哪些关于A的信息呢?分析如下:

  1. 首先jvm第一次碰到A时,比如new A()时,会查看方法区里是否已经存放过关于此类的信息,如果没有,则先调用classloader(还是按照那个Bootstrap, extention…的顺序),最后装载A,此时,方法区里就有关于A类的Class信息了,并且由于在编译期间就能确定成员变量所引用的常量,因此,此时class文件常量池也会有信息,i1所引用的1,s1所引用的”abc”, i2所引用的2,s3所引用的”xyz”。
  2. new A()紧接着会导致在堆中分配关于A的内存,其中包括成员变量i1, s1, s2。其实s2所引用的new String(“abc”)中”abc”也是编译期间就能够确定,因此这里的”abc”也会存在class文件常量池,于是会先去class文件常量池找是否已经有一份相同的,由于之前已经有一份,于是不再存第二份。而new出来的String(“abc”)对象会在堆中有一份。
  3. 由于i2, s3, s4都是静态变量,因此它们存在方法区,2和”xyz”存在class文件常量池,注意”xyz”也只有一份,道理同2。另外s4引用的new对象也会在堆中有一份。
  4. 当程序运行调用A的func函数时,此时,jvm栈就开始工作了,会有一个关于func函数的栈帧(statck Frame),入栈,里面有i3, i4变量引用和常量或者说字面量3,注意3此时在栈中只有一份!如果后期i4被赋值为4,则栈会开辟新的空间存一个4,i3不变仍然为3。s5,s6也在栈中,”china”由于是在运行时才确定,因此存放在方法区的运行时常量池中,s6所引用的new的String(“china”)中的“china”也只在运行时常量池中保存一份,另外new会在堆中开辟一个新的对象空间存放此对象。

因此,对于equals相等的字符串,在常量池(class文件常量池或者运行时常量池)中永远只有一份,在堆中有多份。因为String类重写/覆盖了Object类的equals方法,只要字符串内容相等即为true,而Object是必须同一个对象的引用地址才为true。但是String并没有重写/覆盖==操作符,所以String对象的==还是只有同一个对象的地址引用才为true。

并且,延伸出来很多面试题的答案,比如:

1) String s = new String(“xyz”); 产生几个对象?

一个或两个。如果常量池中原来没有 ”xyz”, 就是两个。如果原来的常量池中存在“xyz”时,就是一个。

2) String作为一个对象来使用

例子一:对象不同,内容相同,”==”返回false,equals返回true

String s1 = new String(“java”);
String s2 = new String(“java”);

System.out.println(s1==s2); //false
System.out.println(s1.equals(s2)); //true

例子二:同一对象,”==”和equals结果相同

String s1 = new String(“java”);
String s2 = s1;

System.out.println(s1==s2); //true
System.out.println(s1.equals(s2)); //true
String作为一个基本类型来使用

如果值不相同,对象就不相同,所以”==” 和equals结果一样

String s1 = “java”;
String s2 = “java”;

System.out.println(s1==s2); //true
System.out.println(s1.equals(s2)); //true
如果String缓冲池内不存在与其指定值相同的String对象,那么此时虚拟机将为此创建新的String对象,并存放在String缓冲池内。

如果String缓冲池内存在与其指定值相同的String对象,那么此时虚拟机将不为此创建新的String对象,而直接返回已存在的String对象的引用。

猜你喜欢

转载自blog.csdn.net/hezuo1181/article/details/82929455
今日推荐