JVM虚拟机

JVM虚拟机

在这里我想记录的东西主要是jvm 内部结构,分析每个组成部分的内存溢出的原因,垃圾回收机制,垃圾回收算法,认识几种常见的垃圾收集器,内存分配回收和回收策略,类加载,java内存模型和线程,高并发如何保证线程安全和锁优化,就一下都是自己总结的,有些我也觉得理解的有些偏,但是毕竟老师是百度,但是感觉jvm理解这些就差不多,面试的时候还是可以吹一吹的.

1.JVM虚拟机的内部结构组成?

        程序计数器,虚拟机栈,本地方法栈,java堆,方法区

2.分析内部结构组成部分的功能

程序计数器:就是当前线程的行号指示器,主要是用来标记当前线程,在运行的过程中,线程是来回进行切换的,所以有这个东西就能保证线程在运行的时候能够高效准确的切换,这是一块很小的内存空间,,在执行本地的nativa方法的时候这块地址是空的,并且 这个区域是唯一个区域在java虚拟机规范中没有规范的任何OutOfMemoryError的区域

虚拟机栈::这个怎么理解呢,就是用来存放局部变量,各种基本数据类型,引用对象,句柄,你看它存放的东西是不是很像一个方法里面的东西,所以可以理解一个方法就是里面的一个栈帧,虚拟机栈存储机栈的过程就像出栈入栈一样,当如果线程请求的栈的深度大于虚拟机所允许的深度,或者在动态扩展的时候申请不到足够的空间这个时候就会出现OOM 异常
在这里面说明一点,局部变量所需要的内存空间是在编译期的时候就分配好了,意思就是在运行期这个内存空间就被固定了

本地方法栈:
本地方法栈和虚拟机栈是十分类似的,这个就是为虚拟机使用到的Native方法服务的,什么是native 方法服务,就是百度了一下,就是你当java要与外界环境打交道的时候,有一些不是java所编写的方法而是向c,C++这种编写的用来操作机器指令的一些方法,一般都是放在.dll 文件中,但是可能就是格式不同,所以了解就行,但是还是要记住本地方法栈抛出异常和虚拟机机栈相同

java堆:堆是用来存放实例化对象,怎么个存放,就是打比方你new了一个对象,这个时候堆就会开辟一份空间给这个对象,用来存储这个实例化对象的内容,就例如我们传的实例值啊,这都是,他是虚拟机内存中最大的一块,也是被所有的线程多共享的
异常:抛出OUtOfMemoryError异常就不用说了

方法区:方法区里面就是用来存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译的代码,这个区域也没称作永久代,也是被所有的线程所共享,也抛出异常,但是在这个里面有一个运行时常量池,里面存放的东西就是编译生成的字面量和符号引用

直接内存:这个直接内存不是虚拟机里面的五大区域,但是也经常出现OOM异常,所以值得注意,他的出生是由本地方法栈里面的IO方式,由native函数库直接创建,然后通过java堆里面的DirectoryByteBuffer对象直接引用,服务器管理员配置虚拟参数的时候通常会忽略直接内存,从而使实际内存大于物理内存限制,从而动态扩展的时候出现OOM异常

3.内存溢出

java 堆内存溢出
怎么可以使java堆内存溢出?
我们不断的创建对象,并且保证GC不清理我们刚才所创建的对象,这样就会在对象数量达到最大堆容量限制后就出现内存溢出异常

如何解决这个异常?
首先要通过内存映像分析工具,分清楚是内存溢出还是内存泄漏,,如果是内存泄漏的话,就可以通过工具进一步查看泄漏对象到GC Roots引用链,就可以分析出泄漏的位置,如果是内存溢出,就说明这个对象还活着,那么就检查某些对象生命周期过长,或者持续时间过长

虚拟机栈和方法区栈内存溢出

溢出异常的原因
首先明白虚拟机栈和方法区栈里面存放的是栈帧,就是线程,这个里面要出异常,无非就是线程请求的深度大于虚拟机允许的深度,要么则是虚拟机在扩展栈的时候申请不到足够的内存,,但其实这两种错误是一个原理,就是线程在里面太多导致内存不够

怎么解决
‘减小内存’,就是通过减小每个线程的内存,从而使他能容纳更多的线程,这里提出的解决办法当然是在不能减少线程和更换虚拟机的前提下,虽然这种解决方式我是感觉有点自欺欺人的感觉,但是还是一种方式吧

运行时常量池内存溢出
这个异常也很好理解,运行时常量池内存在方法区内,就是通过调试方法区来调试运行时常量池内存

