JVM学习笔记(二)

  一个JVM实例只存在一个堆内存,堆也是java内存管理的核心区域
  java堆区在jvm启动的时候即被创建,大小也就确定下来了。是JVM管理的最大一块内存空间(空间可以调节)
  所有的线程共享java堆,在这里还可以划分线程私有的缓冲区
  数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
  在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除

内存细分

现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为
java7 之前堆内存逻辑上分为三部分:新生区(新生代、年轻代)养老区(老年区、老年代)永久区(永久代)
java8 之后堆内存逻辑上分为三部分:新生区 养老区 元空间

OOM(内存溢出)

public static void main(String[] args) {
    ArrayList<Picture> list = new ArrayList<>();
    while (true){
        list.add(new Picture(new Random().nextInt(1024 * 1024)))
    }
}
class Picture{
    private byte[] pixels;
    public Picture(int length){
        this.pixels = new byte[length];
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at Picture.<init>(StringBuilderTest.java:42)
    at StringBuilderTest.main(StringBuilderTest.java:35)

新生代和老年代

存储在JVM中的java对象可以被划分为两类
  *一类事声明周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
  *另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致
java堆进一步细分可以划分年轻代和老年代
  年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫from区、to区)
默认比例 -NewRatio :设置新生代与老年代的比例 默认值 1:2
新生代中 -XX:SUrvivorRatio :设置空间比例  Eden空间和两个Survivor默认值 8:1:1(会自适应调用时可能不是)

内存的分配过程

  1. new的对象先Eden区,此区有大小限制
  2. 当Eden区的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对Eden区进行垃圾回收,将Eden区中的不再被其他对象所引用的对象进行销毁,在加载新的对象放到Eden区
  3. 然后将Eden区中剩余对象移动到S0区
  4. 如果再次触发垃圾回收,此时上次幸存下来放到S0区的,如果没有回收,就会放到S1区
  5. S区不会进行垃圾回收,当Eden区满时被动进行回收
  6. 如果再次经历垃圾回收,此时会重新放回S0区,接着再去S1区
  7. 什么时候区老年代?设置次数,默认是15次
  8. 在老年代,相对悠闲。当老年代内存不足的时候,再次触发GC:Major GC,进行老年代内存清理
  9. 如果老年代执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常
关于垃圾回收:频繁在新生代收集,很少在老年代收集,几乎不再永久代收集

Minor GC、Major GC、Full GC

针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集,一种是整堆收集
部分收集:不是完整收集整个java堆的垃圾收集。其中又分为
新生代收集:只是新生代的垃圾收集
老年代收集:只是老年代的垃圾收集
混合收集:收集整个新生代以及部分老年代的垃圾收集
整堆收集:收集整个java堆和方法区的垃圾收集
年轻代GC(Minor GC)触发机制:
 *当年轻代空间不足时,就会触发Minor GC,这里的年轻代满值的是Eden代满,Survivor满不会引发GC(每次Minor GC会清理年轻代的内存)
 *因为Java对象大多数都是具备朝生夕灭的特征,所以Minor GC非常频繁,一般回收速度也比较快
 *Minor GC会引发STW, 暂停其他用户的线程,等垃圾回收结束,用户线程才会恢复运行
老年代GC(Major GC/Full GC)触发机制:
 *出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对,在Parallel Scavenge收集器的收集策略理就有直接进行Major GC的策略选择过程)
 *Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长
 *如果Major GC后,内存还不足,就报出OOM
Full GC触发机制:
触发Full GC执行的情况:
 *调用System.gc()时,系统建议执行Full GC,但是不必然执行
 *老年代空间不足
 *方法区空间不足
 *通过Minor GC后进入老年代的平均大小大于老年代的可用内存
 *由Eden区、s0区向s1区复制时,对象大小大于s1可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
full GC时开发或调优中尽量避免的。这样暂时时间会短一些

