深入理解Java虚拟机(笔记)

内存分配:

  为对象分配内存有两种方式,第一种是“指针碰撞”,也就是把内存分为两边,一边是已使用区域,另一边是未分配区域,分界线用指针记录,当要分配内存时,只需把指针向未分配区域移动需要的空间即可,通常compact算法的垃圾回收会使用“指针碰撞”,如Serial、ParNew;另一种是空闲列表记录,也就是分配是可以不连续的,中间很多间隔可用的未分配内存,这个时候需要一个列表来对内存进行记录,分配内存时候就在列表找到最合适的,通常这种分配方式对应的垃圾回收器如CMS这种基于Mark-Sweep算法;

  由于分配内存也是多线程,存在内存使用资源的竞争,因此要保证线程安全,解决这个问题有两种方案,第一种是利用CAS加上失败重试方法保持原子性;第二种是用到ThreadLocal思想,就是为每一个线程分配一个线程自己的空间,称为Thread Local Allocation Buffer,TLAB,当线程完成逃逸分析后就把对象分配到该区域内,该类方式还附有更快更好的分配和回收内存。最后,当线程分配完后虚拟机就马上为分配到的空间初始化为零值,不包括对象头。

对象头(Header):

  对象可以分为三部分,对象头、实例数据、对齐填充;

  对象头包括两种信息,第一是运行时数据(Mark Word),第二是类型指针(用作指定该对象是哪个类创建的,但也不一定就这样实现,因为如果reference是通过句柄来实现的,那么对象头就没必要记录类型指针了,因为句柄本身会记录对应类对象的地址);运行时数据包括:哈希吗、GC分代年龄、锁状态标志、线程持有的锁、偏向锁ID、偏向时间戳等等。如果该对象是数组,那么还会对象头还会继续数组大小的数据。

  然后就是真正的实例数据了,实例数据包括父类继承下来的和子类拥有的,会优先安排父类的字段排序在前面,同时会根据基本类型的大小,把大小相同的放到一起分配。最后的数据对齐就是简单的填充作用。

对象定位:

  线程要通过栈上面的reference数据去访问对象,访问对象的方法通常有两种实现,第一种是句柄,第二种是直接的指针引用;那么什么是句柄呢?句柄其实就是一个专门用来描述对象地址和类型地址的数据结构,因此如果用第二种指针引用访问对象,那么就需要对象自身附带指向类型对象的信息,而使用句柄访问对象就没这个必要了。而且还有一个特征就是句柄一般都放在一起,称为句柄池;最后,句柄还有个好处就是,当对象位置发生变化后只需要修改句柄即可,而不需要动所有战上面的reference。做到了一个类似反向代理的作用

StackOverFlowError和OutOfMemoryError:

  先说由于栈分配导致溢出,其实这两种都是可能的,属于一个事件的不同描述,如果用一段代码不断的递归使得线程创建很多栈帧导致了内存溢出,那么在没有一个固定的栈帧数限制下,你是该说是数量太多导致的内存溢出还是总栈帧容量太大导致的呢?其实这类错误的解决很简单,首先总内存固定,方法区和堆内存最大值固定,其他太小忽略不算,所以如果没办法改变代码,就只能把堆内存和方法区内存减少就好了。

方法区内存溢出与String.intern

  方法区能溢出可以通过常量池溢出,或者加载很多的类,前者可以用String.intern(),后者可以利用CGLib动态生成类字节码文件(一般是用来做增强的任务),这个同样是放在虚拟机常量池上面的。这里提及下String.intern(),因为该方法在1.7和之前的版本大不一样;区别在于去掉了永久代,在堆上添加了metaspace,所以如果一个字符串常量在堆中已经存储,那么String.intern(),方法只是在常量池中记录下堆中这个字符串的引用,而如果一个常量池中已经记录了某个字符串(可以是引用或者真字符数组),那么String.intern(),则什么都不做。

直接内存与Netty

  Java1.4后出现了NIO,一种基于通道(channel)与缓冲区(Buffer)的IO方式,使用navtiv函数库直接分配堆外内存,且通过一个堆内的DirectByteBuffer对象作为这块内存的引用,因为不需要在堆内外来回复制数据,能显著提高性能,其中Netty也利用NIO进行封装的一个很好用的框架,NIO之所以能高速,直接原因是因为不需要在用户态和内核态之间来回复制数据。

垃圾收集:

  程序计数器、虚拟机栈、本地方法栈随线程而生随线程而灭;线程每一个栈帧都是基本在编译的时候就确定了大小;如何判断对象是否要回收?引用计数法无法回收环引用,可达性分析法不错,其中根GC Root Set可以是虚拟机栈、本地方法栈、常量池、类静态引用;那么要不要回收还要判断reference:强引用、软引用、弱引用、虚引用,第一个是我们平时用的、第二个是准备要发生OutOfMemory才回收、第三个是到下一次GC回收、第四个有没有没啥区别就是回收的时候能收到一条系统消息;但是一个对象要被回收要被标记两次,这点和finalize()方法有关,需要执行finalize方法的对象会放近F-Queue中,这是它最后一次拯救自己的机会,如果失败;就GG;

  为什么不推荐用finalize呢?这个不同与c的析构函数,它什么时候调用是不确定的,不要把关闭资源等放在里面,否则你可能一直关不了资源。

  方法区的回收,包括常量池的回收和类的回收,例如前者如果一个字符串再也没有引用也会被回收,而一个类会不会被回收非常苛刻:1、该类没有实例;2、该类的类加载器已被回收;3、Class对象没有被引用没有反射调用;通常限制自定义虚拟机利用CGLib技术创建类都需要虚拟机回收类,不然容易爆炸。

内存分配与回收策略:

  一般会分配在Eden区,如果有TLAB就分配在缓存,大对象直接分配年老代,具体取决于垃圾收集器组合;默认在Survivor中“熬过”15(可以通过-XX:MaxTenuringThreshold设定)次minor GC的对象会被移到年老代,当然也不一定,JVM也会动态判定应该放入年老代的年龄,例如如果当Survivor中一半对象的年龄相同,那么大于等于该年龄(<15)的对象都会被回收。

  Minor GC:新生代GC,因为新生代大部分都是朝生夕灭的对象,所以Minor GC非常频繁,回收速度也快;

  Major/Full GC:老年代GC,一般伴随一次Minor GC;速度很慢到Minor的十倍;

Class类文件结构:

  Class文件是以8字节为基础单位的二进制字节流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有任何分隔符;其中超过8位字节以上的数据项采用大端方式存储;

  Class文件格式采用一种类似于C语言结构体的伪结构存储数据,这种结构只有两种类型,无符号数和表,后面的解析都要以这两种数据类型为基础;

  无符号数属于基本的数据类型u1、u2、u4、u8分别代表1字节、2字节、4字节、8字节的无符号数,无符号数可以描述数字、索引引用、数量值或者utf-8的字符串;

  表是具有层次关系的复习结构的数据,整个Class文件本质上就是一张表,习惯性以info结尾;

魔数与class文件的版本:

  每个Class文件头四个字节称为魔数,值为0xCAFEBABE,紧接的四个字节为Class版本号,第5、6个数次版本号,第7、8个是主版本号,其中java 1.8主版本号是52;

  紧接下来的是常量池入口,以一个u2记录常量池的容量计数值(constant_pool_count);只有常量池是以1为索引开始的,因为0代表引用“不引用任何一个常量池项目”含义;

  

、、、、、待续

原文地址:https://www.cnblogs.com/iCanhua/p/8904823.html