方法区内存溢出
方法区内存溢出的原因是,方法区里面存放的是class文件的信息,当class文件是被扩展的时候,这个里面会生成大量的动态的class类,并且一个类被回收机制回收,判断的条件是十分苛刻的,所以容易产生方法区的内存溢出

本机直接内存的溢出
本机内存的计算是由DirectByteBuffer类直接通过反射UnSafe实例进行分配内存,他所抛出的异常是由人为计算出,从而得知内存无法分配的,从而人为手动抛出

4.垃圾回收

1.怎么判断对象已死?
引用计数法:给每个对象上面放置一个计数器,当这个对象被引用一次,对象的计数器就加1,当对象的引用失效的时候,计数值就减1,当计数器为0的时候,这个对象就是不能被引用了,但是这个方法是解决不了对象互相循环引用的问题
根搜索算法:就是使名为GC Roots对象为起始点,从这种节点类型开始向下搜寻,搜索走过的路径就是引用链,当一个对象无法与任何引用链相连的时候,这个对象就被判断是可以回收的对象,意思就当一个对象不能链接到GC Root这个起始点,那么这个对象就是可回收的
什么可以作为GCRoots对象呢?
栈帧中的本地变量表,类静态属性,常量的引用,JIN(native本地方法的引用对象)
引用:
强引用:永远不会被垃圾回收机制所回收,像new Object()就是强引用
软引用:这个引用就是还有一些用,但是不是必须的,所以当内存快溢出的时候,这个时候就会把这些对象列进回收范围之内,并且进行第二次回收
弱引用: 被弱引用关联的对象只能生存到下一个垃圾收集之前
虚引用:就是被垃圾收集回收的时候留下一个日志

对象的最后生命
当一个对象被根搜索算法搜出无连接的时候,这个对象其实是被判了缓刑,而不是死刑,他还有一根救命稻草,那就是finalize方法,当一个被判了缓刑的对象会放在一个F-Queue队列里面,然后通过一个低优先级队列去执行,会筛选他是否有必要执行finalize方法,如果这个方法执行过finaliaze方法或者他没有这个方法,那这个对象是直接判死刑的,如果这个对象是有的finalize方法的时候,他就可以执行这个方法获得最后一个连接对象的方法,如果在执行这个方法的过程中重新连接到了一个对象,那他就复活了,记住finalize()方法只会被执行一次

回收方法区
这里面回收的东西比较少,一般就主要回收废弃常量和无用的类
废弃常量的判定:
就是一个常量在常量池内,但是没有一个对象是引用它的,那么它就是废弃的
无用的类的判定:
1.该类所有的对象实例都已经被回收
2.加载该类的classLoader被回收
3.该类对应的class没有在任何地方被引用

垃圾收集算法

新生代:生命周期比较短的对象
老年代:生命周期比较长的对象
标记-清除算法:标记出所有需要回收的对象,然后标记完成后统一回收掉所有被标记的对象
复制算法:(新生代用比较好) 就是把一块内存分配成两块一样大小的内存,每次只使用一块,当这块使用的内存被用完的时候, 就把存活着的对象复制到另一块没用过得内存上面,然后这块被用过的的内存空间一次清理
但是在现在商业虚拟机都是把内存分割成一块较大的Eden内存空间和两块较小的survivor空间,在使用的时候都是使用eden和一块survivor空间,当回收的时候,就是把还存活的对象一次性拷贝到另一块没用的survivor上面,然后清理那两块被用过的,但是这里有一个问题,就是这块survivor内存不够大,那么就需要内存的分配担保,把存储不了的对象通过分配担保机制进入老年代
标记-整理算法;(针对老年代)就是让存活的对象都像另一端移动,然后直接清理掉边界以外的内存
分代收集算法:就是根据对象的个代年龄的特点采取最适当的收集算法,就例如,新生代多的就可以采用复制算法,老年代多的就可以采用标记-清理或者标记-整理

垃圾收集器

不同的年代,或者需求都会使用最适当的收集器,没有一个收集器是完美的

serial收集器:年代最久远,它在进行垃圾收集的时候,需要将所有线程全部暂停,直到收集结束,并且 他只会使用一个CPU或者一条收集线程去完成垃圾收集工作当然他也有自己的优点:简单而高效

parNew收集器:多线程执行收集的servial收集器

parallel scanvenge:新生代收集器 ,并行的多线程收集器,使用复制算法,特点:关注吞吐量(就是运行用户代码的时间/CPU总消耗时间的比值),吞吐量越高,那么就CPU运行的效率就高,这个说白了就是提高响应速度

