Java内存

在这里插入图片描述

Java内存区域

在1.7以及之前,Java的内存区域如下图:
在这里插入图片描述

Java1.8后,Java的内存区域发生了变化,如下图:
在这里插入图片描述

其中,

程序计数器 ,线程私有

  1. 编译后,程序变为字节码,程序计数器指向某一行字节码,通过改变计数器值,可以指向下一行字节码。
  2. 线程切换时,保存执行现场,重新获得cpu时间片时,继续根据保存的现场执行程序。

,线程私有

支持程序执行的数据结构,栈是FILO的结构,栈中包括多个栈帧,程序的每个方法对应一个栈帧,一个栈帧又中包括:

  • 局部变量表
  • 方法出口
  • 操作数栈,也是一个栈结构,程序运行时借助于程序计数器一行一行的执行,而执行过程中计算得到的中间结果,存放在操作数栈中,在操作数栈中进行读写(出栈、入栈)。
  • 动态链接,方法调用时,目标方法(栈帧)是常量池中的一个符号引用,称为动态引用,在类加载解析阶段,会将其中一部分符号引用转为直接引用。

方法调用就是方法对应的栈帧在栈中 入栈 出栈的过程,方法执行就是每一行代码在栈帧中的操作数栈入栈、出栈的过程。
栈内存不够时会发生OOM错误,需要更大的虚拟机栈时 会发生StackOverFlow异常。

关于动态链接,Java源码被编译为字节码后,所有的变量引用和方法引用都是符号引用,保存在常量池中(符号代表的方法和变量)。在类加载或者第一次使用时,将符号引用转为直接引用,这个过程称为静态解析,而在运行过程中,将符号引用转为直接应用的过程称为动态链接。

使用javap命令可以查看class文件的结构,如:

public class HelloWorld {

    String myName = "James";

    public void sayHello(){
        System.out.println(myName + " say: hello world");
    }

    public static void main(String[] args) {
        HelloWorld hw = new HelloWorld();
        hw.sayHello();
    }
}

编译后使用javap命令,查看常量池符号引用(部分内容),#13就是sayHello()方法的符号引用,而#3是myName属性的符号引用。

javap -v HelloWorld.class
... ...
Constant pool:
   #1 = Methodref          #14.#32        // java/lang/Object."<init>":()V
   #2 = String             #33            // James
   #3 = Fieldref           #11.#34        // com/weihao/jvm/HelloWorld.myName:Ljava/lang/String;
   #4 = Fieldref           #35.#36        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Class              #37            // java/lang/StringBuilder
   #6 = Methodref          #5.#32         // java/lang/StringBuilder."<init>":()V
   #7 = Methodref          #5.#38         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #8 = String             #39            //  say: hello world
   #9 = Methodref          #5.#40         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #10 = Methodref          #41.#42        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = Class              #43            // com/weihao/jvm/HelloWorld
  #12 = Methodref          #11.#32        // com/weihao/jvm/HelloWorld."<init>":()V
  #13 = Methodref          #11.#44        // com/weihao/jvm/HelloWorld.sayHello:()V
  #14 = Class              #45            // java/lang/Object
... ...

本地方法栈,线程私有,栈是调用Java方法,本地方法栈是调用Native方法,不受jvm控制,但是同样会抛出OOM和StackOverflowException,本地方法栈有多种类型,如调用的是C方法,则对应C栈。

,线程共享,内存分为:

  1. 新生代,新生代中又分为survivor1区survivor2区和Eden区三部分,默认大小1:1:8,可指定。发生minorGC,采用标记复制算法,GC时将eden取和survivor1区的存货对象复制到survivor2区,下一次GC时又将Eden区和survivor2区复制到survivor1区,这样始终有一个survivor区是空的,新对象创建在Eden区。
  2. 老年代,新生代的对象,每发生一次minorGC存活下来,就年龄+1,直到阈值16次时,复制到老年区。老年区的GC称为FullGC,当老年区内存满时,发货所能FullGC,FullGC比monorGC耗时,应当减少FullGC的发生,采用标记整理算法
  3. 永久代(1.8版本前),方法区永久代很少发生GC。

方法区

