深入理解jvm读后感(一)

提纲
这里写图片描述

运行时内存区域

1.程序计数器

  1. 当前线程所执行的字节码的行号指示器。
  2. 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是native方法,这个计数器的值则为空。
  3. 此内存区域是唯一一个在Java虚拟机规范中没有定义任何out of memory情况的区域

2. Java虚拟机栈

  1. 每个方法在执行时都会创建一个栈帧用来存储局部变量表,操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成都对应着一个栈帧在虚拟机中入站到出栈的过程。
  2. 堆内存与栈内存分别指的是虚拟机栈与Java堆 。
  3. 局部变量表存放了编译器可知的各种基本数据类型(Boolean、byte、char、short、int、float、long、double)、对象引用类型(reference类型,它不等同与对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
    例:
public void main(string[] args){
    int a =0 ;
    a++;
}

这里的int a为局部变量,则int类型存放到局部变量表中,a不存储该方法的运算直接在操作数栈中运行。

4.如果线程请求的栈深度大于虚拟机所允许的深度,会抛出stackoverflow异常;如果虚拟机栈可以动态拓展,如果拓展时无法申请到足够的内存,将会抛出outofmemoryerror异常。

3.本地方法栈

  1. 为虚拟机使用到的native方法服务。
    虚拟机栈和本地方法栈溢出示例:
public class JavaVMStackSOF {

    private int stackLength = 1;
    public void stackLeak(){
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try{
            oom.stackLeak();
        }catch(Throwable t){
            System.out.println("stack length:"+oom.stackLength);
            throw t;
        }
    }
}

jvm参数设置:
-Xss128k
输出结果:

stack length:1002
Exception in thread "main" java.lang.StackOverflowError
    at outOfMemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:7)
    at outOfMemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
    ...

单线程情况下,栈帧太大或者栈容量太小都会造成溢出

4.Java堆

  1. 存放对象实例。所有对象在堆上分配。
    例:
List<Integer> list = new ArrayList<>();

list变量存放在java堆上,若没有对list的引用则进行GC。
2. Java堆是垃圾收集管理的主要区域,因此很多时候也被称为“GC堆”。由于现在收集器都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代。
3. 如果在堆中没有内存完成实例分配,并且堆无法再拓展时,将会抛出out of memory异常。
outOfMemory异常举例:

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

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

jvm运行参数:
jvm运行参数
运行结果:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid13508.hprof ...
Heap dump file created [27923262 bytes in 0.103 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:2245)
    at java.util.Arrays.copyOf(Arrays.java:2219)
    at java.util.ArrayList.grow(ArrayList.java:242)

通过堆转储分析工具可对堆转储快照进行分析查看:
这里写图片描述

5.方法区

  1. 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  2. 在hotspot虚拟机中把方法区称为永久代。回收主要针对常量池的回收和对类型的装载。
  3. 当方法区无法满足内存分配需求时,将抛出out of memory异常。

6.运行时常量池(属于方法区)

  1. 是方法区的一部分,class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用。这部分内容将在类加载后进入方法区的运行时常量池进行存放。例如有如下示例:
public class hello{
    private int m;

    private int n=0;

    private String s;

    private String t = "abc";
    public void intc(){
        m=0;
        n++;
        s = "123";
        t = "123";
    }
}

对其进行反汇编得
这里写图片描述
在上述示例中,m,n,t,s,都被作为类变量(在类文件中)存放到常量池中,包括他们的变量类型和字段名称,而且方法中的字符串变量(123)还是类变量中的字符串变量(abc)都存放到常量池,在类的加载阶段被存放到运行时常量池中。

  1. 当常量池无法在申请到内存是会抛出out of memory异常。
    例:
public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();//避免fullGC回收常量池
        int i = 0;
        while(true){
            list.add(String.valueOf(i++).intern());
        }
    }
}

虚拟机参数在jdk1.7之后与1.6有所不同,主要是因为常量池位置发生了改变:

-Xmx20m -Xms20m -XX:-UseGCOverheadLimit

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.lang.Integer.toString(Integer.java:333)
    at java.lang.String.valueOf(String.java:2954)
    at outOfMemory.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:12)

7.直接内存

  1. NIO中引入了一种基于通道与缓冲区的I/O方式,它可以使用native函数库直接分配对外内存,然后通过一个存储在java堆中的directByteBuffer对象作为这块内存的引用进行操作。这样提高了性能,避免了在Java堆和native堆中来回复制数据。
  2. 各个内存区域总和大于物理内存限制,从而导致内存拓展时出现out of memory异常。

对象

1.对象的创建

1.当遇到一条new指令时,先执行类的加载检查
类的加载检查:检查这个指令是否被加载、解析和初始化过。如果没有,则先执行类的加载过程。
3.加载检查完成后,便确定出来对象所需的内存大小。接下来便是为对象分配相应的内存空间,为对象分配内存空间有两种方式:指针碰撞与空闲列表。
指针碰撞:假设Java堆中内存是绝对完整的,所有用过的内存都放在一边,空闲的内存放在一边,中间放这一个指针作为分界点的指示器,那所分配的内存就仅仅是把那个向空闲空间那边挪动一段与对象大小相等的距离。serial、parNew等带compact过程的收集器使用的是此方式。
空闲列表:如果Java堆中的内存是不规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须找到一块足够大的空间划分给对象实例,并更新列表上的记录。cms这种基于mark-sweep算法的收集器使用此方法。
4.另外还需要考虑对象创建是否频繁,在并发情况下怎样保证线程安全。
一种方式是采用同步处理,另一种是采用本地线程分配缓冲。
同步处理:采用CAS配上失败重试的方法。
本地线程分配缓冲:把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。
5.设置对象头(object header)。如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的gc分代年龄等信息。
6.执行完new指令后,接着执行init方法,将对象进行初始化。

2.对象的内存布局(偏向锁形成与此有关)

  1. 对象在内存中的布局可以分为3块区域:对象头、实例数据和对齐补充。
  2. 对象头:对象头包含两部分信息,
    第一部分存储对象自身的运行时数据,被称为mark word,包括哈希码、gc分代年龄、所状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;
    另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。
    3.实例数据部分时对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
    存储顺序受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。默认的分配策略为longs/doubles、ints、shorts/chars、bytes/Booleans、oops(ordinary object pointers),相同宽度的字段总是分配到一起。
  3. 对齐填充并不是必然存在的,仅仅起占位符的作用。由于hotspot VM的自动管理系统要求对象起始地址必须是8字节的整数倍,换句话或,就是对象的大小必须是8字节的整数倍,而对象头正式8字节的倍数,当对象的实例数据部分没有对齐时,就需要通过对其填充来补齐。
    markword:java 偏向锁 轻量级锁与偏向锁 多线程之:偏向锁,轻量级锁,重量级锁

3. 对象的访问定位

  1. 主流的访问方式有使用句柄和直接指针两种。
  2. 使用句柄访问:Java堆会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄池,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。好处是reference中存储的时稳定的句柄地址,对象被移动只会改变句柄中的实例数据指针,而reference本身不需要修改。
  3. 使用直接指针访问:Java堆对象中需要考虑的是如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。好处是速度更快。

JVM参数集合 :

设置堆的最大值:-Xmx,最小值:-Xms;
出现内存溢出时dump出当前的内存堆转出快照:-XX:+HeapDumpOnOutOfMemoryError;
设置栈容量:-Xss;
设置方法区的大小:-XX:PermSize -XX:MaxPermSize
设置直接内存容量:-XX:MaxDirectMemorySize
参考:https://blog.csdn.net/weixin_35663229/article/details/52796157

猜你喜欢

转载自blog.csdn.net/yinweicheng/article/details/80607402