JVM是运行在操作系统上面的,Java程序就是运行在JVM上,虚拟机之外的软件是跟JVM并列的,操作系统运行在硬件体系上
这么大个区里面垃圾回收一定不会在那个地方存在。
堆里面肯定有垃圾,
栈不会有垃圾,main方法一进来压在最底下没执行一个方法存一个方法的引用,用完就会把这个方法丢出去,如果这个方法存在垃圾把他堵了引用方法执行不完,main方法结束不了程序就死了
所谓的JVM调优就是在堆里面调
作用:加载class文件
比如:new Student();是在做什么事情?
有一个类叫做Student这个类是抽象的,当我们使用new关键字之后他就变成一个具体的实例了,这个具体的实例引用是在栈里面,实例对象是放在堆里面
类是抽象的模板,对象是具体的
1、虚拟机自带加载器
启动类(根)加载器(Bootstrap)由C++编写的
扩展类加载器(Extension)
应用(系统)加载器(System)
2、用户自定义类加载器
java.lang.ClassLoader的子类
用户可以定制类的加载方式
jdk1.0:本地代码默认信任的、远程代码封存在沙箱里不执行
1.1:增加了安全策略(受信任权限)
1.2:增加了代码签名(差异化代码执行权限控制)
1.6:引入域的概念(也是权限问题)
沙箱了解一下即可
凡是带了native关键字的方法,说明Java的作用范围达不到了,会回去调用底层C语言的库
native 即 JNI,Java Native Interface(上面说JVM结构的本地方法接口)
Java平台有个用户和本地C代码进行互操作的API,称为Java Native Interface (Java本地接口)。
JVM结构中的本地方法栈会调用JNI
JNI就会区调用本地方法库
JNI的作用:扩展Java的使用,融合不同的变成语言为Java所用!最初:C C++
Java诞生的时候C C++ 横行,想要立足,必须要有调用C、C++的程序
它在内存区域中专门开辟了一块标记区域:本地方法栈(Native Method Stack),登记Native方法
现在调用其他接口:Socket,webService,http
在最终执行的时候,加载本地方法库中的方法通过JNI
PC寄存器( PC register ):每个线程启动的时候,都会创建一个PC(Program Counter,程序计数器)寄存器。PC寄存器里保存有当前正在执行的JVM指令的地址。 每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。保存下一条将要执行的指令地址的寄存器是 :PC寄存器。PC寄存器的内容总是指向下一条将被执行指令的地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。
存放所有的①类信息(构造方法、接口定义),②静态变量(static变量),③静态方法,④常量和⑤成员方法,6、运行时的常量池
精简一下就是:static、final、Class信息、运行时常量池
常量池分为:静态常量池(字节码文件常量池)、运行时常量池
关于类的信息可以去看反射那块的类加载过程。
1.又叫静态区,跟堆一样,被所有的线程共享。
2.方法区中存放的都是在整个程序中永远唯一的元素。这也是方法区被所有的线程共享的原因。
—>jdk1.6和jdk1.7方法区可以理解为永久区。
—>jdk1.8已经将方法区取消,替代的是元数据区。
(顺便展开静态变量和常量的区别: 静态变量本质是变量,是整个类所有对象共享的一个变量,其值一旦改变对这个类的所有对象都有影响;常量一旦赋值后不能修改其引用,其中基本数据类型的常量不能修改其值。)
Java里面是没有静态变量这个概念的,不信你自己在某个成员方法里面定义一个static int i = 0;Java里只有静态成员变量。它属于类的属性。至于他放哪里?楼上说的是静态区。我不知道到底有没有这个翻译。但是深入JVM里是翻译为方法区的。虚拟机的体系结构:①Java栈,② 堆,③PC寄存器,④方法区,⑤本地方法栈,⑥运行常量池。而**方法区保存的就是一个类的模板,堆是放类的实例(即对象)**的。栈是一般来用来函数计算的。随便找本计算机底层的书都知道了。栈里的数据,函数执行完就不会存储了。这就是为什么局部变量每一次都是一样的。就算给他加一后,下次执行函数的时候还是原来的样子。
方法区的大小由-XX:PermSize和-XX:MaxPermSize来调节,类太多有可能撑爆永久代。静态变量或常量也有可能撑爆方法区。
一般引用变量找值的话,如果没赋值会去方法区的常量池里面找或者是class模板等里面找,赋值了才会在堆上找
是一种数据结构
程序 = 数据结构 + 算法 持续学习~
程序 = 框架 + 业务 淘汰 吃饭
栈:先进后出、后进先出:桶
队列:先进先出(FIFO:First Input First Output)
程序一执行就是先把main方法丢到栈里面,然后再丢test方法,test方法会先执行完出去,然后main方法执行完出去程序结束
栈溢出原因:
无限压栈,栈就溢出了
栈:栈内存,主管程序的运行,生命周期和线程同步
线程结束,栈内存也就释放了
对于栈来说,不存在垃圾回收问题
一旦线程结束,栈就Over!
栈里面存放的东西:8大基本类型+对象引用+实例的方法(本地栈)
栈运行原理:栈帧
每执行一个方法都会产生一个栈帧
程序正在执行的方法,一定在栈的顶部
栈分为栈顶和栈底
方法内部:方法索引(index),输入输出参数,本地变量(局部变量),Class File:引用,父帧,子帧
(子帧会调用下一个方法,父帧会调用上一个方法)
栈满了:StackOverflowError
栈 + 堆 + 方法区:交互关系
HotSpot
JRockit
J9 VM
一个JVM只有一个堆内存,堆内存大小是可以调节的
类加载器读取了类文件后,一般会把什么东西放到堆中?
类具体实例,方法,常量,变量
栈中一般是一些引用,运行时常量池都在方法区里面
比如对象的引用用个一两次很久都用不到,这时候对象的实例属于垃圾就要被垃圾回收
因为有垃圾,所以这个时候堆内存还要细分三个区
三个区域:
幸存区的意思是对象在经历过一次或者n次GC后还存活下来之后就会进入幸存区(两个幸存区是动态的,可能这次放在0区下次就会交换位置放在1区),假设我们把这个上限定位20次,经历超过20次GC之后就会进入养老区(大多数情况下养老区的对象不会被干掉),养老区空间不足的时候会执行重GC,当执行重GC后养老区仍然空间不足就会OOM
垃圾回收分两种:
轻量级垃圾回收简称轻GC
重量级垃圾回收简称重GC(Full GC)
轻GC针对新生区来的,重GC是养老区都快满了证明新生区和养老区这块要爆了就会进行重GC
永久存储区:基本类型的数据、JVM内置的一些东西
所以垃圾回收主要在新生区和养老区
幸存区主要是新生区和养老区的一个过渡,活下来的对象就进入养老区活不下来的就被GC
假设内存满了,OOM,堆内存不够!java.lang.OutOfMemoryError
示例:
解析:OOM原因就是一开始一直new String新生对象在新生区,然后GC回收,没回收掉的进幸存区然后再一步一步的到养老区直到养老区也满了内存就爆了
上面那张图是在JDK8之前的
在JDK8后,永久存储区改了个名字(元空间)
假设伊甸园区只能存x个对象,new了x个对象之后会触发一次轻GC
轻GC有以下几种情况
有的对象可能还存在引用然后他就活下来了,有的引用清掉他就没用了那他就死了
活下来的进入幸存区,幸存区经过设定的上限,过了上限后进入老年区
养老区空间不足的时候会执行重GC,当执行重GC后养老区仍然空间不足就会OOM
这个区域常驻内存的,跟对象关系不大,存放着JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或类信息~
这个区域不存在垃圾回收!关闭JVM虚拟机就会释放这个区域的内存~
去永久代
,常量池在堆中出现OOM跟这个区没什么关系,这个区一般放些程序启动第三方的jar包
什么情况在永久区就崩了?
一个启动类,加载了大量第三方jar包。
Tomcat部署了太多的应用,大量动态生成的反射类。
不断被加载,直到内存满就会生成OOM
方法区就是永久区(1.8元空间)
可以这么理解,Java堆是一个数据区,新生代、老年代组成了一个堆,永久区是另一个特殊的堆
之前我们也有了解到方法区存的东西,类的信息、static、final、常量池
在伊甸园区new出来的对象可以去方法区拿类的信息,所以这个区是共享的
逻辑上存在,物理上不存在
示例:求最大内存大小和初始化总内存大小
上面可以看到有一定的内存丢失现象,正常
斜体样式可以看出:默认情况下,分配的最大内存是计算机总内存的1/4,然后初始化内存大概是1/64
示例:然后可以去手动调参设置内存大小
-XX:PrintGCDetails打印查看GC信息
可见刚刚的设置生效,最大内存和初始化内存都是1G
面试遇到过OOM,堆内存满了
采取措施解决
1、先尝试着扩大堆内存空间看结果(假设扩大后走原样的代码还是报这个错的话就肯定是我代码有问题,这时候可能有垃圾代码或者无线死循环代码)
2、分析内存,看一下那个地方出现了问题(需要专业工具)
示例:OOM案例观察分析
参数调到特别小,方便查看垃圾回收,然后打印GC信息
分析:一开始前面4次GC,四次之后发现伊甸园区和幸存区都满了就执行了一个重GC,然后又可以在年轻代执行GC,这样反复循环直到最后一次重GC都清不了了,年轻代、老年代、元空间都满了,这时候报错OOM
使用JPofiler工具分析OOM原因
在一个项目中,突然出现了OOM故障,那么该如何排除?(扩大内存后),研究为什么出错?
方法:
MAT,Jprofiler(也有用jmeter的)作用:
使用教程:
1、idea下载
2、百度官网下载Jprofiler客户端
路径自定义,要求没有空格没有中文
这得配置一个地址插件,就是刚刚下载好了的
示例:
知识点:Dump内存文件,跟调优有关的
-XX:+HeapDumpOnOutOfMemoryError
下载好了之后,打开当前项目的文件目录
点开之后
第一个classes页面可以看出,char[]对象占用的内存最多
一般的话建议用biggest Objects
可以很明显的看到java.util.Arraylist占了最多,就知道代码里面很明显是arraylist出问题了,然后点开树状列表可以看到里面放了很多个对象而引起内存不足
可以看一下线程Dump,点开main线程可以看到是哪一行出错了
项目上线看不到具体报错行,只能dump后进行分析。
题目:
成本1:引用计数法要给每个对象分配一个计数器(计算每个对象用了多少次)
成本2:每个对象赋值使用的时候,计数器本身也会有消耗
现在JVM一般不会用这个算法,并不高效,Java里面的对象太多了
谁空谁是to区
每次GC都会将Eden活的对象移到幸存区重:一旦Eden区被GC后,就会是空的!(不是GC了就是到幸存区了,所以肯定是空)
1、假设伊甸园区过来个对象,现在to和from都有一个对象,那么就会复制其中一个区里面的对象进另一个区,然后不管是幸存区to还是from,反正空的那个变成to区
2、当幸存区内部GC,所有对象都在from区,然后from区复制对象进to区,原来的from因为没有对象了就变成了to区他们就是这样动态交换的
只要复制过去后这两个区他们的身份就立马变换了
自己理解:一般不管是eden到to或者是from到to,反正to里面有对象就变from了下次再复制的时候又是from变成to
复制算法就是为了保证to区干净,因为复制过去后to区就变成了空的,并且to区和from区在相互复制并且相互移动位置但是变空的那个就是to区
幸存区进入老年区阈值设置,一般是默认15次GC
(可以百度JVM调优参数)
下面该图:
绿色的表示新的对象,一开始from区一个对象,eden区一个对象,新生区GC的时候from区的对象和eden区的对象到to区,这时候to区变成from区并且拥有两个对象,如果之前那个对象在幸存区达到阈值那么则进入老年区
复制算法最佳使用场景:对象存货都较低的时候;新生区
GC时对活着对象标记一下,清除时对没有标记的对象进行清除
优化上面的标记清除的
两次扫描不好解决,内存碎片可以解决让对象往一端移动过去就行了方便定位
没有内存碎片所以比上面的标记清除算法高效一点
但是多了一个移动成本
标记清除压缩
其实再优化就是先标记清除5次再压缩1次,这样清除5次后内存碎片就变多了,再压缩成本就小很多。
内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度)
(复制算法1次,标记清除算法2次,标记压缩算法3次)
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
从效率上来说复制算法最好但是浪费了内存
思考一个问题:难道没有最优的算法吗?
答案:没有,没有最好的算法,只有最合适的算法 -->GC:也被称之为分带收集算法
分代收集算法:
年轻代:
老年代:
学到这里大概入门了,深究的话可以百度学习还有看思维导图,面试题
书籍推荐:《深入理解JVM》
主内存:Java就一个
线程工作内存:每个线程都有一个自己的工作区域,是从主内存拷贝的
示例:拷贝过去修改其他线程的工作内存是不可见的
Java内存模型带来的问题:
1、可见性问题
解决共享对象可见性问题:volilate
使用volilate
解决可见性问题,使用synchronized
解决同步问题
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类 型的变量来说,load、store、read和write操作在某些平台上允许例外)
JMM对这八种指令的使用,制定了如下规则:
问题: 程序不知道主内存的值已经被修改过了,当A线程修改了工作内存的变量写入到主内存时,线程B还不知道该值被改变过,不可见。
别人问你JMM,就是问你跟并发相关的问题,说白了在问主存和工作内存数据是否一致,解决方案上面有说volatile和synchroized
主要是要学习volatile