jdk7以及以前的版本,称为方法区,线程之间共享,负责存储类信息,常量,静态变量,字节码,常量池,数据存在堆中的永久代。

特定的jvm包含永久代,很少发生GC,永久代会发生 OOM:permgen space异常,原因是动态加载的类太多,如JSP页面太多。

元数据区,线程之间共享

JDK8 将方法区的功能放在元数据,类的信息存储在元数据区,元数据区不在使用堆的内存,而是独有一块内存。

Code Cache,编译产物,线程之间共享

  1. javac,Javac编译器,将Java源代码编译为字节码。Java一次编译导出运行的关键所在。
  2. JIT,(Just Intime Compiler)编译器,对于Javac编译的字节码,还不能直接执行,需要JVM解释执行,这样比较耗费性能。而JIT编译器可以将字节码解释为本地代码直接执行,从而提高程序运行效率。

而JIT编译器编译的本地代码就存在在Code Cache这段内存中。

直接内存

jdk1.4,NIO引入堆外内存,适用于频繁调用场景,作为Channel的缓冲区,由操作系统管理,通过native本地调用分配和回收内存。

内存模型

JMM,Java Memory Model,也称为程序中变量的访问规则,描述线程,工作内存,主内存之间的关系

众所周知,因为距离cpu越近,数据读写速度越快,现代计算机都有cpu寄存器和cpu高速缓存,数据多次读取时,数据从内存中读到后,会先缓存在工作内存中,下次使用时直接从缓存读取,计算完成后再写入内存。如下图
在这里插入图片描述

JMM模型下,线程第一次读数据从内存读取,后续对该数据的读写在线程的工作空间进行,在某一时间再写到内存中,这会导致原子性,有序性,可见性问题。

  • 原子性:线程对数据的多个读写指令之间,插入别的线程的读写指令。

  • 有序性:因为指令重排不保证程序有序,有序性包括:

    1. 编译阶段,在不影响逻辑的情况下,会发生指令重排,优化代码执行。
    2. cpu指令重排,现代cpu支持多个指令并发执行,cpu执行时在保证结构不受影响的情况下,发生指令重排。
    3. 线程写数据在工作空间,写数据到内存的顺序有不确定性。
  • 可见性:线程工作空间的读写数据,而没有写数据到内存是,数据对其他线程不可见。

Java使用volatile和锁来解决在内存模型下原子性,有序性和可见性问题

volatile,保证线程对共享变量的修改,其他线程立即可见,禁止指令重排优化。

volatile细节:
volatitle通过内存屏障,内存屏障是一个cpu指令,在代码指令之前插入这个cpu指令,就是告诉cpu这个指令不能重排优化,还有一个作用就是强制缓存数据写到内存中,读volatitle时失效缓存。

volatile不足

volatile是一个轻量级的同步机制,在并发中只使用volatitle并不能保证线程安全,如i++,需要配合锁来一起实现高性能的安全方案。

happens-before原则:
JMM模型,volatitle,锁机制,happens-before原则一起配合,为Java并行的各个场景服务,尽可能的优化并行度。

happens-before的核心是前一个操作的结果对后一个操作可见,即A happens before B,A的操作对B可见,规则如下:

  1. 程序顺序原则,一个线程内,保证语义串行性,代码按照顺序执行
  2. 锁规则,unlock时,happens before与再次加锁
  3. volatitle规则,volatitle变量的写happens before于读,这保证了volatitle变量的可见性。
  4. 线程启动规则,线程的start()方法happens before线程的每一个操作执行,线程启动后,线程启动前的共享数据操作可见
  5. 传递性,如果A happens before B,B happens before C发生,则A happens before C
  6. 线程终止规则,线程的所有操作先于线程终结,线程终结前将共享变量刷新到内存
  7. 线程中断规则,对线程interrupt()方法的调用happens before被中断线程代码检测到中断发生。
  8. 对象终结规则,对象的构造函数执行、结束happens before finalize()方法

finalized(),垃圾回收前调用对象的finalize方法。

问题定位

内存溢出