CMS收集器:一种以获取最短的回收停顿服务时间为目标的收集器,采用标记-清除算法,里面的标记有:初始标记–>并发标记–>重新标记–>并发清除,但是这个收集器无法处理浮动垃圾,手机完后会产生大量的空间碎片

G1收集器:采用的是标记-整理算法,不会产生空间随便,它将整个java堆分为了很多大小固定的区域,然后进行跟踪,检测垃圾堆堆的程度,然后后台排出优先表,更具允许的收集时间,优先回收垃圾最多的地方

内存分配回收和回收策略

1.对象优先在Eden分配
2.大对象直接进入老年代
3.长期存活的对象直接进入老年代
4.动态对象年龄的判定:如果survicor空间里面的相同年龄大小的所有对象的大小的总和大于survicor空间的一半,那么年龄大于或者等于该年龄的对象就可以直接进入老年代

类加载机制

加载:这个交给虚拟机去把握,.
类加载器;通过类的全限定名获取此类的二进制流文件
不过在遇到new ,statice,反射,父类,主类,这些的时候初始化的时候,这些是一定要被已经加载,要么触发他们初始化
连接:
验证:为了确保这个class文件的字节流信息符合当前虚拟机的要求
准备:为类变量分配内存,并且设置类变量初始值的阶段,在方法区中进行
解析:常量池中的符号引用变成直接引用
初始化:初始化阶段就是执行类构造器方法的过程
使用
卸载

类加载器分为:启动类类加载器(虚拟机的一部分,由C++语言实现的)和其他类类加载器(继承java.lang .classLoad,由java 语言编写)

线程实现方式

实现线程的三种方式:内核实现,用户线程实现,使用用户线程加轻量级锁实现混合实现

内核实现:由内核调度线程实现,但是内核是靠内核线程,但程序不会直接进入内核,从而有了轻量级线程由每个内核线程进行操作

线程调度是系统为线程分配处理器使用权的过程,两种方式:协同式(线程自己控制,这个就容易阻塞)和抢占式调度(系统控制,优先级方式)

线程安全

被final修饰的变量 是绝对的线程安全——不可变
线程安全有;绝对安全,相对安全,线程兼容,线程对立

如何实现线程安全?
互斥同步:就是多线程并发访问共享数据的时候,保证共享数据在同一时刻 就被一条线程所使用,像synchronized,ReentreanLocK就是

非阻塞式同步:就是什么都不干,知道共享数据出现了竞争,那么就进行其他的补偿措施(就是不断的重试),乐观的并发策略

无同步方案:就是一些代码本来就不涉及共享数据的问题,天生就是线程安全的
例如:
可重入代码:就在在代码执行的时候可以随意的中断,中断后回来程序不出错
线程本地存储:就是一段代码中所需要的数据必须和其他代码共享,那么就看数据共享的代码能不能放在同一个线程中.

锁优化

自旋锁:就是当一个线程在进行时,卡了一会本来得进入阻塞的状态,但是有自旋锁,就让外面那个线程等一会,万一这个卡主的线程一会就好,就是自旋锁是用来优化那种线程可能只卡一会给他一点时间在运行会的意思.
锁消除:就是在编译器运行的时候,把那些不可能存在共享数据竞争的锁消除掉
锁粗化:就是有些线程他要运行好久,但是老运行一会你就得给别人用,然后一群线程来回切换,影响性能,这个时候就粗化锁,减少切换次数
轻量级锁:就是通过CAS来解锁加锁,就相当有个人在给每个犯人带锁开锁去吃饭,重量级锁,就相当于一个个犯人进去食堂吃饭,这个轻量级锁只适用于知道大部分的锁不存在竞争的
偏向锁:消除+的情况下的同步原语,就是当没有什么竞争的时候,就把所有的锁全部取消掉,就相当于狱长看到一群好犯人,不惹事,他也就偏心不给他们加锁了

—————————————————————————————————–
嚼了很久周志明的这本深入理解java虚拟机,收获很多,第一遍的时候我是粗略的读,读完就只明白虚拟机里面构成的部分,讲的内容大概是什么,第二遍是最难受的,分析每句话的含义,理解明白里面的原理,第三遍就是开始记忆,把这些内容想象成生活中的一些东西关联起来,第四遍记笔记提取重要的点,第五遍就是这篇博客,但是我感觉过两天又会去读,有点上瘾,一些东西我理解的不到位,先给自己记下来,到时候反过来再看

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值