Java的内存,主要分配给栈、堆和方法区。
栈的内存是固定的,只与类结构有关,在运行前就是确定的,在方法或线程结束时就能回收。
堆和方法区的内存是动态的,比如接口有不同的实现类,内存都不一样,比如方法可能走不同的逻辑分支,内存也不一样。所以堆和方法区的内存,只能在运行期间确定,必须动态地分配和回收,这就会产生垃圾。
所以,Java的垃圾回收,针对的就是堆和方法区。
堆和方法区都是引用对象(基本类型都在栈里),所以要回收的,其实就是不再被人引用的对象,如果一个内存(堆或方法区中)中的对象,没有任何引用,那么这个对象就是无用的对象,需要回收,腾出内存。
引用
要判断一个对象是不是垃圾,主要检查它有没有被引用。
- 引用计数
最简单的做法,就是把对象的引用做一个统计,如果引用为0,就是垃圾,可以回收。
但是,引用计数最大的bug在于循环引用,如果两个对象互相引用了,那么它们就没有机会被回收。 - 可达性分析
现在最常用的是可达性分析法,我们定义了四种GC Roots,如果一个对象向上层层引用之后,能是直接或间接被GC Roots引用,就认为这个对象到GC Roots之间是可达的,就不是垃圾。
所以也可以说,如果一个对象到GC Roots之间有连通的路径,就是可达的。
Java中可以被作为GC Roots中的对象有:
.虚拟机栈中的引用的对象。(栈里引用的对象)
.本地方法栈(jni)即一般说的Native的引用对象。(JNI引用对象)
.方法区中的类静态属性引用的对象。(方法区静态对象)
.方法区中的常量引用的对象。(方法区常量对象)
强软弱虚四种引用
Java通过引用判断对象是否可以回收,为了增加一定的灵活性,Java给引用本身做了细分,分为强软弱虚四种引用:
- 强引用是不可回收。
- 软引用是在内存不足时回收。
- 弱引用是不管内存够不够用,只要遇上了GC扫描,都要回收,不过GC扫描的优先级很低,不会很快触发回收。弱引用可以和引用队列一起使用。
- 虚引用(java.lang.ref.PhantomReference)实际上是个引用关联,必须和引用队列(Reference Queue)一起使用,本身无法实例化,被虚引用关联的对象,可以在任何时候回收,设置虚引用,一般是为了在对象被回收时收到一个系统通知。
另外,在引用里还有两个常用的类:
- 引用队列(ReferenceQueue),本身不属于引用类型,但是和引用类型配合使用,就可以在对象被回收时收到一个系统通知,如果ref类引用了obj,当obj被gc时,ref就会被放到ReferenceQueue中,我们可以对ref做额外处理,例如数据清理。
- WeakHashMap,它将WeakReference作为key对象,如果key被gc了,那么key的整个entry会被移除,WeakHashMap的size就会发生变化(在调用get时,先处理被gc的key值,再把entry重新链接起来)。
二次标记
稳妥起见,失去引用的对象并不会立即被回收,需要经过两次标记,才会真正被判定为可回收的垃圾对象,这就是二次标记。
- 第一次标记
如果发现一个对象失去了可达性(无法链接到GC Roots),就会对它进行第一次标记。 - 过滤和finalize
针对被第一次标记的对象,JVM判断它是否需要执行finalize。
如果这个对象没有覆盖finalize函数,或者已经被执行过一次finalize函数(finalize函数只会被执行一次),就直接跳过,等待第二次标记;
如果这个对象可以执行finalize,JVM会把它放进一个专门的F-Queue队列里,由一个低优先级的Finalizer线程去悠闲地触发finalize,如果在此期间或者在finalize函数中,这个对象又重新实现了可达性(比如被赋给某个对象引用),就取消它的标记。 - 第二次标记
在下一次分析中,被标记过一次的对象,会被标记第二次,被标记两次的对象,就是垃圾对象,可以回收了。
方法区的垃圾
一般来说,堆中的对象会频繁生灭,垃圾回收效率较高,但是方法区的常量和类其实很少回收,所以一般来说它的垃圾回收效率低,但是对方法区的回收是有必要的,特别是反射、动态代理、自定义ClassLoader等,都是可能导致方法区内存溢出的。
方法区的垃圾来源无非是常量和类。
常量回收相对简单,如果一个常量,当下没有任何引用,就可以回收。
类的回收就很苛刻了,要求如下:
- 堆中不再有该类的实例对象
- 加载这个类的ClassLoader已经被回收
- Class没有被引用,也无法通过反射访问到类的方法
2.什么是垃圾回收GC(garbage collection)
手动gc JDK提供一个gc函数,调用这个函数,就完成对jvm虚拟机内存进行一次GC
自动gc,当jvm虚拟机启动后,后台会自动运行一个gc程序,赋值内存的gc操作
这个后台的gc程序定时做gc操作,或者当jvm内存不够的时候,会立刻触发gc
JDK自带jvsualvm工具,安装Visual GC插件
同一个对象可以被多个指针同时指向
一个指针可以指向不同的对象,但是同一时刻只能指向一个对象
测试当对象有指针指向的时候,对象不会被垃圾回收器回收
当对象没有指针指向的时候,对象会被垃圾回收器回收(会不会立即回收?不会)
3JDK自带的jvsualvm工具,安装Visual GC插件
打开cmd,先查看jdk的版本
输入jvisualvm回车,打开Java VisualVM
3.点击工具-》插件-》可用插件
找找里面Visual GC,勾上,然后安装。
4jvm垃圾回收(GC)
Minor GC和Major GC/Full GC的区别
1.年轻代
年轻代是所有新对象产生的地方,当年轻代内存空间被用完时,就会触发垃圾回收,这个垃圾回收叫做Minor GC。
年轻代被分为3个部分——Enden区和两个Survivor区,年轻代空间的要点:
- 大多数新建的对象都位于Eden区。
- 当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区。
- Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区。这样在一段时间内,总会有一个空的survivor区。
- 经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。
2.年老代
年老代内存里包含了长期存活的对象和经过多次Minor GC,年龄到达15(网上查阅的可能跟大家的有点出入)后,依然存活下来的对象,通常会转到老年代,老年代内存被占满时进行垃圾回收。
老年代的垃圾回收叫做Major GC,Major GC通常是跟full GC是等价的,回收整个GC堆。
Minor GC和Major GC其实就是年轻代GC和年老年GC的俗称。
Full GC定义是相对明确的,就是针对整个新生代、老年代、元空间的全局范围的GC。
Minor GC 发生在新生代的GC, 速度很快
Major GC/Full GC发生在老年代的GC, 通常Major GC发生的时候都伴随着Minor GC的发生, 速度比较慢(Major GC 的速度大致会比 Minor GC 慢 10 倍)
5对象和指针的关系
5.1指针是可以传递的
传递指针可以让多个函数访问指针所引用的对象,而不用把对象声明为全局可访问,要在某个函数中修改数据,需要用指针传递数据,当数据是需要修改的指针的时候,就要传递指针的指针,传递参数(包括指针)的时候,传递的是它们的值,也就是说,指针传递也是值传递!因为指针本质就是个地址变量,所以指针传递的参数也是个变量!指针传递是把地址变量复制一份再传递给函数的!传递给函数的是参数值的一个副本
当对象有指针指向的时候,对象不会被垃圾回收器回收
当对象没有指针指向的时候,对象会被垃圾回收器回收
下面举个例子简单的解释一下:
代码部分
当程序运行到第20s的时候,也就是上面这个19的数字的时候,堆区中的就出现了它的对象,从下面我们可以很明显的看到(下图圈起来的部分)
当程序继续往下面走的时候,又走了10s的睡眠时间之后即走到下图的数字9的时候,这个时候程序进程结束,指针被销毁。
我们会发现堆区中的对象没了,如下图所示
到这里我们证明了当对象没有指针指向的时候,对象会被垃圾回收器回收然后,我们再给这个对象再给添加一点代码,把这个ceshi1的指针赋给方法区的静态变量里面一个空指针ce,代码如下(打红色下划线的是添加部分):
走到这里我们发现跟前面的例子还是一样的,但又走了10s的睡眠时间之后即走到下图的数字9的时候,这个时候程序进程结束,指针被销毁,但堆区中的对象却还在。这是为什么?
这时候我们就会发现是我们新增加的代码导致的问题,因为我们把这个ceshi1赋给方法区的静态变量里面一个空指针ce,所以这个时候就算我们销毁了这个栈区里面的指针ceshi1,但是还有一个指针ce指向它,所以对象没有被回收还在堆区中。
到这里我们就证明了指针是可以传递的
5.2禁止直接操作对象中的属性,用成员方法封装操作对象属性的过程
直接操作对象中的属性,可能会输入一个错值,程序直接运行不了。
为了防止这种情况,就需要对成员进行属性封装
在类之中的组成就是属性与方法,一般而言方法都是对外提供服务的,所以不会进行封装处理,对于属性需要较高的安全性,所以往往需要对其进行保护,这个时候就需要采用封装性对属性进行处理。
在默认的情况下。对于类中的属性是可以通过其他类利用对象进行调用的。
示例,属性不封装下的问题:
此时在person类中提供的name与age两个属性并没有进行封装处理,这样外部就可以进行调用,但是数据有可能是错误的数据。
要想解决这样的问题,使用private关键字进行封装处理:
示例:对属性进行封装:
而属性一旦封装,外部不可直接访问,但是对类的内部是可见的,那么如果要想外部的程序可以访问封装的属性,那么在java标准中有如下要求:
[settergetter]设置或取得属性可以使用serXxx()、getXxx() 方法,以private String name 为例:
设置属性方法public void setName(String n);
获取属性方法public String getName().
范例:实现封装:
在以后进行任何类定义时,类中的所有属性都必须使用private封装,并且属性要进行访问必须提供setter、getter方法。
6JVM运行时概况简单的来理解,就是:
① Java源文件—->编译器(工具)—->字节码文件
② 字节码文件—->JVM(工具)—->机器码
嗯,如果还是不太懂得话可以演示一番:
比如写这行代码:
咱们可以去想它是怎么运行的呢?
JVM之基本结构ok,看了上面对JVM的流程,大概也有一点概念了吧,在将java源文件转为class文件后,接下来接手的就是JVM了,也就是最后转为机器码。
不清楚的可以直接看下面:
这个东西实际上就是JVM的基本结构