OOM异常,分为永久区,栈,堆三个部分的溢出

  • 堆空间不足时会给出OOM:Java heap space,当垃圾回收用98%的时间回收了不到2%的空间时,会先给一个警告
  • 栈OOM:stack over flow
  • 1.8之前永久代(方法区):OOM PermGen space

对问题定位
jmap -heap 打印堆分区统计信息,老年代,新生代,Eden去,survivor0,survivor1,Metaspace的大小。
jmap -histo 打印堆内对象的类型统计信息
jmap -clstats 类加载器的统计信息
jmap -dump:live,format=b,file=heap.hprof下载堆信息
使用jps可以查询Java进程的pid
下载的堆信息,可以使用MAT工具分析具体问题,Memory Analyzer,可以直观的看到堆内的各类和问题,从而定位问题。

内存泄漏

其实对象已经不在被使用了,但是不会被GC机制回收的空间。

例如:被root引用的,如栈,线程ThreadLocal,静态变量,常量引用的对象,对象置为空,但是容器不为空,不会被回收。

Java假死

新生代区占满

cpu 100%

  1. top查看cpu占用高进程
  2. top -Hp 以线程模式查看某进程的线程占用情况
  3. jstack <线程id>查看指定pid线程的线程栈信息
  4. 线程栈信息中的线程ID是16进制,
  5. printf “%x” <十进制pid>转为16进制后,定位到对应的代码

死锁
查看栈信息,在栈信息中查找线程或者锁信息,定位到死锁代码位置

jvm工具

  • jstack

    方法栈分析

  • jmap

    堆内存分析

  • jstat

    查看指定的Java HotSpot虚拟机的性能统计信息
    -class 类加载信息
    -compiler 类编译信息
    -gc gc信息
    -gccause gc原因
    -gccapacity 内存池生成和容量信息
    可以使用rmiregistry进行远程查看

    -gc 查看gc统计信息,如:jstat -gc 157270 1000 10,查看进程号为157270的gc统计信息 没1000ms打印一次,打印十次,结果如下:

  $ jstat -gc 157270 1000 10
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
69120.0 102912.0 68808.4  0.0   1115648.0 271510.2  608768.0   92429.5   101324.0 95665.5 12032.0 11097.8     26    1.453   4      0.584    2.037
69120.0 102912.0 68808.4  0.0   1115648.0 271510.2  608768.0   92429.5   101324.0 95665.5 12032.0 11097.8     26    1.453   4      0.584    2.037
69120.0 102912.0 68808.4  0.0   1115648.0 271510.2  608768.0   92429.5   101324.0 95665.5 12032.0 11097.8     26    1.453   4      0.584    2.037
69120.0 102912.0 68808.4  0.0   1115648.0 271510.2  608768.0   92429.5   101324.0 95665.5 12032.0 11097.8     26    1.453   4      0.584    2.037
69120.0 102912.0 68808.4  0.0   1115648.0 271510.2  608768.0   92429.5   101324.0 95665.5 12032.0 11097.8     26    1.453   4      0.584    2.037
69120.0 102912.0 68808.4  0.0   1115648.0 271510.2  608768.0   92429.5   101324.0 95665.5 12032.0 11097.8     26    1.453   4      0.584    2.037
69120.0 102912.0 68808.4  0.0   1115648.0 271510.2  608768.0   92429.5   101324.0 95665.5 12032.0 11097.8     26    1.453   4      0.584    2.037
69120.0 102912.0 68808.4  0.0   1115648.0 271510.2  608768.0   92429.5   101324.0 95665.5 12032.0 11097.8     26    1.453   4      0.584    2.037
69120.0 102912.0 68808.4  0.0   1115648.0 271510.2  608768.0   92429.5   101324.0 95665.5 12032.0 11097.8     26    1.453   4      0.584    2.037
69120.0 102912.0 68808.4  0.0   1115648.0 271510.2  608768.0   92429.5   101324.0 95665.5 12032.0 11097.8     26    1.453   4      0.584    2.037

可以看到结果中分别呈现了S0,S1,Eden,老年代,元数据区, Compressed class空间的初始容量,已使用容量,YoungGC(minor gc)次数,消耗时间,FullGC次数,消耗时间,总的GC消耗时间。

(完 ^_^)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值