java 虚拟机规范中,将其管理的内存划分为:
1.方法区:用于存储类型信息,常量,静态变量等信息,不等价于永久带,只是 HotSpot 虚拟机用永久带(元数据)来实现了方法区,其他虚拟机并没有永久带的概念;
2.堆:虚拟机管理的最大一块内存区域,几乎所有的对象实例都在这里分配,通过-Xmx -Xms 进行内存分配。
3.程序计数器:控制程序流的指示器,循环跳转都依赖于此,也是没有规定任何oom情况的一个区域。
4.虚拟机栈:线程私有的;
5.本地方法栈:执行本地Native 方法
主要以HotSpot 虚拟机进行说明 虚拟机的结构,因为HotSpot 是我们常用的虚拟机;
java 虚拟机初始化分配的内存是本机内存的1/64 ,最大内存是本机的1/4,可以通过Runtime 类查看分配内存的情况:
//返回的单位是b 字节 System.out.println("max="+Runtime.getRuntime().maxMemory()); System.out.println("total="+Runtime.getRuntime().totalMemory());
虚拟机参数:
方法区:
在JDK7 时候,已经将存储在永久带的字符串常量池,静态变量等移出放入了堆之中,在JDK8时候已经将剩余的主要类型信息全部移到了元空间中,完全废弃了永久带的概念;是为了兼容jrockit 虚拟机;
Heap 堆:
由于现代垃圾收集器大部分都是基于分代收集的理论来设计的,所以在Java 堆分为新生代老年代,新生代又包含了Eden FromSurvivor ToSurvivor 这三部分,这仅仅是一种设计风格,并不是所有 jvm 固定的内存分布,java 虚拟机规范并没有对对区域进行具体的划分。
-Xms 堆内存最小空间大小 -Xmx 堆内存最大空间大小 -Xmn 年轻带的大小
-Xss 设置栈的大小
-XX:NewRatio=2 老年代与年轻代的比例大小 -X -X
分析:
jstat 命令参数含义: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
启动java 进程 : java -Xms200m -Xmx200m Hello
1. jstat -gc pid : 垃圾收集的统计信息,可以一眼看出各个区域的使用内存空间的大小
可以算出来 S0C+S1C +EC+OC = 200*1024 为堆的大小。
2. jstat -gcutil pid 可以看出每个区域的内存使用率%。
对象是否存活:
1.引用计数算法:对象中添加一个引用计数器,每当又一个地方引用它,就+1,但单纯的引用计数无法解决对象之间相互引用的问题,在Java中没有采用这种方式;
2.可达性分析算法:Java 是使用这种方式来判断对象的生死,使用GC-ROOT 对象作为起始的根结点,根据引用关系向下搜索,要回收的对象会做一次标记,之后再进行一次筛选,筛选的条件是次对象是否有必要执行finalize() 方法,如果有必要执行(重写了finalize 方法),将会放入f-queue 队列中执行,finalize 只会执行一次;
垃圾收集算法:
目前商业虚拟机的垃圾收集器,大多都遵循了分代收集的理论(1.绝大多数对象都是朝生夕灭的2.熬过越多次垃圾回收对象就越难以消灭)进行设计,所以收集器将Java 堆划分出来了不同的区域,才有了新生代老年代的划分,以及minorgc majorgc fullgc 回收类型的划分。
标记-清除算法
标记-复制算法
标记-整理算法
垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的实践者;
Serial 收集器 :串行收集器,新生代的收集器,采用复制算法进行内存的回收。属于单线程的工作方式。
Serial-old 收集器: 串行老年代收集器,采用标记整理的算法。属于单线程的工作方式。
ParNew 收集器: 属于Serial 收集器的多线程版本,新生代收集器,可与CMS ,Serial-old 老年代收集器一起使用。
Paraller Scavenge: 新生代收集器,诸多特性与ParNew 一致,并行收集器,只不过改收集器是面向吞吐量的新生代收集器,可与Paraller old /Serial-old 老年代收集器一起使用。
Paraller old : 老年代收集器 jdk6才有,并行收集器,面向低吞吐的老年代收集器
CMS 收集器: 并发标记清除收集器,属于老年代收集器,面向低延迟的收集器,用户线程与垃圾收集线程可以并发运行, 使用-XX:+UseConcMarkSweepGC 进行配置,默认采用 ParNew 进行年轻代的回收。但不能与Paraller 收集器一起用,因为面向的目标不一致,一个面向吞吐量一个面向低延迟,技术上Paraller没有使用垃圾收集器的分代框架。CMS 采用的是标记清除,内存中的空间碎片还是要依赖于 fullgc 进行整理。
G1 收集器:分 region 进行垃圾回收,目的是取代cms 垃圾回收。
类加载:
加载->验证->准备->解析->初始化
类加载的时机:
1.在遇到new getstatic putstatic invokestatic 这4条字节码指令时候。
(1).new 关键字实例化对象时候;
(2).读取或者设置一个类的静态变量(被final 修饰已经在编译期间把结果放入常量池的静态变量除外);
(3).调用一个类的静态方法;
2.反射对类型操作。
3.初始化类的时候,如果发现父类没有初始化则先初始化父类。但是在接口初始化的时候不需要父接口完成初始化。
4.虚拟机启动执行的主类。
5.jdk7 引入的动态语言支持。
6.接口定义了java8 default 默认方法,如果接口实现类初始化,那么该接口也要初始化。
验证:
准备:
准备阶段是正式为类中定义的变量(静态变量,被static 修饰的变量)分配内存并设置类初始零值的阶段,仅包含类变量不包含实例变量,实例变量会在对象实例化的时随着对象一起分配在Java 堆中,对于 final 常量类型的静态变量不会进行初始零值的操作直接初始化成指定的初始值。javap -v 可以查看生成的字节码文件信息。
解析:
初始化:
类的初始化是类加载过程中的最后一个步骤,在这个阶段,java 虚拟机才真正开始执行类中编写的java 程序代码,将主导权转交给了应用程序。
在准备阶段时候,变量已经赋过了一次系统要求的初始零值,而在初始化阶段,会根据程序员编写的代码去初始化类变量和其他资源,其实初始化阶段就是执行类构造器<clinit>() 方法的过程,是javac 编译的产物,由编译器自动收集类中的所有类变量的赋值动作和静态语句块的语句合并产生,有上下的顺序。
java 内存模型(JMM):
java 内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值的底层细节,这里的变量是指实例字段,静态字段,和构成数组对象的元素,不包含局部变量与方法参数,因为后者是线程私有的。
Java内存模型规定了所有的变量都存储在主内存里,每条线程都有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的数据,线程间变量值的传递均需要通过主内存来完成。所以会有一些并发问题的存在。
锁(线程安全的实现方法3大类):
1. 互斥同步(悲观并发策略):
多个线程在并发访问一些共享数据的时候,保证共享数据在同一时刻只会被一条线程使用。
synchronized:
java 中最基本的互斥同步手段是synchronized ,是一种块结构的同步语法,在编译之后会生成monitorenter 和monitorexit 两个字节码指令,都需要一个reference 类型的参数来指名要加锁和解锁的对象,如果没有明确指定,将根据synchronized 修饰的方法类型(如实例方法或者类方法)来作为线程的持有锁。
被synchornized 修饰的同步语法块对于一个线程来说是可以重入的,意思就是说明一个同一个线程反复进入同步块是不会出现自己把自己锁死的情况。
被synchronized 修饰的同步语法块在持有锁的线程执行完毕并释放锁之前,会无条件的阻塞后面线程的进入。
ReentrantLock:
与synchronized 一样是可重入的摆脱了语言特性的束缚,与synchronized 不同的是它提供了更强大的支持
等待可中断:锁长时间不能释放的时候,等待的线程可以放弃等待处理其他事情。
公平锁:多个线程在等待同一把锁时,必须按照申请锁的时间顺序获取锁。RrrntrantLock 默认是非公平的锁。synchronized是非公平锁。
锁绑定多个条件:ReentrantLock 对象同时可以绑定Condition 对象。
缺点:java 线程是映射到操作系统的原生内核(kernel)之上的,如果要阻塞以及唤醒一条线程需要操作系统来完成,就不可避免的陷入用户态到核心态的转换。会消耗很多cpu。
2.非阻塞同步(乐观并发策略):
互斥同步面临的问题是线程的阻塞和唤醒所带来的性能开销,随着硬件指令集的发展,我们可以进行冲突检测的乐观并发策略来实现,即不管什么风险先进行操作,如果没有其他线程争用共享数据,操作就直接成功,如果共享数据被争用,产生了冲突那么就会失败我们进行补偿的措施,最常见的补偿就是循环不断重试。
冲突检测的乐观并发策略即cas 操作,它保证了操作与检测冲突的原子性操作,一般需要三个值 内存地址,旧的预期值,设置的新值。
3.无同步方案
要保证线程的安全,也并非一定要进行阻塞或者非阻塞的同步,同步只是保证存在共享数据争用时正确性的手段,如果让一个方法本来就不涉及共享数据,那就自然不需要任何同步措施去保证其正确性。
1.代码天生安全,即不依赖全局变量,存储在堆上的数据以及公用的系统资源,用到的状态量由参数传入,不调用非可重入的方法。
2.本地线程 ThreadLocal 共享数据的可见范围限制在同一个线程之内。
锁优化:
含义: 是为了在线程之间更高效地共享数据以及解决竞争的问题,从而提高程序的执行效率。
1.自旋锁:
挂起线程与恢复线程的操作需要转入内核状态完成,这一部分是很耗费cpu 的,但往往我们应用中锁定但状态只会持续很短的一段时间,为了这点时间去挂起线程和恢复线程是很不值得的,那就让后面请求锁的那个线程稍等一会,但不放弃处理器的执行时间看看持有锁的线程是否很快的释放锁。为了让线程等待,就让线程执行一个忙循环即自旋。
2.自适应自旋锁:
自旋锁的时间不再那么固定了,而是有上一次在同一个锁的自旋时间及锁的拥有者状态决定的。避免浪费cpu 资源。
3.锁消除:被检测到不可能存在共享数据竞争的锁进行消除。
4.锁粗化:
5.轻量级锁:设计的初衷是在没有多线程竞争的前提下,减少传统重量级锁使用系统互斥量产生的性能消耗,轻量级锁是对 hotSpot 虚拟机对象头进行操作的,25比特位存储对象hash 码,4个比特存储对象分代年龄,2格比特币存储锁标记位(01 未锁定),1个比特固定为0标示未进入偏向模式。通过cas 进行来更新锁的状态,如果出现两个以上的线程争用同一个锁的情况,那么轻量级锁会膨胀为重量级锁(10 )
轻量级锁提升程序同步性能的依据是绝大部分锁在同步周期内都是不存在竞争的。如果存在竞争轻量级锁除了互斥量本身开销外还额外发生了cas 操作,因此在有竞争环境下,轻量级锁反而比传统重量锁更慢。
6.偏向锁:无竞争环境下cas 操作都不用做了。
java 线程的实现:
java 目前的线程实现使用的是内核线程的实现方式(1:1 内核线程模型 kernel),每一个java 线程都是直接映射到操作系统原生线程的实现方式,中间没有额外的间接结构,所以会造成切换调用成本昂贵,且容纳的线程数有限。
线程的调度方式使用的是抢占式调度方式,每个线程由系统分配执行时间,线程的执行时间是系统可控的,线程的切换不由线程本身决定。
记一次线上cpu 爆满故障查询(XStream 实例无法释放):
top -H -p pid //查询java 进程中的线程列表,找到cpu 的线程id
printf "%x\n" threadid //线程id 转成16进制
jstack pid |grep 16进制线程号 -A90 //打印堆栈信息