内存分配策略:

  &如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次MinorGC,年龄就增加一岁,当它的年龄增加到一定程度(默认为15岁,每个GC都有所不同)时,就会被晋升到老年代。
  &大对象直接分配到老年代、长期存活的对象分配到老年代

为对象分配内存:TLAB

什么是TLAB?
 *从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
 *多线程同时分配内存是,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略
为什么有TLAB?
 *堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
 *由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
 *为了避免多个线程操作同一个地址,需要使用加锁机制,进而影响分配速度
堆是分配对象存储的唯一选择吗?
在java虚拟机中,对象是在java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸处方法的话,就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收。
如何快速的判断是否发生了逃逸分析,就看new的对象实体是否可能在方法外被调用。
结论:开发中能使用局部变量的,就不要使用在方法外定义

代码优化:

  *栈上分配*  JIT编译器在编译期间根据逃逸分析的结果,发现如果应该对象并没有逃逸出方法的话,就可能优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被收回,局部变量对象也被回收。 
  *同步省略*  线程同步的代价很高,同步的后果是降低并发性和性能,在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步,这个取消同步的过程叫同步省略,也叫锁消除。 
  *标量替换*  标量是指一个无法再分解成更小的数据的数据。java中的原始数据类型就是标量。相对的,那些还可以分解的数据叫做聚合量,java中的对象就是聚合量,再JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员标量来代替,这个过程就是标量替换。

栈、堆、方法区的交互

Person person = new Person();
方法区   java栈          java堆

方法区

线程共享的内存区域
方法区的带线啊哦决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError: PermGen space 或者 java.lang.OutOfMemoryError: Metaspace
加载大量的第三方的jar包;Tomcat部署的工程过多;大量动态的生成反射类
设置方法区内存的大小
方法区的大小不必是固定的,jvm可以根据应用需要的动态调整
jdk7及以前:
通过-XX: PermSize来设置永久代初始分配空间。默认值是20.75M
       -XX: MaxPermSize来设定永久代最大可分配空间。32位机器默认是64M,64位机器默认是82M
jdk8及以后:
默认值依赖于平台,windows下,-XX: MetaspaceSize是21M
                                                   -XX: MaxMetaspaceSize的值是-1,即没有限制

内部结构

存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

类型信息

对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储一下类型信息:
 *这个类型的完整有效名称
 *这个类型直接父类的完整有效名
 *这个类型的修饰符

常量池

一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表,包含各种字面量和对类型域和方法的符号引用。
运行时常量池
常量池表是Class文件的一部分,用于存放编译期生成各种字面量与符号引用这部分内容将在类加载后存放到方法区的运行时常量池中
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换成真实地址。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型
判断一个类型是否属于“不再被使用的类”的条件比较苛刻,满足的三个条件:
 *该类所有的实例都已经被回收,也就是java堆中不存在该类及其任何派生子类的实例
 *加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的
 *该类对应的java.lang.Class对象没有在任何地方被引用,在wu'fa任何地方通过反射访问该类的方法

 创建对象的步骤:

1、判断对象对应的类型是否加载、链接、初始化
2、为对象分配内存 如果内存规整——指针碰撞 
如果内存不规整——虚拟机需要维护一个列表、空闲列表分配
3、处理并发安全问题 采用CAS失败重试、区域加锁保证更新的原子性
每个线程预先分配一块TLAB——通过-XX:+/-UseTLAB参数来设定
4、初始化分配到的空间 所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
5、设定对象的对象头 将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中
6、执行init方法进行初始化 属性的显式初始化、构造器的初始化
内存布局
对象头:运行时元数据(哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳)
             类型指针——指向类元数据,确定该对象所属的类型
实例数据:它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
 *相同宽度字段总是分配在一起
 *父类中定义的变量会出现在子类之前
 *子类的变量可能插入到父类的变量的空隙
对齐填充

执行引擎

执行引擎是java虚拟机核心的组成部分之一
“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是有软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式
什么是解释器,什么是JIT编译器?
解释器:当java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行
JIT解释器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值