今日总结
- 看了黑马jvm35-76p
- 写了桶排序和基数排序,今天总结一下所有的排序
JVM 今日学习
直接内存
是操作系统内存
- 常见于NIO操作,用于数据缓冲区
- 回收成本高,读写性能高
- 不受JVM内存回收管理
- 使用unsafe分配和释放内存(freeMemory)的
- ByteBuffer实现内部(gc的时候ByteBuffer对象没了,所以这个引用就没了,间接调用后面的)虚引用+回调,用一个Cleaner创建需引用对象,虚引用关联的实际对象被回收以后,就会调用clean方法执行删除任务
内存溢出:java.lang.OutOfMemoryError: Direct buffer memory
为什么快??
在操作系统划出一个区域,java代码可以直接访问(java和系统都能用)
之前要走两次缓存,现在走一次就行了,少了一次缓冲区的复制操作
垃圾回收!!
判断是否可以被回收
- 引用计数
会出现循环引用的问题
- 可达性分析算法
根对象:肯定不会是垃圾的
- 系统类
- 操作系统方法(Native Stack)
- 活动线程
- 锁(monitor)
所以不会被根引用的都gg
1 四种引用
- 强引用
- new一个对象给一个变量,该变量就强引用了这个对象
- 就是从gc root能找到
- 所有gc root对他的链都断了,就gc
- 软引用
- 没有被强引用引用
- 如果gc完了返现内存还是不够,且没有强引用引用时,就会gc软引用
- 使用场景:
- 内存敏感的情况下,创建对象时用SoftReference包裹,容易被释放。也可以用弱引用。
- 软引用本身也占空间。用引用队列清除。在创建SoftReference和引用队列关联。
- 软引用关联的对象被回收时,软引用自身就会进入队列。
- 手动remove
- 弱引用
- 不管内存是否充足,都会gc
- 软弱引用可以配合引用队列使用(释放通过引用队列,遍历释放)
- 使用场景:
- 用weakReference
- ??什么事软弱引用没有说
- 虚引用
- 必须配合引用队列使用
- 之前的ByteBffer就是
- 虚引用引用的对象被垃圾回收时,会进入引用队列,有线程会去队列里找,然后会找,调用clean方法,去删掉(unsafe)
- 终结器引用
- 必须配合引用队列使用
- 对象重写了终结方法没有强引时,虚拟机会创建终结器引用,并放入引用队列,会有线程(优先级很低)去队列里找,调用finallize()方法,并回收。
- 太复杂不考虑
2 垃圾回收算法
- 标记清除
优点:速度快
缺点:会产生碎片
- 标记整理
优点:没有碎片
缺点:因为要移动,速度较慢
- 复制
把存活的的放到to,清除from,然后交换from和to,to总是空闲的
优点:不会产生碎片
缺点:会占用双倍的内存空间
3 分代回收
区域划分:
- 工作流程
对象一开始分配伊甸园中,新生代放不下时,会触发Minor GC。
gc之后会用到幸存区,使用复制算法活下来的放到幸存区to中,同时寿命+1。
之后在min GC的时候,对伊甸园和幸存区同时复制算法并修改寿命。
当对象寿命达到阈值(最大15,4 bit)时,放到老年代。
当老年代空间不足时,尝试min gc,空间仍然不足,会执行Full GC(时间更长),实在不行就outof Memory
- tips
- Minor gc 和Full GC会引发stop the world,暂停其他线程,gc线程先工作
- 大对象(新生代不够了)直接晋升到老年代。
- 一个java线程的内存溢出不会导致整个线程gg
- 相关参数
4 垃圾回收器
- 串行
- 单线程
- 堆内存较小,适合个人电脑
,老年代时标记整理
- 吞吐量优先
- 多线程
- 堆内存较大,要求多核cpu支持,适合工作在服务器上
- 单位时间内,stw最短,让整体stw大的事件尽可能小
- 所有的线程都会暂停去回收垃圾
(并行)新生代复制,老年代标记整理,这两个开关是连带开启的。
多个线程一起回收垃圾。几个核几个线程
可以控制线程数
可以自适应新生代(伊甸园、幸存区、阈值)大小
:会改变堆的大小改变吞吐量的大小。
:最大暂停毫秒数。和上面是冲突的,因为堆大了之后要回收的垃圾多了。
- 响应时间优先(cms)
- 多线程单线
- 堆内存较大,要求多核cpu支持,适合工作在服务器上
- 尽可能让单次stw的时间最短,让每个用户体验都好。
:并发标记清除。工作在老年代。
并发失败的时候会退化成单线程,标记整理
:配合上面使用,工作在新生代,复制算法
不需要stw ,可以和用户线程一起执行,工作在老年代
并行(所有工作的)的是cpu核数,并发(处理垃圾的线程)的一般设置为1/4。
:
浮动垃圾:垃圾清理时产生的垃圾,所以需要预留一点空间保留浮动垃圾。
这个值越小,开始gc越早
:标记之前先对新生代做一次垃圾回收,因为新生代的可能会引用老年代,还要做可达性分析等等,但是他们大多有存活不了多久。所以多做了很多无用的查找工作。减少重新标记的压力。
- 工作流程
先初始标记(很快),然后并发标记,不影响用户线程,不用stw。
然后重新标记(因为并发标记的时候可能会产生新的垃圾),这里需要stw。
然后开始并发清楚,循环往复
- 降低了吞吐量,因为给了一核去处理垃圾,剩下的才去做工作,少了一核干事
- 因为是标记清除,所以产生很多碎片,所以退化一下,到时候垃圾回收的时间就会突然增多。
G1
适用场景:
- 同时注重吞吐量和低延迟
- 超大的堆内存,划分为多个大小相等的Regin
- 整体上是标记整理,区域之间用的是复制算法
:打开
回收阶段
- 新生代回收(Young Collection)
触发新生代垃圾回收,会stw
用复制算法放进幸存区
是young gc ,有的会进入老年代,年龄不够的会放在幸存区
- 新生代回收+并发标记(Young Collection + CM)
Young Gc 进行GC Root的初始标记
老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由以下jvm参数决定
- 混合收集(Mixed Collection)
队 E、S、O进行一个全面的垃圾回收
- 进行最终标记:收集并发时没发现的垃圾,会STW
- 拷贝存活:会STW,并不是将所有的垃圾都回收,因为要保证暂停时间尽可能短,所以会有选择的进行垃圾回收。回收垃圾最多的区域 (这也是为什么叫garbage first)
排序算法
● 冒泡排序:
每次两两互换,则每次都能挑出最大的放在尾部。可以增加一个标志位,一轮不需要交换的时候就已经排序完成了跳出循环。
private static void bubbleSort(int[] nums) {
boolean flag = false;
for (int i = nums.length - 1; i >= 0; i--) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[j + 1]) {
swap(nums, j);
flag = true;
}
}
if (!flag) {
break;
}
flag = false;
}
}
● 选择排序
每次选择后面最小的放在当前位置。
private static void selectSort(int[] nums) {
for (int i = 0; i < nums.length - 1; i++) {
int now = nums[i];
int max = Integer.MIN_VALUE;
int index = i;
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] > max) {
max = nums[j];
index = j;
}
}
if (max > now) {
swap(nums, index, i);
}
}
}
● 插入排序
每次选择一个进来放在他该在的地方。
//插入排序,每次插入一个,放在一个
private static void insertSort(int[] nums) {
for (int i = 1; i < nums.length; i++) {
insert(nums, i);
}
}
private static void insert(int[] nums, int index) {
for (int i = index; i > 0; i--) {
if (nums[i] > nums[i - 1]) {
swap(nums, i, i - 1);
} else {
break;
}
}
}
● 希尔排序
插入排序的优化,假如一个增量,因为插入排序在基本有序的情况下效率很高,所以先变成基本有序,效率会提高,但是基本无序的情况下效率会很差
最快可达O1.3
private static void shellSort(int[] nums) {
for (int i = nums.length / 2; i >= 1; i /= 2) {
for (int j = i; j < nums.length; j++) {
insert(nums, j, i);
}
}
}
private static void insert(int[] nums, int start, int depth) {
for (int i = start; i > 0; i -= depth) {
if (i - depth < 0) {
return;
}
if (nums[i] > nums[i - depth]) {
swap(nums, i, i - depth);
} else {
break;
}
}
}
● 归并排序
递归,先分成小段,把小段排序了,再把大段排序。中间卡了一个问题,处理小段的时候没有把排序好的赋值给原来的数组,卡了很久。
一般用于数据量大,且稳定的情况下,时间复杂度为O(nlogn)
private static void mergeSort(int[] nums, int left, int right, int[] res) {
if (left < right) {
int middle = (left + right) / 2;
mergeSort(nums, left, middle, res);
mergeSort(nums, middle + 1, right, res);
merge(nums, left, middle, right, res);
}
}
private static void merge(int[] nums, int left, int middle, int right, int[] res) {
int i = left;
int j = middle + 1;
int index = left;
while (i <= middle && j <= right) {
if (nums[i] > nums[j]) {
res[index++] = nums[i++];
} else {
res[index++] = nums[j++];
}
}
while (i <= middle) {
res[index++] = nums[i++];
}
while (j <= right) {
res[index++] = nums[j++];
}
//这里比较的还是原来的,实际上已经变化了。nums的在之后已经发生了变化了,所以要返回进去
for (int k = left; k <= right; k++) {
nums[k] = res[k];
}
}
● 快速排序
每次把一个放到其应该在的位置上,前面的比他小,后面的比他大。
然后在将基准前后继续进行找基准。
写的过程中出现了几个问题,第一个就是是和基准比,
第二个,比较写错了
快速排序在基本有序的情况下效率不高。
private static void quickSort(int[] nums, int low, int high) {
if(low<high){
int pivot = parition(nums,low,high);
quickSort(nums,low,pivot-1);
quickSort(nums,pivot+1,high);
}
}
private static int parition(int[] nums, int low, int high) {
int pivot =nums[low];
while (low<high){
//第一个问题,这里是和基准进行比较。
while (low<high && nums[high]<=pivot) {
--high;
}
// if(low<high){
nums[low]=nums[high];
// }
//写错了..
while (low<high && nums[low]>=pivot){
++low;
}
// if(low<high){
nums[high]=nums[low];
// }
}
nums[low] = pivot;
return low;
}
● 堆排序
以大根堆为例:堆排序用在大数据量下但是只要几个的情况,通过不断调整堆来找到结果。在建堆的时候,都是从叶子结点比较的(n/2),所以当前的节点只要比两个儿子大,一定是最大的的,之后的情况只要考虑如果我这个节点比较小,要放在的位置会在哪里。
这就是建调整的思想,而在取数的时候其实就是取出来放到最下面,然后交换。
前一种是迭代,后一种是递归(位置没有确认)。
时间复杂度为:O(nlog n + n) =O(nlog n)。是建堆和调整
private static int[] heapSort(int[] nums) {
int[] arr = nums.clone();
int len = nums.length;
buildHeap(arr,len);
for (int i = len-1; i >= 0 ; i--) {
swap(arr,0,i);
adjust(arr,0,i);
}
return arr;
}
private static void buildHeap(int[] arr, int len) {
// 为什么从n/2开始,因为下面的全是叶子节点,不会出现遗漏的情况
for (int j = len/2; j >= 0; j--) {
adjust(arr,j,len);
}
}
//因为是从叶子节点开始调整的,所以不用担心孙子节点有比自己大的
private static void adjust(int[] arr, int i, int len) {
//当前的根节点
//相当于是给max找下家
int max = arr[i];
int left = 2*i+1;
for (int j = left; j < len; j= j*2+1) {
if(j+1<len && arr[j+1]>arr[j]){
j++;
}
//孩子都没我大,无需调整
if(arr[j] < max){
break;
}
//不是最大,需要调整
//到下面区找最大的一个放上了
else {
arr[i] = arr[j];
i=j;
}
}
//最终交换的位置
arr[i] =max;
}
// //不是常规思路
// private static void adjust(int[] arr, int i, int len) {
// int left = 2*i;
// int right = 2*i+1;
// int max = i;
// if(left<len &&arr[left]>arr[max]){
// max=left;
// }
// if(right<len && arr[right]>arr[max]){
// max=right;
// }
// if(max !=i){
// swap(arr,max,i);
// //这里还要进行一次调整,继续向下调整,数值交换了,,所以看看下面有没有更大的
// adjust(arr,max,len);//
// }
// }
● 桶排序
是一种空间换时间的策略,最好是那种分布比较均匀的数据。
将数据分散到n个桶中,再对这些桶内部进行排序。
时间复杂度n*(log n -log m),n == m时变成线性
先设置一个大小,假设均匀每个会有多少,然后计算需要的桶的数量,
然后放进桶里面,,最后将每个桶排序
时间复杂度为O(N+C),其中C=N*(logN-logM)
内部是快速排序。
https://www.cnblogs.com/bigsai/p/13396391.html
这里讲的很清楚
private static void bucketSort(int[] nums) {
//设置桶的大小,每个桶放5个
int bucketSize = 5;
//然后按大小分配桶的个数
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i = 0; i < nums.length; i++) {
if (nums[i] > max) {
max = nums[i];
}
if (nums[i] < min) {
min = nums[i];
}
}
//这个是大小,所以要+1
int bucketCount = (int) Math.floor((max - min) / bucketSize)+1;
//用一个扩容机制
int[][] buckets = new int[bucketCount][0];
for (int num : nums) {
int index = (int) Math.floor((num - min) / bucketSize);
buckets[index] = ArrayAppend(buckets[index], num);
}
int k = 0;
for (int i = 0; i < bucketCount; i++) {
Arrays.sort(buckets[i]);
for (int tp : buckets[i]) {
nums[k++] = tp;
}
}
}
private static int[] ArrayAppend(int[] arr, int num) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = num;
return arr;
}
● 基数排序
桶排序的优化每一位是一个桶,每次都要新建桶,时间复杂度是n*k,k是位数。
private static void RadixSort(int[] nums) {
//用于记录的的数组,负数0~9,正数10~10
int max = Integer.MIN_VALUE;
for (int i = 0; i < nums.length; i++) {
if (nums[i] > max) {
max = nums[i];
}
}
//这个记录要走几轮
int maxDev=0;
while (max>0){
max/=10;
maxDev++;
}
//除以10
// int mod =1 ;
//当前进行到哪里了
int dev = 1;
for (int i = 0; i < maxDev; i++) {
//每次都不一样
int [][] count = new int[20][0];
for (int num : nums) {
int nowDev = getDev(num,dev);
count[nowDev] = ArrayAppend(count[nowDev],num);
}
int k = 0;
for (int j = 0; j < 20; j++) {
for (int tp : count[j]) {
nums[k++] = tp;
}
}
dev*=10;
}
}
private static int getDev(int num, int dev) {
int res = (num/dev)%10;
return res+10;
}
https://blog.51cto.com/u_13281972/4931526
这里总结的挺好,我就不赘述了。
有一部分参考了菜鸟教程,写了三天才写好…太菜了,代码格式在语雀上没问题,复制过来出了点问题。
你画我猜想法
今天找了一个源码试了试可以跑,接下来看懂源码再自己写就可以了。
明天想先把登陆功能简单实现一下,用mybatis试一试,之前都用的mp,面试被问到压根不会。
今日复盘
-
写两个排序先看懂思路再去做,显然快了很多,没有浪费太多时间
-
今天显然比前两天松懈了一点,需要继续坚持。